diff --git a/build.gradle b/build.gradle index 516f4d375..422641c57 100644 --- a/build.gradle +++ b/build.gradle @@ -79,6 +79,7 @@ dependencies { compile 'org.whispersystems:libpastelog:1.0.6' compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' compile 'org.whispersystems:textsecure-android:1.6.2' + compile 'com.h6ah4i.android.compat:mulsellistprefcompat:1.0.0' androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' diff --git a/res/drawable-hdpi/ic_file_download_white_36dp.png b/res/drawable-hdpi/ic_file_download_white_36dp.png new file mode 100644 index 000000000..9f23ce8ee Binary files /dev/null and b/res/drawable-hdpi/ic_file_download_white_36dp.png differ diff --git a/res/drawable-hdpi/ic_forum_black_32dp.png b/res/drawable-hdpi/ic_forum_black_32dp.png new file mode 100644 index 000000000..4911236d4 Binary files /dev/null and b/res/drawable-hdpi/ic_forum_black_32dp.png differ diff --git a/res/drawable-hdpi/ic_forum_grey_32dp.png b/res/drawable-hdpi/ic_forum_grey_32dp.png new file mode 100644 index 000000000..37c69a18a Binary files /dev/null and b/res/drawable-hdpi/ic_forum_grey_32dp.png differ diff --git a/res/drawable-hdpi/ic_textsms_black_32dp.png b/res/drawable-hdpi/ic_textsms_black_32dp.png new file mode 100644 index 000000000..091c4487a Binary files /dev/null and b/res/drawable-hdpi/ic_textsms_black_32dp.png differ diff --git a/res/drawable-hdpi/ic_textsms_grey_32dp.png b/res/drawable-hdpi/ic_textsms_grey_32dp.png new file mode 100644 index 000000000..6882ef608 Binary files /dev/null and b/res/drawable-hdpi/ic_textsms_grey_32dp.png differ diff --git a/res/drawable-mdpi/ic_file_download_white_36dp.png b/res/drawable-mdpi/ic_file_download_white_36dp.png new file mode 100644 index 000000000..6c2665d24 Binary files /dev/null and b/res/drawable-mdpi/ic_file_download_white_36dp.png differ diff --git a/res/drawable-mdpi/ic_forum_black_32dp.png b/res/drawable-mdpi/ic_forum_black_32dp.png new file mode 100644 index 000000000..7840302c1 Binary files /dev/null and b/res/drawable-mdpi/ic_forum_black_32dp.png differ diff --git a/res/drawable-mdpi/ic_forum_grey_32dp.png b/res/drawable-mdpi/ic_forum_grey_32dp.png new file mode 100644 index 000000000..00b00cea1 Binary files /dev/null and b/res/drawable-mdpi/ic_forum_grey_32dp.png differ diff --git a/res/drawable-mdpi/ic_textsms_black_32dp.png b/res/drawable-mdpi/ic_textsms_black_32dp.png new file mode 100644 index 000000000..efb482e66 Binary files /dev/null and b/res/drawable-mdpi/ic_textsms_black_32dp.png differ diff --git a/res/drawable-mdpi/ic_textsms_grey_32dp.png b/res/drawable-mdpi/ic_textsms_grey_32dp.png new file mode 100644 index 000000000..bad115a42 Binary files /dev/null and b/res/drawable-mdpi/ic_textsms_grey_32dp.png differ diff --git a/res/drawable-xhdpi/ic_file_download_white_36dp.png b/res/drawable-xhdpi/ic_file_download_white_36dp.png new file mode 100644 index 000000000..d508aa948 Binary files /dev/null and b/res/drawable-xhdpi/ic_file_download_white_36dp.png differ diff --git a/res/drawable-xhdpi/ic_forum_black_32dp.png b/res/drawable-xhdpi/ic_forum_black_32dp.png new file mode 100644 index 000000000..2048b9c1e Binary files /dev/null and b/res/drawable-xhdpi/ic_forum_black_32dp.png differ diff --git a/res/drawable-xhdpi/ic_forum_grey_32dp.png b/res/drawable-xhdpi/ic_forum_grey_32dp.png new file mode 100644 index 000000000..a9981ee98 Binary files /dev/null and b/res/drawable-xhdpi/ic_forum_grey_32dp.png differ diff --git a/res/drawable-xhdpi/ic_textsms_black_32dp.png b/res/drawable-xhdpi/ic_textsms_black_32dp.png new file mode 100644 index 000000000..ef8cfe04f Binary files /dev/null and b/res/drawable-xhdpi/ic_textsms_black_32dp.png differ diff --git a/res/drawable-xhdpi/ic_textsms_grey_32dp.png b/res/drawable-xhdpi/ic_textsms_grey_32dp.png new file mode 100644 index 000000000..8bf0991bd Binary files /dev/null and b/res/drawable-xhdpi/ic_textsms_grey_32dp.png differ diff --git a/res/drawable-xxhdpi/ic_file_download_white_36dp.png b/res/drawable-xxhdpi/ic_file_download_white_36dp.png new file mode 100644 index 000000000..30b762d70 Binary files /dev/null and b/res/drawable-xxhdpi/ic_file_download_white_36dp.png differ diff --git a/res/drawable-xxhdpi/ic_forum_black_32dp.png b/res/drawable-xxhdpi/ic_forum_black_32dp.png new file mode 100644 index 000000000..22468201a Binary files /dev/null and b/res/drawable-xxhdpi/ic_forum_black_32dp.png differ diff --git a/res/drawable-xxhdpi/ic_forum_grey_32dp.png b/res/drawable-xxhdpi/ic_forum_grey_32dp.png new file mode 100644 index 000000000..07031d106 Binary files /dev/null and b/res/drawable-xxhdpi/ic_forum_grey_32dp.png differ diff --git a/res/drawable-xxhdpi/ic_textsms_black_32dp.png b/res/drawable-xxhdpi/ic_textsms_black_32dp.png new file mode 100644 index 000000000..0b8b2b0f7 Binary files /dev/null and b/res/drawable-xxhdpi/ic_textsms_black_32dp.png differ diff --git a/res/drawable-xxhdpi/ic_textsms_grey_32dp.png b/res/drawable-xxhdpi/ic_textsms_grey_32dp.png new file mode 100644 index 000000000..ffb8a5a70 Binary files /dev/null and b/res/drawable-xxhdpi/ic_textsms_grey_32dp.png differ diff --git a/res/drawable-xxxhdpi/ic_file_download_white_36dp.png b/res/drawable-xxxhdpi/ic_file_download_white_36dp.png new file mode 100644 index 000000000..e261f9a87 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_file_download_white_36dp.png differ diff --git a/res/drawable-xxxhdpi/ic_forum_black_32dp.png b/res/drawable-xxxhdpi/ic_forum_black_32dp.png new file mode 100644 index 000000000..8063a274f Binary files /dev/null and b/res/drawable-xxxhdpi/ic_forum_black_32dp.png differ diff --git a/res/drawable-xxxhdpi/ic_forum_grey_32dp.png b/res/drawable-xxxhdpi/ic_forum_grey_32dp.png new file mode 100644 index 000000000..008345bbc Binary files /dev/null and b/res/drawable-xxxhdpi/ic_forum_grey_32dp.png differ diff --git a/res/drawable-xxxhdpi/ic_textsms_black_32dp.png b/res/drawable-xxxhdpi/ic_textsms_black_32dp.png new file mode 100644 index 000000000..c0712d0bc Binary files /dev/null and b/res/drawable-xxxhdpi/ic_textsms_black_32dp.png differ diff --git a/res/drawable-xxxhdpi/ic_textsms_grey_32dp.png b/res/drawable-xxxhdpi/ic_textsms_grey_32dp.png new file mode 100644 index 000000000..f47cc38ad Binary files /dev/null and b/res/drawable-xxxhdpi/ic_textsms_grey_32dp.png differ diff --git a/res/layout/thumbnail_view.xml b/res/layout/thumbnail_view.xml index ca2d0cf81..80590293d 100644 --- a/res/layout/thumbnail_view.xml +++ b/res/layout/thumbnail_view.xml @@ -20,4 +20,12 @@ android:layout_height="wrap_content" android:layout_gravity="top|right" android:layout="@layout/thumbnail_view_remove_button" /> + + diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 701596684..1461817f1 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -193,4 +193,29 @@ contact none + + + + image + audio + video + + + + @string/arrays__images + @string/arrays__audio + @string/arrays__video + + + + image + + + + image + audio + video + + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index b59dd40a1..cbada251d 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -106,6 +106,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index 5df9a2cf1..296ce0b58 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -762,6 +762,10 @@ Name only Neither + Images + Audio + Video + %d hour @@ -847,6 +851,7 @@ Request a delivery report for each SMS message you send Automatically delete older messages once a conversation thread exceeds a specified length Delete old messages + Chats and media Conversation length limit Trim all threads now Scan through all conversation threads and enforce conversation length limits diff --git a/res/values/themes.xml b/res/values/themes.xml index 26b8ed626..18f90be17 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -169,11 +169,11 @@ #ff1d85d7 - @drawable/ic_message_black + @drawable/ic_textsms_black_32dp @drawable/ic_notifications_black @drawable/ic_app_protection_black @drawable/ic_brightness_6_black - @drawable/ic_delete_black + @drawable/ic_forum_black_32dp @drawable/ic_devices_black_48dp @drawable/ic_advanced_black @@ -281,11 +281,11 @@ @color/textsecure_primary_dark - @drawable/ic_message_gray + @drawable/ic_textsms_grey_32dp @drawable/ic_notifications_gray @drawable/ic_app_protection_gray @drawable/ic_brightness_6_gray - @drawable/ic_delete_gray + @drawable/ic_forum_grey_32dp @drawable/ic_devices_grey600_48dp @drawable/ic_advanced_gray diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 79ba26004..459324a05 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -17,9 +17,9 @@ android:title="@string/preferences__appearance" android:icon="?pref_ic_appearance"/> - + diff --git a/res/xml/preferences_chats.xml b/res/xml/preferences_chats.xml new file mode 100644 index 000000000..ec5e4c70b --- /dev/null +++ b/res/xml/preferences_chats.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/preferences_storage.xml b/res/xml/preferences_storage.xml deleted file mode 100644 index 83a1d88cc..000000000 --- a/res/xml/preferences_storage.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 97565aa64..2eeb5bd21 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule; import org.thoughtcrime.securesms.jobs.GcmRefreshJob; import org.thoughtcrime.securesms.jobs.persistence.EncryptingJobSerializer; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirementProvider; +import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider; import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirementProvider; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.jobqueue.JobManager; @@ -49,9 +50,11 @@ import dagger.ObjectGraph; */ public class ApplicationContext extends Application implements DependencyInjector { - private JobManager jobManager; + private JobManager jobManager; private ObjectGraph objectGraph; + private MediaNetworkRequirementProvider mediaNetworkRequirementProvider = new MediaNetworkRequirementProvider(); + public static ApplicationContext getInstance(Context context) { return (ApplicationContext)context.getApplicationContext(); } @@ -103,11 +106,16 @@ public class ApplicationContext extends Application implements DependencyInjecto .withJobSerializer(new EncryptingJobSerializer()) .withRequirementProviders(new MasterSecretRequirementProvider(this), new ServiceRequirementProvider(this), - new NetworkRequirementProvider(this)) + new NetworkRequirementProvider(this), + mediaNetworkRequirementProvider) .withConsumerThreads(5) .build(); } + public void notifyMediaControlEvent() { + mediaNetworkRequirementProvider.notifyMediaControlEvent(); + } + private void initializeDependencyInjection() { this.objectGraph = ObjectGraph.create(new TextSecureCommunicationModule(this), new AxolotlStorageModule(this)); diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 1ec02f886..d9866ea00 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment; import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment; import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment; -import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment; +import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -54,7 +54,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications"; private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection"; private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance"; - private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage"; + private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats"; private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices"; private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced"; @@ -130,8 +130,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_APP_PROTECTION)); this.findPreference(PREFERENCE_CATEGORY_APPEARANCE) .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_APPEARANCE)); - this.findPreference(PREFERENCE_CATEGORY_STORAGE) - .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_STORAGE)); + this.findPreference(PREFERENCE_CATEGORY_CHATS) + .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_CHATS)); // this.findPreference(PREFERENCE_CATEGORY_DEVICES) // .setOnPreferenceClickListener(new CategoryClickListener(masterSecret, PREFERENCE_CATEGORY_DEVICES)); this.findPreference(PREFERENCE_CATEGORY_ADVANCED) @@ -154,8 +154,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA .setSummary(AppProtectionPreferenceFragment.getSummary(getActivity())); this.findPreference(PREFERENCE_CATEGORY_APPEARANCE) .setSummary(AppearancePreferenceFragment.getSummary(getActivity())); - this.findPreference(PREFERENCE_CATEGORY_STORAGE) - .setSummary(StoragePreferenceFragment.getSummary(getActivity())); + this.findPreference(PREFERENCE_CATEGORY_CHATS) + .setSummary(ChatsPreferenceFragment.getSummary(getActivity())); } private class CategoryClickListener implements Preference.OnPreferenceClickListener { @@ -184,8 +184,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA case PREFERENCE_CATEGORY_APPEARANCE: fragment = new AppearancePreferenceFragment(); break; - case PREFERENCE_CATEGORY_STORAGE: - fragment = new StoragePreferenceFragment(); + case PREFERENCE_CATEGORY_CHATS: + fragment = new ChatsPreferenceFragment(); break; case PREFERENCE_CATEGORY_DEVICES: Intent intent = new Intent(getActivity(), DeviceListActivity.class); diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index f7cad6b2f..408fbcd1d 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -156,6 +157,7 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien if (mediaThumbnail != null) { mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener()); mediaThumbnail.setOnLongClickListener(new MultiSelectLongClickListener()); + mediaThumbnail.setDownloadClickListener(new ThumbnailDownloadClickListener()); } } @@ -275,7 +277,7 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien mediaThumbnail.setImageResource(masterSecret, messageRecord.getId(), messageRecord.getDateReceived(), ((MediaMmsMessageRecord)messageRecord).getSlideDeckFuture()); - mediaThumbnail.setShowProgress(!messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending())); + mediaThumbnail.hideControls(messageRecord.isFailed() || (messageRecord.isOutgoing() && !messageRecord.isPending())); bodyText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } else { mediaThumbnail.setVisibility(View.GONE); @@ -409,6 +411,11 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } } + private class ThumbnailDownloadClickListener implements ThumbnailView.ThumbnailClickListener { + @Override public void onClick(View v, Slide slide) { + DatabaseFactory.getPartDatabase(context).setTransferState(messageRecord.getId(), slide.getPart().getPartId(), PartDatabase.TRANSFER_PROGRESS_STARTED); + } + } private class ThumbnailClickListener implements ThumbnailView.ThumbnailClickListener { private void fireIntent(Slide slide) { Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index f149d60f7..cbc09e40d 100644 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -32,7 +32,11 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsDatabase.Reader; import org.thoughtcrime.securesms.database.PushDatabase; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.jobs.PushDecryptJob; @@ -41,10 +45,14 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.VersionTracker; import java.io.File; +import java.util.List; import java.util.SortedSet; import java.util.TreeSet; +import ws.com.google.android.mms.pdu.PduPart; + public class DatabaseUpgradeActivity extends BaseActivity { + private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName(); public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46; public static final int MMS_BODY_VERSION = 46; @@ -57,6 +65,7 @@ public class DatabaseUpgradeActivity extends BaseActivity { public static final int PUSH_DECRYPT_SERIAL_ID_VERSION = 131; public static final int MIGRATE_SESSION_PLAINTEXT = 136; public static final int CONTACTS_ACCOUNT_VERSION = 136; + public static final int MEDIA_DOWNLOAD_CONTROLS_VERSION = 146; private static final SortedSet UPGRADE_VERSIONS = new TreeSet() {{ add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION); @@ -68,6 +77,7 @@ public class DatabaseUpgradeActivity extends BaseActivity { add(NO_DECRYPT_QUEUE_VERSION); add(PUSH_DECRYPT_SERIAL_ID_VERSION); add(MIGRATE_SESSION_PLAINTEXT); + add(MEDIA_DOWNLOAD_CONTROLS_VERSION); }}; private MasterSecret masterSecret; @@ -205,9 +215,32 @@ public class DatabaseUpgradeActivity extends BaseActivity { .add(new DirectoryRefreshJob(getApplicationContext())); } + if (params[0] < MEDIA_DOWNLOAD_CONTROLS_VERSION) { + schedulePendingIncomingParts(context); + } + return null; } + private void schedulePendingIncomingParts(Context context) { + MmsDatabase db = DatabaseFactory.getMmsDatabase(context); + List pendingParts = DatabaseFactory.getPartDatabase(context).getPendingParts(); + + Log.w(TAG, pendingParts.size() + " pending parts."); + for (PduPart part : pendingParts) { + final Reader reader = db.readerFor(masterSecret, db.getMessage(part.getMmsId())); + final MessageRecord record = reader.getNext(); + + if (record != null && !record.isOutgoing() && record.isPush()) { + Log.w(TAG, "queuing new attachment download job for incoming push part."); + ApplicationContext.getInstance(context) + .getJobManager() + .add(new AttachmentDownloadJob(context, part.getMmsId(), part.getPartId())); + } + reader.close(); + } + } + private void scheduleMessagesInPushDatabase(Context context) { PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(context); Cursor pushReader = null; diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index 3931d29c4..34ebf5e22 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -31,6 +31,7 @@ import com.pnikosis.materialishprogress.ProgressWheel; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.jobs.PartProgressEvent; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.RoundedCorners; @@ -47,16 +48,18 @@ import ws.com.google.android.mms.pdu.PduPart; public class ThumbnailView extends FrameLayout { private static final String TAG = ThumbnailView.class.getSimpleName(); - private boolean showProgress = true; + private boolean hideControls; private ImageView image; private ProgressWheel progress; private ImageView removeButton; + private ImageButton downloadButton; private int backgroundColorHint; private int radius; private ListenableFutureTask slideDeckFuture = null; private SlideDeckListener slideDeckListener = null; private ThumbnailClickListener thumbnailClickListener = null; + private ThumbnailClickListener downloadClickListener = null; private String slideId = null; private Slide slide = null; @@ -68,11 +71,13 @@ public class ThumbnailView extends FrameLayout { this(context, attrs, 0); } - public ThumbnailView(Context context, AttributeSet attrs, int defStyle) { + public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); inflate(context, R.layout.thumbnail_view, this); - radius = getResources().getDimensionPixelSize(R.dimen.message_bubble_corner_radius); - image = (ImageView) findViewById(R.id.thumbnail_image); + radius = getResources().getDimensionPixelSize(R.dimen.message_bubble_corner_radius); + image = (ImageView) findViewById(R.id.thumbnail_image); + progress = (ProgressWheel) findViewById(R.id.progress_wheel); + downloadButton = (ImageButton) findViewById(R.id.download_button); if (attrs != null) { TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0); @@ -162,15 +167,24 @@ public class ThumbnailView extends FrameLayout { return; } - this.slide = slide; - if (slide.isInProgress() && showProgress) { + if (!hideControls && slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_STARTED) { getProgressWheel().spin(); getProgressWheel().setVisibility(VISIBLE); + downloadButton.setVisibility(GONE); + } else if (!hideControls && slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_AUTO_PENDING || + slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_FAILED) + { + hideProgressWheel(); + downloadButton.setVisibility(VISIBLE); } else { hideProgressWheel(); + downloadButton.setVisibility(GONE); } + + this.slide = slide; buildGlideRequest(slide, masterSecret).into(image); setOnClickListener(new ThumbnailClickDispatcher(thumbnailClickListener, slide)); + downloadButton.setOnClickListener(new ThumbnailClickDispatcher(downloadClickListener, slide)); } public void setThumbnailClickListener(ThumbnailClickListener listener) { @@ -181,15 +195,17 @@ public class ThumbnailView extends FrameLayout { getRemoveButton().setOnClickListener(listener); } + public void setDownloadClickListener(ThumbnailClickListener listener) { + this.downloadClickListener = listener; + } + public void clear() { if (isContextValid()) Glide.clear(this); } - public void setShowProgress(boolean showProgress) { - this.showProgress = showProgress; - if (progress != null && progress.getVisibility() == View.VISIBLE && !showProgress) { - animateOutProgress(); - } + public void hideControls(boolean hideControls) { + this.hideControls = hideControls; + if (hideControls) hideProgressWheel(); } @TargetApi(VERSION_CODES.JELLY_BEAN_MR1) @@ -202,7 +218,6 @@ public class ThumbnailView extends FrameLayout { private GenericRequestBuilder buildGlideRequest(@NonNull Slide slide, @Nullable MasterSecret masterSecret) { - Log.w(TAG, "slide type " + slide.getContentType()); final GenericRequestBuilder builder; if (slide.getThumbnailUri() != null) { builder = buildThumbnailGlideRequest(slide, masterSecret); @@ -210,7 +225,7 @@ public class ThumbnailView extends FrameLayout { builder = buildPlaceholderGlideRequest(slide); } - if (slide.isInProgress() && showProgress) { + if (slide.isInProgress() && !hideControls) { return builder; } else { return builder.error(R.drawable.ic_missing_thumbnail_picture); diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 8a269b86f..37349524e 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -752,7 +752,7 @@ public class MmsDatabase extends MessagingDatabase { if (sendRequest.getBody() != null) { for (int i = 0; i < sendRequest.getBody().getPartsNum(); i++) { - sendRequest.getBody().getPart(i).setInProgress(true); + sendRequest.getBody().getPart(i).setTransferProgress(PartDatabase.TRANSFER_PROGRESS_STARTED); } } diff --git a/src/org/thoughtcrime/securesms/database/PartDatabase.java b/src/org/thoughtcrime/securesms/database/PartDatabase.java index d523e694e..17d40fc60 100644 --- a/src/org/thoughtcrime/securesms/database/PartDatabase.java +++ b/src/org/thoughtcrime/securesms/database/PartDatabase.java @@ -23,14 +23,18 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.net.Uri; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUnion; +import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirement; +import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; @@ -43,12 +47,14 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.Serializable; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import de.greenrobot.event.EventBus; import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.pdu.PduBody; @@ -72,12 +78,17 @@ public class PartDatabase extends Database { private static final String CONTENT_TYPE_TYPE = "ctt_t"; private static final String ENCRYPTED = "encrypted"; private static final String DATA = "_data"; - private static final String IN_PROGRESS = "pending_push"; + private static final String TRANSFER_STATE = "pending_push"; private static final String SIZE = "data_size"; private static final String THUMBNAIL = "thumbnail"; private static final String ASPECT_RATIO = "aspect_ratio"; private static final String UNIQUE_ID = "unique_id"; + public static final int TRANSFER_PROGRESS_DONE = 0; + public static final int TRANSFER_PROGRESS_STARTED = 1; + public static final int TRANSFER_PROGRESS_AUTO_PENDING = 2; + public static final int TRANSFER_PROGRESS_FAILED = 3; + private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + @@ -86,12 +97,12 @@ public class PartDatabase extends Database { CONTENT_DISPOSITION + " TEXT, " + FILENAME + " TEXT, " + CONTENT_ID + " TEXT, " + CONTENT_LOCATION + " TEXT, " + CONTENT_TYPE_START + " INTEGER, " + CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " + - IN_PROGRESS + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " + + TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " + THUMBNAIL + " TEXT, " + ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", - "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + IN_PROGRESS + ");", + "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");", }; private final static String IMAGES_QUERY = "SELECT " + TABLE_NAME + "." + ROW_ID + ", " @@ -127,7 +138,7 @@ public class PartDatabase extends Database { SQLiteDatabase database = databaseHelper.getWritableDatabase(); part.setContentDisposition(new byte[0]); - part.setInProgress(false); + part.setTransferProgress(TRANSFER_PROGRESS_FAILED); ContentValues values = getContentValuesForPart(part); @@ -223,6 +234,7 @@ public class PartDatabase extends Database { } void insertParts(MasterSecretUnion masterSecret, long mmsId, PduBody body) throws MmsException { + Log.w(TAG, "insertParts(" + body.getPartsNum() + ")"); for (int i=0;i getPendingParts() { + final SQLiteDatabase database = databaseHelper.getReadableDatabase(); + final List parts = new LinkedList<>(); + + Cursor cursor = null; + try { + cursor = database.query(TABLE_NAME, null, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); + while (cursor != null && cursor.moveToNext()) { + parts.add(getPart(cursor)); + } + } finally { + if (cursor != null) cursor.close(); + } + + return parts; + } + private PartId insertPart(MasterSecretUnion masterSecret, PduPart part, long mmsId, Bitmap thumbnail) throws MmsException { Log.w(TAG, "inserting part to mms " + mmsId); SQLiteDatabase database = databaseHelper.getWritableDatabase(); @@ -472,7 +501,7 @@ public class PartDatabase extends Database { Pair partData = writePartData(masterSecret, part, data); part.setContentDisposition(new byte[0]); - part.setInProgress(false); + part.setTransferProgress(TRANSFER_PROGRESS_DONE); ContentValues values = getContentValuesForPart(part); @@ -492,13 +521,23 @@ public class PartDatabase extends Database { ContentValues values = new ContentValues(1); SQLiteDatabase database = databaseHelper.getWritableDatabase(); - part.setInProgress(false); - values.put(IN_PROGRESS, false); + part.setTransferProgress(TRANSFER_PROGRESS_DONE); + values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); database.update(TABLE_NAME, values, PART_ID_WHERE, part.getPartId().toStrings()); notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); } + public void setTransferState(long messageId, @NonNull PartId partId, int transferState) { + final ContentValues values = new ContentValues(1); + final SQLiteDatabase database = databaseHelper.getWritableDatabase(); + + values.put(TRANSFER_STATE, transferState); + database.update(TABLE_NAME, values, PART_ID_WHERE, partId.toStrings()); + notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); + ApplicationContext.getInstance(context).notifyMediaControlEvent(); + } + public void updatePartData(MasterSecret masterSecret, PduPart part, InputStream data) throws MmsException { diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index 72293e567..a6b45afc0 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.database.PartDatabase.PartId; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; +import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirement; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; @@ -26,7 +27,6 @@ import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.List; import javax.inject.Inject; @@ -35,50 +35,49 @@ import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.pdu.PduPart; public class AttachmentDownloadJob extends MasterSecretJob implements InjectableType { - - private static final String TAG = AttachmentDownloadJob.class.getSimpleName(); + private static final long serialVersionUID = 1L; + private static final String TAG = AttachmentDownloadJob.class.getSimpleName(); @Inject transient TextSecureMessageReceiver messageReceiver; private final long messageId; + private final long partRowId; + private final long partUniqueId; - public AttachmentDownloadJob(Context context, long messageId) { + public AttachmentDownloadJob(Context context, long messageId, PartId partId) { super(context, JobParameters.newBuilder() + .withGroupId(AttachmentDownloadJob.class.getCanonicalName()) .withRequirement(new MasterSecretRequirement(context)) .withRequirement(new NetworkRequirement(context)) + .withRequirement(new MediaNetworkRequirement(context, messageId, partId)) .withPersistence() .create()); - this.messageId = messageId; + this.messageId = messageId; + this.partRowId = partId.getRowId(); + this.partUniqueId = partId.getUniqueId(); } @Override - public void onAdded() {} + public void onAdded() { + } @Override public void onRun(MasterSecret masterSecret) throws IOException { - PartDatabase database = DatabaseFactory.getPartDatabase(context); + final PartId partId = new PartId(partRowId, partUniqueId); + final PduPart part = DatabaseFactory.getPartDatabase(context).getPart(partId); - Log.w(TAG, "Downloading push parts for: " + messageId); - - List parts = database.getParts(messageId); - - for (PduPart part : parts) { - retrievePart(masterSecret, part, messageId); - Log.w(TAG, "Got part: " + part.getPartId()); - } + Log.w(TAG, "Downloading push part " + partId); + retrievePart(masterSecret, part, messageId); MessageNotifier.updateNotification(context, masterSecret); } @Override public void onCanceled() { - PartDatabase database = DatabaseFactory.getPartDatabase(context); - List parts = database.getParts(messageId); - - for (PduPart part : parts) { - markFailed(messageId, part, part.getPartId()); - } + final PartId partId = new PartId(partRowId, partUniqueId); + final PduPart part = DatabaseFactory.getPartDatabase(context).getPart(partId); + markFailed(messageId, part, part.getPartId()); } @Override diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 5aef98659..6fefba422 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -58,9 +58,11 @@ import org.whispersystems.textsecure.api.messages.multidevice.SentTranscriptMess import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; import org.whispersystems.textsecure.api.push.TextSecureAddress; +import java.util.List; import java.util.concurrent.TimeUnit; import ws.com.google.android.mms.MmsException; +import ws.com.google.android.mms.pdu.PduPart; public class PushDecryptJob extends ContextJob { @@ -252,11 +254,14 @@ public class PushDecryptJob extends ContextJob { message.getGroupInfo(), message.getAttachments()); - Pair messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1); + Pair messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1); - ApplicationContext.getInstance(context) - .getJobManager() - .add(new AttachmentDownloadJob(context, messageAndThreadId.first)); + List parts = DatabaseFactory.getPartDatabase(context).getParts(messageAndThreadId.first); + for (PduPart part : parts) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new AttachmentDownloadJob(context, messageAndThreadId.first, part.getPartId())); + } if (smsMessageId.isPresent()) { DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); @@ -284,9 +289,11 @@ public class PushDecryptJob extends ContextJob { database.markAsSent(messageId, "push".getBytes(), 0); database.markAsPush(messageId); - ApplicationContext.getInstance(context) - .getJobManager() - .add(new AttachmentDownloadJob(context, messageId)); + for (PduPart part : DatabaseFactory.getPartDatabase(context).getParts(messageId)) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new AttachmentDownloadJob(context, messageId, part.getPartId())); + } if (smsMessageId.isPresent()) { DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java new file mode 100644 index 000000000..146ac2bfb --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java @@ -0,0 +1,99 @@ +package org.thoughtcrime.securesms.jobs.requirements; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.annotation.NonNull; +import android.util.Log; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.PartDatabase; +import org.thoughtcrime.securesms.database.PartDatabase.PartId; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.jobqueue.dependencies.ContextDependent; +import org.whispersystems.jobqueue.requirements.Requirement; + +import java.util.Collections; +import java.util.Set; + +import ws.com.google.android.mms.pdu.PduPart; + +public class MediaNetworkRequirement implements Requirement, ContextDependent { + private static final long serialVersionUID = 0L; + private static final String TAG = MediaNetworkRequirement.class.getSimpleName(); + + private transient Context context; + + private final long messageId; + private final long partRowId; + private final long partUniqueId; + + public MediaNetworkRequirement(Context context, long messageId, PartId partId) { + this.context = context; + this.messageId = messageId; + this.partRowId = partId.getRowId(); + this.partUniqueId = partId.getUniqueId(); + } + + @Override public void setContext(Context context) { + this.context = context; + } + + private NetworkInfo getNetworkInfo() { + return ServiceUtil.getConnectivityManager(context).getActiveNetworkInfo(); + } + + public boolean isConnectedWifi() { + final NetworkInfo info = getNetworkInfo(); + return info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI; + } + + public boolean isConnectedMobile() { + final NetworkInfo info = getNetworkInfo(); + return info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_MOBILE; + } + + public boolean isConnectedRoaming() { + final NetworkInfo info = getNetworkInfo(); + return info != null && info.isConnected() && info.isRoaming() && info.getType() == ConnectivityManager.TYPE_MOBILE; + } + + private @NonNull Set getAllowedAutoDownloadTypes() { + if (isConnectedWifi()) { + return TextSecurePreferences.getWifiMediaDownloadAllowed(context); + } else if (isConnectedRoaming()) { + return TextSecurePreferences.getRoamingMediaDownloadAllowed(context); + } else if (isConnectedMobile()) { + return TextSecurePreferences.getMobileMediaDownloadAllowed(context); + } else { + return Collections.emptySet(); + } + } + + @Override + public boolean isPresent() { + final PartId partId = new PartId(partRowId, partUniqueId); + final PartDatabase db = DatabaseFactory.getPartDatabase(context); + final PduPart part = db.getPart(partId); + if (part == null) { + Log.w(TAG, "part was null"); + return false; + } + + Log.w(TAG, "part transfer progress is " + part.getTransferProgress()); + switch (part.getTransferProgress()) { + case PartDatabase.TRANSFER_PROGRESS_STARTED: + return true; + case PartDatabase.TRANSFER_PROGRESS_AUTO_PENDING: + final Set allowedTypes = getAllowedAutoDownloadTypes(); + final boolean isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(part)); + + if (isAllowed) db.setTransferState(messageId, partId, PartDatabase.TRANSFER_PROGRESS_STARTED); + return isAllowed; + default: + return false; + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirementProvider.java b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirementProvider.java new file mode 100644 index 000000000..0019c461c --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirementProvider.java @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.jobs.requirements; + +import org.whispersystems.jobqueue.requirements.RequirementListener; +import org.whispersystems.jobqueue.requirements.RequirementProvider; + +public class MediaNetworkRequirementProvider implements RequirementProvider { + + private RequirementListener listener; + + public void notifyMediaControlEvent() { + if (listener != null) listener.onRequirementStatusChanged(); + } + + @Override + public void setListener(RequirementListener listener) { + this.listener = listener; + } +} diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index 4659b647e..f5862e506 100644 --- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -1,13 +1,11 @@ package org.thoughtcrime.securesms.mms; import android.text.TextUtils; +import android.util.Log; -import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; -import org.thoughtcrime.securesms.crypto.MasterCipher; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUnion; import org.thoughtcrime.securesms.crypto.MediaKey; -import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libaxolotl.util.guava.Optional; @@ -83,7 +81,7 @@ public class IncomingMediaMessage { media.setName(Util.toIsoBytes(relay.get())); } - media.setInProgress(true); + media.setTransferProgress(PartDatabase.TRANSFER_PROGRESS_AUTO_PENDING); this.body.addPart(media); } diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index 6eb5f1464..7f5288529 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -78,6 +78,10 @@ public abstract class Slide { return part.isInProgress(); } + public long getTransferProgress() { + return part.getTransferProgress(); + } + public @DrawableRes int getPlaceholderRes(Theme theme) { throw new AssertionError("getPlaceholderRes() called for non-drawable slide"); } @@ -111,7 +115,7 @@ public abstract class Slide { this.hasImage() == that.hasImage() && this.hasVideo() == that.hasVideo() && this.isDraft() == that.isDraft() && - this.isInProgress() == that.isInProgress() && + this.getTransferProgress() == that.getTransferProgress() && Util.equals(this.getUri(), that.getUri()) && Util.equals(this.getThumbnailUri(), that.getThumbnailUri()); } @@ -119,7 +123,7 @@ public abstract class Slide { @Override public int hashCode() { return Util.hashCode(getContentType(), hasAudio(), hasImage(), - hasVideo(), isDraft(), getUri(), getThumbnailUri()); + hasVideo(), isDraft(), getUri(), getThumbnailUri(), getTransferProgress()); } diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java new file mode 100644 index 000000000..3def50cfa --- /dev/null +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -0,0 +1,143 @@ +package org.thoughtcrime.securesms.preferences; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.MultiSelectListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.support.v4.preference.PreferenceFragment; +import android.text.TextUtils; +import android.util.Log; + +import com.afollestad.materialdialogs.AlertDialogWrapper; + +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Trimmer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ChatsPreferenceFragment extends PreferenceFragment { + private static final String TAG = ChatsPreferenceFragment.class.getSimpleName(); + + @Override + public void onCreate(Bundle paramBundle) { + super.onCreate(paramBundle); + addPreferencesFromResource(R.xml.preferences_chats); + + findPreference(TextSecurePreferences.MEDIA_DOWNLOAD_MOBILE_PREF) + .setOnPreferenceChangeListener(new MediaDownloadChangeListener()); + findPreference(TextSecurePreferences.MEDIA_DOWNLOAD_WIFI_PREF) + .setOnPreferenceChangeListener(new MediaDownloadChangeListener()); + findPreference(TextSecurePreferences.MEDIA_DOWNLOAD_ROAMING_PREF) + .setOnPreferenceChangeListener(new MediaDownloadChangeListener()); + + findPreference(TextSecurePreferences.THREAD_TRIM_NOW) + .setOnPreferenceClickListener(new TrimNowClickListener()); + findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH) + .setOnPreferenceChangeListener(new TrimLengthValidationListener()); + + } + + @Override + public void onResume() { + super.onResume(); + ((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.preferences__chats); + setMediaDownloadSummaries(); + } + + private void setMediaDownloadSummaries() { + findPreference(TextSecurePreferences.MEDIA_DOWNLOAD_MOBILE_PREF) + .setSummary(getSummaryForMediaPreference(TextSecurePreferences.getMobileMediaDownloadAllowed(getActivity()))); + findPreference(TextSecurePreferences.MEDIA_DOWNLOAD_WIFI_PREF) + .setSummary(getSummaryForMediaPreference(TextSecurePreferences.getWifiMediaDownloadAllowed(getActivity()))); + findPreference(TextSecurePreferences.MEDIA_DOWNLOAD_ROAMING_PREF) + .setSummary(getSummaryForMediaPreference(TextSecurePreferences.getRoamingMediaDownloadAllowed(getActivity()))); + } + + private CharSequence getSummaryForMediaPreference(Set allowedNetworks) { + String[] keys = getResources().getStringArray(R.array.pref_media_download_entries); + String[] values = getResources().getStringArray(R.array.pref_media_download_values); + List outValues = new ArrayList<>(allowedNetworks.size()); + + for (int i=0; i < keys.length; i++) { + if (allowedNetworks.contains(keys[i])) outValues.add(values[i]); + } + + return outValues.isEmpty() ? getResources().getString(R.string.preferences__none) + : TextUtils.join(", ", outValues); + } + + private class TrimNowClickListener implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(Preference preference) { + final int threadLengthLimit = TextSecurePreferences.getThreadTrimLength(getActivity()); + AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(getActivity()); + builder.setTitle(R.string.ApplicationPreferencesActivity_delete_all_old_messages_now); + builder.setMessage(getString(R.string.ApplicationPreferencesActivity_are_you_sure_you_would_like_to_immediately_trim_all_conversation_threads_to_the_s_most_recent_messages, + threadLengthLimit)); + builder.setPositiveButton(R.string.ApplicationPreferencesActivity_delete, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Trimmer.trimAllThreads(getActivity(), threadLengthLimit); + } + }); + + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + + return true; + } + } + + private class MediaDownloadChangeListener implements OnPreferenceChangeListener { + @SuppressWarnings("unchecked") + @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + Log.w(TAG, "onPreferenceChange"); + preference.setSummary(getSummaryForMediaPreference((Set)newValue)); + return true; + } + } + + private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener { + + public TrimLengthValidationListener() { + EditTextPreference preference = (EditTextPreference)findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH); + preference.setSummary(getString(R.string.ApplicationPreferencesActivity_messages_per_conversation, preference.getText())); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (newValue == null || ((String)newValue).trim().length() == 0) { + return false; + } + + try { + Integer.parseInt((String)newValue); + } catch (NumberFormatException nfe) { + Log.w(TAG, nfe); + return false; + } + + if (Integer.parseInt((String)newValue) < 1) { + return false; + } + + preference.setSummary(getString(R.string.ApplicationPreferencesActivity_messages_per_conversation, newValue)); + return true; + } + } + + public static CharSequence getSummary(Context context) { + return null; + } +} diff --git a/src/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java deleted file mode 100644 index 616ba275b..000000000 --- a/src/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.thoughtcrime.securesms.preferences; - -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.support.v4.preference.PreferenceFragment; -import android.util.Log; - -import com.afollestad.materialdialogs.AlertDialogWrapper; - -import org.thoughtcrime.securesms.ApplicationPreferencesActivity; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Trimmer; - -public class StoragePreferenceFragment extends PreferenceFragment { - private static final String TAG = StoragePreferenceFragment.class.getSimpleName(); - - @Override - public void onCreate(Bundle paramBundle) { - super.onCreate(paramBundle); - addPreferencesFromResource(R.xml.preferences_storage); - - this.findPreference(TextSecurePreferences.THREAD_TRIM_NOW) - .setOnPreferenceClickListener(new TrimNowClickListener()); - this.findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH) - .setOnPreferenceChangeListener(new TrimLengthValidationListener()); - } - - @Override - public void onResume() { - super.onResume(); - ((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__delete_old_messages); - } - - private class TrimNowClickListener implements Preference.OnPreferenceClickListener { - @Override - public boolean onPreferenceClick(Preference preference) { - final int threadLengthLimit = TextSecurePreferences.getThreadTrimLength(getActivity()); - AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(getActivity()); - builder.setTitle(R.string.ApplicationPreferencesActivity_delete_all_old_messages_now); - builder.setMessage(getString(R.string.ApplicationPreferencesActivity_are_you_sure_you_would_like_to_immediately_trim_all_conversation_threads_to_the_s_most_recent_messages, - threadLengthLimit)); - builder.setPositiveButton(R.string.ApplicationPreferencesActivity_delete, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Trimmer.trimAllThreads(getActivity(), threadLengthLimit); - } - }); - - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - - return true; - } - } - - private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener { - - public TrimLengthValidationListener() { - EditTextPreference preference = (EditTextPreference)findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH); - preference.setSummary(getString(R.string.ApplicationPreferencesActivity_messages_per_conversation, preference.getText())); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (newValue == null || ((String)newValue).trim().length() == 0) { - return false; - } - - try { - Integer.parseInt((String)newValue); - } catch (NumberFormatException nfe) { - Log.w(TAG, nfe); - return false; - } - - if (Integer.parseInt((String)newValue) < 1) { - return false; - } - - preference.setSummary(getString(R.string.ApplicationPreferencesActivity_messages_per_conversation, newValue)); - return true; - } - } - - public static CharSequence getSummary(Context context) { - final int onCapsResId = R.string.ApplicationPreferencesActivity_On; - final int offCapsResId = R.string.ApplicationPreferencesActivity_Off; - - return context.getString(TextSecurePreferences.isThreadLengthTrimmingEnabled(context) ? onCapsResId : offCapsResId); - } -} diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java index d63ad362d..b04d4f522 100644 --- a/src/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; @@ -108,6 +110,11 @@ public class MediaUtil { return ContentType.isVideoType(Util.toIsoString(part.getContentType())); } + public static @Nullable String getDiscreteMimeType(@NonNull PduPart part) { + final String[] sections = (Util.toIsoString(part.getContentType()).split("/", 2)); + return sections.length > 1 ? sections[0] : null; + } + public static class ThumbnailData { Bitmap bitmap; float aspectRatio; diff --git a/src/org/thoughtcrime/securesms/util/ServiceUtil.java b/src/org/thoughtcrime/securesms/util/ServiceUtil.java index 049433d43..d1cfdae7a 100644 --- a/src/org/thoughtcrime/securesms/util/ServiceUtil.java +++ b/src/org/thoughtcrime/securesms/util/ServiceUtil.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util; import android.app.Activity; import android.content.Context; +import android.net.ConnectivityManager; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; @@ -13,4 +14,8 @@ public class ServiceUtil { public static WindowManager getWindowManager(Context context) { return (WindowManager) context.getSystemService(Activity.WINDOW_SERVICE); } + + public static ConnectivityManager getConnectivityManager(Context context) { + return (ConnectivityManager) context.getSystemService(Activity.CONNECTIVITY_SERVICE); + } } diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index c5572da59..f94a4efa7 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -4,11 +4,19 @@ import android.content.Context; import android.os.Build; import android.preference.PreferenceManager; import android.provider.Settings; +import android.support.annotation.ArrayRes; +import android.support.annotation.NonNull; import android.util.Log; +import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.preferences.NotificationPrivacyPreference; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; public class TextSecurePreferences { @@ -74,6 +82,10 @@ public class TextSecurePreferences { public static final String REPEAT_ALERTS_PREF = "pref_repeat_alerts"; public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy"; + public static final String MEDIA_DOWNLOAD_MOBILE_PREF = "pref_media_download_mobile"; + public static final String MEDIA_DOWNLOAD_WIFI_PREF = "pref_media_download_wifi"; + public static final String MEDIA_DOWNLOAD_ROAMING_PREF = "pref_media_download_roaming"; + public static NotificationPrivacyPreference getNotificationPrivacy(Context context) { return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")); } @@ -433,6 +445,25 @@ public class TextSecurePreferences { return Integer.parseInt(getStringPreference(context, THREAD_TRIM_LENGTH, "500")); } + public static @NonNull Set getMobileMediaDownloadAllowed(Context context) { + return getMediaDownloadAllowed(context, MEDIA_DOWNLOAD_MOBILE_PREF, R.array.pref_media_download_mobile_data_default); + } + + public static @NonNull Set getWifiMediaDownloadAllowed(Context context) { + return getMediaDownloadAllowed(context, MEDIA_DOWNLOAD_WIFI_PREF, R.array.pref_media_download_wifi_default); + } + + public static @NonNull Set getRoamingMediaDownloadAllowed(Context context) { + return getMediaDownloadAllowed(context, MEDIA_DOWNLOAD_ROAMING_PREF, R.array.pref_media_download_roaming_default); + } + + private static @NonNull Set getMediaDownloadAllowed(Context context, String key, @ArrayRes int defaultValuesRes) { + return getStringSetPreference(context, + key, + new HashSet<>(Arrays.asList(context.getResources().getStringArray(defaultValuesRes)))); + } + + public static void setBooleanPreference(Context context, String key, boolean value) { PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply(); } @@ -468,4 +499,8 @@ public class TextSecurePreferences { private static void setLongPreference(Context context, String key, long value) { PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(key, value).apply(); } + + private static Set getStringSetPreference(Context context, String key, Set defaultValues) { + return PreferenceManager.getDefaultSharedPreferences(context).getStringSet(key, defaultValues); + } } diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index a7ce4ce80..64f96f9dd 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -28,6 +28,7 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.Looper; import android.provider.Telephony; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.telephony.TelephonyManager; import android.text.Spannable; @@ -119,7 +120,7 @@ public class Util { return spanned; } - public static String toIsoString(byte[] bytes) { + public static @NonNull String toIsoString(byte[] bytes) { try { return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1); } catch (UnsupportedEncodingException e) { diff --git a/src/ws/com/google/android/mms/pdu/PduPart.java b/src/ws/com/google/android/mms/pdu/PduPart.java index c17c74270..630f232d6 100644 --- a/src/ws/com/google/android/mms/pdu/PduPart.java +++ b/src/ws/com/google/android/mms/pdu/PduPart.java @@ -134,8 +134,9 @@ public class PduPart { private long rowId = -1; private long uniqueId = -1; + private long mmsId = -1; private boolean isEncrypted; - private boolean isInProgress; + private int transferProgress; private long dataSize; private Bitmap thumbnail; @@ -163,13 +164,17 @@ public class PduPart { return this.dataSize; } - - public void setInProgress(boolean isInProgress) { - this.isInProgress = isInProgress; + public boolean isInProgress() { + return transferProgress != PartDatabase.TRANSFER_PROGRESS_DONE && + transferProgress != PartDatabase.TRANSFER_PROGRESS_FAILED; } - public boolean isInProgress() { - return isInProgress; + public void setTransferProgress(int transferProgress) { + this.transferProgress = transferProgress; + } + + public int getTransferProgress() { + return transferProgress; } /** @@ -475,5 +480,13 @@ public class PduPart { public void setUniqueId(long uniqueId) { this.uniqueId = uniqueId; } + + public long getMmsId() { + return mmsId; + } + + public void setMmsId(long mmsId) { + this.mmsId = mmsId; + } }