From 5fac1897365d8f112097f1e8040b25338361cfd8 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Sun, 18 Jan 2015 16:11:30 -1000 Subject: [PATCH] "All images" view for conversations // FREEBIE --- AndroidManifest.xml | 4 + build.gradle | 3 +- res/drawable-hdpi/ic_menu_attach.png | Bin 1243 -> 0 bytes res/drawable-mdpi/ic_menu_attach.png | Bin 768 -> 0 bytes res/drawable-xhdpi/ic_menu_attach.png | Bin 1887 -> 0 bytes res/layout/media_overview_activity.xml | 25 +++ res/layout/media_overview_item.xml | 15 ++ res/menu/conversation.xml | 9 +- res/values-land/dimens.xml | 4 + res/values/dimens.xml | 2 + res/values/strings.xml | 6 + .../securesms/ConversationActivity.java | 9 + .../securesms/ImageMediaAdapter.java | 118 ++++++++++++ .../securesms/MediaOverviewActivity.java | 179 ++++++++++++++++++ .../components/SquareLinearLayout.java | 35 ++++ .../database/CursorRecyclerViewAdapter.java | 125 ++++++++++++ .../securesms/database/PartDatabase.java | 64 +++++++ .../securesms/mms/AudioSlide.java | 9 +- .../securesms/mms/ImageSlide.java | 21 +- .../securesms/mms/PartAuthority.java | 11 +- src/org/thoughtcrime/securesms/mms/Slide.java | 6 +- .../thoughtcrime/securesms/mms/SlideDeck.java | 20 +- .../securesms/mms/VideoSlide.java | 9 +- .../securesms/util/MediaUtil.java | 18 ++ 24 files changed, 646 insertions(+), 46 deletions(-) delete mode 100644 res/drawable-hdpi/ic_menu_attach.png delete mode 100644 res/drawable-mdpi/ic_menu_attach.png delete mode 100644 res/drawable-xhdpi/ic_menu_attach.png create mode 100644 res/layout/media_overview_activity.xml create mode 100644 res/layout/media_overview_item.xml create mode 100644 res/values-land/dimens.xml create mode 100644 src/org/thoughtcrime/securesms/ImageMediaAdapter.java create mode 100644 src/org/thoughtcrime/securesms/MediaOverviewActivity.java create mode 100644 src/org/thoughtcrime/securesms/components/SquareLinearLayout.java create mode 100644 src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 08fe5c696e..bcc0dfd8f4 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -209,6 +209,10 @@ android:launchMode="singleTask" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + 3P)LMsJPi&i6KH3pPYY9aJuB$N^?*b9i-5L=+z-6_yQ z>UOa+=WN0FgP70lB>;mC+3>I}6JZ#1km9-?dc&9I5KUMgYIl5=65nQg(FJ z(cD95M~%HGMJX%<$nj!xhTQYoPyBEG5Brs$RfGN5Yb*C!Q^FTypsrBr?g(4y$8K>x6g3@PcCjKuC)C_nAM8ozgb6ZyorJ zi@_Uw^)Xb7RPpDSH-hS=-d`klDO&5J6I$yj1$&+O0ElURn0vygp63K81=35g??bJ9 zglEdQ7XUTShv>$;-GG%60#w+sL)O}_@zP$bN+h5&<;O%p#?MdAFFoyFHUTm$)s9^I z9`5fR_Buu<^lU$*K}rY2IBo!ACP)8X+zBylAQ@yQo}dK z&|=Z~xXxeVfiP+pWB|4~;p2-#*e-3h6|bBN{;T+sZZu?dR7Pn#R;HZ)GQL>jrH(ik zJRHT#q!<91B`;0O59vpPSBCNv3&pJX)*0~1k9vK8xp@ zVuA~0D*nXGc|rjDe7rQI3RQpCdStpD!NS8=hm1wR6rZT7)yHl!p?t>jBB zFrmH@L$l2!PXNCkZCnxRINQ*VXG%Q))<4xh%7l94I9fdi-Z3?L3ZTRuy3B-fo>}+f z9_V$yH+-23Wt1t}MZ3j!C&L>FP+oN84~-j8uxp_Q017jF99lS)-|;2^`v>D1VMGsm z5t}EYNqP*+v5c-lqc8jxGs7opolpKm%b-&o$5-@vdfakx@9x;8Nj`3dHzMb{u=$Rj z517!m6Y5|5Dj(tcqpKfXREvAjl>71o z0c<3NJNOaupwpG@$SrjbQ=^%H%-v^#q;WE%vjYF$`WGD@NF1t7f0Fkdg00009c5p#w0000W z0000W0EhaVod5s=vPnciR7l6A)yqp%Q5*;G@42H*js}Sek(d%@sX4wP(1O&$3?d|? z)G;ub)bUaH$Sh+<8FeOY?mQgDd(WBNMYyP43yGpl|3R6HHkLt)S~aB*{#uNc$~be! z&hEnR^Lud4&Dk@QrS{QZuSW_?nh1oWb@m7vJCVSl6%w(&EzWkJFs!Khr?m|b2)o>YmcEz1CER7G= z=d81+1`=7gHy?$YCVq%jrEUu4Ks?)l6Yr8?uCRRy!+(mPdcljz@PAx!huklpMi(c^;=K=PrX_m4>Z#VO#FVYwEyc?m#% zmybk)j4S}a@m#yY7XmjYLe;-VmCvxokke|9iU6c*|=J?G)}HYss1N{A!FdH z_`p;8kOIVo0;?L_Ydks!u$KWiH=>X-KbzQ#6Y&}P9MqepH1dr{nW*o6Do9yU@A@yh znmx}HQoivTp9cPSjI@MqhZ+>b*TuRZ{yI?VKshljd?Q6-5^AyrP4d80r?=d$yN>MG yc2FqPfWs(49#SP1`xa#S|6HOWBte3t75@gnv2_WD8W@ZK0000THb;3rc_=+3xxs^mow;M8f~b)YB@yEXQ4@)VCME<;h!2DqKIj)}0@?y? zJ$H7?76n?mGx&u&*`4HOGXL{CbMLw5+zarmS%S7oQDw3&(2sn3{TY;RLb+R7Mg5p+TjhYNt-h<+NB!`dHoA)0rX*S7g9ts9`jag2rHj5d7UN$7? zzc?St&+62G^Mvs-8RJbCfy6!O_&PdW#p z`u1fgub}*R-_Mw&meP5!j_>`%(=b~4WZ$p265hd{Lb_C^cHIDz=zl_anwEY?Ce*SH zY19*Wz#Mui-=GgU22O4fNL@4F@$}wPPfD+kJO&OigH}h;(|b>Ji(Xn}$als47%mxM z>f`8}I-_?yg1#&AG>hSrk>2w_uD3%(dZ`ooU6}PEy(6Xj^z!iD$Wy_YU5ADR>9z}QJfM7--kA3jqoy}nGRtHzp3jXs?o$FDZb53e#AI#U z8Yn*+-hK&58T2&jIVPJxZ#TINV@q3x&N$yRi} zY|#H=20d+KImqO^Ul|5`gms*h-u@yQAKCFtA?zMkmE-pW3hF1;;M!ms{e2%uuR!_7 zMkU)o;ts&_LHCzWx;`>P#f6ZMn~$Zq5t*mtn@S&^1=FCk3I|#X#=99c?B=);k=KQC z+Ma#mlB5*dWZPBAmShQa3a+BQN*d#ws(QYIsIsAj`^8;l}l!VYuk!%dMON33YT zkIG)Oi>BEarESIBr=#~n7{Uv?c5pdxS@M8O1`z+gBScXN$u1wzz3+V5g zz^8z7aMhKN3Ap-sgHx+nFYJXquve*g@u*L0)k3L?x%AQf002ovPDHLkV1ib0uay7* diff --git a/res/layout/media_overview_activity.xml b/res/layout/media_overview_activity.xml new file mode 100644 index 0000000000..cf8900f382 --- /dev/null +++ b/res/layout/media_overview_activity.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/res/layout/media_overview_item.xml b/res/layout/media_overview_item.xml new file mode 100644 index 0000000000..b2054b9ebe --- /dev/null +++ b/res/layout/media_overview_item.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/res/menu/conversation.xml b/res/menu/conversation.xml index a95d63fff0..0c369f39d7 100644 --- a/res/menu/conversation.xml +++ b/res/menu/conversation.xml @@ -2,11 +2,12 @@ + android:id="@+id/menu_add_attachment" /> + + + android:id="@+id/menu_delete_thread" /> diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml new file mode 100644 index 0000000000..7c0fab9157 --- /dev/null +++ b/res/values-land/dimens.xml @@ -0,0 +1,4 @@ + + + 5 + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 4ca76a77bc..3bdffa66d3 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -14,4 +14,6 @@ 50dp 230dp 8dp + + 3 diff --git a/res/values/strings.xml b/res/values/strings.xml index 4ce4d33b0a..c4874755b9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -526,6 +526,9 @@ Import a plaintext backup file. Compatible with \'SMSBackup And Restore.\' + + No images + Manual MMS settings are required for your phone. Enabled @@ -650,6 +653,8 @@ Complete key exchange Submit debug logs Media Preview + All images + All images with %1$s Import / export @@ -823,6 +828,7 @@ Update group Leave group Delete thread + All images Add to contacts diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 6d83f9b3b9..bb849b42db 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -310,6 +310,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity case R.id.menu_call: handleDial(getRecipients().getPrimaryRecipient()); return true; case R.id.menu_delete_thread: handleDeleteThread(); return true; case R.id.menu_add_attachment: handleAddAttachment(); return true; + case R.id.menu_view_media: handleViewMedia(); return true; case R.id.menu_add_to_contacts: handleAddToContacts(); return true; case R.id.menu_start_secure_session: handleStartSecureSession(); return true; case R.id.menu_abort_session: handleAbortSecureSession(); return true; @@ -423,6 +424,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.show(); } + private void handleViewMedia() { + Intent intent = new Intent(this, MediaOverviewActivity.class); + intent.putExtra(MediaOverviewActivity.THREAD_ID_EXTRA, threadId); + intent.putExtra(MediaOverviewActivity.RECIPIENT_EXTRA, recipients.getPrimaryRecipient().getRecipientId()); + intent.putExtra(MediaOverviewActivity.MASTER_SECRET_EXTRA, masterSecret); + startActivity(intent); + } + private void handleLeavePushGroup() { if (getRecipients() == null) { Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient), diff --git a/src/org/thoughtcrime/securesms/ImageMediaAdapter.java b/src/org/thoughtcrime/securesms/ImageMediaAdapter.java new file mode 100644 index 0000000000..c6753b7868 --- /dev/null +++ b/src/org/thoughtcrime/securesms/ImageMediaAdapter.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2015 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.drawable.ColorDrawable; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; + +import org.thoughtcrime.securesms.ImageMediaAdapter.ViewHolder; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; +import org.thoughtcrime.securesms.database.PartDatabase.ImageRecord; +import org.thoughtcrime.securesms.mms.Slide; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.RecipientFormattingException; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.MediaUtil; + +import ws.com.google.android.mms.pdu.PduPart; + +public class ImageMediaAdapter extends CursorRecyclerViewAdapter { + private static final String TAG = ImageMediaAdapter.class.getSimpleName(); + + private final MasterSecret masterSecret; + private final int gridSize; + + public static class ViewHolder extends RecyclerView.ViewHolder { + public ImageView imageView; + + public ViewHolder(View v) { + super(v); + imageView = (ImageView) v.findViewById(R.id.image); + } + } + + public ImageMediaAdapter(Context context, MasterSecret masterSecret, Cursor c) { + super(context, c); + this.masterSecret = masterSecret; + this.gridSize = context.getResources().getDimensionPixelSize(R.dimen.thumbnail_max_size); + } + + @Override + public ViewHolder onCreateViewHolder(final ViewGroup viewGroup, final int i) { + final View view = LayoutInflater.from(getContext()).inflate(R.layout.media_overview_item, viewGroup, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder viewHolder, final Cursor cursor) { + final ImageView imageView = viewHolder.imageView; + final ImageRecord imageRecord = ImageRecord.from(cursor); + + PduPart part = new PduPart(); + + part.setDataUri(imageRecord.getUri()); + part.setContentType(imageRecord.getContentType().getBytes()); + part.setId(imageRecord.getPartId()); + + Slide slide = MediaUtil.getSlideForPart(getContext(), masterSecret, part, imageRecord.getContentType()); + if (slide != null) slide.setThumbnailOn(imageView, gridSize, gridSize, new ColorDrawable(0x11ffffff)); + + imageView.setOnClickListener(new OnMediaClickListener(imageRecord)); + } + + private class OnMediaClickListener implements OnClickListener { + private ImageRecord record; + + private OnMediaClickListener(ImageRecord record) { + this.record = record; + } + + @Override + public void onClick(View v) { + Intent intent = new Intent(getContext(), MediaPreviewActivity.class); + intent.putExtra(MediaPreviewActivity.MASTER_SECRET_EXTRA, masterSecret); + intent.putExtra(MediaPreviewActivity.DATE_EXTRA, record.getDate()); + + if (!TextUtils.isEmpty(record.getAddress())) { + try { + Recipients recipients = RecipientFactory.getRecipientsFromString(getContext(), + record.getAddress(), + true); + if (recipients != null && recipients.getPrimaryRecipient() != null) { + intent.putExtra(MediaPreviewActivity.RECIPIENT_EXTRA, recipients.getPrimaryRecipient().getRecipientId()); + } + } catch (RecipientFormattingException rfe) { + Log.w(TAG, rfe); + } + } + intent.setDataAndType(record.getUri(), record.getContentType()); + getContext().startActivity(intent); + + } + } +} diff --git a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java new file mode 100644 index 0000000000..63b630bcb9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -0,0 +1,179 @@ +/** + * Copyright (C) 2015 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.database.Cursor; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.util.AbstractCursorLoader; +import org.thoughtcrime.securesms.util.DynamicLanguage; + +/** + * Activity for displaying media attachments in-app + */ +public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity implements LoaderManager.LoaderCallbacks { + private final static String TAG = MediaOverviewActivity.class.getSimpleName(); + + public final static String MASTER_SECRET_EXTRA = "master_secret"; + public final static String RECIPIENT_EXTRA = "recipient"; + public final static String THREAD_ID_EXTRA = "thread_id"; + + private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); + + private MasterSecret masterSecret; + + private RecyclerView gridView; + private GridLayoutManager gridManager; + private TextView noImages; + private Recipient recipient; + private long threadId; + + @Override + protected void onCreate(Bundle bundle) { + this.setTheme(R.style.TextSecure_DarkTheme); + dynamicLanguage.onCreate(this); + + super.onCreate(bundle); + setFullscreenIfPossible(); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.media_overview_activity); + + initializeResources(); + initializeActionBar(); + getSupportLoaderManager().initLoader(0, null, MediaOverviewActivity.this); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (gridManager != null) gridManager.setSpanCount(getResources().getInteger(R.integer.media_overview_cols)); + } + + @TargetApi(VERSION_CODES.JELLY_BEAN) + private void setFullscreenIfPossible() { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + } + } + + @Override + public void onResume() { + super.onResume(); + dynamicLanguage.onResume(this); + } + + private void initializeActionBar() { + getSupportActionBar().setTitle(recipient == null + ? getString(R.string.AndroidManifest__media_overview) + : getString(R.string.AndroidManifest__media_overview_named, recipient.toShortString())); + } + + @Override + public void onPause() { + super.onPause(); + } + + private void initializeResources() { + masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA); + threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); + + noImages = (TextView ) findViewById(R.id.no_images ); + gridView = (RecyclerView) findViewById(R.id.media_grid); + gridManager = new GridLayoutManager(this, getResources().getInteger(R.integer.media_overview_cols)); + gridView.setLayoutManager(gridManager); + gridView.setHasFixedSize(true); + + final long recipientId = getIntent().getLongExtra(RECIPIENT_EXTRA, -1); + if (recipientId > -1) { + recipient = RecipientFactory.getRecipientForId(this, recipientId, true); + recipient.addListener(new RecipientModifiedListener() { + @Override + public void onModified(Recipient recipient) { + initializeActionBar(); + } + }); + } else { + recipient = null; + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + switch (item.getItemId()) { + case android.R.id.home: finish(); return true; + } + + return false; + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return new ThreadMediaLoader(this, threadId); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + Log.w(TAG, "onLoadFinished()"); + gridView.setAdapter(new ImageMediaAdapter(this, masterSecret, cursor)); + noImages.setVisibility(gridView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + ((CursorRecyclerViewAdapter)gridView.getAdapter()).changeCursor(null); + } + + public static class ThreadMediaLoader extends AbstractCursorLoader { + private final long threadId; + + public ThreadMediaLoader(Context context, long threadId) { + super(context); + this.threadId = threadId; + } + + @Override + public Cursor getCursor() { + return DatabaseFactory.getPartDatabase(getContext()).getImagesForThread(threadId); + } + } +} diff --git a/src/org/thoughtcrime/securesms/components/SquareLinearLayout.java b/src/org/thoughtcrime/securesms/components/SquareLinearLayout.java new file mode 100644 index 0000000000..ea24fb7c27 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/SquareLinearLayout.java @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.components; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +public class SquareLinearLayout extends LinearLayout { + @SuppressWarnings("unused") + public SquareLinearLayout(Context context) { + super(context); + } + + @SuppressWarnings("unused") + public SquareLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @TargetApi(VERSION_CODES.HONEYCOMB) @SuppressWarnings("unused") + public SquareLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) @SuppressWarnings("unused") + public SquareLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //noinspection SuspiciousNameCombination + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} diff --git a/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java b/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java new file mode 100644 index 0000000000..b24e587fae --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java @@ -0,0 +1,125 @@ +/** + * Copyright (C) 2015 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.database; + +import android.content.Context; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.support.v7.widget.RecyclerView; + +/** + * RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in ListView/GridView. + */ +public abstract class CursorRecyclerViewAdapter extends RecyclerView.Adapter { + private final Context context; + private final DataSetObserver observer = new AdapterDataSetObserver(); + + private Cursor cursor; + private boolean valid; + + protected CursorRecyclerViewAdapter(Context context, Cursor cursor) { + this.context = context; + this.cursor = cursor; + if (cursor != null) { + valid = true; + cursor.registerDataSetObserver(observer); + } + + setHasStableIds(true); + } + + public Context getContext() { + return context; + } + + public Cursor getCursor() { + return cursor; + } + + public void changeCursor(Cursor cursor) { + Cursor old = swapCursor(cursor); + if (old != null) { + old.close(); + } + } + + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == cursor) { + return null; + } + + final Cursor oldCursor = cursor; + if (oldCursor != null) { + oldCursor.unregisterDataSetObserver(observer); + } + + cursor = newCursor; + if (cursor != null) { + cursor.registerDataSetObserver(observer); + } + + valid = cursor != null; + notifyDataSetChanged(); + return oldCursor; + } + + + @Override + public int getItemCount() { + return isActiveCursor() ? cursor.getCount() : 0; + } + + @Override + public long getItemId(int position) { + return isActiveCursor() && cursor.moveToPosition(position) + ? cursor.getLong(cursor.getColumnIndexOrThrow("_id")) + : 0; + } + + public abstract void onBindViewHolder(VH viewHolder, Cursor cursor); + + @Override + public void onBindViewHolder(VH viewHolder, int position) { + if (!isActiveCursor()) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + if (!cursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + onBindViewHolder(viewHolder, cursor); + } + + private boolean isActiveCursor() { + return valid && cursor != null; + } + + private class AdapterDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + super.onChanged(); + valid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + valid = false; + notifyDataSetChanged(); + } + } +} diff --git a/src/org/thoughtcrime/securesms/database/PartDatabase.java b/src/org/thoughtcrime/securesms/database/PartDatabase.java index 1fc0b58ea1..9fcffdeb8f 100644 --- a/src/org/thoughtcrime/securesms/database/PartDatabase.java +++ b/src/org/thoughtcrime/securesms/database/PartDatabase.java @@ -23,12 +23,14 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; +import android.net.Uri; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; +import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.util.BitmapDecodingException; @@ -91,6 +93,20 @@ public class PartDatabase extends Database { "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + PENDING_PUSH_ATTACHMENT + ");", }; + private final static String IMAGES_QUERY = "SELECT " + TABLE_NAME + "." + ID + ", " + + TABLE_NAME + "." + CONTENT_TYPE + ", " + + TABLE_NAME + "." + ASPECT_RATIO + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.NORMALIZED_DATE_RECEIVED + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ADDRESS + " " + + "FROM " + TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME + + " ON " + TABLE_NAME + "." + MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + + "WHERE " + MMS_ID + " IN (SELECT " + MmsSmsColumns.ID + + " FROM " + MmsDatabase.TABLE_NAME + + " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND " + + CONTENT_TYPE + " LIKE 'image/%' " + + "ORDER BY " + TABLE_NAME + "." + ID + " DESC"; + + private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor(); public PartDatabase(Context context, SQLiteOpenHelper databaseHelper) { @@ -135,6 +151,13 @@ public class PartDatabase extends Database { } } + public Cursor getImagesForThread(long threadId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + Cursor cursor = database.rawQuery(IMAGES_QUERY, new String[]{threadId+""}); + setNotifyConverationListeners(cursor, threadId); + return cursor; + } + public List> getParts(long mmsId) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); List> results = new LinkedList<>(); @@ -509,6 +532,47 @@ public class PartDatabase extends Database { database.update(TABLE_NAME, values, ID_WHERE, new String[]{partId+""}); } + public static class ImageRecord { + private long partId; + private String contentType; + private String address; + private long date; + + private ImageRecord(long partId, String contentType, String address, long date) { + this.partId = partId; + this.contentType = contentType; + this.address = address; + this.date = date; + } + + public static ImageRecord from(Cursor cursor) { + return new ImageRecord(cursor.getLong(cursor.getColumnIndexOrThrow(ID)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), + cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)), + cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED)) * 1000); + } + + public long getPartId() { + return partId; + } + + public String getContentType() { + return contentType; + } + + public String getAddress() { + return address; + } + + public long getDate() { + return date; + } + + public Uri getUri() { + return ContentUris.withAppendedId(PartAuthority.PART_CONTENT_URI, getPartId()); + } + } + @VisibleForTesting class ThumbnailFetchCallable implements Callable { private final MasterSecret masterSecret; private final long partId; diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java index 5d528e19d8..b32c57f150 100644 --- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.mms; import java.io.IOException; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.util.SmilUtil; import org.w3c.dom.smil.SMILDocument; import org.w3c.dom.smil.SMILMediaElement; @@ -34,14 +35,14 @@ import android.provider.MediaStore.Audio; public class AudioSlide extends Slide { - public AudioSlide(Context context, PduPart part) { - super(context, part); - } - public AudioSlide(Context context, Uri uri) throws IOException, MediaTooLargeException { super(context, constructPartFromUri(context, uri)); } + public AudioSlide(Context context, MasterSecret masterSecret, PduPart part) { + super(context, masterSecret, part); + } + @Override public boolean hasImage() { return true; diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index 66160c3135..c3cbf6dc61 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -112,6 +112,11 @@ public class ImageSlide extends Slide { @Override public void setThumbnailOn(ImageView imageView) { + setThumbnailOn(imageView, imageView.getWidth(), imageView.getHeight(), new ColorDrawable(Color.TRANSPARENT)); + } + + @Override + public void setThumbnailOn(ImageView imageView, final int width, final int height, final Drawable placeholder) { Drawable thumbnail = getCachedThumbnail(); if (thumbnail != null) { @@ -120,24 +125,22 @@ public class ImageSlide extends Slide { return; } - final ColorDrawable temporaryDrawable = new ColorDrawable(Color.TRANSPARENT); - final WeakReference weakImageView = new WeakReference(imageView); + final WeakReference weakImageView = new WeakReference<>(imageView); final Handler handler = new Handler(); - final int maxWidth = imageView.getWidth(); - final int maxHeight = imageView.getHeight(); - imageView.setImageDrawable(temporaryDrawable); + imageView.setImageDrawable(placeholder); - if (maxWidth == 0 || maxHeight == 0) + if (width == 0 || height == 0) return; MmsDatabase.slideResolver.execute(new Runnable() { @Override public void run() { - final Drawable bitmap = getThumbnail(maxWidth, maxHeight); + final Drawable bitmap = getThumbnail(width, height); final ImageView destination = weakImageView.get(); - if (destination != null && destination.getDrawable() == temporaryDrawable) { + Log.w(TAG, "slide resolved, destination available? " + (destination == null)); + if (destination != null && destination.getDrawable() == placeholder) { handler.post(new Runnable() { @Override public void run() { @@ -156,7 +159,7 @@ public class ImageSlide extends Slide { imageView.setImageDrawable(thumbnail); ((AnimationDrawable)imageView.getDrawable()).start(); } else { - TransitionDrawable fadingResult = new TransitionDrawable(new Drawable[]{new ColorDrawable(Color.TRANSPARENT), thumbnail}); + TransitionDrawable fadingResult = new TransitionDrawable(new Drawable[]{imageView.getDrawable(), thumbnail}); imageView.setImageDrawable(fadingResult); fadingResult.startTransition(300); } diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index 5ec3e2adac..123c45faf3 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -10,27 +10,21 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.providers.PartProvider; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class PartAuthority { - private static final String PART_URI_STRING = "content://org.thoughtcrime.securesms/part"; - private static final String THUMB_URI_STRING = "content://org.thoughtcrime.securesms/thumb"; - - public static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING); - public static final Uri THUMB_CONTENT_URI = Uri.parse(THUMB_URI_STRING); + private static final String PART_URI_STRING = "content://org.thoughtcrime.securesms/part"; + public static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING); private static final int PART_ROW = 1; - private static final int THUMB_ROW = 2; private static final UriMatcher uriMatcher; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("org.thoughtcrime.securesms", "part/#", PART_ROW); - uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/#", THUMB_ROW); } public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri) @@ -42,7 +36,6 @@ public class PartAuthority { try { switch (match) { case PART_ROW: return partDatabase.getPartStream(masterSecret, ContentUris.parseId(uri)); - case THUMB_ROW: return partDatabase.getThumbnailStream(masterSecret, ContentUris.parseId(uri)); default: return context.getContentResolver().openInputStream(uri); } } catch (SecurityException se) { diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index a06f0f5342..0c9008243b 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -39,7 +39,7 @@ public abstract class Slide { protected final PduPart part; protected final Context context; protected MasterSecret masterSecret; - + public Slide(Context context, PduPart part) { this.part = part; this.context = context; @@ -78,6 +78,10 @@ public abstract class Slide { imageView.setImageDrawable(getThumbnail(imageView.getWidth(), imageView.getHeight())); } + public void setThumbnailOn(ImageView imageView, int height, int width, Drawable placeholder) { + imageView.setImageDrawable(getThumbnail(width, height)); + } + public Bitmap getGeneratedThumbnail() { return null; } public boolean hasImage() { diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java index b1ec2d2fd6..3f664a6663 100644 --- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -20,7 +20,9 @@ import android.content.Context; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.dom.smil.parser.SmilXmlSerializer; +import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.SmilUtil; +import org.thoughtcrime.securesms.util.Util; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; @@ -41,20 +43,10 @@ public class SlideDeck { } public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) { - try { - for (int i=0;i