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"/> + + + + + + + + + + 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