diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c054a91e59..eb9b4f23d5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -148,7 +148,7 @@
-
-
@@ -169,8 +168,15 @@
-
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 52bd08efae..825b0e11fa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -64,6 +64,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 7d5360dc06..4facf9f40a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -1322,11 +1322,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final StickerLocator stickerLocator = getIntent().getParcelableExtra(STICKER_EXTRA);
if (stickerLocator != null && draftMedia != null) {
+ Log.d(TAG, "Handling shared sticker.");
sendSticker(stickerLocator, draftMedia, 0, true);
return new SettableFuture<>(false);
}
if (!Util.isEmpty(mediaList)) {
+ Log.d(TAG, "Handling shared Media.");
Intent sendIntent = MediaSendActivity.buildEditorIntent(this, mediaList, recipient.get(), draftText, sendButton.getSelectedTransport());
startActivityForResult(sendIntent, MEDIA_SENDER);
return new SettableFuture<>(false);
@@ -1339,6 +1341,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
if (draftMedia != null && draftMediaType != null) {
+ Log.d(TAG, "Handling shared Data.");
return setMedia(draftMedia, draftMediaType);
}
@@ -2633,7 +2636,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
slideDeck.addSlide(stickerSlide);
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
-
}
private void silentlySetComposeText(String text) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index d25a1de479..ed1f002ee0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -63,7 +63,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.ShareActivity;
+import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.TooltipPopup;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
index 1cd6c73b21..29dd638270 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
@@ -142,11 +142,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
/**
* Get an intent to launch the media send flow starting with the picker.
*/
- public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
+ public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @Nullable String body, @NonNull TransportOption transport) {
Intent intent = new Intent(context, MediaSendActivity.class);
intent.putExtra(KEY_RECIPIENT, recipient.getId());
intent.putExtra(KEY_TRANSPORT, transport);
- intent.putExtra(KEY_BODY, body);
+ intent.putExtra(KEY_BODY, body == null ? "" : body);
return intent;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendConstants.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendConstants.java
new file mode 100644
index 0000000000..411fd9c8bd
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendConstants.java
@@ -0,0 +1,6 @@
+package org.thoughtcrime.securesms.mediasend;
+
+public class MediaSendConstants {
+ public static final int MAX_PUSH = 32;
+ public static final int MAX_SMS = 1;
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
index e9630b4f77..9c1ec7585c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
@@ -51,9 +51,6 @@ class MediaSendViewModel extends ViewModel {
private static final String TAG = MediaSendViewModel.class.getSimpleName();
- private static final int MAX_PUSH = 32;
- private static final int MAX_SMS = 1;
-
private final Application application;
private final MediaRepository repository;
private final MediaUploadRepository uploadRepository;
@@ -122,11 +119,11 @@ class MediaSendViewModel extends ViewModel {
if (transport.isSms()) {
isSms = true;
- maxSelection = MAX_SMS;
+ maxSelection = MediaSendConstants.MAX_SMS;
mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
} else {
isSms = false;
- maxSelection = MAX_PUSH;
+ maxSelection = MediaSendConstants.MAX_PUSH;
mediaConstraints = MediaConstraints.getPushMediaConstraints();
}
@@ -151,7 +148,9 @@ class MediaSendViewModel extends ViewModel {
if (filteredMedia.size() != newMedia.size()) {
error.setValue(Error.ITEM_TOO_LARGE);
- } else if (filteredMedia.size() > maxSelection) {
+ }
+
+ if (filteredMedia.size() > maxSelection) {
filteredMedia = filteredMedia.subList(0, maxSelection);
error.setValue(Error.TOO_MANY_ITEMS);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java b/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java
index 054351d4d9..5d92537886 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java
@@ -9,7 +9,6 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
-import android.os.Parcel;
import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService;
import androidx.annotation.NonNull;
@@ -17,7 +16,7 @@ import androidx.annotation.RequiresApi;
import androidx.appcompat.view.ContextThemeWrapper;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.ShareActivity;
+import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java
similarity index 55%
rename from app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java
rename to app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java
index 7e11ba2d59..d9230d587d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java
@@ -15,26 +15,28 @@
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms;
+package org.thoughtcrime.securesms.sharing;
-import android.annotation.SuppressLint;
-import android.content.Context;
import android.content.Intent;
-import android.database.Cursor;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.Process;
-import android.provider.OpenableColumns;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.ViewModelProviders;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
+import android.widget.Toast;
+import org.thoughtcrime.securesms.ContactSelectionListFragment;
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
@@ -42,31 +44,28 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
-import org.thoughtcrime.securesms.mms.PartAuthority;
-import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
-import org.thoughtcrime.securesms.util.FileUtils;
-import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
/**
- * An activity to quickly share content with contacts
+ * Entry point for sharing content into the app.
*
- * @author Jake McGinty
+ * Handles contact selection when necessary, but also serves as an entry point for when the contact
+ * is known (such as choosing someone in a direct share).
*/
public class ShareActivity extends PassphraseRequiredActionBarActivity
implements ContactSelectionListFragment.OnContactSelectedListener, SwipeRefreshLayout.OnRefreshListener
@@ -83,10 +82,8 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
private ContactSelectionListFragment contactsFragment;
private SearchToolbar searchToolbar;
private ImageView searchAction;
- private View progressWheel;
- private Uri resolvedExtra;
- private String mimeType;
- private boolean isPassingAlongMedia;
+
+ private ShareViewModel viewModel;
@Override
protected void onPreCreate() {
@@ -114,15 +111,10 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
initializeToolbar();
initializeResources();
initializeSearch();
+ initializeViewModel();
initializeMedia();
- }
- @Override
- protected void onNewIntent(Intent intent) {
- Log.i(TAG, "onNewIntent()");
- super.onNewIntent(intent);
- setIntent(intent);
- initializeMedia();
+ handleDestination();
}
@Override
@@ -134,25 +126,22 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
}
@Override
- public void onPause() {
- super.onPause();
- if (!isPassingAlongMedia && resolvedExtra != null) {
- BlobProvider.getInstance().delete(this, resolvedExtra);
+ public void onStop() {
+ super.onStop();
- if (!isFinishing()) {
- finish();
- }
+ if (!isFinishing()) {
+ finish();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- onBackPressed();
- return true;
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
}
- return super.onOptionsItemSelected(item);
}
@Override
@@ -161,6 +150,30 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
else super.onBackPressed();
}
+ @Override
+ public void onContactSelected(Optional recipientId, String number) {
+ SimpleTask.run(this.getLifecycle(), () -> {
+ Recipient recipient;
+ if (recipientId.isPresent()) {
+ recipient = Recipient.resolved(recipientId.get());
+ } else {
+ Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
+ recipient = Recipient.external(this, number);
+ }
+
+ long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
+ return new Pair<>(existingThread, recipient);
+ }, result -> onDestinationChosen(result.first(), result.second().getId()));
+ }
+
+ @Override
+ public void onContactDeselected(@NonNull Optional recipientId, String number) {
+ }
+
+ @Override
+ public void onRefresh() {
+ }
+
private void initializeToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -173,15 +186,24 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
}
private void initializeResources() {
- progressWheel = findViewById(R.id.progress_wheel);
searchToolbar = findViewById(R.id.search_toolbar);
searchAction = findViewById(R.id.search_action);
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
+
+ if (contactsFragment == null) {
+ throw new IllegalStateException("Could not find contacts fragment!");
+ }
+
contactsFragment.setOnContactSelectedListener(this);
contactsFragment.setOnRefreshListener(this);
}
+ private void initializeViewModel() {
+ this.viewModel = ViewModelProviders.of(this, new ShareViewModel.Factory()).get(ShareViewModel.class);
+ }
+
private void initializeSearch() {
+ //noinspection IntegerDivisionInFloatingPointContext
searchAction.setOnClickListener(v -> searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
searchAction.getY() + (searchAction.getHeight() / 2)));
@@ -203,24 +225,25 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
}
private void initializeMedia() {
- final Context context = this;
- isPassingAlongMedia = false;
+ if (Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction())) {
+ Log.i(TAG, "Multiple media share.");
+ List uris = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- Uri streamExtra = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
- mimeType = getMimeType(streamExtra);
+ viewModel.onMultipleMediaShared(uris);
+ } else if (Intent.ACTION_SEND.equals(getIntent().getAction()) || getIntent().hasExtra(Intent.EXTRA_STREAM)) {
+ Log.i(TAG, "Single media share.");
+ Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+ String type = getIntent().getType();
- if (streamExtra != null && PartAuthority.isLocalUri(streamExtra)) {
- isPassingAlongMedia = true;
- resolvedExtra = streamExtra;
- handleResolvedMedia(getIntent(), false);
+ viewModel.onSingleMediaShared(uri, type);
} else {
- contactsFragment.getView().setVisibility(View.GONE);
- progressWheel.setVisibility(View.VISIBLE);
- new ResolveMediaTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, streamExtra);
+ Log.i(TAG, "Internal media share.");
+ viewModel.onNonExternalShare();
}
}
- private void handleResolvedMedia(Intent intent, boolean animate) {
+ private void handleDestination() {
+ Intent intent = getIntent();
long threadId = intent.getLongExtra(EXTRA_THREAD_ID, -1);
int distributionType = intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, -1);
RecipientId recipientId = null;
@@ -229,151 +252,75 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
recipientId = RecipientId.from(intent.getStringExtra(EXTRA_RECIPIENT_ID));
}
- boolean hasResolvedDestination = threadId != -1 && recipientId != null && distributionType != -1;
+ boolean hasPreexistingDestination = threadId != -1 && recipientId != null && distributionType != -1;
- if (hasResolvedDestination) {
- createConversation(threadId, recipientId, distributionType);
- } else if (animate) {
- ViewUtil.fadeIn(contactsFragment.requireView(), 300);
- ViewUtil.fadeOut(progressWheel, 300);
- } else {
- contactsFragment.requireView().setVisibility(View.VISIBLE);
- progressWheel.setVisibility(View.GONE);
+ if (hasPreexistingDestination) {
+ if (contactsFragment.getView() != null) {
+ contactsFragment.getView().setVisibility(View.GONE);
+ }
+ onDestinationChosen(threadId, recipientId);
}
}
- private void createConversation(long threadId, @NonNull RecipientId recipientId, int distributionType) {
- final Intent intent = getBaseShareIntent(ConversationActivity.class);
- intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
- intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
- intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
+ private void onDestinationChosen(long threadId, @NonNull RecipientId recipientId) {
+ if (!viewModel.isExternalShare()) {
+ openConversation(threadId, recipientId, null);
+ return;
+ }
- isPassingAlongMedia = true;
- startActivity(intent);
+ AtomicReference progressWheel = new AtomicReference<>();
+
+ if (viewModel.getShareData().getValue() == null) {
+ progressWheel.set(SimpleProgressDialog.show(this));
+ }
+
+ viewModel.getShareData().observe(this, (data) -> {
+ if (data == null) return;
+
+ if (progressWheel.get() != null) {
+ progressWheel.get().dismiss();
+ progressWheel.set(null);
+ }
+
+ if (!data.isPresent()) {
+ Log.w(TAG, "No data to share!");
+ Toast.makeText(this, R.string.ShareActivity_multiple_attachments_are_only_supported, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ openConversation(threadId, recipientId, data.get());
+ });
}
- private Intent getBaseShareIntent(final @NonNull Class> target) {
- final Intent intent = new Intent(this, target);
- final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
- final ArrayList mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
- final StickerLocator stickerExtra = getIntent().getParcelableExtra(ConversationActivity.STICKER_EXTRA);
+ private void openConversation(long threadId, @NonNull RecipientId recipientId, @Nullable ShareData shareData) {
+ Intent intent = new Intent(this, ConversationActivity.class);
+ String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+ ArrayList mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
+ StickerLocator stickerExtra = getIntent().getParcelableExtra(ConversationActivity.STICKER_EXTRA);
intent.putExtra(ConversationActivity.TEXT_EXTRA, textExtra);
intent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaExtra);
intent.putExtra(ConversationActivity.STICKER_EXTRA, stickerExtra);
- if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType);
-
- return intent;
- }
-
- private String getMimeType(@Nullable Uri uri) {
- if (uri != null) {
- final String mimeType = MediaUtil.getMimeType(getApplicationContext(), uri);
- if (mimeType != null) return mimeType;
- }
- return MediaUtil.getCorrectedMimeType(getIntent().getType());
- }
-
- @Override
- public void onContactSelected(Optional recipientId, String number) {
- SimpleTask.run(this.getLifecycle(), () -> {
- Recipient recipient;
- if (recipientId.isPresent()) {
- recipient = Recipient.resolved(recipientId.get());
- } else {
- Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
- recipient = Recipient.external(this, number);
- }
-
- long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
- return new Pair<>(existingThread, recipient);
- }, result -> {
- createConversation(result.first(), result.second().getId(), ThreadDatabase.DistributionTypes.DEFAULT);
- });
- }
-
- @Override
- public void onContactDeselected(@NonNull Optional recipientId, String number) {
-
- }
-
- @Override
- public void onRefresh() {
-
- }
-
- @SuppressLint("StaticFieldLeak")
- private class ResolveMediaTask extends AsyncTask {
- private final Context context;
-
- ResolveMediaTask(Context context) {
- this.context = context;
+ if (shareData != null && shareData.isForIntent()) {
+ Log.i(TAG, "Shared data is a single file.");
+ intent.setDataAndType(shareData.getUri(), shareData.getMimeType());
+ } else if (shareData != null && shareData.isForMedia()) {
+ Log.i(TAG, "Shared data is set of media.");
+ intent.putExtra(ConversationActivity.MEDIA_EXTRA, shareData.getMedia());
+ } else if (shareData != null && shareData.isForPrimitive()) {
+ Log.i(TAG, "Shared data is a primitive type.");
+ } else {
+ Log.i(TAG, "Shared data was not external.");
}
- @Override
- protected Uri doInBackground(Uri... uris) {
- try {
- if (uris.length != 1 || uris[0] == null) {
- return null;
- }
+ intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
+ intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
+ intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
- InputStream inputStream;
+ viewModel.onSuccessulShare();
- if ("file".equals(uris[0].getScheme())) {
- inputStream = openFileUri(uris[0]);
- } else {
- inputStream = context.getContentResolver().openInputStream(uris[0]);
- }
-
- if (inputStream == null) {
- return null;
- }
-
- Cursor cursor = getContentResolver().query(uris[0], new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null);
- String fileName = null;
- Long fileSize = null;
-
- try {
- if (cursor != null && cursor.moveToFirst()) {
- try {
- fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
- fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
- } catch (IllegalArgumentException e) {
- Log.w(TAG, e);
- }
- }
- } finally {
- if (cursor != null) cursor.close();
- }
-
- return BlobProvider.getInstance()
- .forData(inputStream, fileSize == null ? 0 : fileSize)
- .withMimeType(mimeType)
- .withFileName(fileName)
- .createForMultipleSessionsOnDisk(context);
- } catch (IOException ioe) {
- Log.w(TAG, ioe);
- return null;
- }
- }
-
- @Override
- protected void onPostExecute(Uri uri) {
- resolvedExtra = uri;
- handleResolvedMedia(getIntent(), true);
- }
-
- private InputStream openFileUri(Uri uri) throws IOException {
- FileInputStream fin = new FileInputStream(uri.getPath());
- int owner = FileUtils.getFileDescriptorOwner(fin.getFD());
-
- if (owner == -1 || owner == Process.myUid()) {
- fin.close();
- throw new IOException("File owned by application");
- }
-
- return fin;
- }
+ startActivity(intent);
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareData.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareData.java
new file mode 100644
index 0000000000..483896c1ab
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareData.java
@@ -0,0 +1,66 @@
+package org.thoughtcrime.securesms.sharing;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.mediasend.Media;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class ShareData {
+
+ private final Optional uri;
+ private final Optional mimeType;
+ private final Optional> media;
+ private final boolean external;
+
+ static ShareData forIntentData(@NonNull Uri uri, @NonNull String mimeType, boolean external) {
+ return new ShareData(Optional.of(uri), Optional.of(mimeType), Optional.absent(), external);
+ }
+
+ static ShareData forPrimitiveTypes() {
+ return new ShareData(Optional.absent(), Optional.absent(), Optional.absent(), true);
+ }
+
+ static ShareData forMedia(@NonNull List media) {
+ return new ShareData(Optional.absent(), Optional.absent(), Optional.of(new ArrayList<>(media)), true);
+ }
+
+ private ShareData(Optional uri, Optional mimeType, Optional> media, boolean external) {
+ this.uri = uri;
+ this.mimeType = mimeType;
+ this.media = media;
+ this.external = external;
+ }
+
+ boolean isForIntent() {
+ return uri.isPresent();
+ }
+
+ boolean isForPrimitive() {
+ return !uri.isPresent() && !media.isPresent();
+ }
+
+ boolean isForMedia() {
+ return media.isPresent();
+ }
+
+ public @NonNull Uri getUri() {
+ return uri.get();
+ }
+
+ public @NonNull String getMimeType() {
+ return mimeType.get();
+ }
+
+ public @NonNull ArrayList getMedia() {
+ return media.get();
+ }
+
+ public boolean isExternal() {
+ return external;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareRepository.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareRepository.java
new file mode 100644
index 0000000000..053d0a8e07
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareRepository.java
@@ -0,0 +1,209 @@
+package org.thoughtcrime.securesms.sharing;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.annimon.stream.Collectors;
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mediasend.Media;
+import org.thoughtcrime.securesms.mediasend.MediaSendConstants;
+import org.thoughtcrime.securesms.mms.PartAuthority;
+import org.thoughtcrime.securesms.providers.BlobProvider;
+import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+class ShareRepository {
+
+ private static final String TAG = Log.tag(ShareRepository.class);
+
+ /**
+ * Handles a single URI that may be local or external.
+ */
+ void getResolved(@NonNull Uri uri, @Nullable String mimeType, @NonNull Callback> callback) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ try {
+ callback.onResult(Optional.of(getResolvedInternal(uri, mimeType)));
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to resolve!", e);
+ callback.onResult(Optional.absent());
+ }
+ });
+ }
+
+ /**
+ * Handles multiple URIs that are all assumed to be external images/videos.
+ */
+ void getResolved(@NonNull List uris, @NonNull Callback> callback) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ try {
+ callback.onResult(Optional.fromNullable(getResolvedInternal(uris)));
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to resolve!", e);
+ callback.onResult(Optional.absent());
+ }
+ });
+ }
+
+
+
+ @WorkerThread
+ private @NonNull ShareData getResolvedInternal(@Nullable Uri uri, @Nullable String mimeType) throws IOException {
+ Context context = ApplicationDependencies.getApplication();
+
+ if (uri == null) {
+ return ShareData.forPrimitiveTypes();
+ }
+
+ if (mimeType == null) {
+ mimeType = context.getContentResolver().getType(uri);
+ }
+
+ if (PartAuthority.isLocalUri(uri) && mimeType != null) {
+ return ShareData.forIntentData(uri, mimeType, false);
+ } else {
+ InputStream stream = context.getContentResolver().openInputStream(uri);
+
+ if (stream == null) {
+ throw new IOException("Failed to open stream!");
+ }
+
+ long size = getSize(context, uri);
+ String fileName = getFileName(context, uri);
+ String fillMimeType = Optional.fromNullable(mimeType).or(MediaUtil.UNKNOWN);
+
+ Uri blobUri;
+
+ if (MediaUtil.isImageType(fillMimeType) || MediaUtil.isVideoType(fillMimeType)) {
+ blobUri = BlobProvider.getInstance()
+ .forData(stream, size)
+ .withMimeType(fillMimeType)
+ .withFileName(fileName)
+ .createForSingleSessionOnDisk(context);
+ } else {
+ blobUri = BlobProvider.getInstance()
+ .forData(stream, size)
+ .withMimeType(fillMimeType)
+ .withFileName(fileName)
+ .createForMultipleSessionsOnDisk(context);
+ }
+
+ return ShareData.forIntentData(blobUri, fillMimeType, true);
+ }
+ }
+
+ @WorkerThread
+ private @Nullable
+ ShareData getResolvedInternal(@NonNull List uris) throws IOException {
+ Context context = ApplicationDependencies.getApplication();
+ ContentResolver resolver = context.getContentResolver();
+
+ Map mimeTypes = Stream.of(uris)
+ .map(uri -> new Pair<>(uri, Optional.fromNullable(resolver.getType(uri)).or(MediaUtil.UNKNOWN)))
+ .filter(p -> MediaUtil.isImageType(p.second) || MediaUtil.isVideoType(p.second))
+ .collect(Collectors.toMap(p -> p.first, p -> p.second));
+
+ if (mimeTypes.isEmpty()) {
+ return null;
+ }
+
+ List media = new ArrayList<>(mimeTypes.size());
+
+ for (Map.Entry entry : mimeTypes.entrySet()) {
+ Uri uri = entry.getKey();
+ String mimeType = entry.getValue();
+
+ InputStream stream;
+ try {
+ stream = context.getContentResolver().openInputStream(uri);
+ if (stream == null) {
+ throw new IOException("Failed to open stream!");
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to open: " + uri);
+ continue;
+ }
+
+ long size = getSize(context, uri);
+ Pair dimens = MediaUtil.getDimensions(context, mimeType, uri);
+ long duration = getDuration(context, uri);
+ Uri blobUri = BlobProvider.getInstance()
+ .forData(stream, size)
+ .withMimeType(mimeType)
+ .createForSingleSessionOnDisk(context);
+
+ media.add(new Media(blobUri,
+ mimeType,
+ System.currentTimeMillis(),
+ dimens.first,
+ dimens.second,
+ size,
+ duration,
+ Optional.of(Media.ALL_MEDIA_BUCKET_ID),
+ Optional.absent(),
+ Optional.absent()));
+
+ if (media.size() >= MediaSendConstants.MAX_PUSH) {
+ Log.w(TAG, "Exceeded the attachment limit! Skipping the rest.");
+ break;
+ }
+ }
+
+ if (media.size() > 0) {
+ return ShareData.forMedia(media);
+ } else {
+ return null;
+ }
+ }
+
+ private static long getSize(@NonNull Context context, @NonNull Uri uri) throws IOException {
+ long size = 0;
+
+ try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) {
+ size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
+ }
+ }
+
+ if (size <= 0) {
+ size = MediaUtil.getMediaSize(context, uri);
+ }
+
+ return size;
+ }
+
+ private static @NonNull String getFileName(@NonNull Context context, @NonNull Uri uri) {
+ try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) >= 0) {
+ return cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
+ }
+ }
+
+ return "";
+ }
+
+ private static long getDuration(@NonNull Context context, @NonNull Uri uri) {
+ return 0;
+ }
+
+ interface Callback {
+ void onResult(@NonNull E result);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareViewModel.java
new file mode 100644
index 0000000000..7aa4d9e960
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareViewModel.java
@@ -0,0 +1,80 @@
+package org.thoughtcrime.securesms.sharing;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.providers.BlobProvider;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.List;
+
+public class ShareViewModel extends ViewModel {
+
+ private static final String TAG = Log.tag(ShareViewModel.class);
+
+ private final Context context;
+ private final ShareRepository shareRepository;
+ private final MutableLiveData> shareData;
+
+ private boolean mediaUsed;
+ private boolean externalShare;
+
+ private ShareViewModel() {
+ this.context = ApplicationDependencies.getApplication();
+ this.shareRepository = new ShareRepository();
+ this.shareData = new MutableLiveData<>();
+ }
+
+ void onSingleMediaShared(@NonNull Uri uri, @Nullable String mimeType) {
+ externalShare = true;
+ shareRepository.getResolved(uri, mimeType, shareData::postValue);
+ }
+
+ void onMultipleMediaShared(@NonNull List uris) {
+ externalShare = true;
+ shareRepository.getResolved(uris, shareData::postValue);
+ }
+
+ void onNonExternalShare() {
+ externalShare = false;
+ }
+
+ void onSuccessulShare() {
+ mediaUsed = true;
+ }
+
+ @NonNull LiveData> getShareData() {
+ return shareData;
+ }
+
+ boolean isExternalShare() {
+ return externalShare;
+ }
+
+ @Override
+ protected void onCleared() {
+ ShareData data = shareData.getValue() != null ? shareData.getValue().orNull() : null;
+
+ if (data != null && data.isExternal() && !mediaUsed) {
+ Log.i(TAG, "Clearing out unused data.");
+ BlobProvider.getInstance().delete(context, data.getUri());
+ }
+ }
+
+ public static class Factory extends ViewModelProvider.NewInstanceFactory {
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ //noinspection ConstantConditions
+ return modelClass.cast(new ShareViewModel());
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java
index df524556aa..30a8b3cbd4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.java
@@ -12,8 +12,7 @@ import android.view.MenuItem;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.ShareActivity;
-import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.DynamicTheme;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java
index bee54d7cfd..f7ee2a9b1d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java
@@ -5,8 +5,6 @@ import androidx.lifecycle.ViewModelProviders;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Point;
-import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
@@ -19,23 +17,15 @@ import android.widget.Toast;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
-import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.ShareActivity;
-import org.thoughtcrime.securesms.database.model.StickerRecord;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
+import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.stickers.StickerManifest.Sticker;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.thoughtcrime.securesms.util.ThemeUtil;
-import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java
index 3304025cba..49562ccf01 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java
@@ -58,6 +58,7 @@ public class MediaUtil {
public static final String VCARD = "text/x-vcard";
public static final String LONG_TEXT = "text/x-signal-plain";
public static final String VIEW_ONCE = "application/x-signal-view-once";
+ public static final String UNKNOWN = "*/*";
public static SlideType getSlideTypeFromContentType(@NonNull String contentType) {
if (isGif(contentType)) {
diff --git a/app/src/main/res/layout/share_activity.xml b/app/src/main/res/layout/share_activity.xml
index a0236224d3..8dad942c7a 100644
--- a/app/src/main/res/layout/share_activity.xml
+++ b/app/src/main/res/layout/share_activity.xml
@@ -56,11 +56,4 @@
android:visibility="invisible"
tools:visibility="invisible"/>
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 25ef4276e7..52155fbb6a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -371,6 +371,7 @@
Share with
+ Multiple attachments are only supported for images and videos
Welcome to Signal.