mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Add the ability to forward content to multiple chats at once.
This commit is contained in:
committed by
Greyson Parrelli
parent
eacf03768f
commit
8d187c8ba1
@@ -99,7 +99,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
|
||||
private void initializeResources() {
|
||||
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
contactsFragment.setOnRefreshListener(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,22 +110,26 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public static final String SELECTION_LIMITS = "selection_limits";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
public static final String HIDE_COUNT = "hide_count";
|
||||
public static final String CAN_SELECT_SELF = "can_select_self";
|
||||
public static final String DISPLAY_CHIPS = "display_chips";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private WarningTextView groupLimit;
|
||||
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private WarningTextView groupLimit;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@@ -136,6 +140,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
private boolean canSelectSelf;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -148,6 +153,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (context instanceof ScrollCallback) {
|
||||
scrollCallback = (ScrollCallback) context;
|
||||
}
|
||||
|
||||
if (context instanceof OnContactSelectedListener) {
|
||||
onContactSelectedListener = (OnContactSelectedListener) context;
|
||||
}
|
||||
|
||||
if (context instanceof OnSelectionLimitReachedListener) {
|
||||
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -217,6 +230,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
|
||||
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||
isMulti = selectionLimit != null;
|
||||
canSelectSelf = intent.getBooleanExtra(CAN_SELECT_SELF, !isMulti);
|
||||
|
||||
if (!isMulti) {
|
||||
selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
@@ -464,14 +478,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionHardLimitReached()) {
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext());
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
|
||||
} else {
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -489,11 +507,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (onContactSelectedListener != null) {
|
||||
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
@@ -507,16 +525,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (onContactSelectedListener != null) {
|
||||
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
markContactUnselected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||
@@ -611,7 +629,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
if (onSelectionLimitReachedListener != null) {
|
||||
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
|
||||
} else {
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,6 +655,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
private void setChipGroupVisibility(int visibility) {
|
||||
if (!requireActivity().getIntent().getBooleanExtra(DISPLAY_CHIPS, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
@@ -641,10 +667,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
constraintSet.applyTo(constraintLayout);
|
||||
}
|
||||
|
||||
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
||||
this.onContactSelectedListener = onContactSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener onRefreshListener) {
|
||||
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
|
||||
}
|
||||
@@ -660,6 +682,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
}
|
||||
|
||||
public interface OnSelectionLimitReachedListener {
|
||||
void onSuggestedLimitReached(int limit);
|
||||
void onHardLimitReached(int limit);
|
||||
}
|
||||
|
||||
public interface ListCallback {
|
||||
void onInvite();
|
||||
void onNewGroup(boolean forceV1);
|
||||
|
||||
@@ -107,7 +107,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
|
||||
@@ -124,8 +124,6 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
ContactSelectionListFragment fragment = new ContactSelectionListFragment();
|
||||
Intent intent = getIntent();
|
||||
|
||||
fragment.setOnContactSelectedListener(this);
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, 1);
|
||||
intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -17,6 +16,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class LabeledEditText extends FrameLayout implements View.OnFocusChangeListener {
|
||||
|
||||
@@ -94,16 +94,6 @@ public class LabeledEditText extends FrameLayout implements View.OnFocusChangeLi
|
||||
}
|
||||
|
||||
public void focusAndMoveCursorToEndAndOpenKeyboard() {
|
||||
input.requestFocus();
|
||||
|
||||
int numberLength = getText().length();
|
||||
input.setSelection(numberLength, numberLength);
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
|
||||
|
||||
if (!imm.isAcceptingText()) {
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
||||
}
|
||||
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
|
||||
/**
|
||||
* Selection aware {@link EmojiEditText}. This view allows the developer to provide an
|
||||
* {@link OnSelectionChangedListener} that will be notified when the selection is changed.
|
||||
*/
|
||||
public class SelectionAwareEmojiEditText extends EmojiEditText {
|
||||
|
||||
private OnSelectionChangedListener onSelectionChangedListener;
|
||||
|
||||
public SelectionAwareEmojiEditText(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public SelectionAwareEmojiEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SelectionAwareEmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setOnSelectionChangedListener(@Nullable OnSelectionChangedListener onSelectionChangedListener) {
|
||||
this.onSelectionChangedListener = onSelectionChangedListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
||||
if (onSelectionChangedListener != null) {
|
||||
onSelectionChangedListener.onSelectionChanged(selStart, selEnd);
|
||||
}
|
||||
super.onSelectionChanged(selStart, selEnd);
|
||||
}
|
||||
|
||||
public interface OnSelectionChangedListener {
|
||||
void onSelectionChanged(int selStart, int selEnd);
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,7 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory.MediaType;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
@@ -216,6 +216,7 @@ import org.thoughtcrime.securesms.mms.QuoteId;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
@@ -612,10 +613,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
switch (reqCode) {
|
||||
case PICK_DOCUMENT:
|
||||
setMedia(data.getData(), MediaType.DOCUMENT);
|
||||
setMedia(data.getData(), SlideFactory.MediaType.DOCUMENT);
|
||||
break;
|
||||
case PICK_AUDIO:
|
||||
setMedia(data.getData(), MediaType.AUDIO);
|
||||
setMedia(data.getData(), SlideFactory.MediaType.AUDIO);
|
||||
break;
|
||||
case PICK_CONTACT:
|
||||
if (isSecureText && !isSmsForced()) {
|
||||
@@ -655,7 +656,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
break;
|
||||
case PICK_GIF:
|
||||
setMedia(data.getData(),
|
||||
MediaType.GIF,
|
||||
SlideFactory.MediaType.GIF,
|
||||
data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0),
|
||||
data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0),
|
||||
data.getBooleanExtra(GiphyActivity.EXTRA_BORDERLESS, false));
|
||||
@@ -766,7 +767,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
getContentResolver().delete(attachmentManager.getCaptureUri(), null, null);
|
||||
|
||||
setMedia(mediaUri, MediaType.IMAGE);
|
||||
setMedia(mediaUri, SlideFactory.MediaType.IMAGE);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Could not handle public image", ioe);
|
||||
}
|
||||
@@ -1470,7 +1471,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
final CharSequence draftText = args.getDraftText();
|
||||
final Uri draftMedia = getIntent().getData();
|
||||
final String draftContentType = getIntent().getType();
|
||||
final MediaType draftMediaType = MediaType.from(draftContentType);
|
||||
final MediaType draftMediaType = SlideFactory.MediaType.from(draftContentType);
|
||||
final List<Media> mediaList = args.getMedia();
|
||||
final StickerLocator stickerLocator = args.getStickerLocator();
|
||||
final boolean borderless = args.isBorderless();
|
||||
@@ -1652,13 +1653,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener);
|
||||
break;
|
||||
case Draft.IMAGE:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener);
|
||||
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.IMAGE).addListener(listener);
|
||||
break;
|
||||
case Draft.AUDIO:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener);
|
||||
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.AUDIO).addListener(listener);
|
||||
break;
|
||||
case Draft.VIDEO:
|
||||
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener);
|
||||
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.VIDEO).addListener(listener);
|
||||
break;
|
||||
case Draft.QUOTE:
|
||||
SettableFuture<Boolean> quoteResult = new SettableFuture<>();
|
||||
@@ -2335,10 +2336,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
if (MediaType.VCARD.equals(mediaType) && isSecureText) {
|
||||
if (SlideFactory.MediaType.VCARD.equals(mediaType) && isSecureText) {
|
||||
openContactShareEditor(uri);
|
||||
return new SettableFuture<>(false);
|
||||
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
|
||||
} else if (SlideFactory.MediaType.IMAGE.equals(mediaType) || SlideFactory.MediaType.GIF.equals(mediaType) || SlideFactory.MediaType.VIDEO.equals(mediaType)) {
|
||||
String mimeType = MediaUtil.getMimeType(this, uri);
|
||||
if (mimeType == null) {
|
||||
mimeType = mediaType.toFallbackMimeType();
|
||||
@@ -3033,9 +3034,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
() -> getKeyboardImageDetails(uri),
|
||||
details -> sendKeyboardImage(uri, contentType, details));
|
||||
} else if (MediaUtil.isVideoType(contentType)) {
|
||||
setMedia(uri, MediaType.VIDEO);
|
||||
setMedia(uri, SlideFactory.MediaType.VIDEO);
|
||||
} else if (MediaUtil.isAudioType(contentType)) {
|
||||
setMedia(uri, MediaType.AUDIO);
|
||||
setMedia(uri, SlideFactory.MediaType.AUDIO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3536,7 +3537,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
private void sendKeyboardImage(@NonNull Uri uri, @NonNull String contentType, @Nullable KeyboardImageDetails details) {
|
||||
if (details == null || !details.hasTransparency) {
|
||||
setMedia(uri, Objects.requireNonNull(MediaType.from(contentType)));
|
||||
setMedia(uri, Objects.requireNonNull(SlideFactory.MediaType.from(contentType)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -962,6 +963,19 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<RecipientId, Long> getThreadIdsIfExistsFor(@NonNull RecipientId ... recipientIds) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
SqlUtil.Query query = SqlUtil.buildCollectionQuery(RECIPIENT_ID, Arrays.asList(recipientIds));
|
||||
|
||||
Map<RecipientId, Long> results = new HashMap<>();
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ID, RECIPIENT_ID }, query.getWhere(), query.getWhereArgs(), null, null, null, "1")) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.put(RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)), CursorUtil.requireLong(cursor, ID));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public long getOrCreateValidThreadId(@NonNull Recipient recipient, long candidateId) {
|
||||
return getOrCreateValidThreadId(recipient, candidateId, DistributionTypes.DEFAULT);
|
||||
}
|
||||
|
||||
@@ -111,11 +111,12 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
|
||||
public static final String EXTRA_RESULT = "result";
|
||||
|
||||
private static final String KEY_RECIPIENT = "recipient_id";
|
||||
private static final String KEY_BODY = "body";
|
||||
private static final String KEY_MEDIA = "media";
|
||||
private static final String KEY_TRANSPORT = "transport";
|
||||
private static final String KEY_IS_CAMERA = "is_camera";
|
||||
private static final String KEY_RECIPIENT = "recipient_id";
|
||||
private static final String KEY_RECIPIENTS = "recipient_ids";
|
||||
private static final String KEY_BODY = "body";
|
||||
private static final String KEY_MEDIA = "media";
|
||||
private static final String KEY_TRANSPORT = "transport";
|
||||
private static final String KEY_IS_CAMERA = "is_camera";
|
||||
|
||||
private static final String TAG_FOLDER_PICKER = "folder_picker";
|
||||
private static final String TAG_ITEM_PICKER = "item_picker";
|
||||
@@ -195,6 +196,20 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent buildShareIntent(@NonNull Context context,
|
||||
@NonNull List<Media> media,
|
||||
@NonNull List<RecipientId> recipientIds,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull TransportOption transportOption)
|
||||
{
|
||||
Intent intent = new Intent(context, MediaSendActivity.class);
|
||||
intent.putParcelableArrayListExtra(KEY_MEDIA, new ArrayList<>(media));
|
||||
intent.putExtra(KEY_TRANSPORT, transportOption);
|
||||
intent.putExtra(KEY_BODY, body == null ? "" : body);
|
||||
intent.putParcelableArrayListExtra(KEY_RECIPIENTS, new ArrayList<>(recipientIds));
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
@@ -332,7 +347,18 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
initViewModel();
|
||||
|
||||
revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled());
|
||||
continueButton.setOnClickListener(v -> navigateToContactSelect());
|
||||
|
||||
List<RecipientId> recipientIds = getIntent().getParcelableArrayListExtra(KEY_RECIPIENTS);
|
||||
continueButton.setOnClickListener(v -> {
|
||||
continueButton.setEnabled(false);
|
||||
if (recipientIds == null || recipientIds.isEmpty()) {
|
||||
navigateToContactSelect();
|
||||
} else {
|
||||
SimpleTask.run(getLifecycle(),
|
||||
() -> Stream.of(recipientIds).map(Recipient::resolved).toList(),
|
||||
this::onCameraContactsSendClicked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -551,7 +577,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
||||
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this, 300, 0);
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> {
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
setActivityResultAndFinish(result);
|
||||
});
|
||||
} else {
|
||||
throw new AssertionError("No editor fragment available!");
|
||||
|
||||
@@ -29,7 +29,6 @@ import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
@@ -44,7 +43,6 @@ import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
|
||||
@@ -223,7 +221,7 @@ public class AttachmentManager {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests,
|
||||
@NonNull final Uri uri,
|
||||
@NonNull final MediaType mediaType,
|
||||
@NonNull final SlideFactory.MediaType mediaType,
|
||||
@NonNull final MediaConstraints constraints,
|
||||
final int width,
|
||||
final int height)
|
||||
@@ -286,7 +284,7 @@ public class AttachmentManager {
|
||||
} else {
|
||||
Attachment attachment = slide.asAttachment();
|
||||
result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()));
|
||||
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
|
||||
removableMediaView.display(thumbnail, mediaType == SlideFactory.MediaType.IMAGE);
|
||||
}
|
||||
|
||||
attachmentListener.onAttachmentChanged();
|
||||
@@ -480,58 +478,4 @@ public class AttachmentManager {
|
||||
void onAttachmentChanged();
|
||||
}
|
||||
|
||||
public enum MediaType {
|
||||
IMAGE(MediaUtil.IMAGE_JPEG),
|
||||
GIF(MediaUtil.IMAGE_GIF),
|
||||
AUDIO(MediaUtil.AUDIO_AAC),
|
||||
VIDEO(MediaUtil.VIDEO_MP4),
|
||||
DOCUMENT(MediaUtil.UNKNOWN),
|
||||
VCARD(MediaUtil.VCARD);
|
||||
|
||||
private final String fallbackMimeType;
|
||||
|
||||
MediaType(String fallbackMimeType) {
|
||||
this.fallbackMimeType = fallbackMimeType;
|
||||
}
|
||||
|
||||
|
||||
public @NonNull Slide createSlide(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@Nullable String fileName,
|
||||
@Nullable String mimeType,
|
||||
@Nullable BlurHash blurHash,
|
||||
long dataSize,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
if (mimeType == null) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
switch (this) {
|
||||
case IMAGE: return new ImageSlide(context, uri, dataSize, width, height, blurHash);
|
||||
case GIF: return new GifSlide(context, uri, dataSize, width, height);
|
||||
case AUDIO: return new AudioSlide(context, uri, dataSize, false);
|
||||
case VIDEO: return new VideoSlide(context, uri, dataSize);
|
||||
case VCARD:
|
||||
case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
|
||||
default: throw new AssertionError("unrecognized enum");
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable MediaType from(final @Nullable String mimeType) {
|
||||
if (TextUtils.isEmpty(mimeType)) return null;
|
||||
if (MediaUtil.isGif(mimeType)) return GIF;
|
||||
if (MediaUtil.isImageType(mimeType)) return IMAGE;
|
||||
if (MediaUtil.isAudioType(mimeType)) return AUDIO;
|
||||
if (MediaUtil.isVideoType(mimeType)) return VIDEO;
|
||||
if (MediaUtil.isVcard(mimeType)) return VCARD;
|
||||
|
||||
return DOCUMENT;
|
||||
}
|
||||
|
||||
public String toFallbackMimeType() {
|
||||
return fallbackMimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* SlideFactory encapsulates logic related to constructing slides from a set of paramaeters as defined
|
||||
* by {@link SlideFactory#getSlide}.
|
||||
*/
|
||||
public final class SlideFactory {
|
||||
|
||||
private static final String TAG = Log.tag(SlideFactory.class);
|
||||
|
||||
private SlideFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a slide from the given parameters.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param contentType The contentType of the given Uri
|
||||
* @param uri The Uri pointing to the resource to create a slide out of
|
||||
* @param width (Optional) width, can be 0.
|
||||
* @param height (Optional) height, can be 0.
|
||||
*
|
||||
* @return A Slide with all the information we can gather about it.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @Nullable Slide getSlide(@NonNull Context context, @Nullable String contentType, @NonNull Uri uri, int width, int height) {
|
||||
MediaType mediaType = MediaType.from(contentType);
|
||||
|
||||
try {
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height);
|
||||
} else {
|
||||
Slide result = getContentResolverSlideInfo(context, mediaType, uri, width, height);
|
||||
|
||||
if (result == null) return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height);
|
||||
else return result;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Slide getContentResolverSlideInfo(@NonNull Context context, @NonNull MediaType mediaType, @NonNull Uri uri, int width, int height) {
|
||||
Cursor cursor = null;
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
|
||||
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
|
||||
String mimeType = context.getContentResolver().getType(uri);
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
width = dimens.first;
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @NonNull Slide getManuallyCalculatedSlideInfo(@NonNull Context context, @NonNull MediaType mediaType, @NonNull Uri uri, int width, int height) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Long mediaSize = null;
|
||||
String fileName = null;
|
||||
String mimeType = null;
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
mediaSize = PartAuthority.getAttachmentSize(context, uri);
|
||||
fileName = PartAuthority.getAttachmentFileName(context, uri);
|
||||
mimeType = PartAuthority.getAttachmentContentType(context, uri);
|
||||
}
|
||||
|
||||
if (mediaSize == null) {
|
||||
mediaSize = MediaUtil.getMediaSize(context, uri);
|
||||
}
|
||||
|
||||
if (mimeType == null) {
|
||||
mimeType = MediaUtil.getMimeType(context, uri);
|
||||
}
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
width = dimens.first;
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height);
|
||||
}
|
||||
|
||||
public enum MediaType {
|
||||
|
||||
IMAGE(MediaUtil.IMAGE_JPEG),
|
||||
GIF(MediaUtil.IMAGE_GIF),
|
||||
AUDIO(MediaUtil.AUDIO_AAC),
|
||||
VIDEO(MediaUtil.VIDEO_MP4),
|
||||
DOCUMENT(MediaUtil.UNKNOWN),
|
||||
VCARD(MediaUtil.VCARD);
|
||||
|
||||
private final String fallbackMimeType;
|
||||
|
||||
MediaType(String fallbackMimeType) {
|
||||
this.fallbackMimeType = fallbackMimeType;
|
||||
}
|
||||
|
||||
|
||||
public @NonNull Slide createSlide(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@Nullable String fileName,
|
||||
@Nullable String mimeType,
|
||||
@Nullable BlurHash blurHash,
|
||||
long dataSize,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
if (mimeType == null) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
switch (this) {
|
||||
case IMAGE: return new ImageSlide(context, uri, dataSize, width, height, blurHash);
|
||||
case GIF: return new GifSlide(context, uri, dataSize, width, height);
|
||||
case AUDIO: return new AudioSlide(context, uri, dataSize, false);
|
||||
case VIDEO: return new VideoSlide(context, uri, dataSize);
|
||||
case VCARD:
|
||||
case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
|
||||
default: throw new AssertionError("unrecognized enum");
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable MediaType from(final @Nullable String mimeType) {
|
||||
if (TextUtils.isEmpty(mimeType)) return null;
|
||||
if (MediaUtil.isGif(mimeType)) return GIF;
|
||||
if (MediaUtil.isImageType(mimeType)) return IMAGE;
|
||||
if (MediaUtil.isAudioType(mimeType)) return AUDIO;
|
||||
if (MediaUtil.isVideoType(mimeType)) return VIDEO;
|
||||
if (MediaUtil.isVcard(mimeType)) return VCARD;
|
||||
|
||||
return DOCUMENT;
|
||||
}
|
||||
|
||||
public String toFallbackMimeType() {
|
||||
return fallbackMimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -499,6 +499,15 @@ public class Recipient {
|
||||
return StringUtil.isolateBidi(name);
|
||||
}
|
||||
|
||||
public @NonNull String getShortDisplayNameIncludingUsername(@NonNull Context context) {
|
||||
String name = Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().getGivenName(),
|
||||
getDisplayName(context),
|
||||
getUsername().orNull());
|
||||
|
||||
return StringUtil.isolateBidi(name);
|
||||
}
|
||||
|
||||
public @NonNull MaterialColor getColor() {
|
||||
if (isGroupInternal()) {
|
||||
return MaterialColor.GROUP;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
public enum InterstitialContentType {
|
||||
MEDIA,
|
||||
TEXT,
|
||||
NONE
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class MultiShareArgs implements Parcelable {
|
||||
|
||||
private static final String ARGS = "ShareInterstitialArgs";
|
||||
|
||||
private final Set<ShareContactAndThread> shareContactAndThreads;
|
||||
private final ArrayList<Media> media;
|
||||
private final String draftText;
|
||||
private final StickerLocator stickerLocator;
|
||||
private final boolean borderless;
|
||||
private final Uri dataUri;
|
||||
private final String dataType;
|
||||
private final boolean viewOnce;
|
||||
private final LinkPreview linkPreview;
|
||||
|
||||
private MultiShareArgs(@NonNull Builder builder) {
|
||||
shareContactAndThreads = builder.shareContactAndThreads;
|
||||
media = builder.media == null ? new ArrayList<>() : builder.media;
|
||||
draftText = builder.draftText;
|
||||
stickerLocator = builder.stickerLocator;
|
||||
borderless = builder.borderless;
|
||||
dataUri = builder.dataUri;
|
||||
dataType = builder.dataType;
|
||||
viewOnce = builder.viewOnce;
|
||||
linkPreview = builder.linkPreview;
|
||||
}
|
||||
|
||||
protected MultiShareArgs(Parcel in) {
|
||||
shareContactAndThreads = new HashSet<>(in.createTypedArrayList(ShareContactAndThread.CREATOR));
|
||||
media = in.createTypedArrayList(Media.CREATOR);
|
||||
draftText = in.readString();
|
||||
stickerLocator = in.readParcelable(StickerLocator.class.getClassLoader());
|
||||
borderless = in.readByte() != 0;
|
||||
dataUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
dataType = in.readString();
|
||||
viewOnce = in.readByte() != 0;
|
||||
|
||||
LinkPreview preview;
|
||||
try {
|
||||
preview = LinkPreview.deserialize(in.readString());
|
||||
} catch (IOException e) {
|
||||
preview = null;
|
||||
}
|
||||
|
||||
linkPreview = preview;
|
||||
}
|
||||
|
||||
public Set<ShareContactAndThread> getShareContactAndThreads() {
|
||||
return shareContactAndThreads;
|
||||
}
|
||||
|
||||
public ArrayList<Media> getMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
public StickerLocator getStickerLocator() {
|
||||
return stickerLocator;
|
||||
}
|
||||
|
||||
public String getDataType() {
|
||||
return dataType;
|
||||
}
|
||||
|
||||
public String getDraftText() {
|
||||
return draftText;
|
||||
}
|
||||
|
||||
public Uri getDataUri() {
|
||||
return dataUri;
|
||||
}
|
||||
|
||||
public boolean isBorderless() {
|
||||
return borderless;
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
return viewOnce;
|
||||
}
|
||||
|
||||
public @Nullable LinkPreview getLinkPreview() {
|
||||
return linkPreview;
|
||||
}
|
||||
|
||||
public @NonNull InterstitialContentType getInterstitialContentType() {
|
||||
if (!requiresInterstitial()) {
|
||||
return InterstitialContentType.NONE;
|
||||
} else if (!this.getMedia().isEmpty() ||
|
||||
(this.getDataUri() != null && this.getDataUri() != Uri.EMPTY && this.getDataType() != null))
|
||||
{
|
||||
return InterstitialContentType.MEDIA;
|
||||
} else if (!TextUtils.isEmpty(this.getDraftText())) {
|
||||
return InterstitialContentType.TEXT;
|
||||
} else {
|
||||
return InterstitialContentType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static final Creator<MultiShareArgs> CREATOR = new Creator<MultiShareArgs>() {
|
||||
@Override
|
||||
public MultiShareArgs createFromParcel(Parcel in) {
|
||||
return new MultiShareArgs(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiShareArgs[] newArray(int size) {
|
||||
return new MultiShareArgs[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeTypedList(Stream.of(shareContactAndThreads).toList());
|
||||
dest.writeTypedList(media);
|
||||
dest.writeString(draftText);
|
||||
dest.writeParcelable(stickerLocator, flags);
|
||||
dest.writeByte((byte) (borderless ? 1 : 0));
|
||||
dest.writeParcelable(dataUri, flags);
|
||||
dest.writeString(dataType);
|
||||
dest.writeByte((byte) (viewOnce ? 1 : 0));
|
||||
|
||||
if (linkPreview != null) {
|
||||
try {
|
||||
dest.writeString(linkPreview.serialize());
|
||||
} catch (IOException e) {
|
||||
dest.writeString("");
|
||||
}
|
||||
} else {
|
||||
dest.writeString("");
|
||||
}
|
||||
}
|
||||
|
||||
public Builder buildUpon() {
|
||||
return new Builder(shareContactAndThreads).asBorderless(borderless)
|
||||
.asViewOnce(viewOnce)
|
||||
.withDataType(dataType)
|
||||
.withDataUri(dataUri)
|
||||
.withDraftText(draftText)
|
||||
.withLinkPreview(linkPreview)
|
||||
.withMedia(media)
|
||||
.withStickerLocator(stickerLocator);
|
||||
}
|
||||
|
||||
private boolean requiresInterstitial() {
|
||||
return !media.isEmpty() || !TextUtils.isEmpty(draftText) || MediaUtil.isImageOrVideoType(dataType);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final Set<ShareContactAndThread> shareContactAndThreads;
|
||||
|
||||
private ArrayList<Media> media;
|
||||
private String draftText;
|
||||
private StickerLocator stickerLocator;
|
||||
private boolean borderless;
|
||||
private Uri dataUri;
|
||||
private String dataType;
|
||||
private LinkPreview linkPreview;
|
||||
private boolean viewOnce;
|
||||
|
||||
public Builder(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
this.shareContactAndThreads = shareContactAndThreads;
|
||||
}
|
||||
|
||||
public @NonNull Builder withMedia(@Nullable ArrayList<Media> media) {
|
||||
this.media = media;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withDraftText(@Nullable String draftText) {
|
||||
this.draftText = draftText;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withStickerLocator(@Nullable StickerLocator stickerLocator) {
|
||||
this.stickerLocator = stickerLocator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder asBorderless(boolean borderless) {
|
||||
this.borderless = borderless;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withDataUri(@Nullable Uri dataUri) {
|
||||
this.dataUri = dataUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withDataType(@Nullable String dataType) {
|
||||
this.dataType = dataType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withLinkPreview(@Nullable LinkPreview linkPreview) {
|
||||
this.linkPreview = linkPreview;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder asViewOnce(boolean viewOnce) {
|
||||
this.viewOnce = viewOnce;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull MultiShareArgs build() {
|
||||
return new MultiShareArgs(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class MultiShareDialogs {
|
||||
private MultiShareDialogs() {
|
||||
}
|
||||
|
||||
public static void displayResultDialog(@NonNull Context context,
|
||||
@NonNull MultiShareSender.MultiShareSendResultCollection resultCollection,
|
||||
@NonNull Runnable onDismiss)
|
||||
{
|
||||
if (resultCollection.containsFailures()) {
|
||||
displayFailuresDialog(context, onDismiss);
|
||||
} else {
|
||||
onDismiss.run();
|
||||
}
|
||||
}
|
||||
|
||||
public static void displayMaxSelectedDialog(@NonNull Context context, int hardLimit) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.MultiShareDialogs__you_can_only_share_with_up_to, hardLimit))
|
||||
.setPositiveButton(android.R.string.ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void displayFailuresDialog(@NonNull Context context,
|
||||
@NonNull Runnable onDismiss)
|
||||
{
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(R.string.MultiShareDialogs__failed_to_send_to_some_users)
|
||||
.setPositiveButton(android.R.string.ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.setOnDismissListener(dialog -> onDismiss.run())
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MultiShareSender encapsulates send logic (stolen from {@link org.thoughtcrime.securesms.conversation.ConversationActivity}
|
||||
* and provides a means to:
|
||||
*
|
||||
* 1. Send messages based off a {@link MultiShareArgs} object and
|
||||
* 1. Parse through the result of the send via a {@link MultiShareSendResultCollection}
|
||||
*/
|
||||
public final class MultiShareSender {
|
||||
|
||||
private static final String TAG = Log.tag(MultiShareSender.class);
|
||||
|
||||
private MultiShareSender() {
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public static void send(@NonNull MultiShareArgs multiShareArgs, @NonNull Consumer<MultiShareSendResultCollection> results) {
|
||||
SimpleTask.run(() -> sendInternal(multiShareArgs), results::accept);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static MultiShareSendResultCollection sendInternal(@NonNull MultiShareArgs multiShareArgs) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
boolean isMmsEnabled = Util.isMmsCapable(context);
|
||||
String message = multiShareArgs.getDraftText();
|
||||
SlideDeck slideDeck = buildSlideDeck(context, multiShareArgs);
|
||||
|
||||
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getShareContactAndThreads().size());
|
||||
|
||||
for (ShareContactAndThread shareContactAndThread : multiShareArgs.getShareContactAndThreads()) {
|
||||
Recipient recipient = Recipient.resolved(shareContactAndThread.getRecipientId());
|
||||
|
||||
TransportOption transport = resolveTransportOption(context, recipient);
|
||||
boolean forceSms = recipient.isForceSmsSelection() && transport.isSms();
|
||||
int subscriptionId = transport.getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
boolean needsSplit = !transport.isSms() && message.length() > transport.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
boolean isMediaMessage = !multiShareArgs.getMedia().isEmpty() ||
|
||||
(multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) ||
|
||||
multiShareArgs.getStickerLocator() != null ||
|
||||
multiShareArgs.getLinkPreview() != null ||
|
||||
recipient.isGroup() ||
|
||||
recipient.getEmail().isPresent() ||
|
||||
needsSplit;
|
||||
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
|
||||
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.MMS_NOT_ENABLED));
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(context, multiShareArgs, recipient, slideDeck, transport, shareContactAndThread.getThreadId(), forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId);
|
||||
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
|
||||
} else {
|
||||
sendTextMessage(context, multiShareArgs, recipient, shareContactAndThread.getThreadId() ,forceSms, expiresIn, subscriptionId);
|
||||
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
|
||||
}
|
||||
}
|
||||
|
||||
return new MultiShareSendResultCollection(results);
|
||||
}
|
||||
|
||||
public static @NonNull TransportOption getWorseTransportOption(@NonNull Context context, @NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
for (ShareContactAndThread shareContactAndThread : shareContactAndThreads) {
|
||||
TransportOption option = resolveTransportOption(context, shareContactAndThread.isForceSms());
|
||||
if (option.isSms()) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
return TransportOptions.getPushTransportOption(context);
|
||||
}
|
||||
|
||||
private static @NonNull TransportOption resolveTransportOption(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
return resolveTransportOption(context, recipient.isForceSmsSelection() || !recipient.isRegistered());
|
||||
}
|
||||
|
||||
public static @NonNull TransportOption resolveTransportOption(@NonNull Context context, boolean forceSms) {
|
||||
if (forceSms) {
|
||||
TransportOptions options = new TransportOptions(context, false);
|
||||
options.setDefaultTransport(TransportOption.Type.SMS);
|
||||
return options.getSelectedTransport();
|
||||
} else {
|
||||
return TransportOptions.getPushTransportOption(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendMediaMessage(@NonNull Context context,
|
||||
@NonNull MultiShareArgs multiShareArgs,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull SlideDeck slideDeck,
|
||||
@NonNull TransportOption transportOption,
|
||||
long threadId,
|
||||
boolean forceSms,
|
||||
long expiresIn,
|
||||
boolean isViewOnce,
|
||||
int subscriptionId)
|
||||
{
|
||||
String body = multiShareArgs.getDraftText();
|
||||
if (transportOption.isType(TransportOption.Type.TEXTSECURE) && !forceSms) {
|
||||
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body, transportOption.calculateCharacters(body).maxPrimaryMessageSize);
|
||||
body = splitMessage.getBody();
|
||||
|
||||
if (splitMessage.getTextSlide().isPresent()) {
|
||||
slideDeck.addSlide(splitMessage.getTextSlide().get());
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
||||
slideDeck,
|
||||
body,
|
||||
System.currentTimeMillis(),
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
isViewOnce,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
multiShareArgs.getLinkPreview() != null ? Collections.singletonList(multiShareArgs.getLinkPreview())
|
||||
: Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
MessageSender.send(context, outgoingMediaMessage, threadId, forceSms, null);
|
||||
}
|
||||
|
||||
private static void sendTextMessage(@NonNull Context context,
|
||||
@NonNull MultiShareArgs multiShareArgs,
|
||||
@NonNull Recipient recipient,
|
||||
long threadId,
|
||||
boolean forceSms,
|
||||
long expiresIn,
|
||||
int subscriptionId)
|
||||
{
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, multiShareArgs.getDraftText(), expiresIn, subscriptionId);
|
||||
|
||||
MessageSender.send(context, outgoingTextMessage, threadId, forceSms, null);
|
||||
}
|
||||
|
||||
private static @NonNull SlideDeck buildSlideDeck(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) {
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
if (multiShareArgs.getStickerLocator() != null) {
|
||||
slideDeck.addSlide(buildStickerSlide(context, multiShareArgs.getStickerLocator()));
|
||||
} else if (!multiShareArgs.getMedia().isEmpty()) {
|
||||
for (Media media : multiShareArgs.getMedia()) {
|
||||
slideDeck.addSlide(SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight()));
|
||||
}
|
||||
} else if (multiShareArgs.getDataUri() != null) {
|
||||
slideDeck.addSlide(SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0));
|
||||
}
|
||||
|
||||
return slideDeck;
|
||||
}
|
||||
|
||||
private static @NonNull StickerSlide buildStickerSlide(@NonNull Context context, @NonNull StickerLocator stickerLocator) {
|
||||
StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
||||
StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false);
|
||||
|
||||
return new StickerSlide(context, stickerRecord.getUri(), stickerRecord.getSize(), stickerLocator, stickerRecord.getContentType());
|
||||
}
|
||||
|
||||
public static final class MultiShareSendResultCollection {
|
||||
private final List<MultiShareSendResult> results;
|
||||
|
||||
private MultiShareSendResultCollection(List<MultiShareSendResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
public boolean containsFailures() {
|
||||
return Stream.of(results).anyMatch(result -> result.type != MultiShareSendResult.Type.SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MultiShareSendResult {
|
||||
private final ShareContactAndThread contactAndThread;
|
||||
private final Type type;
|
||||
|
||||
private MultiShareSendResult(ShareContactAndThread contactAndThread, Type type) {
|
||||
this.contactAndThread = contactAndThread;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ShareContactAndThread getContactAndThread() {
|
||||
return contactAndThread;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
MMS_NOT_ENABLED,
|
||||
SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,19 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
@@ -41,19 +49,25 @@ import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.interstitial.ShareInterstitialActivity;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
@@ -63,10 +77,14 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
* is known (such as choosing someone in a direct share).
|
||||
*/
|
||||
public class ShareActivity extends PassphraseRequiredActivity
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener, SwipeRefreshLayout.OnRefreshListener
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.OnSelectionLimitReachedListener
|
||||
{
|
||||
private static final String TAG = ShareActivity.class.getSimpleName();
|
||||
|
||||
private static final short RESULT_TEXT_CONFIRMATION = 1;
|
||||
private static final short RESULT_MEDIA_CONFIRMATION = 2;
|
||||
|
||||
public static final String EXTRA_THREAD_ID = "thread_id";
|
||||
public static final String EXTRA_RECIPIENT_ID = "recipient_id";
|
||||
public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
|
||||
@@ -74,9 +92,12 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private ConstraintLayout shareContainer;
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
private View shareConfirm;
|
||||
private ShareSelectionAdapter adapter;
|
||||
|
||||
private ShareViewModel viewModel;
|
||||
|
||||
@@ -88,31 +109,14 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(this)) {
|
||||
mode |= DisplayMode.FLAG_SMS;
|
||||
}
|
||||
|
||||
if (FeatureFlags.groupsV1ForcedMigration()) {
|
||||
mode |= DisplayMode.FLAG_HIDE_GROUPS_V1;
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, mode);
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.RECENTS, true);
|
||||
|
||||
setContentView(R.layout.share_activity);
|
||||
|
||||
initializeViewModel();
|
||||
initializeMedia();
|
||||
initializeIntent();
|
||||
initializeToolbar();
|
||||
initializeResources();
|
||||
initializeSearch();
|
||||
initializeViewModel();
|
||||
initializeMedia();
|
||||
|
||||
handleDestination();
|
||||
}
|
||||
|
||||
@@ -128,7 +132,7 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
|
||||
if (!isFinishing()) {
|
||||
if (!isFinishing() && !viewModel.isMultiShare()) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -149,31 +153,75 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
else super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
switch (requestCode) {
|
||||
case RESULT_MEDIA_CONFIRMATION:
|
||||
case RESULT_TEXT_CONFIRMATION:
|
||||
viewModel.onSuccessulShare();
|
||||
finish();
|
||||
break;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
} else {
|
||||
shareConfirm.setClickable(true);
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> 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.getId());
|
||||
return new Pair<>(existingThread, recipient);
|
||||
}, result -> onDestinationChosen(result.first(), result.second().getId()));
|
||||
|
||||
return true;
|
||||
return viewModel.onContactSelected(new ShareContact(recipientId, number));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
|
||||
viewModel.onContactDeselected(new ShareContact(recipientId, number));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
private void animateInSelection() {
|
||||
TransitionManager.endTransitions(shareContainer);
|
||||
TransitionManager.beginDelayedTransition(shareContainer);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(shareContainer);
|
||||
constraintSet.setVisibility(R.id.selection_group, ConstraintSet.VISIBLE);
|
||||
constraintSet.applyTo(shareContainer);
|
||||
}
|
||||
|
||||
private void animateOutSelection() {
|
||||
TransitionManager.endTransitions(shareContainer);
|
||||
TransitionManager.beginDelayedTransition(shareContainer);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(shareContainer);
|
||||
constraintSet.setVisibility(R.id.selection_group, ConstraintSet.GONE);
|
||||
constraintSet.applyTo(shareContainer);
|
||||
}
|
||||
|
||||
private void initializeIntent() {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(this) && viewModel.isExternalShare()) {
|
||||
mode |= DisplayMode.FLAG_SMS;
|
||||
}
|
||||
|
||||
if (FeatureFlags.groupsV1ForcedMigration()) {
|
||||
mode |= DisplayMode.FLAG_HIDE_GROUPS_V1;
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, mode);
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.RECENTS, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.shareSelectionLimit());
|
||||
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_CHIPS, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.CAN_SELECT_SELF, true);
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
@@ -190,14 +238,37 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
private void initializeResources() {
|
||||
searchToolbar = findViewById(R.id.search_toolbar);
|
||||
searchAction = findViewById(R.id.search_action);
|
||||
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
shareConfirm = findViewById(R.id.share_confirm);
|
||||
shareContainer = findViewById(R.id.container);
|
||||
contactsFragment = new ContactSelectionListFragment();
|
||||
adapter = new ShareSelectionAdapter();
|
||||
|
||||
if (contactsFragment == null) {
|
||||
throw new IllegalStateException("Could not find contacts fragment!");
|
||||
}
|
||||
RecyclerView contactsRecycler = findViewById(R.id.selected_list);
|
||||
contactsRecycler.setAdapter(adapter);
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
contactsFragment.setOnRefreshListener(this);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.contact_selection_list_fragment, contactsFragment)
|
||||
.commit();
|
||||
|
||||
shareConfirm.setOnClickListener(unused -> {
|
||||
Set<ShareContact> shareContacts = viewModel.getShareContacts();
|
||||
|
||||
if (shareContacts.isEmpty()) throw new AssertionError();
|
||||
else if (shareContacts.size() == 1) onConfirmSingleDestination(shareContacts.iterator().next());
|
||||
else onConfirmMultipleDestinations(shareContacts);
|
||||
});
|
||||
|
||||
viewModel.getSelectedContactModels().observe(this, models -> {
|
||||
adapter.submitList(models, () -> contactsRecycler.scrollToPosition(models.size() - 1));
|
||||
|
||||
shareConfirm.setEnabled(!models.isEmpty());
|
||||
shareConfirm.setAlpha(models.isEmpty() ? 0.5f : 1f);
|
||||
if (models.isEmpty()) {
|
||||
animateOutSelection();
|
||||
} else {
|
||||
animateInSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
@@ -260,16 +331,71 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
if (contactsFragment.getView() != null) {
|
||||
contactsFragment.getView().setVisibility(View.GONE);
|
||||
}
|
||||
onDestinationChosen(threadId, recipientId);
|
||||
onSingleDestinationChosen(threadId, recipientId);
|
||||
} else if (viewModel.isExternalShare()) {
|
||||
validateAvailableRecipients();
|
||||
}
|
||||
}
|
||||
|
||||
private void onDestinationChosen(long threadId, @NonNull RecipientId recipientId) {
|
||||
if (!viewModel.isExternalShare()) {
|
||||
openConversation(threadId, recipientId, null);
|
||||
return;
|
||||
private void onConfirmSingleDestination(@NonNull ShareContact shareContact) {
|
||||
shareConfirm.setClickable(false);
|
||||
SimpleTask.run(this.getLifecycle(),
|
||||
() -> resolveShareContact(shareContact),
|
||||
result -> onSingleDestinationChosen(result.getThreadId(), result.getRecipientId()));
|
||||
}
|
||||
|
||||
private void onConfirmMultipleDestinations(@NonNull Set<ShareContact> shareContacts) {
|
||||
shareConfirm.setClickable(false);
|
||||
SimpleTask.run(this.getLifecycle(),
|
||||
() -> resolvedShareContacts(shareContacts),
|
||||
this::onMultipleDestinationsChosen);
|
||||
}
|
||||
|
||||
private Set<ShareContactAndThread> resolvedShareContacts(@NonNull Set<ShareContact> sharedContacts) {
|
||||
Set<Recipient> recipients = Stream.of(sharedContacts)
|
||||
.map(contact -> contact.getRecipientId()
|
||||
.transform(Recipient::resolved)
|
||||
.or(() -> Recipient.external(this, contact.getNumber())))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<RecipientId, Long> existingThreads = DatabaseFactory.getThreadDatabase(this)
|
||||
.getThreadIdsIfExistsFor(Stream.of(recipients)
|
||||
.map(Recipient::getId)
|
||||
.toArray(RecipientId[]::new));
|
||||
|
||||
return Stream.of(recipients)
|
||||
.map(recipient -> new ShareContactAndThread(recipient.getId(), Util.getOrDefault(existingThreads, recipient.getId(), -1L), recipient.isForceSmsSelection() || !recipient.isRegistered()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private ShareContactAndThread resolveShareContact(@NonNull ShareContact shareContact) {
|
||||
Recipient recipient;
|
||||
if (shareContact.getRecipientId().isPresent()) {
|
||||
recipient = Recipient.resolved(shareContact.getRecipientId().get());
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
recipient = Recipient.external(this, shareContact.getNumber());
|
||||
}
|
||||
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
return new ShareContactAndThread(recipient.getId(), existingThread, recipient.isForceSmsSelection() || !recipient.isRegistered());
|
||||
}
|
||||
|
||||
private void validateAvailableRecipients() {
|
||||
resolveShareData(data -> {
|
||||
int mode = getIntent().getIntExtra(ContactSelectionListFragment.DISPLAY_MODE, -1);
|
||||
|
||||
if (mode == -1) return;
|
||||
|
||||
mode = data.isMmsOrSmsSupported() ? mode | DisplayMode.FLAG_SMS : mode & ~DisplayMode.FLAG_SMS;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, mode);
|
||||
|
||||
contactsFragment.reset();
|
||||
});
|
||||
}
|
||||
|
||||
private void resolveShareData(@NonNull Consumer<ShareData> onResolved) {
|
||||
AtomicReference<AlertDialog> progressWheel = new AtomicReference<>();
|
||||
|
||||
if (viewModel.getShareData().getValue() == null) {
|
||||
@@ -291,10 +417,28 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
openConversation(threadId, recipientId, data.get());
|
||||
onResolved.accept(data.get());
|
||||
});
|
||||
}
|
||||
|
||||
private void onMultipleDestinationsChosen(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
if (!viewModel.isExternalShare()) {
|
||||
openInterstitial(shareContactAndThreads, null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolveShareData(data -> openInterstitial(shareContactAndThreads, data));
|
||||
}
|
||||
|
||||
private void onSingleDestinationChosen(long threadId, @NonNull RecipientId recipientId) {
|
||||
if (!viewModel.isExternalShare()) {
|
||||
openConversation(threadId, recipientId, null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolveShareData(data -> openConversation(threadId, recipientId, data));
|
||||
}
|
||||
|
||||
private void openConversation(long threadId, @NonNull RecipientId recipientId, @Nullable ShareData shareData) {
|
||||
ShareIntents.Args args = ShareIntents.Args.from(getIntent());
|
||||
ConversationIntents.Builder builder = ConversationIntents.createBuilder(this, recipientId, threadId)
|
||||
@@ -322,4 +466,77 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
|
||||
startActivity(builder.build());
|
||||
}
|
||||
|
||||
private void openInterstitial(@NonNull Set<ShareContactAndThread> shareContactAndThreads, @Nullable ShareData shareData) {
|
||||
ShareIntents.Args args = ShareIntents.Args.from(getIntent());
|
||||
MultiShareArgs.Builder builder = new MultiShareArgs.Builder(shareContactAndThreads)
|
||||
.withMedia(args.getExtraMedia())
|
||||
.withDraftText(args.getExtraText() != null ? args.getExtraText().toString() : null)
|
||||
.withStickerLocator(args.getExtraSticker())
|
||||
.asBorderless(args.isBorderless());
|
||||
|
||||
if (shareData != null && shareData.isForIntent()) {
|
||||
Log.i(TAG, "Shared data is a single file.");
|
||||
builder.withDataUri(shareData.getUri())
|
||||
.withDataType(shareData.getMimeType());
|
||||
} else if (shareData != null && shareData.isForMedia()) {
|
||||
Log.i(TAG, "Shared data is set of media.");
|
||||
builder.withMedia(shareData.getMedia());
|
||||
} else if (shareData != null && shareData.isForPrimitive()) {
|
||||
Log.i(TAG, "Shared data is a primitive type.");
|
||||
} else if (shareData == null && args.getExtraSticker() != null) {
|
||||
builder.withDataType(getIntent().getType());
|
||||
} else {
|
||||
Log.i(TAG, "Shared data was not external.");
|
||||
}
|
||||
|
||||
MultiShareArgs multiShareArgs = builder.build();
|
||||
InterstitialContentType interstitialContentType = multiShareArgs.getInterstitialContentType();
|
||||
switch (interstitialContentType) {
|
||||
case TEXT:
|
||||
startActivityForResult(ShareInterstitialActivity.createIntent(this, multiShareArgs), RESULT_TEXT_CONFIRMATION);
|
||||
break;
|
||||
case MEDIA:
|
||||
List<Media> media = new ArrayList<>(multiShareArgs.getMedia());
|
||||
if (media.isEmpty()) {
|
||||
media.add(new Media(multiShareArgs.getDataUri(),
|
||||
multiShareArgs.getDataType(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent()));
|
||||
}
|
||||
|
||||
startActivityForResult(MediaSendActivity.buildShareIntent(this,
|
||||
media,
|
||||
Stream.of(multiShareArgs.getShareContactAndThreads()).map(ShareContactAndThread::getRecipientId).toList(),
|
||||
multiShareArgs.getDraftText(),
|
||||
MultiShareSender.getWorseTransportOption(this, multiShareArgs.getShareContactAndThreads())),
|
||||
RESULT_MEDIA_CONFIRMATION);
|
||||
break;
|
||||
default:
|
||||
//noinspection CodeBlock2Expr
|
||||
MultiShareSender.send(multiShareArgs, results -> {
|
||||
MultiShareDialogs.displayResultDialog(this, results, () -> {
|
||||
viewModel.onSuccessulShare();
|
||||
finish();
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuggestedLimitReached(int limit) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHardLimitReached(int limit) {
|
||||
MultiShareDialogs.displayMaxSelectedDialog(this, limit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class ShareContact {
|
||||
private final Optional<RecipientId> recipientId;
|
||||
private final String number;
|
||||
|
||||
ShareContact(@NonNull Optional<RecipientId> recipientId, @Nullable String number) {
|
||||
this.recipientId = recipientId;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public Optional<RecipientId> getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ShareContact that = (ShareContact) o;
|
||||
return recipientId.equals(that.recipientId) &&
|
||||
Objects.equals(number, that.number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipientId, number);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ShareContactAndThread implements Parcelable {
|
||||
private final RecipientId recipientId;
|
||||
private final long threadId;
|
||||
private final boolean forceSms;
|
||||
|
||||
ShareContactAndThread(@NonNull RecipientId recipientId, long threadId, boolean forceSms) {
|
||||
this.recipientId = recipientId;
|
||||
this.threadId = threadId;
|
||||
this.forceSms = forceSms;
|
||||
}
|
||||
|
||||
protected ShareContactAndThread(@NonNull Parcel in) {
|
||||
recipientId = in.readParcelable(RecipientId.class.getClassLoader());
|
||||
threadId = in.readLong();
|
||||
forceSms = in.readByte() == 1;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public boolean isForceSms() {
|
||||
return forceSms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ShareContactAndThread that = (ShareContactAndThread) o;
|
||||
return threadId == that.threadId &&
|
||||
forceSms == that.forceSms &&
|
||||
recipientId.equals(that.recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipientId, threadId, forceSms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(recipientId, flags);
|
||||
dest.writeLong(threadId);
|
||||
dest.writeByte((byte) (forceSms ? 1 : 0));
|
||||
}
|
||||
|
||||
public static final Creator<ShareContactAndThread> CREATOR = new Creator<ShareContactAndThread>() {
|
||||
@Override
|
||||
public ShareContactAndThread createFromParcel(@NonNull Parcel in) {
|
||||
return new ShareContactAndThread(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShareContactAndThread[] newArray(int size) {
|
||||
return new ShareContactAndThread[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -16,24 +16,26 @@ class ShareData {
|
||||
private final Optional<String> mimeType;
|
||||
private final Optional<ArrayList<Media>> media;
|
||||
private final boolean external;
|
||||
private final boolean isMmsOrSmsSupported;
|
||||
|
||||
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 forIntentData(@NonNull Uri uri, @NonNull String mimeType, boolean external, boolean isMmsOrSmsSupported) {
|
||||
return new ShareData(Optional.of(uri), Optional.of(mimeType), Optional.absent(), external, isMmsOrSmsSupported);
|
||||
}
|
||||
|
||||
static ShareData forPrimitiveTypes() {
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.absent(), true);
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.absent(), true, true);
|
||||
}
|
||||
|
||||
static ShareData forMedia(@NonNull List<Media> media) {
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.of(new ArrayList<>(media)), true);
|
||||
static ShareData forMedia(@NonNull List<Media> media, boolean isMmsOrSmsSupported) {
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.of(new ArrayList<>(media)), true, isMmsOrSmsSupported);
|
||||
}
|
||||
|
||||
private ShareData(Optional<Uri> uri, Optional<String> mimeType, Optional<ArrayList<Media>> media, boolean external) {
|
||||
this.uri = uri;
|
||||
this.mimeType = mimeType;
|
||||
this.media = media;
|
||||
this.external = external;
|
||||
private ShareData(Optional<Uri> uri, Optional<String> mimeType, Optional<ArrayList<Media>> media, boolean external, boolean isMmsOrSmsSupported) {
|
||||
this.uri = uri;
|
||||
this.mimeType = mimeType;
|
||||
this.media = media;
|
||||
this.external = external;
|
||||
this.isMmsOrSmsSupported = isMmsOrSmsSupported;
|
||||
}
|
||||
|
||||
boolean isForIntent() {
|
||||
@@ -63,4 +65,8 @@ class ShareData {
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
public boolean isMmsOrSmsSupported() {
|
||||
return isMmsOrSmsSupported;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,17 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendConstants;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -72,7 +77,7 @@ class ShareRepository {
|
||||
mimeType = getMimeType(context, uri, mimeType);
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
return ShareData.forIntentData(uri, mimeType, false);
|
||||
return ShareData.forIntentData(uri, mimeType, false, false);
|
||||
} else {
|
||||
InputStream stream = context.getContentResolver().openInputStream(uri);
|
||||
|
||||
@@ -99,10 +104,35 @@ class ShareRepository {
|
||||
.createForMultipleSessionsOnDisk(context);
|
||||
}
|
||||
|
||||
return ShareData.forIntentData(blobUri, mimeType, true);
|
||||
return ShareData.forIntentData(blobUri, mimeType, true, isMmsSupported(context, mimeType, size));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMmsSupported(@NonNull Context context, @NonNull String mimeType, long size) {
|
||||
if (!Util.isMmsCapable(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TransportOptions options = new TransportOptions(context, true);
|
||||
options.setDefaultTransport(TransportOption.Type.SMS);
|
||||
MediaConstraints mmsConstraints = MediaConstraints.getMmsMediaConstraints(options.getSelectedTransport().getSimSubscriptionId().or(-1));
|
||||
|
||||
final boolean canMmsSupportFileSize;
|
||||
if (MediaUtil.isGif(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getGifMaxSize(context);
|
||||
} else if (MediaUtil.isVideo(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getVideoMaxSize(context);
|
||||
} else if (MediaUtil.isImageType(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getImageMaxSize(context);
|
||||
} else if (MediaUtil.isAudioType(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getAudioMaxSize(context);
|
||||
} else {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getDocumentMaxSize(context);
|
||||
}
|
||||
|
||||
return canMmsSupportFileSize;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable ShareData getResolvedInternal(@NonNull List<Uri> uris) throws IOException {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
@@ -160,7 +190,9 @@ class ShareRepository {
|
||||
}
|
||||
|
||||
if (media.size() > 0) {
|
||||
return ShareData.forMedia(media);
|
||||
boolean isMmsSupported = Stream.of(media)
|
||||
.allMatch(m -> isMmsSupported(context, m.getMimeType(), m.getSize()));
|
||||
return ShareData.forMedia(media, isMmsSupported);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
|
||||
class ShareSelectionAdapter extends MappingAdapter {
|
||||
ShareSelectionAdapter() {
|
||||
registerFactory(ShareSelectionMappingModel.class,
|
||||
ShareSelectionViewHolder.createFactory(R.layout.share_contact_selection_item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
|
||||
class ShareSelectionMappingModel implements MappingModel<ShareSelectionMappingModel> {
|
||||
|
||||
private final ShareContact shareContact;
|
||||
private final boolean isLast;
|
||||
|
||||
ShareSelectionMappingModel(@NonNull ShareContact shareContact, boolean isLast) {
|
||||
this.shareContact = shareContact;
|
||||
this.isLast = isLast;
|
||||
}
|
||||
|
||||
@NonNull String getName(@NonNull Context context) {
|
||||
String name = shareContact.getRecipientId()
|
||||
.transform(Recipient::resolved)
|
||||
.transform(recipient -> recipient.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: recipient.getShortDisplayNameIncludingUsername(context))
|
||||
.or(shareContact.getNumber());
|
||||
|
||||
return isLast ? name : context.getString(R.string.ShareActivity__s_comma, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull ShareSelectionMappingModel newItem) {
|
||||
return newItem.shareContact.equals(shareContact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull ShareSelectionMappingModel newItem) {
|
||||
return areItemsTheSame(newItem) && newItem.isLast == isLast;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
||||
public class ShareSelectionViewHolder extends MappingViewHolder<ShareSelectionMappingModel> {
|
||||
|
||||
protected final @NonNull TextView name;
|
||||
|
||||
public ShareSelectionViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
name = findViewById(R.id.recipient_view_name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull ShareSelectionMappingModel model) {
|
||||
name.setText(model.getName(context));
|
||||
}
|
||||
|
||||
public static @NonNull MappingAdapter.Factory<ShareSelectionMappingModel> createFactory(@LayoutRes int layout) {
|
||||
return new MappingAdapter.LayoutFactory<>(ShareSelectionViewHolder::new, layout);
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,23 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class ShareViewModel extends ViewModel {
|
||||
|
||||
@@ -24,14 +32,16 @@ public class ShareViewModel extends ViewModel {
|
||||
private final Context context;
|
||||
private final ShareRepository shareRepository;
|
||||
private final MutableLiveData<Optional<ShareData>> shareData;
|
||||
private final MutableLiveData<Set<ShareContact>> selectedContacts;
|
||||
|
||||
private boolean mediaUsed;
|
||||
private boolean externalShare;
|
||||
|
||||
private ShareViewModel() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.shareRepository = new ShareRepository();
|
||||
this.shareData = new MutableLiveData<>();
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.shareRepository = new ShareRepository();
|
||||
this.shareData = new MutableLiveData<>();
|
||||
this.selectedContacts = new DefaultValueLiveData<>(Collections.emptySet());
|
||||
}
|
||||
|
||||
void onSingleMediaShared(@NonNull Uri uri, @Nullable String mimeType) {
|
||||
@@ -44,11 +54,47 @@ public class ShareViewModel extends ViewModel {
|
||||
shareRepository.getResolved(uris, shareData::postValue);
|
||||
}
|
||||
|
||||
boolean isMultiShare() {
|
||||
return selectedContacts.getValue().size() > 1;
|
||||
}
|
||||
|
||||
boolean onContactSelected(@NonNull ShareContact selectedContact) {
|
||||
Set<ShareContact> contacts = new LinkedHashSet<>(selectedContacts.getValue());
|
||||
if (contacts.add(selectedContact)) {
|
||||
selectedContacts.setValue(contacts);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void onContactDeselected(@NonNull ShareContact selectedContact) {
|
||||
Set<ShareContact> contacts = new LinkedHashSet<>(selectedContacts.getValue());
|
||||
if (contacts.remove(selectedContact)) {
|
||||
selectedContacts.setValue(contacts);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull Set<ShareContact> getShareContacts() {
|
||||
Set<ShareContact> contacts = selectedContacts.getValue();
|
||||
if (contacts == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<MappingModel<?>>> getSelectedContactModels() {
|
||||
return Transformations.map(selectedContacts, set -> Stream.of(set)
|
||||
.<MappingModel<?>>mapIndexed((i, c) -> new ShareSelectionMappingModel(c, i == set.size() - 1))
|
||||
.toList());
|
||||
}
|
||||
|
||||
void onNonExternalShare() {
|
||||
externalShare = false;
|
||||
}
|
||||
|
||||
void onSuccessulShare() {
|
||||
public void onSuccessulShare() {
|
||||
mediaUsed = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||
import org.thoughtcrime.securesms.components.SelectionAwareEmojiEditText;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareDialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
/**
|
||||
* Handles display and editing of a text message (with possible link preview) before it is forwarded
|
||||
* to multiple users.
|
||||
*/
|
||||
public class ShareInterstitialActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String ARGS = "args";
|
||||
|
||||
private ShareInterstitialViewModel viewModel;
|
||||
private LinkPreviewViewModel linkPreviewViewModel;
|
||||
private CircularProgressButton confirm;
|
||||
private RecyclerView contactsRecycler;
|
||||
private Toolbar toolbar;
|
||||
private LinkPreviewView preview;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final ShareInterstitialSelectionAdapter adapter = new ShareInterstitialSelectionAdapter();
|
||||
|
||||
public static Intent createIntent(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) {
|
||||
Intent intent = new Intent(context, ShareInterstitialActivity.class);
|
||||
|
||||
intent.putExtra(ARGS, multiShareArgs);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
dynamicTheme.onCreate(this);
|
||||
setContentView(R.layout.share_interstitial_activity);
|
||||
|
||||
MultiShareArgs args = getIntent().getParcelableExtra(ARGS);
|
||||
|
||||
initializeViewModels(args);
|
||||
initializeViews(args);
|
||||
initializeObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void initializeViewModels(@NonNull MultiShareArgs args) {
|
||||
ShareInterstitialRepository repository = new ShareInterstitialRepository();
|
||||
ShareInterstitialViewModel.Factory factory = new ShareInterstitialViewModel.Factory(args, repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ShareInterstitialViewModel.class);
|
||||
|
||||
LinkPreviewRepository linkPreviewRepository = new LinkPreviewRepository();
|
||||
LinkPreviewViewModel.Factory linkPreviewViewModelFactory = new LinkPreviewViewModel.Factory(linkPreviewRepository);
|
||||
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, linkPreviewViewModelFactory).get(LinkPreviewViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull MultiShareArgs args) {
|
||||
confirm = findViewById(R.id.share_confirm);
|
||||
toolbar = findViewById(R.id.toolbar);
|
||||
preview = findViewById(R.id.link_preview);
|
||||
|
||||
confirm.setOnClickListener(unused -> onConfirm());
|
||||
|
||||
SelectionAwareEmojiEditText text = findViewById(R.id.text);
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> finish());
|
||||
|
||||
text.addTextChangedListener(new AfterTextChanged(editable -> {
|
||||
linkPreviewViewModel.onTextChanged(this, editable.toString(), text.getSelectionStart(), text.getSelectionEnd());
|
||||
viewModel.onDraftTextChanged(editable.toString());
|
||||
}));
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
text.setOnSelectionChangedListener(((selStart, selEnd) -> {
|
||||
linkPreviewViewModel.onTextChanged(this, text.getText().toString(), text.getSelectionStart(), text.getSelectionEnd());
|
||||
}));
|
||||
|
||||
preview.setCloseClickedListener(linkPreviewViewModel::onUserCancel);
|
||||
|
||||
int defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
||||
preview.setCorners(defaultRadius, defaultRadius);
|
||||
|
||||
text.setText(args.getDraftText());
|
||||
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(text);
|
||||
|
||||
contactsRecycler = findViewById(R.id.selected_list);
|
||||
contactsRecycler.setAdapter(adapter);
|
||||
|
||||
confirm.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
int pad = Math.abs(v.getWidth() + ViewUtil.dpToPx(16));
|
||||
ViewUtil.setPaddingEnd(contactsRecycler, pad);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
viewModel.getRecipients().observe(this, models -> adapter.submitList(models,
|
||||
() -> contactsRecycler.scrollToPosition(models.size() - 1)));
|
||||
viewModel.hasDraftText().observe(this, this::handleHasDraftText);
|
||||
|
||||
linkPreviewViewModel.getLinkPreviewState().observe(this, linkPreviewState -> {
|
||||
preview.setVisibility(View.VISIBLE);
|
||||
if (linkPreviewState.getError() != null) {
|
||||
preview.setNoPreview(linkPreviewState.getError());
|
||||
viewModel.onLinkPreviewChanged(null);
|
||||
} else if (linkPreviewState.isLoading()) {
|
||||
preview.setLoading();
|
||||
viewModel.onLinkPreviewChanged(null);
|
||||
} else if (linkPreviewState.getLinkPreview().isPresent()) {
|
||||
preview.setLinkPreview(GlideApp.with(this), linkPreviewState.getLinkPreview().get(), true);
|
||||
viewModel.onLinkPreviewChanged(linkPreviewState.getLinkPreview().get());
|
||||
} else if (!linkPreviewState.hasLinks()) {
|
||||
preview.setVisibility(View.GONE);
|
||||
viewModel.onLinkPreviewChanged(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleHasDraftText(boolean hasDraftText) {
|
||||
confirm.setEnabled(hasDraftText);
|
||||
confirm.setAlpha(hasDraftText ? 1f : 0.5f);
|
||||
}
|
||||
|
||||
private void onConfirm() {
|
||||
confirm.setClickable(false);
|
||||
confirm.setIndeterminateProgressMode(true);
|
||||
confirm.setProgress(50);
|
||||
|
||||
viewModel.send(results -> {
|
||||
MultiShareDialogs.displayResultDialog(this, results, () -> {
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
||||
class ShareInterstitialMappingModel extends RecipientMappingModel<ShareInterstitialMappingModel> {
|
||||
|
||||
private final Recipient recipient;
|
||||
private final boolean isLast;
|
||||
|
||||
ShareInterstitialMappingModel(@NonNull Recipient recipient, boolean isLast) {
|
||||
this.recipient = recipient;
|
||||
this.isLast = isLast;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getName(@NonNull Context context) {
|
||||
String name = recipient.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: recipient.getShortDisplayNameIncludingUsername(context);
|
||||
|
||||
return isLast ? name : context.getString(R.string.ShareActivity__s_comma, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull ShareInterstitialMappingModel newItem) {
|
||||
return super.areContentsTheSame(newItem) && isLast == newItem.isLast;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sharing.ShareContactAndThread;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class ShareInterstitialRepository {
|
||||
|
||||
void loadRecipients(@NonNull Set<ShareContactAndThread> shareContactAndThreads, Consumer<List<Recipient>> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> consumer.accept(resolveRecipients(shareContactAndThreads)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private List<Recipient> resolveRecipients(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
return Stream.of(shareContactAndThreads)
|
||||
.map(ShareContactAndThread::getRecipientId)
|
||||
.map(Recipient::resolved)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
|
||||
|
||||
class ShareInterstitialSelectionAdapter extends MappingAdapter {
|
||||
ShareInterstitialSelectionAdapter() {
|
||||
registerFactory(ShareInterstitialMappingModel.class, RecipientViewHolder.createFactory(R.layout.share_contact_selection_item, null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareSender;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class ShareInterstitialViewModel extends ViewModel {
|
||||
|
||||
private final MultiShareArgs args;
|
||||
private final MutableLiveData<List<MappingModel<?>>> recipients;
|
||||
private final MutableLiveData<String> draftText;
|
||||
|
||||
private LinkPreview linkPreview;
|
||||
|
||||
ShareInterstitialViewModel(@NonNull MultiShareArgs args, @NonNull ShareInterstitialRepository repository) {
|
||||
this.args = args;
|
||||
this.recipients = new MutableLiveData<>();
|
||||
this.draftText = new DefaultValueLiveData<>(Util.firstNonNull(args.getDraftText(), ""));
|
||||
|
||||
repository.loadRecipients(args.getShareContactAndThreads(),
|
||||
list -> recipients.postValue(Stream.of(list)
|
||||
.<MappingModel<?>>mapIndexed((i, r) -> new ShareInterstitialMappingModel(r, i == list.size() - 1))
|
||||
.toList()));
|
||||
|
||||
}
|
||||
|
||||
LiveData<List<MappingModel<?>>> getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
LiveData<Boolean> hasDraftText() {
|
||||
return Transformations.map(draftText, text -> !TextUtils.isEmpty(text));
|
||||
}
|
||||
|
||||
void onDraftTextChanged(@NonNull String change) {
|
||||
draftText.setValue(change);
|
||||
}
|
||||
|
||||
void onLinkPreviewChanged(@Nullable LinkPreview linkPreview) {
|
||||
this.linkPreview = linkPreview;
|
||||
}
|
||||
|
||||
void send(@NonNull Consumer<MultiShareSender.MultiShareSendResultCollection> resultsConsumer) {
|
||||
LinkPreview linkPreview = this.linkPreview;
|
||||
String draftText = this.draftText.getValue();
|
||||
|
||||
MultiShareArgs.Builder builder = args.buildUpon()
|
||||
.withDraftText(draftText)
|
||||
.withLinkPreview(linkPreview);
|
||||
|
||||
MultiShareSender.send(builder.build(), resultsConsumer);
|
||||
}
|
||||
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final MultiShareArgs args;
|
||||
private final ShareInterstitialRepository repository;
|
||||
|
||||
Factory(@NonNull MultiShareArgs args, @NonNull ShareInterstitialRepository repository) {
|
||||
this.args = args;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new ShareInterstitialViewModel(args, repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ public final class FeatureFlags {
|
||||
private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff";
|
||||
private static final String OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry";
|
||||
private static final String ABOUT = "android.about";
|
||||
private static final String SHARE_SELECTION_LIMIT = "android.share.limit";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -100,7 +101,8 @@ public final class FeatureFlags {
|
||||
AUTOMATIC_SESSION_INTERVAL,
|
||||
DEFAULT_MAX_BACKOFF,
|
||||
OKHTTP_AUTOMATIC_RETRY,
|
||||
ABOUT
|
||||
ABOUT,
|
||||
SHARE_SELECTION_LIMIT
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -139,7 +141,8 @@ public final class FeatureFlags {
|
||||
AUTOMATIC_SESSION_INTERVAL,
|
||||
DEFAULT_MAX_BACKOFF,
|
||||
OKHTTP_AUTOMATIC_RETRY,
|
||||
ABOUT
|
||||
ABOUT,
|
||||
SHARE_SELECTION_LIMIT
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -295,6 +298,11 @@ public final class FeatureFlags {
|
||||
return getInteger(CDS_REFRESH_INTERVAL, (int) TimeUnit.HOURS.toSeconds(48));
|
||||
}
|
||||
|
||||
public static @NonNull SelectionLimits shareSelectionLimit() {
|
||||
int limit = getInteger(SHARE_SELECTION_LIMIT, 5);
|
||||
return new SelectionLimits(limit, limit);
|
||||
}
|
||||
|
||||
/** The maximum number of grapheme */
|
||||
public static int getMaxGroupNameGraphemeLength() {
|
||||
return Math.max(32, getInteger(GROUP_NAME_MAX_LENGTH, -1));
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.view.ViewTreeObserver;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
@@ -51,6 +52,20 @@ public final class ViewUtil {
|
||||
private ViewUtil() {
|
||||
}
|
||||
|
||||
public static void focusAndMoveCursorToEndAndOpenKeyboard(@NonNull EditText input) {
|
||||
input.requestFocus();
|
||||
|
||||
int numberLength = input.getText().length();
|
||||
input.setSelection(numberLength, numberLength);
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) input.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
|
||||
|
||||
if (!imm.isAcceptingText()) {
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
||||
}
|
||||
}
|
||||
|
||||
public static void focusAndShowKeyboard(@NonNull View view) {
|
||||
view.requestFocus();
|
||||
if (view.hasWindowFocus()) {
|
||||
|
||||
Reference in New Issue
Block a user