"All images" view for conversations

// FREEBIE
master
Jake McGinty 2015-01-18 16:11:30 -10:00
parent d3271f548c
commit 5fac189736
24 changed files with 646 additions and 46 deletions

View File

@ -209,6 +209,10 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaOverviewActivity"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DummyActivity"
android:theme="@android:style/Theme.NoDisplay"
android:enabled="true"

View File

@ -44,6 +44,7 @@ dependencies {
compile 'com.afollestad:material-dialogs:0.6.1.5'
compile 'com.soundcloud.android:android-crop:0.9.10@aar'
compile 'com.android.support:appcompat-v7:21.0.3'
compile 'com.android.support:recyclerview-v7:21.0.3'
compile 'com.melnykov:floatingactionbutton:1.1.0'
compile 'com.google.zxing:android-integration:3.1.0'
compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){
@ -89,6 +90,7 @@ dependencyVerification {
'com.afollestad:material-dialogs:ccb013e6572c86cfcca433855cf0dbfbff9b5e7bb9d1f504b761a6bc6f467b60',
'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
'com.android.support:appcompat-v7:5dbeb5316d0a6027d646ae552804c3baa5e3bd53f7f33db50904d51505c8a0e5',
'com.android.support:recyclerview-v7:e525ad3f33c84bb12b73d2dc975b55364a53f0f2d0697e043efba59ba73e22d2',
'com.melnykov:floatingactionbutton:0679ad9f7d61eb7aeab91e8dc56358cdedd5b1c1b9c48464499ffa05c40d3985',
'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
@ -96,7 +98,6 @@ dependencyVerification {
'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
'org.whispersystems:jobmanager:01f35586c43aa3806f1c18d3d6a5a972def98103ba1a5a9ca3eec08d15f974b7',
'org.whispersystems:libpastelog:3ccf00fe1597eb8ca1e5de99b17fc225387a1b80b5bbc00ec1bc4d4f3ea9cdde',
'com.android.support:recyclerview-v7:ab2390d688601b65e2f3a0718b3d25487e61546c4e20f81eb0b033f30ca15b31',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray95">
<android.support.v7.widget.RecyclerView
android:id="@+id/media_grid"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView android:id="@+id/no_images"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textStyle="italic"
android:textSize="24sp"
android:gravity="center_horizontal"
android:paddingTop="30dp"
android:visibility="gone"
android:text="@string/media_overview_activity__no_images" />
</RelativeLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.SquareLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="@string/media_preview_activity__image_content_description" />
</org.thoughtcrime.securesms.components.SquareLinearLayout>

View File

@ -2,11 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/conversation__menu_add_attachment"
android:id="@+id/menu_add_attachment"
android:icon="@drawable/ic_menu_attach" />
android:id="@+id/menu_add_attachment" />
<item android:title="@string/conversation__menu_view_media"
android:id="@+id/menu_view_media" />
<item android:title="@string/conversation__menu_delete_thread"
android:id="@+id/menu_delete_thread"
android:icon="@android:drawable/ic_menu_delete" />
android:id="@+id/menu_delete_thread" />
</menu>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="media_overview_cols">5</integer>
</resources>

View File

@ -14,4 +14,6 @@
<dimen name="contact_selection_photo_size">50dp</dimen>
<dimen name="thumbnail_max_size">230dp</dimen>
<dimen name="preference_fragment_padding_side">8dp</dimen>
<integer name="media_overview_cols">3</integer>
</resources>

View File

@ -526,6 +526,9 @@
<string name="import_fragment__import_a_plaintext_backup_file">
Import a plaintext backup file. Compatible with \'SMSBackup And Restore.\'</string>
<!-- media_overview_activity -->
<string name="media_overview_activity__no_images">No images</string>
<!-- MmsPreferencesFragment -->
<string name="MmsPreferencesFragment__manual_mms_settings_are_required">Manual MMS settings are required for your phone.</string>
<string name="MmsPreferencesFragment__enabled">Enabled</string>
@ -650,6 +653,8 @@
<string name="AndroidManifest__complete_key_exchange">Complete key exchange</string>
<string name="AndroidManifest__log_submit">Submit debug logs</string>
<string name="AndroidManifest__media_preview">Media Preview</string>
<string name="AndroidManifest__media_overview">All images</string>
<string name="AndroidManifest__media_overview_named">All images with %1$s</string>
<!-- arrays.xml -->
<string name="arrays__import_export">Import / export</string>
@ -823,6 +828,7 @@
<string name="conversation__menu_update_group">Update group</string>
<string name="conversation__menu_leave_group">Leave group</string>
<string name="conversation__menu_delete_thread">Delete thread</string>
<string name="conversation__menu_view_media">All images</string>
<!-- conversation_callable -->
<string name="conversation_add_to_contacts__menu_add_to_contacts">Add to contacts</string>

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ViewHolder> {
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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Cursor> {
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<Cursor> onCreateLoader(int i, Bundle bundle) {
return new ThreadMediaLoader(this, threadId);
}
@Override
public void onLoadFinished(Loader<Cursor> 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<Cursor> 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
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();
}
}
}

View File

@ -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<Pair<Long, PduPart>> getParts(long mmsId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<Pair<Long, PduPart>> 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<InputStream> {
private final MasterSecret masterSecret;
private final long partId;

View File

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

View File

@ -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<ImageView> weakImageView = new WeakReference<ImageView>(imageView);
final WeakReference<ImageView> 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);
}

View File

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

View File

@ -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() {

View File

@ -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<body.getPartsNum();i++) {
String contentType = new String(body.getPart(i).getContentType(), CharacterSets.MIMENAME_ISO_8859_1);
if (ContentType.isImageType(contentType))
slides.add(new ImageSlide(context, masterSecret, body.getPart(i)));
else if (ContentType.isVideoType(contentType))
slides.add(new VideoSlide(context, body.getPart(i)));
else if (ContentType.isAudioType(contentType))
slides.add(new AudioSlide(context, body.getPart(i)));
else if (ContentType.isTextType(contentType))
slides.add(new TextSlide(context, masterSecret, body.getPart(i)));
}
} catch (UnsupportedEncodingException uee) {
throw new AssertionError(uee);
for (int i=0;i<body.getPartsNum();i++) {
String contentType = Util.toIsoString(body.getPart(i).getContentType());
Slide slide = MediaUtil.getSlideForPart(context, masterSecret, body.getPart(i), contentType);
if (slide != null) slides.add(slide);
}
}

View File

@ -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;
@ -35,14 +36,14 @@ import android.util.Log;
public class VideoSlide extends Slide {
public VideoSlide(Context context, PduPart part) {
super(context, part);
}
public VideoSlide(Context context, Uri uri) throws IOException, MediaTooLargeException {
super(context, constructPartFromUri(context, uri));
}
public VideoSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
}
@Override
public Drawable getThumbnail(int width, int height) {
return context.getResources().getDrawable(R.drawable.ic_launcher_video_player);

View File

@ -9,8 +9,13 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import java.io.ByteArrayInputStream;
@ -62,6 +67,19 @@ public class MediaUtil {
return BitmapUtil.createScaledBitmap(context, masterSecret, uri, maxSize, maxSize);
}
public static Slide getSlideForPart(Context context, MasterSecret masterSecret, PduPart part, String contentType) {
Slide slide = null;
if (ContentType.isImageType(contentType)) {
slide = new ImageSlide(context, masterSecret, part);
} else if (ContentType.isVideoType(contentType)) {
slide = new VideoSlide(context, masterSecret, part);
} else if (ContentType.isAudioType(contentType)) {
slide = new AudioSlide(context, masterSecret, part);
}
return slide;
}
public static boolean isImage(PduPart part) {
return ContentType.isImageType(Util.toIsoString(part.getContentType()));
}