Refactor how message send types are selected.

This commit is contained in:
Greyson Parrelli
2022-06-03 18:07:29 -04:00
committed by GitHub
parent bf90909496
commit 4da422fd3c
29 changed files with 591 additions and 808 deletions

View File

@@ -1,149 +0,0 @@
package org.thoughtcrime.securesms;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import java.util.Optional;
public class TransportOption implements Parcelable {
public enum Type {
SMS,
TEXTSECURE
}
private final int drawable;
private final int backgroundColor;
private final @NonNull String text;
private final @NonNull Type type;
private final @NonNull String composeHint;
private final @NonNull CharacterCalculator characterCalculator;
private final @NonNull Optional<CharSequence> simName;
private final @NonNull Optional<Integer> simSubscriptionId;
public TransportOption(@NonNull Type type,
@DrawableRes int drawable,
int backgroundColor,
@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator)
{
this(type, drawable, backgroundColor, text, composeHint, characterCalculator,
Optional.empty(), Optional.empty());
}
public TransportOption(@NonNull Type type,
@DrawableRes int drawable,
int backgroundColor,
@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator,
@NonNull Optional<CharSequence> simName,
@NonNull Optional<Integer> simSubscriptionId)
{
this.type = type;
this.drawable = drawable;
this.backgroundColor = backgroundColor;
this.text = text;
this.composeHint = composeHint;
this.characterCalculator = characterCalculator;
this.simName = simName;
this.simSubscriptionId = simSubscriptionId;
}
TransportOption(Parcel in) {
this(Type.valueOf(in.readString()),
in.readInt(),
in.readInt(),
in.readString(),
in.readString(),
CharacterCalculator.readFromParcel(in),
Optional.ofNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.empty());
}
public @NonNull Type getType() {
return type;
}
public boolean isType(Type type) {
return this.type == type;
}
public boolean isSms() {
return type == Type.SMS;
}
public CharacterState calculateCharacters(String messageBody) {
return characterCalculator.calculateCharacters(messageBody);
}
public @DrawableRes int getDrawable() {
return drawable;
}
public int getBackgroundColor() {
return backgroundColor;
}
public @NonNull String getComposeHint() {
return composeHint;
}
public @NonNull String getDescription() {
return text;
}
@NonNull
public Optional<CharSequence> getSimName() {
return simName;
}
@NonNull
public Optional<Integer> getSimSubscriptionId() {
return simSubscriptionId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(type.name());
dest.writeInt(drawable);
dest.writeInt(backgroundColor);
dest.writeString(text);
dest.writeString(composeHint);
CharacterCalculator.writeToParcel(dest, characterCalculator);
TextUtils.writeToParcel(simName.orElse(null), dest, flags);
if (simSubscriptionId.isPresent()) {
dest.writeInt(1);
dest.writeInt(simSubscriptionId.get());
} else {
dest.writeInt(0);
}
}
public static final Creator<TransportOption> CREATOR = new Creator<TransportOption>() {
@Override
public TransportOption createFromParcel(Parcel in) {
return new TransportOption(in);
}
@Override
public TransportOption[] newArray(int size) {
return new TransportOption[size];
}
};
}

View File

@@ -1,226 +0,0 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.MmsCharacterCalculator;
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import static org.thoughtcrime.securesms.TransportOption.Type;
public class TransportOptions {
private static final String TAG = Log.tag(TransportOptions.class);
private final List<OnTransportChangedListener> listeners = new LinkedList<>();
private final Context context;
private final List<TransportOption> enabledTransports;
private Type defaultTransportType = Type.SMS;
private Optional<Integer> defaultSubscriptionId = Optional.empty();
private Optional<TransportOption> selectedOption = Optional.empty();
private final Optional<Integer> systemSubscriptionId;
public TransportOptions(Context context, boolean media) {
this.context = context;
this.enabledTransports = initializeAvailableTransports(media);
this.systemSubscriptionId = new SubscriptionManagerCompat(context).getPreferredSubscriptionId();
}
public void reset(boolean media) {
List<TransportOption> transportOptions = initializeAvailableTransports(media);
this.enabledTransports.clear();
this.enabledTransports.addAll(transportOptions);
if (selectedOption.isPresent() && !isEnabled(selectedOption.get())) {
setSelectedTransport(null);
} else {
this.defaultTransportType = Type.SMS;
this.defaultSubscriptionId = Optional.empty();
notifyTransportChangeListeners();
}
}
public void setDefaultTransport(Type type) {
this.defaultTransportType = type;
if (!selectedOption.isPresent()) {
notifyTransportChangeListeners();
}
}
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
if (defaultSubscriptionId.equals(subscriptionId)) {
return;
}
this.defaultSubscriptionId = subscriptionId;
if (!selectedOption.isPresent()) {
notifyTransportChangeListeners();
}
}
public void setSelectedTransport(@Nullable TransportOption transportOption) {
this.selectedOption = Optional.ofNullable(transportOption);
notifyTransportChangeListeners();
}
public boolean isManualSelection() {
return this.selectedOption.isPresent();
}
public @NonNull TransportOption getSelectedTransport() {
if (selectedOption.isPresent()) return selectedOption.get();
if (defaultTransportType == Type.SMS) {
TransportOption transportOption = findEnabledSmsTransportOption(OptionalUtil.or(defaultSubscriptionId, systemSubscriptionId));
if (transportOption != null) {
return transportOption;
}
}
for (TransportOption transportOption : enabledTransports) {
if (transportOption.getType() == defaultTransportType) {
return transportOption;
}
}
throw new AssertionError("No options of default type!");
}
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
return new TransportOption(Type.TEXTSECURE,
R.drawable.ic_send_lock_24,
context.getResources().getColor(R.color.core_ultramarine),
context.getString(R.string.ConversationActivity_transport_signal),
context.getString(R.string.conversation_activity__type_message_push),
new PushCharacterCalculator());
}
private @Nullable TransportOption findEnabledSmsTransportOption(Optional<Integer> subscriptionId) {
if (subscriptionId.isPresent()) {
final int subId = subscriptionId.get();
for (TransportOption transportOption : enabledTransports) {
if (transportOption.getType() == Type.SMS &&
subId == transportOption.getSimSubscriptionId().orElse(-1)) {
return transportOption;
}
}
}
return null;
}
public void disableTransport(Type type) {
TransportOption selected = selectedOption.orElse(null);
Iterator<TransportOption> iterator = enabledTransports.iterator();
while (iterator.hasNext()) {
TransportOption option = iterator.next();
if (option.isType(type)) {
if (selected == option) {
setSelectedTransport(null);
}
iterator.remove();
}
}
}
public List<TransportOption> getEnabledTransports() {
return enabledTransports;
}
public void addOnTransportChangedListener(OnTransportChangedListener listener) {
this.listeners.add(listener);
}
private List<TransportOption> initializeAvailableTransports(boolean isMediaMessage) {
List<TransportOption> results = new LinkedList<>();
if (isMediaMessage) {
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_mms),
context.getString(R.string.conversation_activity__type_message_mms_insecure),
new MmsCharacterCalculator()));
} else {
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_sms),
context.getString(R.string.conversation_activity__type_message_sms_insecure),
new SmsCharacterCalculator()));
}
results.add(getPushTransportOption(context));
return results;
}
private @NonNull List<TransportOption> getTransportOptionsForSimCards(@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator)
{
List<TransportOption> results = new LinkedList<>();
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context);
Collection<SubscriptionInfoCompat> subscriptions;
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE)) {
subscriptions = subscriptionManager.getActiveAndReadySubscriptionInfos();
} else {
subscriptions = Collections.emptyList();
}
if (subscriptions.size() < 2) {
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
context.getResources().getColor(R.color.core_grey_50),
text, composeHint, characterCalculator));
} else {
for (SubscriptionInfoCompat subscriptionInfo : subscriptions) {
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
context.getResources().getColor(R.color.core_grey_50),
text, composeHint, characterCalculator,
Optional.of(subscriptionInfo.getDisplayName()),
Optional.of(subscriptionInfo.getSubscriptionId())));
}
}
return results;
}
private void notifyTransportChangeListeners() {
for (OnTransportChangedListener listener : listeners) {
listener.onChange(getSelectedTransport(), selectedOption.isPresent());
}
}
private boolean isEnabled(TransportOption transportOption) {
for (TransportOption option : enabledTransports) {
if (option.equals(transportOption)) return true;
}
return false;
}
public interface OnTransportChangedListener {
public void onChange(TransportOption newTransport, boolean manuallySelected);
}
}

View File

