mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-19 19:28:26 +00:00
"All images" view for conversations
// FREEBIE
This commit is contained in:
parent
d3271f548c
commit
5fac189736
@ -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"
|
||||
|
@ -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 |
25
res/layout/media_overview_activity.xml
Normal file
25
res/layout/media_overview_activity.xml
Normal 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>
|
15
res/layout/media_overview_item.xml
Normal file
15
res/layout/media_overview_item.xml
Normal 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>
|
@ -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>
|
||||
|
4
res/values-land/dimens.xml
Normal file
4
res/values-land/dimens.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="media_overview_cols">5</integer>
|
||||
</resources>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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),
|
||||
|
118
src/org/thoughtcrime/securesms/ImageMediaAdapter.java
Normal file
118
src/org/thoughtcrime/securesms/ImageMediaAdapter.java
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
179
src/org/thoughtcrime/securesms/MediaOverviewActivity.java
Normal file
179
src/org/thoughtcrime/securesms/MediaOverviewActivity.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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()));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user