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 @@
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