@@ -1,73 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.graphics.PorterDuff.Mode;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.List;
public class TransportOptionsAdapter extends BaseAdapter {
private final LayoutInflater inflater;
private List<TransportOption> enabledTransports;
public TransportOptionsAdapter(@NonNull Context context,
@NonNull List<TransportOption> enabledTransports)
{
super();
this.inflater = LayoutInflater.from(context);
this.enabledTransports = enabledTransports;
}
public void setEnabledTransports(List<TransportOption> enabledTransports) {
this.enabledTransports = enabledTransports;
}
@Override
public int getCount() {
return enabledTransports.size();
}
@Override
public Object getItem(int position) {
return enabledTransports.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = inflater.inflate(R.layout.transport_selection_list_item, parent, false);
}
TransportOption transport = (TransportOption) getItem(position);
ImageView imageView = convertView.findViewById(R.id.icon);
TextView textView = convertView.findViewById(R.id.text);
TextView subtextView = convertView.findViewById(R.id.subtext);
imageView.getBackground().setColorFilter(transport.getBackgroundColor(), Mode.MULTIPLY);
imageView.setImageResource(transport.getDrawable());
textView.setText(transport.getDescription());
if (transport.getSimName().isPresent()) {
subtextView.setText(transport.getSimName().get());
subtextView.setVisibility(View.VISIBLE);
} else {
subtextView.setVisibility(View.GONE);
}
return convertView;
}
}

View File

@@ -1,50 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.ListPopupWindow;
import java.util.LinkedList;
import java.util.List;
public class TransportOptionsPopup extends ListPopupWindow implements ListView.OnItemClickListener {
private final TransportOptionsAdapter adapter;
private final SelectedListener listener;
public TransportOptionsPopup(@NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) {
super(context);
this.listener = listener;
this.adapter = new TransportOptionsAdapter(context, new LinkedList<TransportOption>());
setVerticalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff));
setHorizontalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff));
setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
setModal(true);
setAnchorView(anchor);
setAdapter(adapter);
setContentWidth(context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width));
setOnItemClickListener(this);
}
public void display(List<TransportOption> enabledTransports) {
adapter.setEnabledTransports(enabledTransports);
adapter.notifyDataSetChanged();
show();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
listener.onSelected((TransportOption)adapter.getItem(position));
}
public interface SelectedListener {
void onSelected(TransportOption option);
}
}

View File

@@ -28,12 +28,12 @@ import androidx.core.view.inputmethod.InputContentInfoCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionDeleter;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -201,13 +201,13 @@ public class ComposeText extends EmojiEditText {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
public void setTransport(TransportOption transport) {
public void setMessageSendType(MessageSendType messageSendType) {
final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
int inputType = getInputType();
if (isLandscape()) setImeActionLabel(transport.getComposeHint(), EditorInfo.IME_ACTION_SEND);
if (isLandscape()) setImeActionLabel(getContext().getString(messageSendType.getComposeHintRes()), EditorInfo.IME_ACTION_SEND);
else setImeActionLabel(null, 0);
if (useSystemEmoji) {
@@ -215,9 +215,9 @@ public class ComposeText extends EmojiEditText {
}
setImeOptions(imeOptions);
setHint(transport.getComposeHint(),
transport.getSimName().isPresent()
? getContext().getString(R.string.conversation_activity__from_sim_name, transport.getSimName().get())
setHint(getContext().getString(messageSendType.getComposeHintRes()),
messageSendType.getSimName() != null
? getContext().getString(R.string.conversation_activity__from_sim_name, messageSendType.getSimName())
: null);
setInputType(inputType);
}

View File

@@ -1,121 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatImageButton;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.TransportOptions;
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
import org.thoughtcrime.securesms.TransportOptionsPopup;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Optional;
public class SendButton extends AppCompatImageButton
implements TransportOptions.OnTransportChangedListener,
TransportOptionsPopup.SelectedListener,
View.OnLongClickListener
{
private final TransportOptions transportOptions;
private Optional<TransportOptionsPopup> transportOptionsPopup = Optional.empty();
@SuppressWarnings("unused")
public SendButton(Context context) {
super(context);
this.transportOptions = initializeTransportOptions(false);
ViewUtil.mirrorIfRtl(this, getContext());
}
@SuppressWarnings("unused")
public SendButton(Context context, AttributeSet attrs) {
super(context, attrs);
this.transportOptions = initializeTransportOptions(false);
ViewUtil.mirrorIfRtl(this, getContext());
}
@SuppressWarnings("unused")
public SendButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.transportOptions = initializeTransportOptions(false);
ViewUtil.mirrorIfRtl(this, getContext());
}
private TransportOptions initializeTransportOptions(boolean media) {
if (isInEditMode()) return null;
TransportOptions transportOptions = new TransportOptions(getContext(), media);
transportOptions.addOnTransportChangedListener(this);
setOnLongClickListener(this);
return transportOptions;
}
private TransportOptionsPopup getTransportOptionsPopup() {
if (!transportOptionsPopup.isPresent()) {
transportOptionsPopup = Optional.of(new TransportOptionsPopup(getContext(), this, this));
}
return transportOptionsPopup.get();
}
public boolean isManualSelection() {
return transportOptions.isManualSelection();
}
public void addOnTransportChangedListener(OnTransportChangedListener listener) {
transportOptions.addOnTransportChangedListener(listener);
}
public TransportOption getSelectedTransport() {
return transportOptions.getSelectedTransport();
}
public void resetAvailableTransports(boolean isMediaMessage) {
transportOptions.reset(isMediaMessage);
}
public void disableTransport(TransportOption.Type type) {
transportOptions.disableTransport(type);
}
public void setDefaultTransport(TransportOption.Type type) {
transportOptions.setDefaultTransport(type);
}
public void setTransport(@NonNull TransportOption option) {
transportOptions.setSelectedTransport(option);
}
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
transportOptions.setDefaultSubscriptionId(subscriptionId);
}
@Override
public void onSelected(TransportOption option) {
transportOptions.setSelectedTransport(option);
getTransportOptionsPopup().dismiss();
}
@Override
public void onChange(TransportOption newTransport, boolean isManualSelection) {
setImageResource(newTransport.getDrawable());
setContentDescription(newTransport.getDescription());
}
@Override
public boolean onLongClick(View v) {
if (isEnabled() && transportOptions.getEnabledTransports().size() > 1) {
getTransportOptionsPopup().display(transportOptions.getEnabledTransports());
return true;
}
return false;
}
}

View File

@@ -0,0 +1,165 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.View.OnLongClickListener
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageButton
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.util.ViewUtil
import java.lang.AssertionError
import java.util.concurrent.CopyOnWriteArrayList
/**
* The send button you see in a conversation.
* Also encapsulates the long-press menu that allows users to switch [MessageSendType]s.
*/
class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImageButton(context, attributeSet), OnLongClickListener {
companion object {
private val TAG = Log.tag(SendButton::class.java)
}
private val listeners: MutableList<SendTypeChangedListener> = CopyOnWriteArrayList()
private var availableSendTypes: List<MessageSendType> = MessageSendType.getAllAvailable(context, false)
private var activeMessageSendType: MessageSendType? = null
private var defaultTransportType: MessageSendType.TransportType = MessageSendType.TransportType.SMS
private var defaultSubscriptionId: Int? = null
private var popupContainer: ViewGroup? = null
init {
setOnLongClickListener(this)
ViewUtil.mirrorIfRtl(this, getContext())
}
/**
* @return True if the [selectedSendType] was chosen manually by the user, otherwise false.
*/
val isManualSelection: Boolean
get() = activeMessageSendType != null
/**
* The actively-selected send type.
*/
val selectedSendType: MessageSendType
get() {
activeMessageSendType?.let {
return it
}
if (defaultTransportType === MessageSendType.TransportType.SMS) {
for (type in availableSendTypes) {
if (type.usesSmsTransport && (defaultSubscriptionId == null || type.simSubscriptionId == defaultSubscriptionId)) {
return type
}
}
}
for (type in availableSendTypes) {
if (type.transportType === defaultTransportType) {
return type
}
}
throw AssertionError("No options of default type!")
}
fun addOnSelectionChangedListener(listener: SendTypeChangedListener) {
listeners.add(listener)
}
fun triggerSelectedChangedEvent() {
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
}
fun resetAvailableTransports(isMediaMessage: Boolean) {
availableSendTypes = MessageSendType.getAllAvailable(context, isMediaMessage)
if (!availableSendTypes.contains(activeMessageSendType)) {
Log.w(TAG, "[resetAvailableTransports] The active send type is no longer available. Unsetting.")
setSendType(null)
} else {
defaultTransportType = MessageSendType.TransportType.SMS
defaultSubscriptionId = null
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
}
}
fun disableTransportType(type: MessageSendType.TransportType) {
availableSendTypes = availableSendTypes.filterNot { it.transportType == type }
}
fun setDefaultTransport(type: MessageSendType.TransportType) {
if (defaultTransportType == type) {
return
}
defaultTransportType = type
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
}
fun setSendType(sendType: MessageSendType?) {
if (activeMessageSendType == sendType) {
return
}
activeMessageSendType = sendType
onSelectionChanged(newType = selectedSendType, isManualSelection = true)
}
fun setDefaultSubscriptionId(subscriptionId: Int?) {
if (defaultSubscriptionId == subscriptionId) {
return
}
defaultSubscriptionId = subscriptionId
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
}
/**
* Must be called with a view that is acceptable for determining the bounds of the popup selector.
*/
fun setPopupContainer(container: ViewGroup) {
popupContainer = container
}
private fun onSelectionChanged(newType: MessageSendType, isManualSelection: Boolean) {
setImageResource(newType.buttonDrawableRes)
contentDescription = context.getString(newType.titleRes)
for (listener in listeners) {
listener.onSendTypeChanged(newType, isManualSelection)
}
}
override fun onLongClick(v: View): Boolean {
if (!isEnabled || availableSendTypes.size == 1) {
return false
}
val currentlySelected: MessageSendType = selectedSendType
val items = availableSendTypes
.filterNot { it == currentlySelected }
.map { option ->
ActionItem(
iconRes = option.menuDrawableRes,
title = option.getTitle(context),
action = { setSendType(option) }
)
}
SignalContextMenu.Builder((parent as View), popupContainer!!)
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.ABOVE)
.offsetY(ViewUtil.dpToPx(8))
.show(items)
return true
}
interface SendTypeChangedListener {
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
}
}

View File

@@ -25,6 +25,7 @@ class SignalContextMenu private constructor(
val baseOffsetX: Int = 0,
val baseOffsetY: Int = 0,
val horizontalPosition: HorizontalPosition = HorizontalPosition.START,
val verticalPosition: VerticalPosition = VerticalPosition.BELOW,
val onDismiss: Runnable? = null
) : PopupWindow(
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
@@ -41,6 +42,7 @@ class SignalContextMenu private constructor(
init {
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
inputMethodMode = INPUT_METHOD_NOT_NEEDED
isFocusable = true
@@ -80,7 +82,10 @@ class SignalContextMenu private constructor(
val offsetY: Int
if (menuBottomBound < screenBottomBound) {
if (verticalPosition == VerticalPosition.ABOVE && menuTopBound > screenTopBound) {
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
contextMenuList.setItems(items.reversed())
} else if (menuBottomBound < screenBottomBound) {
offsetY = baseOffsetY
} else if (menuTopBound > screenTopBound) {
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
@@ -115,6 +120,10 @@ class SignalContextMenu private constructor(
START, END
}
enum class VerticalPosition {
ABOVE, BELOW
}
/**
* @param anchor The view to put the pop-up on
* @param container A parent of [anchor] that represents the acceptable boundaries of the popup
@@ -128,6 +137,7 @@ class SignalContextMenu private constructor(
var offsetX = 0
var offsetY = 0
var horizontalPosition = HorizontalPosition.START
var verticalPosition = VerticalPosition.BELOW
fun onDismiss(onDismiss: Runnable): Builder {
this.onDismiss = onDismiss
@@ -149,6 +159,11 @@ class SignalContextMenu private constructor(
return this
}
fun preferredVerticalPosition(verticalPosition: VerticalPosition): Builder {
this.verticalPosition = verticalPosition
return this
}
fun show(items: List<ActionItem>): SignalContextMenu {
return SignalContextMenu(
anchor = anchor,
@@ -157,6 +172,7 @@ class SignalContextMenu private constructor(
baseOffsetX = offsetX,
baseOffsetY = offsetY,
horizontalPosition = horizontalPosition,
verticalPosition = verticalPosition,
onDismiss = onDismiss
).show()
}

View File

@@ -115,7 +115,6 @@ import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PromptMmsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
@@ -328,7 +327,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import kotlin.Unit;
import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
/**
@@ -590,6 +588,8 @@ public class ConversationParentFragment extends Fragment
if (isSearchRequested && savedInstanceState == null) {
onCreateOptionsMenu(toolbar.getMenu(), requireActivity().getMenuInflater());
}
sendButton.post(() -> sendButton.triggerSelectedChangedEvent());
}
// TODO [alex] LargeScreenSupport -- This needs to be fed a stream of intents
@@ -672,7 +672,7 @@ public class ConversationParentFragment extends Fragment
EventBus.getDefault().register(this);
initializeMmsEnabledCheck();
initializeIdentityRecords();
composeText.setTransport(sendButton.getSelectedTransport());
composeText.setMessageSendType(sendButton.getSelectedSendType());
Recipient recipientSnapshot = recipient.get();
@@ -731,7 +731,7 @@ public class ConversationParentFragment extends Fragment
public void onConfigurationChanged(Configuration newConfig) {
Log.i(TAG, "onConfigurationChanged(" + newConfig.orientation + ")");
super.onConfigurationChanged(newConfig);
composeText.setTransport(sendButton.getSelectedTransport());
composeText.setMessageSendType(sendButton.getSelectedSendType());
if (emojiDrawerStub.resolved() && container.getCurrentInput() == emojiDrawerStub.get()) {
container.hideAttachedInput(true);
@@ -823,7 +823,7 @@ public class ConversationParentFragment extends Fragment
return;
}
sendButton.setTransport(result.getTransport());
sendButton.setSendType(result.getMessageSendType());
if (result.isPushPreUpload()) {
sendMediaMessage(result);
@@ -831,7 +831,7 @@ public class ConversationParentFragment extends Fragment
}
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().orElse(-1);
int subscriptionId = sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1);
boolean initiating = threadId == -1;
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
SlideDeck slideDeck = new SlideDeck();
@@ -852,7 +852,7 @@ public class ConversationParentFragment extends Fragment
final Context context = requireContext().getApplicationContext();
sendMediaMessage(result.getRecipientId(),
result.getTransport().isSms(),
result.getMessageSendType().usesSmsTransport(),
result.getBody(),
slideDeck,
quote,
@@ -1231,7 +1231,7 @@ public class ConversationParentFragment extends Fragment
@Override
public void onAttachmentMediaClicked(@NonNull Media media) {
linkPreviewViewModel.onUserCancel();
startActivityForResult(MediaSelectionActivity.editor(requireActivity(), sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
startActivityForResult(MediaSelectionActivity.editor(requireActivity(), sendButton.getSelectedSendType(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
container.hideCurrentInput(composeText);
}
@@ -1239,7 +1239,7 @@ public class ConversationParentFragment extends Fragment
public void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button) {
switch (button) {
case GALLERY:
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport(), inputPanel.getQuote().isPresent());
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedSendType(), inputPanel.getQuote().isPresent());
break;
case FILE:
AttachmentManager.selectDocument(this, PICK_DOCUMENT);
@@ -1629,21 +1629,21 @@ public class ConversationParentFragment extends Fragment
boolean smsEnabled = true;
if (recipient.get().isPushGroup() || (!recipient.get().isMmsGroup() && !recipient.get().hasSmsAddress())) {
sendButton.disableTransport(Type.SMS);
sendButton.disableTransportType(MessageSendType.TransportType.SMS);
smsEnabled = false;
}
if (!isSecureText && !isPushGroupConversation() && !recipient.get().isServiceIdOnly() && !recipient.get().isReleaseNotes() && smsEnabled) {
sendButton.disableTransport(Type.TEXTSECURE);
sendButton.disableTransportType(MessageSendType.TransportType.SIGNAL);
}
if (!recipient.get().isPushGroup() && recipient.get().isForceSmsSelection() && smsEnabled) {
sendButton.setDefaultTransport(Type.SMS);
sendButton.setDefaultTransport(MessageSendType.TransportType.SMS);
} else {
if (isSecureText || isPushGroupConversation() || recipient.get().isServiceIdOnly() || recipient.get().isReleaseNotes() || !smsEnabled) {
sendButton.setDefaultTransport(Type.TEXTSECURE);
sendButton.setDefaultTransport(MessageSendType.TransportType.SIGNAL);
} else {
sendButton.setDefaultTransport(Type.SMS);
sendButton.setDefaultTransport(MessageSendType.TransportType.SMS);
}
}
@@ -1680,7 +1680,7 @@ public class ConversationParentFragment extends Fragment
if (!Util.isEmpty(mediaList)) {
Log.d(TAG, "Handling shared Media.");
Intent sendIntent = MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedTransport(), mediaList, recipient.getId(), draftText);
Intent sendIntent = MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedSendType(), mediaList, recipient.getId(), draftText);
startActivityForResult(sendIntent, MEDIA_SENDER);
return new SettableFuture<>(false);
}
@@ -2029,7 +2029,7 @@ public class ConversationParentFragment extends Fragment
private void updateDefaultSubscriptionId(Optional<Integer> defaultSubscriptionId) {
Log.i(TAG, "updateDefaultSubscriptionId(" + defaultSubscriptionId.orElse(null) + ")");
sendButton.setDefaultSubscriptionId(defaultSubscriptionId);
sendButton.setDefaultSubscriptionId(defaultSubscriptionId.orElse(null));
}
private void initializeMmsEnabledCheck() {
@@ -2150,6 +2150,8 @@ public class ConversationParentFragment extends Fragment
releaseChannelUnmute = ViewUtil.findStubById(view, R.id.conversation_release_notes_unmute_stub);
joinGroupCallButton = view.findViewById(R.id.conversation_group_call_join);
sendButton.setPopupContainer((ViewGroup) view);
container.setIsBubble(isInBubble());
container.addOnKeyboardShownListener(this);
inputPanel.setListener(this);
@@ -2168,16 +2170,16 @@ public class ConversationParentFragment extends Fragment
attachButton.setOnLongClickListener(new AttachButtonLongClickListener());
sendButton.setOnClickListener(sendButtonListener);
sendButton.setEnabled(true);
sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
sendButton.addOnSelectionChangedListener((newMessageSendType, manuallySelected) -> {
calculateCharactersRemaining();
updateLinkPreviewState();
linkPreviewViewModel.onTransportChanged(newTransport.isSms());
composeText.setTransport(newTransport);
linkPreviewViewModel.onTransportChanged(newMessageSendType.usesSmsTransport());
composeText.setMessageSendType(newMessageSendType);
buttonToggle.getBackground().setColorFilter(getButtonToggleBackgroundColor(newTransport), PorterDuff.Mode.MULTIPLY);
buttonToggle.getBackground().setColorFilter(getButtonToggleBackgroundColor(newMessageSendType), PorterDuff.Mode.MULTIPLY);
buttonToggle.getBackground().invalidateSelf();
if (manuallySelected) recordTransportPreference(newTransport);
if (manuallySelected) recordTransportPreference(newMessageSendType);
});
titleView.setOnStoryRingClickListener(v -> handleStoryRingClick());
@@ -2222,13 +2224,13 @@ public class ConversationParentFragment extends Fragment
material3OnScrollHelper = new Material3OnScrollHelper(Collections.singletonList(toolbarBackground), Collections.emptyList(), this::updateStatusBarColor);
}
private @ColorInt int getButtonToggleBackgroundColor(TransportOption newTransport) {
if (newTransport.isSms()) {
return newTransport.getBackgroundColor();
private @ColorInt int getButtonToggleBackgroundColor(MessageSendType newTransport) {
if (newTransport.usesSmsTransport()) {
return getResources().getColor(newTransport.getBackgroundColorRes());
} else if (recipient != null) {
return getRecipient().getChatColors().asSingleColor();
} else {
return newTransport.getBackgroundColor();
return getResources().getColor(newTransport.getBackgroundColorRes());
}
}
@@ -2728,7 +2730,7 @@ public class ConversationParentFragment extends Fragment
}
Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, videoGif, Optional.empty(), Optional.empty(), Optional.empty());
startActivityForResult(MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
startActivityForResult(MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedSendType(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
return new SettableFuture<>(false);
} else {
return attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
@@ -2749,7 +2751,7 @@ public class ConversationParentFragment extends Fragment
}
private void sendSharedContact(List<Contact> contacts) {
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().orElse(-1);
int subscriptionId = sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1);
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
boolean initiating = threadId == -1;
@@ -2909,8 +2911,8 @@ public class ConversationParentFragment extends Fragment
private void calculateCharactersRemaining() {
String messageBody = composeText.getTextTrimmed().toString();
TransportOption transportOption = sendButton.getSelectedTransport();
CharacterState characterState = transportOption.calculateCharacters(messageBody);
MessageSendType sendType = sendButton.getSelectedSendType();
CharacterState characterState = sendType.calculateCharacters(messageBody);
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
charactersLeft.setText(String.format(Locale.getDefault(),
@@ -2968,7 +2970,7 @@ public class ConversationParentFragment extends Fragment
}
private boolean isSmsForced() {
return sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
return sendButton.isManualSelection() && sendButton.getSelectedSendType().usesSmsTransport();
}
protected Recipient getRecipient() {
@@ -2989,9 +2991,9 @@ public class ConversationParentFragment extends Fragment
}
private MediaConstraints getCurrentMediaConstraints() {
return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE
return sendButton.getSelectedSendType().usesSignalTransport()
? MediaConstraints.getPushMediaConstraints()
: MediaConstraints.getMmsMediaConstraints(sendButton.getSelectedTransport().getSimSubscriptionId().orElse(-1));
: MediaConstraints.getMmsMediaConstraints(sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1));
}
private void markLastSeen() {
@@ -3050,12 +3052,12 @@ public class ConversationParentFragment extends Fragment
}
String message = getMessage();
TransportOption transport = sendButton.getSelectedTransport();
boolean forceSms = (recipient.isForceSmsSelection() || sendButton.isManualSelection()) && transport.isSms();
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().orElse(-1);
MessageSendType sendType = sendButton.getSelectedSendType();
boolean forceSms = (recipient.isForceSmsSelection() || sendButton.isManualSelection()) && sendType.usesSmsTransport();
int subscriptionId = sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1);
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
boolean initiating = threadId == -1;
boolean needsSplit = !transport.isSms() && message.length() > transport.calculateCharacters(message).maxPrimaryMessageSize;
boolean needsSplit = !sendType.usesSmsTransport() && message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
recipient.isGroup() ||
recipient.getEmail().isPresent() ||
@@ -3160,7 +3162,7 @@ public class ConversationParentFragment extends Fragment
final long thread = this.threadId;
if (sendPush) {
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(requireContext(), body, sendButton.getSelectedTransport().calculateCharacters(body).maxPrimaryMessageSize);
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(requireContext(), body, sendButton.getSelectedSendType().calculateCharacters(body).maxPrimaryMessageSize);
body = splitMessage.getBody();
if (splitMessage.getTextSlide().isPresent()) {
@@ -3294,7 +3296,7 @@ public class ConversationParentFragment extends Fragment
}
private void updateLinkPreviewState() {
if (SignalStore.settings().isLinkPreviewsEnabled() && isSecureText && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent() && getContext() != null) {
if (SignalStore.settings().isLinkPreviewsEnabled() && isSecureText && !sendButton.getSelectedSendType().usesSmsTransport() && !attachmentManager.isAttachmentPresent() && getContext() != null) {
linkPreviewViewModel.onEnabled();
linkPreviewViewModel.onTextChanged(requireContext(), composeText.getTextTrimmed().toString(), composeText.getSelectionStart(), composeText.getSelectionEnd());
} else {
@@ -3302,16 +3304,16 @@ public class ConversationParentFragment extends Fragment
}
}
private void recordTransportPreference(TransportOption transportOption) {
private void recordTransportPreference(MessageSendType sendType) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
recipientDatabase.setDefaultSubscriptionId(recipient.getId(), transportOption.getSimSubscriptionId().orElse(-1));
recipientDatabase.setDefaultSubscriptionId(recipient.getId(), sendType.getSimSubscriptionIdOr(-1));
if (!recipient.resolve().isPushGroup()) {
recipientDatabase.setForceSmsSelection(recipient.getId(), recipient.get().getRegistered() == RegisteredState.REGISTERED && transportOption.isSms());
recipientDatabase.setForceSmsSelection(recipient.getId(), recipient.get().getRegistered() == RegisteredState.REGISTERED && sendType.usesSmsTransport());
}
return null;
@@ -3446,9 +3448,9 @@ public class ConversationParentFragment extends Fragment
}
private void sendVoiceNote(@NonNull Uri uri, long size) {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedSendType().usesSmsTransport();
boolean initiating = threadId == -1;
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().orElse(-1);
int subscriptionId = sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1);
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
AudioSlide audioSlide = new AudioSlide(requireContext(), uri, size, MediaUtil.AUDIO_AAC, true);
SlideDeck slideDeck = new SlideDeck();
@@ -3487,23 +3489,23 @@ public class ConversationParentFragment extends Fragment
}
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull String contentType, @NonNull Uri uri, long size, boolean clearCompose) {
if (sendButton.getSelectedTransport().isSms()) {
if (sendButton.getSelectedSendType().usesSmsTransport()) {
Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty());
Intent intent = MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed());
Intent intent = MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedSendType(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed());
startActivityForResult(intent, MEDIA_SENDER);
return;
}
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().orElse(-1);
int subscriptionId = sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1);
boolean initiating = threadId == -1;
TransportOption transport = sendButton.getSelectedTransport();
MessageSendType sendType = sendButton.getSelectedSendType();
SlideDeck slideDeck = new SlideDeck();
Slide stickerSlide = new StickerSlide(requireContext(), uri, size, stickerLocator, contentType);
slideDeck.addSlide(stickerSlide);
sendMediaMessage(recipient.getId(), transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose, null);
sendMediaMessage(recipient.getId(), sendType.usesSmsTransport(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose, null);
}
private void silentlySetComposeText(String text) {
@@ -3558,7 +3560,7 @@ public class ConversationParentFragment extends Fragment
@Override
public void openGifSearch() {
AttachmentManager.selectGif(this, ConversationParentFragment.PICK_GIF, recipient.getId(), sendButton.getSelectedTransport(), isMms(), composeText.getTextTrimmed());
AttachmentManager.selectGif(this, ConversationParentFragment.PICK_GIF, recipient.getId(), sendButton.getSelectedSendType(), isMms(), composeText.getTextTrimmed());
}
@Override
@@ -3651,7 +3653,7 @@ public class ConversationParentFragment extends Fragment
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
.onAllGranted(() -> {
composeText.clearFocus();
startActivityForResult(MediaSelectionActivity.camera(requireActivity(), sendButton.getSelectedTransport(), recipient.getId(), inputPanel.getQuote().isPresent()), MEDIA_SENDER);
startActivityForResult(MediaSelectionActivity.camera(requireActivity(), sendButton.getSelectedSendType(), recipient.getId(), inputPanel.getQuote().isPresent()), MEDIA_SENDER);
requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary);
})
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
@@ -4146,7 +4148,7 @@ public class ConversationParentFragment extends Fragment
}
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().orElse(-1);
int subscriptionId = sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1);
boolean initiating = threadId == -1;
SlideDeck slideDeck = new SlideDeck();

View File

@@ -0,0 +1,164 @@
package org.thoughtcrime.securesms.conversation
import android.Manifest
import android.content.Context
import android.os.Parcelable
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import kotlinx.parcelize.Parcelize
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.CharacterCalculator
import org.thoughtcrime.securesms.util.MmsCharacterCalculator
import org.thoughtcrime.securesms.util.PushCharacterCalculator
import org.thoughtcrime.securesms.util.SmsCharacterCalculator
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat
import java.lang.IllegalArgumentException
/**
* The kinds of messages you can send, e.g. a plain Signal message, an SMS message, etc.
*/
@Parcelize
sealed class MessageSendType(
@StringRes
val titleRes: Int,
@StringRes
val composeHintRes: Int,
@DrawableRes
val buttonDrawableRes: Int,
@DrawableRes
val menuDrawableRes: Int,
@ColorRes
val backgroundColorRes: Int,
val transportType: TransportType,
val characterCalculator: CharacterCalculator,
open val simName: CharSequence? = null,
open val simSubscriptionId: Int? = null
) : Parcelable {
@get:JvmName("usesSmsTransport")
val usesSmsTransport
get() = transportType == TransportType.SMS
@get:JvmName("usesSignalTransport")
val usesSignalTransport
get() = transportType == TransportType.SIGNAL
fun calculateCharacters(body: String): CharacterCalculator.CharacterState {
return characterCalculator.calculateCharacters(body)
}
fun getSimSubscriptionIdOr(fallback: Int): Int {
return simSubscriptionId ?: fallback
}
open fun getTitle(context: Context): String {
return context.getString(titleRes)
}
/**
* A type representing an SMS message, with optional SIM fields for multi-SIM devices.
*/
@Parcelize
data class SmsMessageSendType(override val simName: CharSequence? = null, override val simSubscriptionId: Int? = null) : MessageSendType(
titleRes = R.string.ConversationActivity_transport_insecure_sms,
composeHintRes = R.string.conversation_activity__type_message_sms_insecure,
buttonDrawableRes = R.drawable.ic_send_unlock_24,
menuDrawableRes = R.drawable.ic_insecure_24,
backgroundColorRes = R.color.core_grey_50,
transportType = TransportType.SMS,
characterCalculator = SmsCharacterCalculator(),
simName = simName,
simSubscriptionId = simSubscriptionId
) {
override fun getTitle(context: Context): String {
return if (simName == null) {
super.getTitle(context)
} else {
context.getString(R.string.ConversationActivity_transport_insecure_sms_with_sim, simName)
}
}
}
/**
* A type representing an MMS message, with optional SIM fields for multi-SIM devices.
*/
@Parcelize
data class MmsMessageSendType(override val simName: CharSequence? = null, override val simSubscriptionId: Int? = null) : MessageSendType(
titleRes = R.string.ConversationActivity_transport_insecure_mms,
composeHintRes = R.string.conversation_activity__type_message_mms_insecure,
buttonDrawableRes = R.drawable.ic_send_unlock_24,
menuDrawableRes = R.drawable.ic_insecure_24,
backgroundColorRes = R.color.core_grey_50,
transportType = TransportType.SMS,
characterCalculator = MmsCharacterCalculator(),
simName = simName,
simSubscriptionId = simSubscriptionId
) {
override fun getTitle(context: Context): String {
return if (simName == null) {
super.getTitle(context)
} else {
context.getString(R.string.ConversationActivity_transport_insecure_sms_with_sim, simName)
}
}
}
/**
* A type representing a basic Signal message.
*/
@Parcelize
object SignalMessageSendType : MessageSendType(
titleRes = R.string.ConversationActivity_transport_signal,
composeHintRes = R.string.conversation_activity__type_message_push,
buttonDrawableRes = R.drawable.ic_send_lock_24,
menuDrawableRes = R.drawable.ic_secure_24,
backgroundColorRes = R.color.core_ultramarine,
transportType = TransportType.SIGNAL,
characterCalculator = PushCharacterCalculator()
)
enum class TransportType {
SIGNAL, SMS
}
companion object {
/**
* Returns a list of all available [MessageSendType]s. Requires [Manifest.permission.READ_PHONE_STATE] in order to get available
* SMS options.
*/
@JvmStatic
fun getAllAvailable(context: Context, isMedia: Boolean = false): List<MessageSendType> {
val options: MutableList<MessageSendType> = mutableListOf()
options += SignalMessageSendType
if (!Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE)) {
return options
}
val subscriptions: Collection<SubscriptionInfoCompat> = SubscriptionManagerCompat(context).activeAndReadySubscriptionInfos
if (subscriptions.size < 2) {
options += if (isMedia) MmsMessageSendType() else SmsMessageSendType()
} else {
options += subscriptions.map {
if (isMedia) {
MmsMessageSendType(simName = it.displayName, simSubscriptionId = it.subscriptionId)
} else {
SmsMessageSendType(simName = it.displayName, simSubscriptionId = it.subscriptionId)
}
}
}
return options
}
@JvmStatic
fun getFirstForTransport(context: Context, isMedia: Boolean, transportType: TransportType): MessageSendType {
return getAllAvailable(context, isMedia).firstOrNull { it.transportType == transportType } ?: throw IllegalArgumentException("No options available for desired type $transportType!")
}
}
}

View File

@@ -5,10 +5,9 @@ import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.TransportOptions
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.MediaConstraints
@@ -84,10 +83,9 @@ object Multiselect {
return false
}
val options = TransportOptions(context, true)
options.setDefaultTransport(TransportOption.Type.SMS)
val sendType: MessageSendType = MessageSendType.getFirstForTransport(context, true, MessageSendType.TransportType.SMS)
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(options.selectedTransport.simSubscriptionId.orElse(-1))
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(sendType.simSubscriptionId ?: -1)
return mmsConstraints.isSatisfied(context, mediaUri, mediaType, mediaSize) || mmsConstraints.canResize(mediaType)
}
@@ -108,10 +106,9 @@ object Multiselect {
return false
}
val options = TransportOptions(context, true)
options.setDefaultTransport(TransportOption.Type.SMS)
val sendType: MessageSendType = MessageSendType.getFirstForTransport(context, true, MessageSendType.TransportType.SMS)
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(options.selectedTransport.simSubscriptionId.orElse(-1))
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(sendType.simSubscriptionId ?: -1)
return mmsConstraints.isSatisfied(context, attachment) || mmsConstraints.canResize(attachment)
}
}

View File

@@ -13,7 +13,7 @@ import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Fragment;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4SaveResult;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ViewModel;
@@ -47,7 +47,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar
private GiphyMp4ViewModel giphyMp4ViewModel;
private AlertDialog progressDialog;
private RecipientId recipientId;
private TransportOption transport;
private MessageSendType sendType;
private CharSequence text;
@Override
@@ -62,7 +62,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar
final boolean forMms = getIntent().getBooleanExtra(EXTRA_IS_MMS, false);
recipientId = getIntent().getParcelableExtra(EXTRA_RECIPIENT_ID);
transport = getIntent().getParcelableExtra(EXTRA_TRANSPORT);
sendType = getIntent().getParcelableExtra(EXTRA_TRANSPORT);
text = getIntent().getCharSequenceExtra(EXTRA_TEXT);
giphyMp4ViewModel = ViewModelProviders.of(this, new GiphyMp4ViewModel.Factory(forMms)).get(GiphyMp4ViewModel.class);
@@ -121,7 +121,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar
}
Media media = new Media(success.getBlobUri(), mimeType, 0, success.getWidth(), success.getHeight(), 0, 0, false, true, Optional.empty(), Optional.empty(), Optional.empty());
startActivityForResult(MediaSelectionActivity.editor(this, transport, Collections.singletonList(media), recipientId, text), MEDIA_SENDER);
startActivityForResult(MediaSelectionActivity.editor(this, sendType, Collections.singletonList(media), recipientId, text), MEDIA_SENDER);
}
private void handleGiphyMp4ErrorResult(@NonNull GiphyMp4SaveResult.Error error) {

View File

@@ -6,8 +6,8 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -30,7 +30,7 @@ public class MediaSendActivityResult implements Parcelable {
private final Collection<PreUploadResult> uploadResults;
private final Collection<Media> nonUploadedMedia;
private final String body;
private final TransportOption transport;
private final MessageSendType sendType;
private final boolean viewOnce;
private final Collection<Mention> mentions;
private final StoryType storyType;
@@ -47,32 +47,32 @@ public class MediaSendActivityResult implements Parcelable {
public static @NonNull MediaSendActivityResult forPreUpload(@NonNull RecipientId recipientId,
@NonNull Collection<PreUploadResult> uploadResults,
@NonNull String body,
@NonNull TransportOption transport,
@NonNull MessageSendType sendType,
boolean viewOnce,
@NonNull List<Mention> mentions,
@NonNull StoryType storyType)
{
Preconditions.checkArgument(uploadResults.size() > 0, "Must supply uploadResults!");
return new MediaSendActivityResult(recipientId, uploadResults, Collections.emptyList(), body, transport, viewOnce, mentions, storyType);
return new MediaSendActivityResult(recipientId, uploadResults, Collections.emptyList(), body, sendType, viewOnce, mentions, storyType);
}
public static @NonNull MediaSendActivityResult forTraditionalSend(@NonNull RecipientId recipientId,
@NonNull List<Media> nonUploadedMedia,
@NonNull String body,
@NonNull TransportOption transport,
@NonNull MessageSendType sendType,
boolean viewOnce,
@NonNull List<Mention> mentions,
@NonNull StoryType storyType)
{
Preconditions.checkArgument(nonUploadedMedia.size() > 0, "Must supply media!");
return new MediaSendActivityResult(recipientId, Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce, mentions, storyType);
return new MediaSendActivityResult(recipientId, Collections.emptyList(), nonUploadedMedia, body, sendType, viewOnce, mentions, storyType);
}
private MediaSendActivityResult(@NonNull RecipientId recipientId,
@NonNull Collection<PreUploadResult> uploadResults,
@NonNull List<Media> nonUploadedMedia,
@NonNull String body,
@NonNull TransportOption transport,
@NonNull MessageSendType sendType,
boolean viewOnce,
@NonNull List<Mention> mentions,
@NonNull StoryType storyType)
@@ -81,7 +81,7 @@ public class MediaSendActivityResult implements Parcelable {
this.uploadResults = uploadResults;
this.nonUploadedMedia = nonUploadedMedia;
this.body = body;
this.transport = transport;
this.sendType = sendType;
this.viewOnce = viewOnce;
this.mentions = mentions;
this.storyType = storyType;
@@ -92,7 +92,7 @@ public class MediaSendActivityResult implements Parcelable {
this.uploadResults = ParcelUtil.readParcelableCollection(in, PreUploadResult.class);
this.nonUploadedMedia = ParcelUtil.readParcelableCollection(in, Media.class);
this.body = in.readString();
this.transport = in.readParcelable(TransportOption.class.getClassLoader());
this.sendType = in.readParcelable(MessageSendType.class.getClassLoader());
this.viewOnce = ParcelUtil.readBoolean(in);
this.mentions = ParcelUtil.readParcelableCollection(in, Mention.class);
this.storyType = StoryType.fromCode(in.readInt());
@@ -118,8 +118,8 @@ public class MediaSendActivityResult implements Parcelable {
return body;
}
public @NonNull TransportOption getTransport() {
return transport;
public @NonNull MessageSendType getMessageSendType() {
return sendType;
}
public boolean isViewOnce() {
@@ -157,7 +157,7 @@ public class MediaSendActivityResult implements Parcelable {
ParcelUtil.writeParcelableCollection(dest, uploadResults);
ParcelUtil.writeParcelableCollection(dest, nonUploadedMedia);
dest.writeString(body);
dest.writeParcelable(transport, 0);
dest.writeParcelable(sendType, 0);
ParcelUtil.writeBoolean(dest, viewOnce);
ParcelUtil.writeParcelableCollection(dest, mentions);
dest.writeInt(storyType.getCode());

View File

@@ -23,12 +23,11 @@ import org.signal.core.util.BreakIteratorCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.TransportOptions
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFullScreenDialogFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigurationProvider
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
@@ -87,12 +86,12 @@ class MediaSelectionActivity :
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
setContentView(R.layout.media_selection_activity)
val transportOption: TransportOption = requireNotNull(intent.getParcelableExtra(TRANSPORT_OPTION))
val sendType: MessageSendType = requireNotNull(intent.getParcelableExtra(MESSAGE_SEND_TYPE))
val initialMedia: List<Media> = intent.getParcelableArrayListExtra(MEDIA) ?: listOf()
val message: CharSequence? = if (shareToTextStory) null else draftText
val isReply: Boolean = intent.getBooleanExtra(IS_REPLY, false)
val factory = MediaSelectionViewModel.Factory(destination, transportOption, initialMedia, message, isReply, isStory, MediaSelectionRepository(this))
val factory = MediaSelectionViewModel.Factory(destination, sendType, initialMedia, message, isReply, isStory, MediaSelectionRepository(this))
viewModel = ViewModelProvider(this, factory)[MediaSelectionViewModel::class.java]
val textStoryToggle: ConstraintLayout = findViewById(R.id.switch_widget)
@@ -346,7 +345,7 @@ class MediaSelectionActivity :
private const val NAV_HOST_TAG = "NAV_HOST"
private const val START_ACTION = "start.action"
private const val TRANSPORT_OPTION = "transport.option"
private const val MESSAGE_SEND_TYPE = "message.send.type"
private const val MEDIA = "media"
private const val MESSAGE = "message"
private const val DESTINATION = "destination"
@@ -371,14 +370,14 @@ class MediaSelectionActivity :
@JvmStatic
fun camera(
context: Context,
transportOption: TransportOption,
messageSendType: MessageSendType,
recipientId: RecipientId,
isReply: Boolean
): Intent {
return buildIntent(
context = context,
startAction = R.id.action_directly_to_mediaCaptureFragment,
transportOption = transportOption,
messageSendType = messageSendType,
destination = MediaSelectionDestination.SingleRecipient(recipientId),
isReply = isReply
)
@@ -387,7 +386,7 @@ class MediaSelectionActivity :
@JvmStatic
fun gallery(
context: Context,
transportOption: TransportOption,
messageSendType: MessageSendType,
media: List<Media>,
recipientId: RecipientId,
message: CharSequence?,
@@ -396,7 +395,7 @@ class MediaSelectionActivity :
return buildIntent(
context = context,
startAction = R.id.action_directly_to_mediaGalleryFragment,
transportOption = transportOption,
messageSendType = messageSendType,
media = media,
destination = MediaSelectionDestination.SingleRecipient(recipientId),
message = message,
@@ -407,14 +406,14 @@ class MediaSelectionActivity :
@JvmStatic
fun editor(
context: Context,
transportOption: TransportOption,
messageSendType: MessageSendType,
media: List<Media>,
recipientId: RecipientId,
message: CharSequence?
): Intent {
return buildIntent(
context = context,
transportOption = transportOption,
messageSendType = messageSendType,
media = media,
destination = MediaSelectionDestination.SingleRecipient(recipientId),
message = message
@@ -424,7 +423,7 @@ class MediaSelectionActivity :
@JvmStatic
fun share(
context: Context,
transportOption: TransportOption,
messageSendType: MessageSendType,
media: List<Media>,
recipientSearchKeys: List<ContactSearchKey.RecipientSearchKey>,
message: CharSequence?,
@@ -432,7 +431,7 @@ class MediaSelectionActivity :
): Intent {
return buildIntent(
context = context,
transportOption = transportOption,
messageSendType = messageSendType,
media = media,
destination = MediaSelectionDestination.MultipleRecipients(recipientSearchKeys),
message = message,
@@ -444,7 +443,7 @@ class MediaSelectionActivity :
private fun buildIntent(
context: Context,
startAction: Int = -1,
transportOption: TransportOption = TransportOptions.getPushTransportOption(context),
messageSendType: MessageSendType = MessageSendType.SignalMessageSendType,
media: List<Media> = listOf(),
destination: MediaSelectionDestination = MediaSelectionDestination.ChooseAfterMediaSelection,
message: CharSequence? = null,
@@ -454,7 +453,7 @@ class MediaSelectionActivity :
): Intent {
return Intent(context, MediaSelectionActivity::class.java).apply {
putExtra(START_ACTION, startAction)
putExtra(TRANSPORT_OPTION, transportOption)
putExtra(MESSAGE_SEND_TYPE, messageSendType)
putParcelableArrayListExtra(MEDIA, ArrayList(media))
putExtra(MESSAGE, message)
putExtra(DESTINATION, destination.toBundle())

View File

@@ -11,8 +11,8 @@ import org.signal.core.util.BreakIteratorCompat
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.signal.imageeditor.core.model.EditorModel
import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
@@ -76,7 +76,7 @@ class MediaSelectionRepository(context: Context) {
singleContact: ContactSearchKey.RecipientSearchKey?,
contacts: List<ContactSearchKey.RecipientSearchKey>,
mentions: List<Mention>,
transport: TransportOption
sendType: MessageSendType
): Maybe<MediaSendActivityResult> {
if (isSms && contacts.isNotEmpty()) {
throw IllegalStateException("Provided recipients to send to, but this is SMS!")
@@ -106,9 +106,9 @@ class MediaSelectionRepository(context: Context) {
if (isSms || MessageSender.isLocalSelfSend(context, singleRecipient, isSms)) {
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.")
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, transport, isViewOnce, trimmedMentions, StoryType.NONE))
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, sendType, isViewOnce, trimmedMentions, StoryType.NONE))
} else {
val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody, transport.calculateCharacters(trimmedBody).maxPrimaryMessageSize)
val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody, sendType.calculateCharacters(trimmedBody).maxPrimaryMessageSize)
val splitBody = splitMessage.body
if (splitMessage.textSlide.isPresent) {
@@ -135,10 +135,10 @@ class MediaSelectionRepository(context: Context) {
uploadRepository.deleteAbandonedAttachments()
emitter.onComplete()
} else if (uploadResults.isNotEmpty()) {
emitter.onSuccess(MediaSendActivityResult.forPreUpload(singleRecipient!!.id, uploadResults, splitBody, transport, isViewOnce, trimmedMentions, storyType))
emitter.onSuccess(MediaSendActivityResult.forPreUpload(singleRecipient!!.id, uploadResults, splitBody, sendType, isViewOnce, trimmedMentions, storyType))
} else {
Log.w(TAG, "Got empty upload results! isSms: $isSms, updatedMedia.size(): ${updatedMedia.size}, isViewOnce: $isViewOnce, target: $singleContact")
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, transport, isViewOnce, trimmedMentions, storyType))
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, sendType, isViewOnce, trimmedMentions, storyType))
}
}
}

View File

@@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.mediasend.v2
import android.net.Uri
import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendConstants
@@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.recipients.Recipient
data class MediaSelectionState(
val transportOption: TransportOption,
val sendType: MessageSendType,
val selectedMedia: List<Media> = listOf(),
val focusedMedia: Media? = null,
val recipient: Recipient? = null,
@@ -25,7 +25,7 @@ data class MediaSelectionState(
val isStory: Boolean
) {
val maxSelection = if (transportOption.isSms) {
val maxSelection = if (sendType.usesSmsTransport) {
MediaSendConstants.MAX_SMS
} else {
MediaSendConstants.MAX_PUSH

View File

@@ -10,9 +10,9 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.subjects.PublishSubject
import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
import org.thoughtcrime.securesms.mediasend.VideoEditorFragment
@@ -31,7 +31,7 @@ import java.util.Collections
*/
class MediaSelectionViewModel(
val destination: MediaSelectionDestination,
transportOption: TransportOption,
sendType: MessageSendType,
initialMedia: List<Media>,
initialMessage: CharSequence?,
val isReply: Boolean,
@@ -41,7 +41,7 @@ class MediaSelectionViewModel(
private val store: Store<MediaSelectionState> = Store(
MediaSelectionState(
transportOption = transportOption,
sendType = sendType,
message = initialMessage,
isStory = isStory
)
@@ -62,7 +62,7 @@ class MediaSelectionViewModel(
store.update {
it.copy(
isMeteredConnection = metered,
isPreUploadEnabled = shouldPreUpload(metered, it.transportOption.isSms, it.recipient)
isPreUploadEnabled = shouldPreUpload(metered, it.sendType.usesSmsTransport, it.recipient)
)
}
}
@@ -75,7 +75,7 @@ class MediaSelectionViewModel(
store.update(Recipient.live(recipientSearchKey.recipientId).liveData) { r, s ->
s.copy(
recipient = r,
isPreUploadEnabled = shouldPreUpload(s.isMeteredConnection, s.transportOption.isSms, r)
isPreUploadEnabled = shouldPreUpload(s.isMeteredConnection, s.sendType.usesSmsTransport, r)
)
}
}
@@ -246,8 +246,8 @@ class MediaSelectionViewModel(
}
fun getMediaConstraints(): MediaConstraints {
return if (store.state.transportOption.isSms) {
MediaConstraints.getMmsMediaConstraints(store.state.transportOption.simSubscriptionId.orElse(-1))
return if (store.state.sendType.usesSmsTransport) {
MediaConstraints.getMmsMediaConstraints(store.state.sendType.simSubscriptionId ?: -1)
} else {
MediaConstraints.getPushMediaConstraints()
}
@@ -293,18 +293,18 @@ class MediaSelectionViewModel(
store.state.editorStateMap,
store.state.quality,
store.state.message,
store.state.transportOption.isSms,
store.state.sendType.usesSmsTransport,
isViewOnceEnabled(),
destination.getRecipientSearchKey(),
selectedContacts.ifEmpty { destination.getRecipientSearchKeyList() },
MentionAnnotation.getMentionsFromAnnotations(store.state.message),
store.state.transportOption
store.state.sendType
)
)
}
private fun isViewOnceEnabled(): Boolean {
return !store.state.transportOption.isSms &&
return !store.state.sendType.usesSmsTransport &&
store.state.selectedMedia.size == 1 &&
store.state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE
}
@@ -427,7 +427,7 @@ class MediaSelectionViewModel(
class Factory(
private val destination: MediaSelectionDestination,
private val transportOption: TransportOption,
private val sendType: MessageSendType,
private val initialMedia: List<Media>,
private val initialMessage: CharSequence?,
private val isReply: Boolean,
@@ -435,7 +435,7 @@ class MediaSelectionViewModel(
private val repository: MediaSelectionRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(MediaSelectionViewModel(destination, transportOption, initialMedia, initialMessage, isReply, isStory, repository)))
return requireNotNull(modelClass.cast(MediaSelectionViewModel(destination, sendType, initialMedia, initialMessage, isReply, isStory, repository)))
}
}
}

View File

@@ -26,8 +26,8 @@ import androidx.viewpager2.widget.ViewPager2
import app.cash.exhaustive.Exhaustive
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
@@ -200,7 +200,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) {
state.selectedMedia.map { MediaReviewSelectedItem.Model(it, state.focusedMedia == it) } + MediaReviewAddItem.Model
)
presentSendButton(state.transportOption)
presentSendButton(state.sendType)
presentPager(state)
presentAddMessageEntry(state.message)
presentImageQualityToggle(state.quality)
@@ -289,8 +289,8 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) {
)
}
private fun presentSendButton(transportOption: TransportOption) {
val sendButtonTint = if (transportOption.type == TransportOption.Type.TEXTSECURE) {
private fun presentSendButton(sendType: MessageSendType) {
val sendButtonTint = if (sendType.usesSignalTransport) {
R.color.core_ultramarine
} else {
R.color.core_grey_50

View File

@@ -42,7 +42,6 @@ import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
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.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
@@ -50,6 +49,7 @@ import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
@@ -374,12 +374,12 @@ public class AttachmentManager {
selectMediaType(fragment, "*/*", null, requestCode);
}
public static void selectGallery(Fragment fragment, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull TransportOption transport, boolean hasQuote) {
public static void selectGallery(Fragment fragment, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull MessageSendType messageSendType, boolean hasQuote) {
Permissions.with(fragment)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> fragment.startActivityForResult(MediaSelectionActivity.gallery(fragment.requireContext(), transport, Collections.emptyList(), recipient.getId(), body, hasQuote), requestCode))
.onAllGranted(() -> fragment.startActivityForResult(MediaSelectionActivity.gallery(fragment.requireContext(), messageSendType, Collections.emptyList(), recipient.getId(), body, hasQuote), requestCode))
.execute();
}
@@ -404,11 +404,11 @@ public class AttachmentManager {
.execute();
}
public static void selectGif(Fragment fragment, int requestCode, RecipientId id, TransportOption selectedTransport, boolean isForMms, CharSequence textTrimmed) {
public static void selectGif(Fragment fragment, int requestCode, RecipientId id, MessageSendType sendType, boolean isForMms, CharSequence textTrimmed) {
Intent intent = new Intent(fragment.requireContext(), GiphyActivity.class);
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, isForMms);
intent.putExtra(GiphyActivity.EXTRA_RECIPIENT_ID, id);
intent.putExtra(GiphyActivity.EXTRA_TRANSPORT, selectedTransport);
intent.putExtra(GiphyActivity.EXTRA_TRANSPORT, sendType);
intent.putExtra(GiphyActivity.EXTRA_TEXT, textTrimmed);
fragment.startActivityForResult(intent, requestCode);
}

View File

@@ -16,9 +16,8 @@ import org.signal.core.util.BreakIteratorCompat;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.TransportOptions;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
@@ -98,13 +97,13 @@ public final class MultiShareSender {
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
List<Mention> mentions = getValidMentionsForRecipient(recipient, multiShareArgs.getMentions());
TransportOption transport = resolveTransportOption(context, recipient);
boolean forceSms = recipient.isForceSmsSelection() && transport.isSms();
int subscriptionId = transport.getSimSubscriptionId().orElse(-1);
MessageSendType sendType = resolveTransportOption(context, recipient);
boolean forceSms = recipient.isForceSmsSelection() && sendType.usesSmsTransport();
int subscriptionId = sendType.getSimSubscriptionIdOr(-1);
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
boolean needsSplit = !transport.isSms() &&
message != null &&
message.length() > transport.calculateCharacters(message).maxPrimaryMessageSize;
boolean needsSplit = !sendType.usesSmsTransport() &&
message != null &&
message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
boolean hasMmsMedia = !multiShareArgs.getMedia().isEmpty() ||
(multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) ||
multiShareArgs.getStickerLocator() != null ||
@@ -119,8 +118,8 @@ public final class MultiShareSender {
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.MMS_NOT_ENABLED));
} else if (hasMmsMedia && transport.isSms() || hasPushMedia && !transport.isSms() || canSendAsTextStory) {
sendMediaMessageOrCollectStoryToBatch(context, multiShareArgs, recipient, slideDeck, transport, threadId, forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId, mentions, recipientSearchKey.isStory(), sentTimestamp, canSendAsTextStory, storiesBatch);
} else if (hasMmsMedia && sendType.usesSmsTransport() || hasPushMedia && !sendType.usesSmsTransport() || canSendAsTextStory) {
sendMediaMessageOrCollectStoryToBatch(context, multiShareArgs, recipient, slideDeck, sendType, threadId, forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId, mentions, recipientSearchKey.isStory(), sentTimestamp, canSendAsTextStory, storiesBatch);
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.SUCCESS));
} else if (recipientSearchKey.isStory()) {
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.INVALID_SHARE_TO_STORY));
@@ -146,28 +145,26 @@ public final class MultiShareSender {
return new MultiShareSendResultCollection(results);
}
public static @NonNull TransportOption getWorstTransportOption(@NonNull Context context, @NonNull Set<ContactSearchKey.RecipientSearchKey> recipientSearchKeys) {
public static @NonNull MessageSendType getWorstTransportOption(@NonNull Context context, @NonNull Set<ContactSearchKey.RecipientSearchKey> recipientSearchKeys) {
for (ContactSearchKey.RecipientSearchKey recipientSearchKey : recipientSearchKeys) {
TransportOption option = resolveTransportOption(context, Recipient.resolved(recipientSearchKey.getRecipientId()).isForceSmsSelection() && !recipientSearchKey.isStory());
if (option.isSms()) {
return option;
MessageSendType type = resolveTransportOption(context, Recipient.resolved(recipientSearchKey.getRecipientId()).isForceSmsSelection() && !recipientSearchKey.isStory());
if (type.usesSmsTransport()) {
return type;
}
}
return TransportOptions.getPushTransportOption(context);
return MessageSendType.SignalMessageSendType.INSTANCE;
}
private static @NonNull TransportOption resolveTransportOption(@NonNull Context context, @NonNull Recipient recipient) {
private static @NonNull MessageSendType resolveTransportOption(@NonNull Context context, @NonNull Recipient recipient) {
return resolveTransportOption(context, !recipient.isDistributionList() && (recipient.isForceSmsSelection() || !recipient.isRegistered()));
}
public static @NonNull TransportOption resolveTransportOption(@NonNull Context context, boolean forceSms) {
public static @NonNull MessageSendType resolveTransportOption(@NonNull Context context, boolean forceSms) {
if (forceSms) {
TransportOptions options = new TransportOptions(context, false);
options.setDefaultTransport(TransportOption.Type.SMS);
return options.getSelectedTransport();
return MessageSendType.getFirstForTransport(context, false, MessageSendType.TransportType.SMS);
} else {
return TransportOptions.getPushTransportOption(context);
return MessageSendType.SignalMessageSendType.INSTANCE;
}
}
@@ -175,7 +172,7 @@ public final class MultiShareSender {
@NonNull MultiShareArgs multiShareArgs,
@NonNull Recipient recipient,
@NonNull SlideDeck slideDeck,
@NonNull TransportOption transportOption,
@NonNull MessageSendType sendType,
long threadId,
boolean forceSms,
long expiresIn,
@@ -188,8 +185,8 @@ public final class MultiShareSender {
@NonNull List<OutgoingMediaMessage> storiesToBatchSend)
{
String body = multiShareArgs.getDraftText();
if (transportOption.isType(TransportOption.Type.TEXTSECURE) && !forceSms && body != null) {
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body, transportOption.calculateCharacters(body).maxPrimaryMessageSize);
if (sendType.usesSignalTransport() && !forceSms && body != null) {
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body, sendType.calculateCharacters(body).maxPrimaryMessageSize);
body = splitMessage.getBody();
if (splitMessage.getTextSlide().isPresent()) {

View File

@@ -11,10 +11,9 @@ import androidx.core.content.ContextCompat
import androidx.core.util.toKotlinPair
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.TransportOptions
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendConstants
import org.thoughtcrime.securesms.mms.MediaConstraints
@@ -180,9 +179,8 @@ class ShareRepository(context: Context) {
return false
}
val options = TransportOptions(context, true)
options.setDefaultTransport(TransportOption.Type.SMS)
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(options.selectedTransport.simSubscriptionId.orElse(-1))
val sendType: MessageSendType = MessageSendType.getFirstForTransport(context, true, MessageSendType.TransportType.SMS)
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(sendType.simSubscriptionId ?: -1)
return mmsConstraints.isSatisfied(context, attachment) || mmsConstraints.canResize(attachment)
}
}

View File

@@ -16,35 +16,13 @@
*/
package org.thoughtcrime.securesms.util;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public abstract class CharacterCalculator {
public abstract class CharacterCalculator implements Parcelable {
public abstract CharacterState calculateCharacters(String messageBody);
public static CharacterCalculator readFromParcel(@NonNull Parcel in) {
switch (in.readInt()) {
case 1: return new SmsCharacterCalculator();
case 2: return new MmsCharacterCalculator();
case 3: return new PushCharacterCalculator();
default: throw new IllegalArgumentException("Read an unsupported value for a calculator.");
}
}
public static void writeToParcel(@NonNull Parcel dest, @NonNull CharacterCalculator calculator) {
if (calculator instanceof SmsCharacterCalculator) {
dest.writeInt(1);
} else if (calculator instanceof MmsCharacterCalculator) {
dest.writeInt(2);
} else if (calculator instanceof PushCharacterCalculator) {
dest.writeInt(3);
} else {
throw new IllegalArgumentException("Tried to write an unsupported calculator to a parcel.");
}
}
public static class CharacterState {
public final int charactersRemaining;
public final int messagesSpent;

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.util;
import android.os.Parcel;
public class MmsCharacterCalculator extends CharacterCalculator {
private static final int MAX_SIZE = 5000;
@@ -8,4 +10,25 @@ public class MmsCharacterCalculator extends CharacterCalculator {
public CharacterState calculateCharacters(String messageBody) {
return new CharacterState(1, MAX_SIZE - messageBody.length(), MAX_SIZE, MAX_SIZE);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
}
public static final Creator<MmsCharacterCalculator> CREATOR = new Creator<MmsCharacterCalculator>() {
@Override
public MmsCharacterCalculator createFromParcel(Parcel in) {
return new MmsCharacterCalculator();
}
@Override
public MmsCharacterCalculator[] newArray(int size) {
return new MmsCharacterCalculator[size];
}
};
}

View File

@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms.util;
import android.os.Parcel;
public class PushCharacterCalculator extends CharacterCalculator {
private static final int MAX_TOTAL_SIZE = 64 * 1024;
private static final int MAX_PRIMARY_SIZE = 2000;
@@ -23,5 +25,26 @@ public class PushCharacterCalculator extends CharacterCalculator {
public CharacterState calculateCharacters(String messageBody) {
return new CharacterState(1, MAX_TOTAL_SIZE - messageBody.length(), MAX_TOTAL_SIZE, MAX_PRIMARY_SIZE);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
}
public static final Creator<PushCharacterCalculator> CREATOR = new Creator<PushCharacterCalculator>() {
@Override
public PushCharacterCalculator createFromParcel(Parcel in) {
return new PushCharacterCalculator();
}
@Override
public PushCharacterCalculator[] newArray(int size) {
return new PushCharacterCalculator[size];
}
};
}

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms.util;
import android.os.Parcel;
import android.telephony.SmsMessage;
import org.signal.core.util.logging.Log;
@@ -62,4 +63,25 @@ public class SmsCharacterCalculator extends CharacterCalculator {
return new CharacterState(messagesSpent, charactersRemaining, maxMessageSize, maxMessageSize);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
}
public static final Creator<SmsCharacterCalculator> CREATOR = new Creator<SmsCharacterCalculator>() {
@Override
public SmsCharacterCalculator createFromParcel(Parcel in) {
return new SmsCharacterCalculator();
}
@Override
public SmsCharacterCalculator[] newArray(int size) {
return new SmsCharacterCalculator[size];
}
};
}