mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 21:24:42 +00:00
Remove more SMS vestiges.
This commit is contained in:
@@ -198,7 +198,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(this::initializeTrimThreadsByDateManager)
|
||||
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
|
||||
@@ -73,8 +73,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
int displayMode = includeSms ? ContactSelectionDisplayMode.FLAG_ALL : ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
|
||||
int displayMode = ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
}
|
||||
|
||||
|
||||
@@ -119,14 +119,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
|
||||
if (Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setOnClickListener(new SmsClickListener());
|
||||
} else {
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
|
||||
private Animation loadAnimation(@AnimRes int animResId) {
|
||||
@@ -200,13 +195,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ViewUtil.animateIn(smsSendFrame, slideInAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsCancelClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
@@ -121,8 +121,6 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
boolean smsSupported = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -138,7 +136,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
|
||||
if (result instanceof RecipientRepository.LookupResult.Success) {
|
||||
Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId());
|
||||
if (smsSupported || resolved.isRegistered() && resolved.hasServiceId()) {
|
||||
if (resolved.isRegistered() && resolved.hasServiceId()) {
|
||||
launch(resolved);
|
||||
}
|
||||
} else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) {
|
||||
@@ -153,8 +151,6 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else if (smsSupported) {
|
||||
launch(Recipient.external(this, number));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +300,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
return null;
|
||||
}
|
||||
|
||||
if (recipient.isRegistered() || (SignalStore.misc().getSmsExportPhase().allowSmsFeatures())) {
|
||||
if (recipient.isRegistered()) {
|
||||
return new ActionItem(
|
||||
R.drawable.ic_phone_right_24,
|
||||
getString(R.string.NewConversationActivity__audio_call),
|
||||
|
||||
@@ -38,7 +38,6 @@ class GiftFlowRecipientSelectionFragment : Fragment(R.layout.gift_flow_recipient
|
||||
R.id.multiselect_container,
|
||||
MultiselectForwardFragment.create(
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = false,
|
||||
multiShareArgs = emptyList(),
|
||||
forceDisableAddMessage = true,
|
||||
selectSingleRecipient = true
|
||||
|
||||
@@ -467,7 +467,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
is CallLogDeletionResult.FailedToRevoke -> {
|
||||
errorDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(resources.getQuantityString(R.plurals.CallLogFragment__cant_delete_call_link, it.failedRevocations))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
CallLogDeletionResult.Success -> {
|
||||
|
||||
@@ -99,7 +99,7 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
|
||||
}
|
||||
|
||||
private fun showErrorDialog(context: Context, @StringRes message: Int, onPositiveButtonClickListener: OnClickListener?) {
|
||||
MaterialAlertDialogBuilder(context).setMessage(message).setPositiveButton(R.string.ok, onPositiveButtonClickListener).show()
|
||||
MaterialAlertDialogBuilder(context).setMessage(message).setPositiveButton(android.R.string.ok, onPositiveButtonClickListener).show()
|
||||
}
|
||||
|
||||
private sealed interface RequestCodeResult {
|
||||
|
||||
@@ -16,9 +16,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
import java.util.Optional
|
||||
@@ -100,17 +98,12 @@ class SelectRecipientsFragment : LoggingFragment(), ContactSelectionListFragment
|
||||
}
|
||||
|
||||
private fun getDefaultDisplayMode(): Int {
|
||||
var mode = ContactSelectionDisplayMode.FLAG_PUSH or
|
||||
return ContactSelectionDisplayMode.FLAG_PUSH or
|
||||
ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS or
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_NEW or
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER or
|
||||
ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS
|
||||
|
||||
if (Util.isDefaultSmsProvider(requireContext()) && SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
mode = mode or ContactSelectionDisplayMode.FLAG_SMS
|
||||
}
|
||||
|
||||
return mode or ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1
|
||||
ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS or
|
||||
ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1
|
||||
}
|
||||
|
||||
override fun onBeforeContactSelected(isFromUnknownSearchKey: Boolean, recipientId: Optional<RecipientId>, number: String?, callback: Consumer<Boolean>) {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.wrapped
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.preferences.MmsPreferencesFragment
|
||||
|
||||
class WrappedMmsPreferencesFragment : SettingsWrapperFragment() {
|
||||
override fun getFragment(): Fragment {
|
||||
toolbar.setTitle(R.string.preferences__advanced_mms_access_point_names)
|
||||
return MmsPreferencesFragment()
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ sealed class ConversationSettingsViewModel(
|
||||
}
|
||||
|
||||
store.update(liveRecipient.liveData) { recipient, state ->
|
||||
val isAudioAvailable = (recipient.isRegistered || SignalStore.misc().smsExportPhase.allowSmsFeatures()) &&
|
||||
val isAudioAvailable = recipient.isRegistered &&
|
||||
!recipient.isGroup &&
|
||||
!recipient.isBlocked &&
|
||||
!recipient.isSelf &&
|
||||
|
||||
@@ -79,10 +79,6 @@ public class ContactsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
try {
|
||||
ContactDiscovery.refresh(context, recipients, true);
|
||||
|
||||
if (Util.isDefaultSmsProvider(context)) {
|
||||
ContactDiscovery.syncRecipientInfoWithSystemContacts(context);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to refresh! Scheduling for later.", e);
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(true));
|
||||
|
||||
@@ -54,7 +54,6 @@ import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.media3.common.MediaItem;
|
||||
@@ -107,8 +106,6 @@ import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationE
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemUtils;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.database.MediaTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
@@ -118,8 +115,6 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
|
||||
@@ -2687,8 +2682,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (eventListener != null) {
|
||||
eventListener.onIncomingIdentityMismatchClicked(messageRecord.getFromRecipient().getId());
|
||||
}
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
handleMessageApproval();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2782,45 +2775,4 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
footer.setAudioDuration(durationMillis, playheadMillis);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessageApproval() {
|
||||
final int title;
|
||||
final int message;
|
||||
|
||||
if (messageRecord.isMms()) title = R.string.ConversationItem_click_to_approve_unencrypted_mms_dialog_title;
|
||||
else title = R.string.ConversationItem_click_to_approve_unencrypted_sms_dialog_title;
|
||||
|
||||
message = R.string.ConversationItem_click_to_approve_unencrypted_dialog_message;
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
builder.setTitle(title);
|
||||
|
||||
if (message > -1) builder.setMessage(message);
|
||||
|
||||
builder.setPositiveButton(R.string.yes, (dialogInterface, i) -> {
|
||||
MessageTable db = SignalDatabase.messages();
|
||||
|
||||
db.markAsInsecure(messageRecord.getId());
|
||||
db.markAsOutbox(messageRecord.getId());
|
||||
db.markAsForcedSms(messageRecord.getId());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
MmsSendJob.enqueue(context,
|
||||
ApplicationDependencies.getJobManager(),
|
||||
messageRecord.getId());
|
||||
} else {
|
||||
ApplicationDependencies.getJobManager().add(new SmsSendJob(messageRecord.getId(),
|
||||
messageRecord.getToRecipient()));
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton(R.string.no, (dialogInterface, i) -> {
|
||||
if (messageRecord.isMms()) {
|
||||
SignalDatabase.messages().markAsSentFailed(messageRecord.getId());
|
||||
} else {
|
||||
SignalDatabase.messages().markAsSentFailed(messageRecord.getId());
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import androidx.annotation.StringRes
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.R
|
||||
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 java.lang.IllegalArgumentException
|
||||
|
||||
/**
|
||||
@@ -29,15 +27,9 @@ sealed class MessageSendType(
|
||||
@ColorRes
|
||||
val backgroundColorRes: Int,
|
||||
val transportType: TransportType,
|
||||
val characterCalculator: CharacterCalculator,
|
||||
open val simName: CharSequence? = null,
|
||||
open val simSubscriptionId: Int? = null
|
||||
val characterCalculator: CharacterCalculator
|
||||
) : Parcelable {
|
||||
|
||||
@get:JvmName("usesSmsTransport")
|
||||
val usesSmsTransport
|
||||
get() = transportType == TransportType.SMS
|
||||
|
||||
@get:JvmName("usesSignalTransport")
|
||||
val usesSignalTransport
|
||||
get() = transportType == TransportType.SIGNAL
|
||||
@@ -50,54 +42,6 @@ sealed class MessageSendType(
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
package org.thoughtcrime.securesms.conversation.mutiselect
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.core.content.ContextCompat
|
||||
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.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.mms.TextSlide
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
/**
|
||||
* General helper object for all things multiselect. This is only utilized by
|
||||
@@ -65,51 +54,4 @@ object Multiselect {
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
fun canSendToNonPush(context: Context, multiselectPart: MultiselectPart): Boolean {
|
||||
return when (multiselectPart) {
|
||||
is MultiselectPart.Attachments -> canSendAllAttachmentsToNonPush(context, multiselectPart.conversationMessage.messageRecord)
|
||||
is MultiselectPart.Message -> canSendAllAttachmentsToNonPush(context, multiselectPart.conversationMessage.messageRecord)
|
||||
is MultiselectPart.Text -> true
|
||||
is MultiselectPart.Update -> throw AssertionError("Should never get to here.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine whether a given attachment can be sent via MMS.
|
||||
*/
|
||||
fun isMmsSupported(context: Context, mediaUri: Uri, mediaType: String, mediaSize: Long): Boolean {
|
||||
val canReadPhoneState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
|
||||
if (!Util.isDefaultSmsProvider(context) || !canReadPhoneState || !Util.isMmsCapable(context) || !SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val sendType: MessageSendType = MessageSendType.getFirstForTransport(MessageSendType.TransportType.SMS)
|
||||
|
||||
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(sendType.simSubscriptionId ?: -1)
|
||||
return mmsConstraints.isSatisfied(context, mediaUri, mediaType, mediaSize) || mmsConstraints.canResize(mediaType)
|
||||
}
|
||||
|
||||
private fun canSendAllAttachmentsToNonPush(context: Context, messageRecord: MessageRecord): Boolean {
|
||||
return if (messageRecord is MmsMessageRecord) {
|
||||
messageRecord.slideDeck.asAttachments().all { isMmsSupported(context, it) }
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine whether a given attachment can be sent via MMS.
|
||||
*/
|
||||
private fun isMmsSupported(context: Context, attachment: Attachment): Boolean {
|
||||
val canReadPhoneState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
|
||||
if (!Util.isDefaultSmsProvider(context) || !canReadPhoneState || !Util.isMmsCapable(context) || !SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val sendType: MessageSendType = MessageSendType.getFirstForTransport(MessageSendType.TransportType.SMS)
|
||||
|
||||
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(sendType.simSubscriptionId ?: -1)
|
||||
return mmsConstraints.isSatisfied(context, attachment) || mmsConstraints.canResize(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryM
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
@@ -470,7 +469,7 @@ class MultiselectForwardFragment :
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Individuals(
|
||||
includeHeader = true,
|
||||
transportType = if (includeSms()) ContactSearchConfiguration.TransportType.ALL else ContactSearchConfiguration.TransportType.PUSH,
|
||||
transportType = ContactSearchConfiguration.TransportType.PUSH,
|
||||
includeSelf = true
|
||||
)
|
||||
)
|
||||
@@ -485,18 +484,13 @@ class MultiselectForwardFragment :
|
||||
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Groups(
|
||||
includeHeader = true,
|
||||
includeMms = includeSms()
|
||||
includeHeader = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun includeSms(): Boolean {
|
||||
return Util.isDefaultSmsProvider(requireContext()) && args.canSendToNonPush
|
||||
}
|
||||
|
||||
private fun isSelectedMediaValidForStories(): Boolean {
|
||||
return !args.isViewOnce && args.multiShareArgs.all { it.isValidForStories }
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.color.ViewColorSet
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
@@ -23,7 +22,6 @@ import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.hasSharedContact
|
||||
import java.util.Optional
|
||||
import java.util.function.Consumer
|
||||
@@ -41,7 +39,6 @@ import java.util.function.Consumer
|
||||
*/
|
||||
@Parcelize
|
||||
data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
||||
val canSendToNonPush: Boolean,
|
||||
val multiShareArgs: List<MultiShareArgs> = listOf(),
|
||||
@StringRes val title: Int = R.string.MultiselectForwardFragment__forward_to,
|
||||
val forceDisableAddMessage: Boolean = false,
|
||||
@@ -60,8 +57,6 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
||||
@JvmStatic
|
||||
fun create(context: Context, threadId: Long, mediaUri: Uri, mediaType: String, consumer: Consumer<MultiselectForwardFragmentArgs>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val mediaSize = MediaUtil.getMediaSize(context, mediaUri)
|
||||
val isMmsSupported = Multiselect.isMmsSupported(context, mediaUri, mediaType, mediaSize)
|
||||
val multiShareArgs = MultiShareArgs.Builder(setOf())
|
||||
.withDataUri(mediaUri)
|
||||
.withDataType(mediaType)
|
||||
@@ -76,7 +71,6 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
||||
ThreadUtil.runOnMain {
|
||||
consumer.accept(
|
||||
MultiselectForwardFragmentArgs(
|
||||
isMmsSupported,
|
||||
listOf(multiShareArgs),
|
||||
storySendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND,
|
||||
sendButtonColors = sendButtonColors ?: ViewColorSet.PRIMARY
|
||||
@@ -97,13 +91,11 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
||||
throw AssertionError("Cannot forward view once media")
|
||||
}
|
||||
|
||||
val canSendToNonPush: Boolean = selectedParts.all { Multiselect.canSendToNonPush(context, it) }
|
||||
val multiShareArgs: List<MultiShareArgs> = conversationMessages.map { buildMultiShareArgs(context, it, selectedParts) }
|
||||
|
||||
ThreadUtil.runOnMain {
|
||||
consumer.accept(
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush,
|
||||
multiShareArgs,
|
||||
storySendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ object ConversationDialogs {
|
||||
fun displayCannotStartGroupCallDueToPermissionsDialog(context: Context) {
|
||||
MaterialAlertDialogBuilder(context).setTitle(R.string.ConversationActivity_cant_start_group_call)
|
||||
.setMessage(R.string.ConversationActivity_only_admins_of_this_group_can_start_a_call)
|
||||
.setPositiveButton(R.string.ok) { d: DialogInterface, w: Int -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { d: DialogInterface, w: Int -> d.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.LegacyMmsConnection.Apn;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Database to query APN and MMSC information
|
||||
*/
|
||||
public class ApnDatabase {
|
||||
private static final String TAG = Log.tag(ApnDatabase.class);
|
||||
|
||||
private final SQLiteDatabase db;
|
||||
private final Context context;
|
||||
|
||||
private static final String DATABASE_NAME = "apns.db";
|
||||
private static final String ASSET_PATH = "databases" + File.separator + DATABASE_NAME;
|
||||
|
||||
private static final String TABLE_NAME = "apns";
|
||||
private static final String ID_COLUMN = "_id";
|
||||
private static final String MCC_MNC_COLUMN = "mccmnc";
|
||||
private static final String MCC_COLUMN = "mcc";
|
||||
private static final String MNC_COLUMN = "mnc";
|
||||
private static final String CARRIER_COLUMN = "carrier";
|
||||
private static final String APN_COLUMN = "apn";
|
||||
private static final String MMSC_COLUMN = "mmsc";
|
||||
private static final String PORT_COLUMN = "port";
|
||||
private static final String TYPE_COLUMN = "type";
|
||||
private static final String PROTOCOL_COLUMN = "protocol";
|
||||
private static final String BEARER_COLUMN = "bearer";
|
||||
private static final String ROAMING_PROTOCOL_COLUMN = "roaming_protocol";
|
||||
private static final String CARRIER_ENABLED_COLUMN = "carrier_enabled";
|
||||
private static final String MMS_PROXY_COLUMN = "mmsproxy";
|
||||
private static final String MMS_PORT_COLUMN = "mmsport";
|
||||
private static final String PROXY_COLUMN = "proxy";
|
||||
private static final String MVNO_MATCH_DATA_COLUMN = "mvno_match_data";
|
||||
private static final String MVNO_TYPE_COLUMN = "mvno";
|
||||
private static final String AUTH_TYPE_COLUMN = "authtype";
|
||||
private static final String USER_COLUMN = "user";
|
||||
private static final String PASSWORD_COLUMN = "password";
|
||||
private static final String SERVER_COLUMN = "server";
|
||||
|
||||
private static final String BASE_SELECTION = MCC_MNC_COLUMN + " = ?";
|
||||
|
||||
private static ApnDatabase instance = null;
|
||||
|
||||
public synchronized static ApnDatabase getInstance(Context context) throws IOException {
|
||||
if (instance == null) instance = new ApnDatabase(context.getApplicationContext());
|
||||
return instance;
|
||||
}
|
||||
|
||||
private ApnDatabase(final Context context) throws IOException {
|
||||
this.context = context;
|
||||
|
||||
File dbFile = context.getDatabasePath(DATABASE_NAME);
|
||||
|
||||
if (!dbFile.getParentFile().exists() && !dbFile.getParentFile().mkdir()) {
|
||||
throw new IOException("couldn't make databases directory");
|
||||
}
|
||||
|
||||
StreamUtil.copy(context.getAssets().open(ASSET_PATH, AssetManager.ACCESS_STREAMING),
|
||||
new FileOutputStream(dbFile));
|
||||
|
||||
try {
|
||||
this.db = SQLiteDatabase.openDatabase(context.getDatabasePath(DATABASE_NAME).getPath(),
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
|
||||
} catch (SQLiteException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Apn getCustomApnParameters() {
|
||||
String mmsc = TextSecurePreferences.getMmscUrl(context).trim();
|
||||
|
||||
if (!TextUtils.isEmpty(mmsc) && !mmsc.startsWith("http"))
|
||||
mmsc = "http://" + mmsc;
|
||||
|
||||
String proxy = TextSecurePreferences.getMmscProxy(context);
|
||||
String port = TextSecurePreferences.getMmscProxyPort(context);
|
||||
String user = TextSecurePreferences.getMmscUsername(context);
|
||||
String pass = TextSecurePreferences.getMmscPassword(context);
|
||||
|
||||
return new Apn(mmsc, proxy, port, user, pass);
|
||||
}
|
||||
|
||||
public Apn getDefaultApnParameters(String mccmnc, String apn) {
|
||||
if (mccmnc == null) {
|
||||
Log.w(TAG, "mccmnc was null, returning null");
|
||||
return Apn.EMPTY;
|
||||
}
|
||||
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
if (apn != null) {
|
||||
Log.d(TAG, "Querying table for MCC+MNC " + mccmnc + " and APN name " + apn);
|
||||
cursor = db.query(TABLE_NAME, null,
|
||||
BASE_SELECTION + " AND " + APN_COLUMN + " = ?",
|
||||
new String[] {mccmnc, apn},
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
if (cursor != null) cursor.close();
|
||||
Log.d(TAG, "Querying table for MCC+MNC " + mccmnc + " without APN name");
|
||||
cursor = db.query(TABLE_NAME, null,
|
||||
BASE_SELECTION,
|
||||
new String[] {mccmnc},
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
Apn params = new Apn(cursor.getString(cursor.getColumnIndexOrThrow(MMSC_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(MMS_PROXY_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(MMS_PORT_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(USER_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD_COLUMN)));
|
||||
Log.d(TAG, "Returning preferred APN " + params);
|
||||
return params;
|
||||
}
|
||||
|
||||
Log.w(TAG, "No matching APNs found, returning null");
|
||||
|
||||
return Apn.EMPTY;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Optional<Apn> getMmsConnectionParameters(String mccmnc, String apn) {
|
||||
Apn customApn = getCustomApnParameters();
|
||||
Apn defaultApn = getDefaultApnParameters(mccmnc, apn);
|
||||
Apn result = new Apn(customApn, defaultApn,
|
||||
TextSecurePreferences.getUseCustomMmsc(context),
|
||||
TextSecurePreferences.getUseCustomMmscProxy(context),
|
||||
TextSecurePreferences.getUseCustomMmscProxyPort(context),
|
||||
TextSecurePreferences.getUseCustomMmscUsername(context),
|
||||
TextSecurePreferences.getUseCustomMmscPassword(context));
|
||||
|
||||
if (TextUtils.isEmpty(result.getMmsc())) return Optional.empty();
|
||||
else return Optional.of(result);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class ContentValuesBuilder {
|
||||
|
||||
private final ContentValues contentValues;
|
||||
|
||||
public ContentValuesBuilder(ContentValues contentValues) {
|
||||
this.contentValues = contentValues;
|
||||
}
|
||||
|
||||
public void add(String key, String charsetKey, EncodedStringValue value) {
|
||||
if (value != null) {
|
||||
contentValues.put(key, Util.toIsoString(value.getTextString()));
|
||||
contentValues.put(charsetKey, value.getCharacterSet());
|
||||
}
|
||||
}
|
||||
|
||||
public void add(String contentKey, byte[] value) {
|
||||
if (value != null) {
|
||||
contentValues.put(contentKey, Util.toIsoString(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void add(String contentKey, int b) {
|
||||
if (b != 0)
|
||||
contentValues.put(contentKey, b);
|
||||
}
|
||||
|
||||
public void add(String contentKey, long value) {
|
||||
if (value != -1L)
|
||||
contentValues.put(contentKey, value);
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
return contentValues;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import android.text.SpannableString
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
@@ -305,7 +304,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
READ,
|
||||
MMS_CONTENT_LOCATION,
|
||||
MMS_EXPIRY,
|
||||
MMS_MESSAGE_TYPE,
|
||||
MMS_MESSAGE_SIZE,
|
||||
MMS_STATUS,
|
||||
MMS_TRANSACTION_ID,
|
||||
@@ -2506,7 +2504,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
FROM_RECIPIENT_ID to retrieved.from.serialize(),
|
||||
TO_RECIPIENT_ID to Recipient.self().id.serialize(),
|
||||
TYPE to type,
|
||||
MMS_MESSAGE_TYPE to PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF,
|
||||
THREAD_ID to threadId,
|
||||
MMS_STATUS to MmsStatus.DOWNLOAD_INITIALIZED,
|
||||
DATE_RECEIVED to retrieved.receivedTimeMillis,
|
||||
@@ -2678,17 +2675,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
TrimThreadJob.enqueueAsync(threadId)
|
||||
}
|
||||
|
||||
fun markIncomingNotificationReceived(threadId: Long) {
|
||||
notifyConversationListeners(threadId)
|
||||
|
||||
if (Util.isDefaultSmsProvider(context)) {
|
||||
threads.incrementUnread(threadId, 1, 0)
|
||||
}
|
||||
|
||||
threads.update(threadId, true)
|
||||
TrimThreadJob.enqueueAsync(threadId)
|
||||
}
|
||||
|
||||
fun markGiftRedemptionCompleted(messageId: Long) {
|
||||
markGiftRedemptionState(messageId, GiftBadge.RedemptionState.REDEEMED)
|
||||
}
|
||||
@@ -2874,7 +2860,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(DATE_SENT, message.sentTimeMillis)
|
||||
contentValues.put(MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ)
|
||||
contentValues.put(TYPE, type)
|
||||
contentValues.put(THREAD_ID, threadId)
|
||||
contentValues.put(READ, 1)
|
||||
@@ -4919,13 +4904,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val insertedAttachments: Map<Attachment, AttachmentId>? = null
|
||||
)
|
||||
|
||||
data class MmsNotificationInfo(
|
||||
val from: RecipientId,
|
||||
val contentLocation: String,
|
||||
val transactionId: String,
|
||||
val subscriptionId: Int
|
||||
)
|
||||
|
||||
data class MessageReceiptUpdate(
|
||||
val threadId: Long,
|
||||
val messageId: MessageId,
|
||||
@@ -5043,8 +5021,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
override fun getCurrent(): MessageRecord {
|
||||
val mmsType = cursor.requireLong(MMS_MESSAGE_TYPE)
|
||||
|
||||
return getMediaMmsMessageRecord(cursor)
|
||||
}
|
||||
|
||||
|
||||
@@ -266,8 +266,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
} else if (isSmsExportType()) {
|
||||
int messageResource = SignalStore.misc().getSmsExportPhase().isSmsSupported() ? R.string.MessageRecord__you_will_no_longer_be_able_to_send_sms_messages_from_signal_soon
|
||||
: R.string.MessageRecord__you_can_no_longer_send_sms_messages_in_signal;
|
||||
int messageResource = R.string.MessageRecord__you_can_no_longer_send_sms_messages_in_signal;
|
||||
return fromRecipient(getFromRecipient(), r -> context.getString(messageResource, r.getDisplayName(context)), R.drawable.ic_update_info_16);
|
||||
} else if (isPaymentsRequestToActivate()) {
|
||||
return isOutgoing() ? fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_you_sent_request, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments)
|
||||
|
||||
@@ -59,9 +59,7 @@ public class CreateGroupActivity extends ContactSelectionActivity implements Con
|
||||
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.create_group_activity);
|
||||
|
||||
boolean smsEnabled = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
int displayMode = smsEnabled ? ContactSelectionDisplayMode.FLAG_SMS | ContactSelectionDisplayMode.FLAG_PUSH
|
||||
: ContactSelectionDisplayMode.FLAG_PUSH;
|
||||
int displayMode = ContactSelectionDisplayMode.FLAG_PUSH;
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.groupLimits().excludingSelf());
|
||||
|
||||
@@ -162,8 +162,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
MediaConstraints mediaConstraints = mms ? MediaConstraints.getMmsMediaConstraints(mmsSubscriptionId)
|
||||
: MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(transformProperties.sentMediaQuality));
|
||||
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(transformProperties.sentMediaQuality));
|
||||
|
||||
compress(database, mediaConstraints, databaseAttachment);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class ConversationShortcutUpdateJob extends BaseJob {
|
||||
int maxShortcuts = ConversationUtil.getMaxShortcuts(context);
|
||||
List<Recipient> ranked = new ArrayList<>(maxShortcuts);
|
||||
|
||||
try (ThreadTable.Reader reader = threadTable.readerFor(threadTable.getRecentConversationList(maxShortcuts, false, false, false, true, !Util.isDefaultSmsProvider(context), false))) {
|
||||
try (ThreadTable.Reader reader = threadTable.readerFor(threadTable.getRecentConversationList(maxShortcuts, false, false, false, true, true, false))) {
|
||||
ThreadRecord record;
|
||||
while ((record = reader.getNext()) != null) {
|
||||
ranked.add(record.getRecipient().resolve());
|
||||
|
||||
@@ -139,7 +139,6 @@ public final class JobManagerFactories {
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory());
|
||||
put(MarkerJob.KEY, new MarkerJob.Factory());
|
||||
put(MmsSendJob.KEY, new MmsSendJob.Factory());
|
||||
put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory());
|
||||
put(MultiDeviceCallLinkSyncJob.KEY, new MultiDeviceCallLinkSyncJob.Factory());
|
||||
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
|
||||
@@ -206,8 +205,6 @@ public final class JobManagerFactories {
|
||||
put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory());
|
||||
put(ResetSvrGuessCountJob.KEY, new ResetSvrGuessCountJob.Factory());
|
||||
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
|
||||
put(SmsSendJob.KEY, new SmsSendJob.Factory());
|
||||
put(SmsSentJob.KEY, new SmsSentJob.Factory());
|
||||
put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory());
|
||||
put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory());
|
||||
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
|
||||
@@ -312,6 +309,9 @@ public final class JobManagerFactories {
|
||||
put("StoryReadStateMigrationJob", new PassingMigrationJob.Factory());
|
||||
put("GroupV1MigrationJob", new FailingJob.Factory());
|
||||
put("NewRegistrationUsernameSyncJob", new FailingJob.Factory());
|
||||
put("SmsSendJob", new FailingJob.Factory());
|
||||
put("SmsSentJob", new FailingJob.Factory());
|
||||
put("MmsSendJobV2", new FailingJob.Factory());
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.mms.dom.smil.parser.SmilXmlSerializer;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.ContentType;
|
||||
import com.google.android.mms.InvalidHeaderValueException;
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
import com.google.android.mms.pdu_alt.PduBody;
|
||||
import com.google.android.mms.pdu_alt.PduComposer;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.PduPart;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
import com.google.android.mms.pdu_alt.SendReq;
|
||||
import com.google.android.mms.smil.SmilHelper;
|
||||
import com.klinker.android.send_message.Utils;
|
||||
|
||||
import org.signal.core.util.Hex;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.MmsSendResult;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class MmsSendJob extends SendJob {
|
||||
|
||||
public static final String KEY = "MmsSendJobV2";
|
||||
|
||||
private static final String TAG = Log.tag(MmsSendJob.class);
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
private final long messageId;
|
||||
|
||||
private MmsSendJob(long messageId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("mms-operation")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(15)
|
||||
.build(),
|
||||
messageId);
|
||||
}
|
||||
|
||||
/** Enqueues compression jobs for attachments and finally the MMS send job. */
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId) {
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
OutgoingMessage message;
|
||||
|
||||
try {
|
||||
message = database.getOutgoingMessage(messageId);
|
||||
} catch (MmsException | NoSuchMessageException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
List<Job> compressionJobs = Stream.of(message.getAttachments())
|
||||
.map(a -> (Job) AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, true, -1))
|
||||
.toList();
|
||||
|
||||
MmsSendJob sendJob = new MmsSendJob(messageId);
|
||||
|
||||
jobManager.startChain(compressionJobs)
|
||||
.then(sendJob)
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
private MmsSendJob(@NonNull Job.Parameters parameters, long messageId) {
|
||||
super(parameters);
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] serialize() {
|
||||
return new JsonJobData.Builder().putLong(KEY_MESSAGE_ID, messageId).serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
SignalDatabase.messages().markAsSending(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSend() throws MmsException, NoSuchMessageException, IOException {
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
OutgoingMessage message = database.getOutgoingMessage(messageId);
|
||||
|
||||
if (database.isSent(messageId)) {
|
||||
Log.w(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Sending message: " + messageId);
|
||||
|
||||
SendReq pdu = constructSendPdu(message);
|
||||
|
||||
validateDestinations(message, pdu);
|
||||
|
||||
final byte[] pduBytes = getPduBytes(pdu);
|
||||
final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, -1);
|
||||
final MmsSendResult result = getSendResult(sendConf, pdu);
|
||||
|
||||
database.markAsSent(messageId, false);
|
||||
markAttachmentsUploaded(messageId, message);
|
||||
|
||||
Log.i(TAG, "Sent message: " + messageId);
|
||||
} catch (UndeliverableMessageException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
} catch (InsecureFallbackApprovalException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsPendingInsecureSmsFallback(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
Log.i(TAG, JobLogger.format(this, "onFailure() messageId: " + messageId));
|
||||
SignalDatabase.messages().markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
|
||||
private byte[] getPduBytes(SendReq message)
|
||||
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
|
||||
{
|
||||
byte[] pduBytes = new PduComposer(context, message).make();
|
||||
|
||||
if (pduBytes == null) {
|
||||
throw new UndeliverableMessageException("PDU composition failed, null payload");
|
||||
}
|
||||
|
||||
return pduBytes;
|
||||
}
|
||||
|
||||
private MmsSendResult getSendResult(SendConf conf, SendReq message)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
if (conf == null) {
|
||||
throw new UndeliverableMessageException("No M-Send.conf received in response to send.");
|
||||
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
|
||||
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus());
|
||||
} else if (isInconsistentResponse(message, conf)) {
|
||||
throw new UndeliverableMessageException("Mismatched response!");
|
||||
} else {
|
||||
return new MmsSendResult(conf.getMessageId(), conf.getResponseStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInconsistentResponse(SendReq message, SendConf response) {
|
||||
Log.i(TAG, "Comparing: " + Hex.toString(message.getTransactionId()));
|
||||
Log.i(TAG, "With: " + Hex.toString(response.getTransactionId()));
|
||||
return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
|
||||
}
|
||||
|
||||
private void validateDestinations(EncodedStringValue[] destinations) throws UndeliverableMessageException {
|
||||
if (destinations == null) return;
|
||||
|
||||
for (EncodedStringValue destination : destinations) {
|
||||
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
|
||||
throw new UndeliverableMessageException("Invalid destination: " +
|
||||
(destination == null ? null : destination.getString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDestinations(OutgoingMessage media, SendReq message) throws UndeliverableMessageException {
|
||||
validateDestinations(message.getTo());
|
||||
validateDestinations(message.getCc());
|
||||
validateDestinations(message.getBcc());
|
||||
|
||||
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
|
||||
throw new UndeliverableMessageException("No to, cc, or bcc specified!");
|
||||
}
|
||||
|
||||
if (media.isSecure()) {
|
||||
throw new UndeliverableMessageException("Attempt to send encrypted MMS?");
|
||||
}
|
||||
}
|
||||
|
||||
private SendReq constructSendPdu(OutgoingMessage message)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
SendReq req = new SendReq();
|
||||
String lineNumber = getMyNumber(context);
|
||||
MediaConstraints mediaConstraints = MediaConstraints.getMmsMediaConstraints(-1);
|
||||
List<Attachment> scaledAttachments = message.getAttachments();
|
||||
|
||||
if (!TextUtils.isEmpty(lineNumber)) {
|
||||
req.setFrom(new EncodedStringValue(lineNumber));
|
||||
} else {
|
||||
req.setFrom(new EncodedStringValue(SignalStore.account().getE164()));
|
||||
}
|
||||
|
||||
if (message.getThreadRecipient().isMmsGroup()) {
|
||||
List<Recipient> members = SignalDatabase.groups().getGroupMembers(message.getThreadRecipient().requireGroupId(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
for (Recipient member : members) {
|
||||
if (!member.hasSmsAddress()) {
|
||||
throw new UndeliverableMessageException("One of the group recipients did not have an SMS address! " + member.getId());
|
||||
}
|
||||
|
||||
if (message.getDistributionType() == ThreadTable.DistributionTypes.BROADCAST) {
|
||||
req.addBcc(new EncodedStringValue(member.requireSmsAddress()));
|
||||
} else {
|
||||
req.addTo(new EncodedStringValue(member.requireSmsAddress()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!message.getThreadRecipient().hasSmsAddress()) {
|
||||
throw new UndeliverableMessageException("Recipient did not have an SMS address! " + message.getThreadRecipient().getId());
|
||||
}
|
||||
|
||||
req.addTo(new EncodedStringValue(message.getThreadRecipient().requireSmsAddress()));
|
||||
}
|
||||
|
||||
req.setDate(System.currentTimeMillis() / 1000);
|
||||
|
||||
PduBody body = new PduBody();
|
||||
int size = 0;
|
||||
|
||||
if (!TextUtils.isEmpty(message.getBody())) {
|
||||
PduPart part = new PduPart();
|
||||
String name = String.valueOf(System.currentTimeMillis());
|
||||
part.setData(Util.toUtf8Bytes(message.getBody()));
|
||||
part.setCharset(CharacterSets.UTF_8);
|
||||
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
|
||||
part.setContentId(name.getBytes());
|
||||
part.setContentLocation((name + ".txt").getBytes());
|
||||
part.setName((name + ".txt").getBytes());
|
||||
|
||||
body.addPart(part);
|
||||
size += getPartSize(part);
|
||||
}
|
||||
|
||||
for (Attachment attachment : scaledAttachments) {
|
||||
try {
|
||||
if (attachment.getUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!");
|
||||
|
||||
String fileName = attachment.fileName;
|
||||
PduPart part = new PduPart();
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = String.valueOf(Math.abs(new SecureRandom().nextLong()));
|
||||
String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.contentType);
|
||||
|
||||
if (fileExtension != null) fileName = fileName + "." + fileExtension;
|
||||
}
|
||||
|
||||
if (attachment.contentType.startsWith("text")) {
|
||||
part.setCharset(CharacterSets.UTF_8);
|
||||
}
|
||||
|
||||
part.setContentType(attachment.contentType.getBytes());
|
||||
part.setContentLocation(fileName.getBytes());
|
||||
part.setName(fileName.getBytes());
|
||||
|
||||
int index = fileName.lastIndexOf(".");
|
||||
String contentId = (index == -1) ? fileName : fileName.substring(0, index);
|
||||
part.setContentId(contentId.getBytes());
|
||||
part.setData(StreamUtil.readFully(PartAuthority.getAttachmentStream(context, attachment.getUri())));
|
||||
|
||||
body.addPart(part);
|
||||
size += getPartSize(part);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
SmilXmlSerializer.serialize(SmilHelper.createSmilDocument(body), out);
|
||||
PduPart smilPart = new PduPart();
|
||||
smilPart.setContentId("smil".getBytes());
|
||||
smilPart.setContentLocation("smil.xml".getBytes());
|
||||
smilPart.setContentType(ContentType.APP_SMIL.getBytes());
|
||||
smilPart.setData(out.toByteArray());
|
||||
body.addPart(0, smilPart);
|
||||
|
||||
req.setBody(body);
|
||||
req.setMessageSize(size);
|
||||
req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes());
|
||||
req.setExpiry(7 * 24 * 60 * 60);
|
||||
|
||||
try {
|
||||
req.setPriority(PduHeaders.PRIORITY_NORMAL);
|
||||
req.setDeliveryReport(PduHeaders.VALUE_NO);
|
||||
req.setReadReport(PduHeaders.VALUE_NO);
|
||||
} catch (InvalidHeaderValueException e) {}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
private long getPartSize(PduPart part) {
|
||||
return part.getName().length + part.getContentLocation().length +
|
||||
part.getContentType().length + part.getData().length +
|
||||
part.getContentId().length;
|
||||
}
|
||||
|
||||
private void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
|
||||
long threadId = SignalDatabase.messages().getThreadIdForMessage(messageId);
|
||||
Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
|
||||
|
||||
if (recipient != null) {
|
||||
ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.forConversation(threadId));
|
||||
}
|
||||
}
|
||||
|
||||
private String getMyNumber(Context context) throws UndeliverableMessageException {
|
||||
try {
|
||||
return Utils.getMyPhoneNumber(context);
|
||||
} catch (SecurityException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<MmsSendJob> {
|
||||
@Override
|
||||
public @NonNull MmsSendJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
|
||||
JsonJobData data = JsonJobData.deserialize(serializedData);
|
||||
return new MmsSendJob(parameters, data.getLong(KEY_MESSAGE_ID));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.telephony.SmsManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.PendingIntentFlags;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class SmsSendJob extends SendJob {
|
||||
|
||||
public static final String KEY = "SmsSendJob";
|
||||
|
||||
private static final String TAG = Log.tag(SmsSendJob.class);
|
||||
private static final int MAX_ATTEMPTS = 15;
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_RUN_ATTEMPT = "run_attempt";
|
||||
|
||||
private final long messageId;
|
||||
private final int runAttempt;
|
||||
|
||||
public SmsSendJob(long messageId, @NonNull Recipient destination) {
|
||||
this(messageId, destination, 0);
|
||||
}
|
||||
|
||||
public SmsSendJob(long messageId, @NonNull Recipient destination, int runAttempt) {
|
||||
this(constructParameters(destination), messageId, runAttempt);
|
||||
}
|
||||
|
||||
private SmsSendJob(@NonNull Job.Parameters parameters, long messageId, int runAttempt) {
|
||||
super(parameters);
|
||||
|
||||
this.messageId = messageId;
|
||||
this.runAttempt = runAttempt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] serialize() {
|
||||
return new JsonJobData.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putInt(KEY_RUN_ATTEMPT, runAttempt)
|
||||
.serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
SignalDatabase.messages().markAsSending(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSend() throws NoSuchMessageException, TooManyRetriesException, UndeliverableMessageException {
|
||||
if (runAttempt >= MAX_ATTEMPTS) {
|
||||
warn(TAG, "Hit the retry limit. Failing.");
|
||||
throw new TooManyRetriesException();
|
||||
}
|
||||
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
MessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
if (!record.isPending() && !record.isFailed()) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!record.getToRecipient().hasSmsAddress()) {
|
||||
throw new UndeliverableMessageException("Recipient didn't have an SMS address! " + record.getToRecipient().getId());
|
||||
}
|
||||
|
||||
try {
|
||||
log(TAG, String.valueOf(record.getDateSent()), "Sending message: " + messageId + " (attempt " + runAttempt + ")");
|
||||
deliver(record);
|
||||
log(TAG, String.valueOf(record.getDateSent()), "Sent message: " + messageId);
|
||||
} catch (UndeliverableMessageException ude) {
|
||||
warn(TAG, ude);
|
||||
SignalDatabase.messages().markAsSentFailed(record.getId());
|
||||
ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getToRecipient(), ConversationId.fromMessageRecord(record));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
warn(TAG, "onFailure() messageId: " + messageId);
|
||||
long threadId = SignalDatabase.messages().getThreadIdForMessage(messageId);
|
||||
Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
|
||||
|
||||
SignalDatabase.messages().markAsSentFailed(messageId);
|
||||
|
||||
if (threadId != -1 && recipient != null) {
|
||||
ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.forConversation(threadId));
|
||||
} else {
|
||||
Log.w(TAG, "Could not find message! threadId: " + threadId + ", recipient: " + (recipient != null ? recipient.getId().toString() : "null"));
|
||||
}
|
||||
}
|
||||
|
||||
private void deliver(MessageRecord message)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) {
|
||||
throw new UndeliverableMessageException("Trying to send a secure SMS?");
|
||||
}
|
||||
|
||||
String recipient = message.getToRecipient().requireSmsAddress();
|
||||
|
||||
// See issue #1516 for bug report, and discussion on commits related to #4833 for problems
|
||||
// related to the original fix to #1516. This still may not be a correct fix if networks allow
|
||||
// SMS/MMS sending to alphanumeric recipients other than email addresses, but should also
|
||||
// help to fix issue #3099.
|
||||
if (!NumberUtil.isValidEmail(recipient)) {
|
||||
recipient = PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(recipient));
|
||||
}
|
||||
|
||||
if (!NumberUtil.isValidSmsOrEmail(recipient)) {
|
||||
throw new UndeliverableMessageException("Not a valid SMS destination! " + recipient);
|
||||
}
|
||||
|
||||
SmsManager smsManager = getSmsManagerFor(message.getSubscriptionId());
|
||||
ArrayList<String> messages = smsManager.divideMessage(message.getBody());
|
||||
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages);
|
||||
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
|
||||
|
||||
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
|
||||
// and messages, this will throw an NPE. We have no idea why, so we're just
|
||||
// catching it and marking the message as a failure. That way at least it doesn't
|
||||
// repeatedly crash every time you start the app.
|
||||
try {
|
||||
smsManager.sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
|
||||
} catch (NullPointerException | IllegalArgumentException npe) {
|
||||
warn(TAG, npe);
|
||||
log(TAG, String.valueOf(message.getDateSent()), "Recipient: " + recipient);
|
||||
log(TAG, String.valueOf(message.getDateSent()), "Message Parts: " + messages.size());
|
||||
|
||||
try {
|
||||
for (int i=0;i<messages.size();i++) {
|
||||
smsManager.sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i),
|
||||
deliveredIntents == null ? null : deliveredIntents.get(i));
|
||||
}
|
||||
} catch (NullPointerException | IllegalArgumentException npe2) {
|
||||
warn(TAG, npe);
|
||||
throw new UndeliverableMessageException(npe2);
|
||||
}
|
||||
} catch (SecurityException se) {
|
||||
warn(TAG, se);
|
||||
throw new UndeliverableMessageException(se);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type, ArrayList<String> messages)
|
||||
{
|
||||
ArrayList<PendingIntent> sentIntents = new ArrayList<>(messages.size());
|
||||
boolean isMultipart = messages.size() > 1;
|
||||
|
||||
for (String ignored : messages) {
|
||||
sentIntents.add(PendingIntent.getBroadcast(context, 0,
|
||||
constructSentIntent(context, messageId, type, isMultipart),
|
||||
PendingIntentFlags.mutable()));
|
||||
}
|
||||
|
||||
return sentIntents;
|
||||
}
|
||||
|
||||
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
|
||||
if (!SignalStore.settings().isSmsDeliveryReportsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>(messages.size());
|
||||
boolean isMultipart = messages.size() > 1;
|
||||
|
||||
for (String ignored : messages) {
|
||||
deliveredIntents.add(PendingIntent.getBroadcast(context, 0,
|
||||
constructDeliveredIntent(context, messageId, type, isMultipart),
|
||||
PendingIntentFlags.mutable()));
|
||||
}
|
||||
|
||||
return deliveredIntents;
|
||||
}
|
||||
|
||||
private Intent constructSentIntent(Context context, long messageId, long type, boolean isMultipart) {
|
||||
Intent pending = new Intent(SmsDeliveryListener.SENT_SMS_ACTION,
|
||||
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
|
||||
context, SmsDeliveryListener.class);
|
||||
|
||||
pending.putExtra("type", type);
|
||||
pending.putExtra("message_id", messageId);
|
||||
pending.putExtra("run_attempt", Math.max(runAttempt, getRunAttempt()));
|
||||
pending.putExtra("is_multipart", isMultipart);
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
private Intent constructDeliveredIntent(Context context, long messageId, long type, boolean isMultipart) {
|
||||
Intent pending = new Intent(SmsDeliveryListener.DELIVERED_SMS_ACTION,
|
||||
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
|
||||
context, SmsDeliveryListener.class);
|
||||
pending.putExtra("type", type);
|
||||
pending.putExtra("message_id", messageId);
|
||||
pending.putExtra("is_multipart", isMultipart);
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
private SmsManager getSmsManagerFor(int subscriptionId) {
|
||||
if (Build.VERSION.SDK_INT >= 22 && subscriptionId != -1) {
|
||||
return SmsManager.getSmsManagerForSubscriptionId(subscriptionId);
|
||||
} else {
|
||||
return SmsManager.getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private static Job.Parameters constructParameters(@NonNull Recipient destination) {
|
||||
return new Job.Parameters.Builder()
|
||||
.setMaxAttempts(MAX_ATTEMPTS)
|
||||
.setQueue(destination.getId().toQueueKey() + "::SMS")
|
||||
.addConstraint(NetworkOrCellServiceConstraint.KEY)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class TooManyRetriesException extends Exception { }
|
||||
|
||||
public static class Factory implements Job.Factory<SmsSendJob> {
|
||||
@Override
|
||||
public @NonNull SmsSendJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
|
||||
JsonJobData data = JsonJobData.deserialize(serializedData);
|
||||
return new SmsSendJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getInt(KEY_RUN_ATTEMPT));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.telephony.SmsManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
|
||||
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
|
||||
|
||||
public class SmsSentJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "SmsSentJob";
|
||||
|
||||
private static final String TAG = Log.tag(SmsSentJob.class);
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_IS_MULTIPART = "is_multipart";
|
||||
private static final String KEY_ACTION = "action";
|
||||
private static final String KEY_RESULT = "result";
|
||||
private static final String KEY_RUN_ATTEMPT = "run_attempt";
|
||||
|
||||
private final long messageId;
|
||||
private final boolean isMultipart;
|
||||
private final String action;
|
||||
private final int result;
|
||||
private final int runAttempt;
|
||||
|
||||
public SmsSentJob(long messageId, boolean isMultipart, String action, int result, int runAttempt) {
|
||||
this(new Job.Parameters.Builder().build(),
|
||||
messageId,
|
||||
isMultipart,
|
||||
action,
|
||||
result,
|
||||
runAttempt);
|
||||
}
|
||||
|
||||
private SmsSentJob(@NonNull Job.Parameters parameters, long messageId, boolean isMultipart, String action, int result, int runAttempt) {
|
||||
super(parameters);
|
||||
|
||||
this.messageId = messageId;
|
||||
this.isMultipart = isMultipart;
|
||||
this.action = action;
|
||||
this.result = result;
|
||||
this.runAttempt = runAttempt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] serialize() {
|
||||
return new JsonJobData.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putBoolean(KEY_IS_MULTIPART, isMultipart)
|
||||
.putString(KEY_ACTION, action)
|
||||
.putInt(KEY_RESULT, result)
|
||||
.putInt(KEY_RUN_ATTEMPT, runAttempt)
|
||||
.serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() {
|
||||
Log.i(TAG, "Got SMS callback: " + action + " , " + result);
|
||||
|
||||
switch (action) {
|
||||
case SmsDeliveryListener.SENT_SMS_ACTION:
|
||||
handleSentResult(messageId, result);
|
||||
break;
|
||||
case SmsDeliveryListener.DELIVERED_SMS_ACTION:
|
||||
handleDeliveredResult(messageId, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
private void handleDeliveredResult(long messageId, int result) {
|
||||
SignalDatabase.messages().markSmsStatus(messageId, result);
|
||||
}
|
||||
|
||||
private void handleSentResult(long messageId, int result) {
|
||||
try {
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
MessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
switch (result) {
|
||||
case Activity.RESULT_OK:
|
||||
database.markAsSent(messageId, false);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_NO_SERVICE:
|
||||
case SmsManager.RESULT_ERROR_RADIO_OFF:
|
||||
if (isMultipart) {
|
||||
Log.w(TAG, "Service connectivity problem, but not retrying due to multipart");
|
||||
database.markAsSentFailed(messageId);
|
||||
ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getToRecipient(), ConversationId.forConversation(record.getThreadId()));
|
||||
} else {
|
||||
Log.w(TAG, "Service connectivity problem, requeuing...");
|
||||
ApplicationDependencies.getJobManager().add(new SmsSendJob(messageId, record.getToRecipient(), runAttempt + 1));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
database.markAsSentFailed(messageId);
|
||||
ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getToRecipient(), ConversationId.forConversation(record.getThreadId()));
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<SmsSentJob> {
|
||||
@Override
|
||||
public @NonNull SmsSentJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
|
||||
JsonJobData data = JsonJobData.deserialize(serializedData);
|
||||
|
||||
return new SmsSentJob(parameters,
|
||||
data.getLong(KEY_MESSAGE_ID),
|
||||
data.getBooleanOrDefault(KEY_IS_MULTIPART, true),
|
||||
data.getString(KEY_ACTION),
|
||||
data.getInt(KEY_RESULT),
|
||||
data.getInt(KEY_RUN_ATTEMPT));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,10 +235,6 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
putBoolean(PNI_INITIALIZED_DEVICES, value);
|
||||
}
|
||||
|
||||
public @NonNull SmsExportPhase getSmsExportPhase() {
|
||||
return SmsExportPhase.getCurrentPhase();
|
||||
}
|
||||
|
||||
public void setHasLinkedDevices(boolean value) {
|
||||
putBoolean(HAS_LINKED_DEVICES, value);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ public final class SettingsValues extends SignalStoreValues {
|
||||
public static final String CALL_RINGTONE = "settings.call.ringtone";
|
||||
public static final String CALL_VIBRATE_ENABLED = "settings.call.vibrate.enabled";
|
||||
public static final String NOTIFY_WHEN_CONTACT_JOINS_SIGNAL = "settings.notify.when.contact.joins.signal";
|
||||
private static final String DEFAULT_SMS = "settings.default_sms";
|
||||
private static final String UNIVERSAL_EXPIRE_TIMER = "settings.universal.expire.timer";
|
||||
private static final String SENT_MEDIA_QUALITY = "settings.sentMediaQuality";
|
||||
private static final String CENSORSHIP_CIRCUMVENTION_ENABLED = "settings.censorshipCircumventionEnabled";
|
||||
@@ -411,26 +410,6 @@ public final class SettingsValues extends SignalStoreValues {
|
||||
putBoolean(NOTIFY_WHEN_CONTACT_JOINS_SIGNAL, notifyWhenContactJoinsSignal);
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to keep track of when the default status changes so we can sync to storage service.
|
||||
* So call this when you think it might have changed, but *don't* rely on it for knowing if we
|
||||
* *are* the default SMS. For that, continue to use
|
||||
* {@link org.thoughtcrime.securesms.util.Util#isDefaultSmsProvider(Context)}.
|
||||
*/
|
||||
public void setDefaultSms(boolean value) {
|
||||
boolean lastKnown = getBoolean(DEFAULT_SMS, false);
|
||||
|
||||
if (value != lastKnown && SignalStore.registrationValues().isRegistrationComplete()) {
|
||||
Log.i(TAG, "Default SMS state changed! Scheduling a storage sync.");
|
||||
putBoolean(DEFAULT_SMS, value);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
SignalDatabase.recipients().markNeedsSync(Recipient.self().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void setUniversalExpireTimer(int seconds) {
|
||||
putInteger(UNIVERSAL_EXPIRE_TIMER, seconds);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
enum class SmsExportPhase(val duration: Long) {
|
||||
PHASE_3(0);
|
||||
|
||||
fun allowSmsFeatures(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun isSmsSupported(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun isBlockingUi(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getCurrentPhase(): SmsExportPhase {
|
||||
return PHASE_3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ internal class ConfirmSvrPinFragment : BaseSvrPinFragment<ConfirmSvrPinViewModel
|
||||
MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.ConfirmKbsPinFragment__pin_creation_failed)
|
||||
.setMessage(R.string.ConfirmKbsPinFragment__your_pin_was_not_saved)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { d: DialogInterface, w: Int ->
|
||||
.setPositiveButton(android.R.string.ok) { d: DialogInterface, w: Int ->
|
||||
d.dismiss()
|
||||
markMegaphoneSeenIfNecessary()
|
||||
requireActivity().setResult(Activity.RESULT_CANCELED)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.thoughtcrime.securesms.logsubmit
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
/**
|
||||
* Prints off the current SMS settings
|
||||
*/
|
||||
|
||||
class LogSectionSMS : LogSection {
|
||||
override fun getTitle(): String = "SMS"
|
||||
|
||||
override fun getContent(context: Context): CharSequence {
|
||||
val isDefaultSMS = Util.isDefaultSmsProvider(context)
|
||||
val settings = SignalStore.settings()
|
||||
val output = StringBuilder()
|
||||
|
||||
output.append("Default SMS : ${isDefaultSMS}\n")
|
||||
output.append("SMS delivery reports : ${settings.isSmsDeliveryReportsEnabled}\n")
|
||||
output.append("WiFi SMS : ${settings.isWifiCallingCompatibilityModeEnabled}\n")
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,6 @@ public class SubmitDebugLogRepository {
|
||||
add(new LogSectionNotificationProfiles());
|
||||
add(new LogSectionExoPlayerPool());
|
||||
add(new LogSectionKeyPreferences());
|
||||
add(new LogSectionSMS());
|
||||
add(new LogSectionStories());
|
||||
add(new LogSectionBadges());
|
||||
add(new LogSectionPermissions());
|
||||
|
||||
@@ -79,7 +79,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
|
||||
|
||||
@Override
|
||||
public void onCameraError() {
|
||||
Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, R.string.default_error_msg, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ class MediaSelectionRepository(context: Context) {
|
||||
stateMap: Map<Uri, Any>,
|
||||
quality: SentMediaQuality,
|
||||
message: CharSequence?,
|
||||
isSms: Boolean,
|
||||
isViewOnce: Boolean,
|
||||
singleContact: ContactSearchKey.RecipientSearchKey?,
|
||||
contacts: List<ContactSearchKey.RecipientSearchKey>,
|
||||
@@ -89,10 +88,6 @@ class MediaSelectionRepository(context: Context) {
|
||||
sendType: MessageSendType,
|
||||
scheduledTime: Long = -1
|
||||
): Maybe<MediaSendActivityResult> {
|
||||
if (isSms && contacts.isNotEmpty()) {
|
||||
throw IllegalStateException("Provided recipients to send to, but this is SMS!")
|
||||
}
|
||||
|
||||
if (selectedMedia.isEmpty()) {
|
||||
throw IllegalStateException("No selected media!")
|
||||
}
|
||||
@@ -121,8 +116,8 @@ class MediaSelectionRepository(context: Context) {
|
||||
StoryType.NONE
|
||||
}
|
||||
|
||||
if (isSms || MessageSender.isLocalSelfSend(context, singleRecipient, SendType.SIGNAL)) {
|
||||
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.")
|
||||
if (MessageSender.isLocalSelfSend(context, singleRecipient, SendType.SIGNAL)) {
|
||||
Log.i(TAG, "Local self-send. Skipping pre-upload.")
|
||||
emitter.onSuccess(
|
||||
MediaSendActivityResult(
|
||||
recipientId = singleRecipient!!.id,
|
||||
@@ -207,7 +202,7 @@ class MediaSelectionRepository(context: Context) {
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "Got empty upload results! isSms: $isSms, updatedMedia.size(): ${updatedMedia.size}, isViewOnce: $isViewOnce, target: $singleContact")
|
||||
Log.w(TAG, "Got empty upload results! updatedMedia.size(): ${updatedMedia.size}, isViewOnce: $isViewOnce, target: $singleContact")
|
||||
emitter.onSuccess(
|
||||
MediaSendActivityResult(
|
||||
recipientId = singleRecipient!!.id,
|
||||
@@ -249,8 +244,8 @@ class MediaSelectionRepository(context: Context) {
|
||||
uploadRepository.deleteAbandonedAttachments()
|
||||
}
|
||||
|
||||
fun isLocalSelfSend(recipient: Recipient?, isSms: Boolean): Boolean {
|
||||
return MessageSender.isLocalSelfSend(context, recipient, if (isSms) MessageSender.SendType.SMS else MessageSender.SendType.SIGNAL)
|
||||
fun isLocalSelfSend(recipient: Recipient?): Boolean {
|
||||
return MessageSender.isLocalSelfSend(context, recipient, SendType.SIGNAL)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.net.Uri
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendConstants
|
||||
import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
@@ -37,11 +36,7 @@ data class MediaSelectionState(
|
||||
|
||||
val transcodingPreset: TranscodingPreset = MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(quality.code)).videoTranscodingSettings
|
||||
|
||||
val maxSelection = if (sendType.usesSmsTransport) {
|
||||
MediaSendConstants.MAX_SMS
|
||||
} else {
|
||||
FeatureFlags.maxAttachmentCount()
|
||||
}
|
||||
val maxSelection = FeatureFlags.maxAttachmentCount()
|
||||
|
||||
val canSend = !isSent && selectedMedia.isNotEmpty()
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ class MediaSelectionViewModel(
|
||||
store.update {
|
||||
it.copy(
|
||||
isMeteredConnection = metered,
|
||||
isPreUploadEnabled = shouldPreUpload(metered, it.sendType.usesSmsTransport, it.recipient)
|
||||
isPreUploadEnabled = shouldPreUpload(metered, it.recipient)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class MediaSelectionViewModel(
|
||||
store.update(Recipient.live(recipientSearchKey.recipientId).liveData) { r, s ->
|
||||
s.copy(
|
||||
recipient = r,
|
||||
isPreUploadEnabled = shouldPreUpload(s.isMeteredConnection, s.sendType.usesSmsTransport, r)
|
||||
isPreUploadEnabled = shouldPreUpload(s.isMeteredConnection, r)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -320,11 +320,7 @@ class MediaSelectionViewModel(
|
||||
}
|
||||
|
||||
fun getMediaConstraints(): MediaConstraints {
|
||||
return if (store.state.sendType.usesSmsTransport) {
|
||||
MediaConstraints.getMmsMediaConstraints(store.state.sendType.simSubscriptionId ?: -1)
|
||||
} else {
|
||||
MediaConstraints.getPushMediaConstraints()
|
||||
}
|
||||
return MediaConstraints.getPushMediaConstraints()
|
||||
}
|
||||
|
||||
fun setSentMediaQuality(sentMediaQuality: SentMediaQuality) {
|
||||
@@ -396,7 +392,6 @@ class MediaSelectionViewModel(
|
||||
stateMap = store.state.editorStateMap,
|
||||
quality = store.state.quality,
|
||||
message = store.state.message,
|
||||
isSms = store.state.sendType.usesSmsTransport,
|
||||
isViewOnce = isViewOnceEnabled(),
|
||||
singleContact = destination.getRecipientSearchKey(),
|
||||
contacts = selectedContacts.ifEmpty { destination.getRecipientSearchKeyList() },
|
||||
@@ -409,8 +404,7 @@ class MediaSelectionViewModel(
|
||||
}
|
||||
|
||||
private fun isViewOnceEnabled(): Boolean {
|
||||
return !store.state.sendType.usesSmsTransport &&
|
||||
store.state.selectedMedia.size == 1 &&
|
||||
return store.state.selectedMedia.size == 1 &&
|
||||
store.state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE
|
||||
}
|
||||
|
||||
@@ -432,8 +426,8 @@ class MediaSelectionViewModel(
|
||||
repository.uploadRepository.cancelUpload(media)
|
||||
}
|
||||
|
||||
private fun shouldPreUpload(metered: Boolean, isSms: Boolean, recipient: Recipient?): Boolean {
|
||||
return !metered && !isSms && !repository.isLocalSelfSend(recipient, isSms)
|
||||
private fun shouldPreUpload(metered: Boolean, recipient: Recipient?): Boolean {
|
||||
return !metered && !repository.isLocalSelfSend(recipient)
|
||||
}
|
||||
|
||||
fun onSaveState(outState: Bundle) {
|
||||
|
||||
@@ -205,7 +205,6 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
|
||||
if (sharedViewModel.isContactSelectionRequired) {
|
||||
val args = MultiselectForwardFragmentArgs(
|
||||
false,
|
||||
title = R.string.MediaReviewFragment__send_to,
|
||||
storySendRequirements = sharedViewModel.getStorySendRequirements(),
|
||||
isSearchEnabled = !sharedViewModel.isStory(),
|
||||
|
||||
@@ -146,7 +146,6 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati
|
||||
StoriesMultiselectForwardActivity.Args(
|
||||
MultiselectForwardFragmentArgs(
|
||||
title = R.string.MediaReviewFragment__send_to,
|
||||
canSendToNonPush = false,
|
||||
storySendRequirements = Stories.MediaTransform.SendRequirements.VALID_DURATION,
|
||||
isSearchEnabled = false
|
||||
),
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class ApnUnavailableException extends Exception {
|
||||
|
||||
public ApnUnavailableException() {
|
||||
}
|
||||
|
||||
public ApnUnavailableException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public ApnUnavailableException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public ApnUnavailableException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CompatMmsConnection implements OutgoingMmsConnection, IncomingMmsConnection {
|
||||
private static final String TAG = Log.tag(CompatMmsConnection.class);
|
||||
|
||||
private Context context;
|
||||
|
||||
public CompatMmsConnection(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SendConf send(@NonNull byte[] pduBytes, int subscriptionId)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
|
||||
try {
|
||||
Log.i(TAG, "Sending via Lollipop API");
|
||||
return new OutgoingLollipopMmsConnection(context).send(pduBytes, subscriptionId);
|
||||
} catch (UndeliverableMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Falling back to legacy connection...");
|
||||
}
|
||||
|
||||
if (subscriptionId == -1) {
|
||||
Log.i(TAG, "Sending via legacy connection");
|
||||
try {
|
||||
SendConf result = new OutgoingLegacyMmsConnection(context).send(pduBytes, subscriptionId);
|
||||
|
||||
if (result != null && result.getResponseStatus() == PduHeaders.RESPONSE_STATUS_OK) {
|
||||
return result;
|
||||
} else {
|
||||
Log.w(TAG, "Got bad legacy response: " + (result != null ? result.getResponseStatus() : null));
|
||||
}
|
||||
} catch (UndeliverableMessageException | ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "Falling back to sending via Lollipop API");
|
||||
return new OutgoingLollipopMmsConnection(context).send(pduBytes, subscriptionId);
|
||||
}
|
||||
|
||||
throw new UndeliverableMessageException("Both lollipop and legacy connections failed...");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RetrieveConf retrieve(@NonNull String contentLocation,
|
||||
byte[] transactionId,
|
||||
int subscriptionId)
|
||||
throws MmsException, MmsRadioException, ApnUnavailableException, IOException
|
||||
{
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "Receiving via Lollipop API");
|
||||
try {
|
||||
return new IncomingLollipopMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Falling back to receiving via legacy connection");
|
||||
}
|
||||
|
||||
if (VERSION.SDK_INT < 22 || subscriptionId == -1) {
|
||||
Log.i(TAG, "Receiving via legacy API");
|
||||
try {
|
||||
return new IncomingLegacyMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
|
||||
} catch (MmsRadioException | ApnUnavailableException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "Falling back to receiving via Lollipop API");
|
||||
return new IncomingLollipopMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
|
||||
}
|
||||
|
||||
throw new IOException("Both lollipop and fallback APIs failed...");
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.InvalidHeaderValueException;
|
||||
import com.google.android.mms.pdu_alt.NotifyRespInd;
|
||||
import com.google.android.mms.pdu_alt.PduComposer;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.HttpGetHC4;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class IncomingLegacyMmsConnection extends LegacyMmsConnection implements IncomingMmsConnection {
|
||||
private static final String TAG = Log.tag(IncomingLegacyMmsConnection.class);
|
||||
|
||||
public IncomingLegacyMmsConnection(Context context) throws ApnUnavailableException {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private HttpUriRequest constructRequest(Apn contentApn, boolean useProxy) throws IOException {
|
||||
HttpGetHC4 request;
|
||||
|
||||
try {
|
||||
request = new HttpGetHC4(contentApn.getMmsc());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// #7339
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
for (Header header : getBaseHeaders()) {
|
||||
request.addHeader(header);
|
||||
}
|
||||
|
||||
if (useProxy) {
|
||||
HttpHost proxy = new HttpHost(contentApn.getProxy(), contentApn.getPort());
|
||||
request.setConfig(RequestConfig.custom().setProxy(proxy).build());
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable RetrieveConf retrieve(@NonNull String contentLocation,
|
||||
byte[] transactionId, int subscriptionId)
|
||||
throws MmsRadioException, ApnUnavailableException, IOException
|
||||
{
|
||||
MmsRadio radio = MmsRadio.getInstance(context);
|
||||
Apn contentApn = new Apn(contentLocation, apn.getProxy(), Integer.toString(apn.getPort()), apn.getUsername(), apn.getPassword());
|
||||
|
||||
if (isDirectConnect()) {
|
||||
Log.i(TAG, "Connecting directly...");
|
||||
try {
|
||||
return retrieve(contentApn, transactionId, false, false);
|
||||
} catch (IOException | ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Changing radio to MMS mode..");
|
||||
radio.connect();
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Downloading in MMS mode with proxy...");
|
||||
|
||||
try {
|
||||
return retrieve(contentApn, transactionId, true, true);
|
||||
} catch (IOException | ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Downloading in MMS mode without proxy...");
|
||||
|
||||
return retrieve(contentApn, transactionId, true, false);
|
||||
|
||||
} finally {
|
||||
radio.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public RetrieveConf retrieve(Apn contentApn, byte[] transactionId, boolean usingMmsRadio, boolean useProxyIfAvailable)
|
||||
throws IOException, ApnUnavailableException
|
||||
{
|
||||
byte[] pdu = null;
|
||||
|
||||
final boolean useProxy = useProxyIfAvailable && contentApn.hasProxy();
|
||||
final String targetHost = useProxy
|
||||
? contentApn.getProxy()
|
||||
: Uri.parse(contentApn.getMmsc()).getHost();
|
||||
if (checkRouteToHost(context, targetHost, usingMmsRadio)) {
|
||||
Log.i(TAG, "got successful route to host " + targetHost);
|
||||
pdu = execute(constructRequest(contentApn, useProxy));
|
||||
}
|
||||
|
||||
if (pdu == null) {
|
||||
throw new IOException("Connection manager could not obtain route to host.");
|
||||
}
|
||||
|
||||
RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse();
|
||||
|
||||
if (retrieved == null) {
|
||||
Log.w(TAG, "Couldn't parse PDU, byte response: " + Arrays.toString(pdu));
|
||||
Log.w(TAG, "Couldn't parse PDU, ASCII: " + new String(pdu));
|
||||
throw new IOException("Bad retrieved PDU");
|
||||
}
|
||||
|
||||
sendRetrievedAcknowledgement(transactionId, usingMmsRadio, useProxy);
|
||||
return retrieved;
|
||||
}
|
||||
|
||||
private void sendRetrievedAcknowledgement(byte[] transactionId,
|
||||
boolean usingRadio,
|
||||
boolean useProxy)
|
||||
throws ApnUnavailableException
|
||||
{
|
||||
try {
|
||||
NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION,
|
||||
transactionId,
|
||||
PduHeaders.STATUS_RETRIEVED);
|
||||
|
||||
OutgoingLegacyMmsConnection connection = new OutgoingLegacyMmsConnection(context);
|
||||
connection.sendNotificationReceived(new PduComposer(context, notifyResponse).make(), usingRadio, useProxy);
|
||||
} catch (InvalidHeaderValueException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.SmsManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.InvalidHeaderValueException;
|
||||
import com.google.android.mms.pdu_alt.NotifyRespInd;
|
||||
import com.google.android.mms.pdu_alt.PduComposer;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.providers.MmsBodyProvider;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class IncomingLollipopMmsConnection extends LollipopMmsConnection implements IncomingMmsConnection {
|
||||
|
||||
public static final String ACTION = IncomingLollipopMmsConnection.class.getCanonicalName() + "MMS_DOWNLOADED_ACTION";
|
||||
private static final String TAG = Log.tag(IncomingLollipopMmsConnection.class);
|
||||
|
||||
public IncomingLollipopMmsConnection(Context context) {
|
||||
super(context, ACTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onResult(Context context, Intent intent) {
|
||||
if (VERSION.SDK_INT >= 22) {
|
||||
Log.i(TAG, "HTTP status: " + intent.getIntExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, -1));
|
||||
}
|
||||
Log.i(TAG, "code: " + getResultCode() + ", result string: " + getResultData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized @Nullable RetrieveConf retrieve(@NonNull String contentLocation,
|
||||
byte[] transactionId,
|
||||
int subscriptionId) throws MmsException
|
||||
{
|
||||
beginTransaction();
|
||||
|
||||
try {
|
||||
MmsBodyProvider.Pointer pointer = MmsBodyProvider.makeTemporaryPointer(getContext());
|
||||
|
||||
final String transactionIdString = Util.toIsoString(transactionId);
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "Downloading subscriptionId=%s multimedia from '%s' [transactionId='%s'] to '%s'",
|
||||
subscriptionId,
|
||||
contentLocation,
|
||||
transactionIdString,
|
||||
pointer.getUri()));
|
||||
|
||||
SmsManager smsManager;
|
||||
|
||||
if (VERSION.SDK_INT >= 22 && subscriptionId != -1) {
|
||||
smsManager = SmsManager.getSmsManagerForSubscriptionId(subscriptionId);
|
||||
} else {
|
||||
smsManager = SmsManager.getDefault();
|
||||
}
|
||||
|
||||
final Bundle configOverrides = smsManager.getCarrierConfigValues();
|
||||
|
||||
if (configOverrides.getBoolean(SmsManager.MMS_CONFIG_APPEND_TRANSACTION_ID)) {
|
||||
if (!contentLocation.contains(transactionIdString)) {
|
||||
Log.i(TAG, "Appending transactionId to contentLocation at the direction of CarrierConfigValues. New location: " + contentLocation);
|
||||
contentLocation += transactionIdString;
|
||||
} else {
|
||||
Log.i(TAG, "Skipping 'append transaction id' as contentLocation already contains it");
|
||||
}
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(configOverrides.getString(SmsManager.MMS_CONFIG_USER_AGENT))) {
|
||||
configOverrides.remove(SmsManager.MMS_CONFIG_USER_AGENT);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(configOverrides.getString(SmsManager.MMS_CONFIG_UA_PROF_URL))) {
|
||||
configOverrides.remove(SmsManager.MMS_CONFIG_UA_PROF_URL);
|
||||
}
|
||||
|
||||
smsManager.downloadMultimediaMessage(getContext(),
|
||||
contentLocation,
|
||||
pointer.getUri(),
|
||||
configOverrides,
|
||||
getPendingIntent());
|
||||
|
||||
waitForResult();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
StreamUtil.copy(pointer.getInputStream(), baos);
|
||||
pointer.close();
|
||||
|
||||
Log.i(TAG, baos.size() + "-byte response: ");// + Hex.dump(baos.toByteArray()));
|
||||
|
||||
Bundle configValues = smsManager.getCarrierConfigValues();
|
||||
boolean parseContentDisposition = configValues.getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION);
|
||||
|
||||
RetrieveConf retrieved;
|
||||
|
||||
try {
|
||||
retrieved = (RetrieveConf) new PduParser(baos.toByteArray(), parseContentDisposition).parse();
|
||||
} catch (AssertionError | NullPointerException e) {
|
||||
Log.w(TAG, "Badly formatted MMS message caused the parser to fail.", e);
|
||||
throw new MmsException(e);
|
||||
}
|
||||
|
||||
if (retrieved == null) return null;
|
||||
|
||||
sendRetrievedAcknowledgement(transactionId, retrieved.getMmsVersion(), subscriptionId);
|
||||
return retrieved;
|
||||
} catch (IOException | TimeoutException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new MmsException(e);
|
||||
} finally {
|
||||
endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRetrievedAcknowledgement(byte[] transactionId, int mmsVersion, int subscriptionId) {
|
||||
try {
|
||||
NotifyRespInd retrieveResponse = new NotifyRespInd(mmsVersion, transactionId, PduHeaders.STATUS_RETRIEVED);
|
||||
new OutgoingLollipopMmsConnection(getContext()).send(new PduComposer(getContext(), retrieveResponse).make(), subscriptionId);
|
||||
} catch (UndeliverableMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
} catch (InvalidHeaderValueException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface IncomingMmsConnection {
|
||||
@Nullable
|
||||
RetrieveConf retrieve(@NonNull String contentLocation, byte[] transactionId, int subscriptionId) throws MmsException, MmsRadioException, ApnUnavailableException, IOException;
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.CredentialsProvider;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.impl.NoConnectionReuseStrategyHC4;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.ApnDatabase;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TelephonyUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public abstract class LegacyMmsConnection {
|
||||
|
||||
public static final String USER_AGENT = "Android-Mms/2.0";
|
||||
|
||||
private static final String TAG = Log.tag(LegacyMmsConnection.class);
|
||||
|
||||
protected final Context context;
|
||||
protected final Apn apn;
|
||||
|
||||
protected LegacyMmsConnection(Context context) throws ApnUnavailableException {
|
||||
this.context = context;
|
||||
this.apn = getApn(context);
|
||||
}
|
||||
|
||||
public static Apn getApn(Context context) throws ApnUnavailableException {
|
||||
|
||||
try {
|
||||
Optional<Apn> params = ApnDatabase.getInstance(context)
|
||||
.getMmsConnectionParameters(TelephonyUtil.getMccMnc(context),
|
||||
TelephonyUtil.getApn(context));
|
||||
|
||||
if (!params.isPresent()) {
|
||||
throw new ApnUnavailableException("No parameters available from ApnDefaults.");
|
||||
}
|
||||
|
||||
return params.get();
|
||||
} catch (IOException ioe) {
|
||||
throw new ApnUnavailableException("ApnDatabase threw an IOException", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isDirectConnect() {
|
||||
// We think Sprint supports direct connection over wifi/data, but not Verizon
|
||||
Set<String> sprintMccMncs = new HashSet<String>() {{
|
||||
add("312530");
|
||||
add("311880");
|
||||
add("311870");
|
||||
add("311490");
|
||||
add("310120");
|
||||
add("316010");
|
||||
add("312190");
|
||||
}};
|
||||
|
||||
return ServiceUtil.getTelephonyManager(context).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA &&
|
||||
sprintMccMncs.contains(TelephonyUtil.getMccMnc(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("TryWithIdenticalCatches")
|
||||
protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio)
|
||||
throws IOException
|
||||
{
|
||||
InetAddress inetAddress = InetAddress.getByName(host);
|
||||
if (!usingMmsRadio) {
|
||||
if (inetAddress.isSiteLocalAddress()) {
|
||||
throw new IOException("RFC1918 address in non-MMS radio situation!");
|
||||
}
|
||||
Log.w(TAG, "returning vacuous success since MMS radio is not in use");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (inetAddress == null) {
|
||||
throw new IOException("Unable to lookup host: InetAddress.getByName() returned null.");
|
||||
}
|
||||
|
||||
byte[] ipAddressBytes = inetAddress.getAddress();
|
||||
if (ipAddressBytes == null) {
|
||||
Log.w(TAG, "resolved IP address bytes are null, returning true to attempt a connection anyway.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Checking route to address: " + host + ", " + inetAddress.getHostAddress());
|
||||
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
|
||||
try {
|
||||
final Method requestRouteMethod = manager.getClass().getMethod("requestRouteToHostAddress", Integer.TYPE, InetAddress.class);
|
||||
final boolean routeToHostObtained = (Boolean) requestRouteMethod.invoke(manager, MmsRadio.TYPE_MOBILE_MMS, inetAddress);
|
||||
Log.i(TAG, "requestRouteToHostAddress(" + inetAddress + ") -> " + routeToHostObtained);
|
||||
return routeToHostObtained;
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
Log.w(TAG, nsme);
|
||||
} catch (IllegalAccessException iae) {
|
||||
Log.w(TAG, iae);
|
||||
} catch (InvocationTargetException ite) {
|
||||
Log.w(TAG, ite);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static byte[] parseResponse(InputStream is) throws IOException {
|
||||
InputStream in = new BufferedInputStream(is);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
StreamUtil.copy(in, baos);
|
||||
|
||||
Log.i(TAG, "Received full server response, " + baos.size() + " bytes");
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
protected CloseableHttpClient constructHttpClient() throws IOException {
|
||||
RequestConfig config = RequestConfig.custom()
|
||||
.setConnectTimeout(20 * 1000)
|
||||
.setConnectionRequestTimeout(20 * 1000)
|
||||
.setSocketTimeout(20 * 1000)
|
||||
.setMaxRedirects(20)
|
||||
.build();
|
||||
|
||||
URL mmsc = new URL(apn.getMmsc());
|
||||
CredentialsProvider credsProvider = new BasicCredentialsProvider();
|
||||
|
||||
if (apn.hasAuthentication()) {
|
||||
credsProvider.setCredentials(new AuthScope(mmsc.getHost(), mmsc.getPort() > -1 ? mmsc.getPort() : mmsc.getDefaultPort()),
|
||||
new UsernamePasswordCredentials(apn.getUsername(), apn.getPassword()));
|
||||
}
|
||||
|
||||
return HttpClients.custom()
|
||||
.setConnectionReuseStrategy(new NoConnectionReuseStrategyHC4())
|
||||
.setRedirectStrategy(new LaxRedirectStrategy())
|
||||
.setUserAgent(TextSecurePreferences.getMmsUserAgent(context, USER_AGENT))
|
||||
.setConnectionManager(new BasicHttpClientConnectionManager())
|
||||
.setDefaultRequestConfig(config)
|
||||
.setDefaultCredentialsProvider(credsProvider)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected byte[] execute(HttpUriRequest request) throws IOException {
|
||||
Log.i(TAG, "connecting to " + apn.getMmsc());
|
||||
|
||||
CloseableHttpClient client = null;
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
client = constructHttpClient();
|
||||
response = client.execute(request);
|
||||
|
||||
Log.i(TAG, "* response code: " + response.getStatusLine());
|
||||
|
||||
if (response.getStatusLine().getStatusCode() == 200) {
|
||||
return parseResponse(response.getEntity().getContent());
|
||||
}
|
||||
} catch (NullPointerException npe) {
|
||||
// TODO determine root cause
|
||||
// see: https://github.com/signalapp/Signal-Android/issues/4379
|
||||
throw new IOException(npe);
|
||||
} finally {
|
||||
if (response != null) response.close();
|
||||
if (client != null) client.close();
|
||||
}
|
||||
|
||||
throw new IOException("unhandled response code");
|
||||
}
|
||||
|
||||
protected List<Header> getBaseHeaders() {
|
||||
final String number = getLine1Number(context);
|
||||
|
||||
return new LinkedList<Header>() {{
|
||||
add(new BasicHeader("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"));
|
||||
add(new BasicHeader("x-wap-profile", "http://www.google.com/oha/rdf/ua-profile-kila.xml"));
|
||||
add(new BasicHeader("Content-Type", "application/vnd.wap.mms-message"));
|
||||
add(new BasicHeader("x-carrier-magic", "http://magic.google.com"));
|
||||
if (!TextUtils.isEmpty(number)) {
|
||||
add(new BasicHeader("x-up-calling-line-id", number));
|
||||
add(new BasicHeader("X-MDN", number));
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
private static String getLine1Number(@NonNull Context context) {
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
|
||||
return TelephonyUtil.getManager(context).getLine1Number();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Apn {
|
||||
|
||||
public static Apn EMPTY = new Apn("", "", "", "", "");
|
||||
|
||||
private final String mmsc;
|
||||
private final String proxy;
|
||||
private final String port;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public Apn(String mmsc, String proxy, String port, String username, String password) {
|
||||
this.mmsc = mmsc;
|
||||
this.proxy = proxy;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Apn(Apn customApn, Apn defaultApn,
|
||||
boolean useCustomMmsc,
|
||||
boolean useCustomProxy,
|
||||
boolean useCustomProxyPort,
|
||||
boolean useCustomUsername,
|
||||
boolean useCustomPassword)
|
||||
{
|
||||
this.mmsc = useCustomMmsc ? customApn.mmsc : defaultApn.mmsc;
|
||||
this.proxy = useCustomProxy ? customApn.proxy : defaultApn.proxy;
|
||||
this.port = useCustomProxyPort ? customApn.port : defaultApn.port;
|
||||
this.username = useCustomUsername ? customApn.username : defaultApn.username;
|
||||
this.password = useCustomPassword ? customApn.password : defaultApn.password;
|
||||
}
|
||||
|
||||
public boolean hasProxy() {
|
||||
return !TextUtils.isEmpty(proxy);
|
||||
}
|
||||
|
||||
public String getMmsc() {
|
||||
return mmsc;
|
||||
}
|
||||
|
||||
public String getProxy() {
|
||||
return hasProxy() ? proxy : null;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return TextUtils.isEmpty(port) ? 80 : Integer.parseInt(port);
|
||||
}
|
||||
|
||||
public boolean hasAuthentication() {
|
||||
return !TextUtils.isEmpty(username);
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return Log.tag(Apn.class) +
|
||||
"{ mmsc: \"" + mmsc + "\"" +
|
||||
", proxy: " + (proxy == null ? "none" : '"' + proxy + '"') +
|
||||
", port: " + (port == null ? "(none)" : port) +
|
||||
", user: " + (username == null ? "none" : '"' + username + '"') +
|
||||
", pass: " + (password == null ? "none" : '"' + password + '"') + " }";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import org.signal.core.util.PendingIntentFlags;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public abstract class LollipopMmsConnection extends BroadcastReceiver {
|
||||
private static final String TAG = Log.tag(LollipopMmsConnection.class);
|
||||
|
||||
private final Context context;
|
||||
private final String action;
|
||||
|
||||
private boolean resultAvailable;
|
||||
|
||||
public abstract void onResult(Context context, Intent intent);
|
||||
|
||||
protected LollipopMmsConnection(Context context, String action) {
|
||||
super();
|
||||
this.context = context;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "onReceive()");
|
||||
if (!action.equals(intent.getAction())) {
|
||||
Log.w(TAG, "received broadcast with unexpected action " + intent.getAction());
|
||||
return;
|
||||
}
|
||||
|
||||
onResult(context, intent);
|
||||
|
||||
resultAvailable = true;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
protected void beginTransaction() {
|
||||
getContext().getApplicationContext().registerReceiver(this, new IntentFilter(action));
|
||||
}
|
||||
|
||||
protected void endTransaction() {
|
||||
getContext().getApplicationContext().unregisterReceiver(this);
|
||||
resultAvailable = false;
|
||||
}
|
||||
|
||||
protected void waitForResult() throws TimeoutException {
|
||||
long timeoutExpiration = System.currentTimeMillis() + 60000;
|
||||
while (!resultAvailable) {
|
||||
Util.wait(this, Math.max(1, timeoutExpiration - System.currentTimeMillis()));
|
||||
if (System.currentTimeMillis() >= timeoutExpiration) {
|
||||
throw new TimeoutException("timeout when waiting for MMS");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected PendingIntent getPendingIntent() {
|
||||
return PendingIntent.getBroadcast(getContext(), 1, new Intent(action), PendingIntentFlags.oneShot());
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,6 @@ public abstract class MediaConstraints {
|
||||
return new PushMediaConstraints(sentMediaQuality);
|
||||
}
|
||||
|
||||
public static MediaConstraints getMmsMediaConstraints(int subscriptionId) {
|
||||
return new MmsMediaConstraints(subscriptionId);
|
||||
}
|
||||
|
||||
public abstract int getImageMaxWidth(Context context);
|
||||
public abstract int getImageMaxHeight(Context context);
|
||||
public abstract int getImageMaxSize(Context context);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MediaNotFoundException extends Exception {
|
||||
|
||||
public MediaNotFoundException() {
|
||||
}
|
||||
|
||||
public MediaNotFoundException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public MediaNotFoundException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public MediaNotFoundException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MediaTooLargeException extends Exception {
|
||||
|
||||
public MediaTooLargeException() {
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public MediaTooLargeException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public MediaTooLargeException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public MediaTooLargeException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.mms.service_alt.MmsConfig;
|
||||
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
final class MmsConfigManager {
|
||||
|
||||
private static final Map<Integer, MmsConfig> mmsConfigMap = new HashMap<>();
|
||||
|
||||
@WorkerThread
|
||||
synchronized static @NonNull MmsConfig getMmsConfig(Context context, int subscriptionId) {
|
||||
MmsConfig mmsConfig = mmsConfigMap.get(subscriptionId);
|
||||
if (mmsConfig != null) {
|
||||
return mmsConfig;
|
||||
}
|
||||
|
||||
MmsConfig loadedConfig = loadMmsConfig(context, subscriptionId);
|
||||
|
||||
mmsConfigMap.put(subscriptionId, loadedConfig);
|
||||
|
||||
return loadedConfig;
|
||||
}
|
||||
|
||||
private static @NonNull MmsConfig loadMmsConfig(Context context, int subscriptionId) {
|
||||
Optional<SubscriptionInfoCompat> subscriptionInfo = new SubscriptionManagerCompat(context).getActiveSubscriptionInfo(subscriptionId);
|
||||
|
||||
if (subscriptionInfo.isPresent()) {
|
||||
SubscriptionInfoCompat subscriptionInfoCompat = subscriptionInfo.get();
|
||||
Configuration configuration = context.getResources().getConfiguration();
|
||||
configuration.mcc = subscriptionInfoCompat.getMcc();
|
||||
configuration.mnc = subscriptionInfoCompat.getMnc();
|
||||
|
||||
Context subContext = context.createConfigurationContext(configuration);
|
||||
return new MmsConfig(subContext, subscriptionId);
|
||||
}
|
||||
|
||||
return new MmsConfig(context, subscriptionId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.mms.service_alt.MmsConfig;
|
||||
|
||||
final class MmsMediaConstraints extends MediaConstraints {
|
||||
|
||||
private final int subscriptionId;
|
||||
|
||||
private static final int MIN_IMAGE_DIMEN = 1024;
|
||||
|
||||
MmsMediaConstraints(int subscriptionId) {
|
||||
this.subscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxWidth(Context context) {
|
||||
return Math.max(MIN_IMAGE_DIMEN, getOverriddenMmsConfig(context).getMaxImageWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxHeight(Context context) {
|
||||
return Math.max(MIN_IMAGE_DIMEN, getOverriddenMmsConfig(context).getMaxImageHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getImageDimensionTargets(Context context) {
|
||||
int[] targets = new int[4];
|
||||
|
||||
targets[0] = getImageMaxHeight(context);
|
||||
|
||||
for (int i = 1; i < targets.length; i++) {
|
||||
targets[i] = targets[i - 1] / 2;
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGifMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getVideoMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUncompressedVideoMaxSize(Context context) {
|
||||
return Math.max(getVideoMaxSize(context), 15 * 1024 * 1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAudioMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDocumentMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
private int getMaxMessageSize(Context context) {
|
||||
return getOverriddenMmsConfig(context).getMaxMessageSize();
|
||||
}
|
||||
|
||||
private MmsConfig.Overridden getOverriddenMmsConfig(Context context) {
|
||||
MmsConfig mmsConfig = MmsConfigManager.getMmsConfig(context, subscriptionId);
|
||||
|
||||
return new MmsConfig.Overridden(mmsConfig, null);
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.PowerManager;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class MmsRadio {
|
||||
|
||||
private static final String TAG = Log.tag(MmsRadio.class);
|
||||
|
||||
private static MmsRadio instance;
|
||||
|
||||
public static synchronized MmsRadio getInstance(Context context) {
|
||||
if (instance == null)
|
||||
instance = new MmsRadio(context.getApplicationContext());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
private static final String FEATURE_ENABLE_MMS = "enableMMS";
|
||||
private static final int APN_ALREADY_ACTIVE = 0;
|
||||
public static final int TYPE_MOBILE_MMS = 2;
|
||||
|
||||
private final Context context;
|
||||
|
||||
private ConnectivityManager connectivityManager;
|
||||
private ConnectivityListener connectivityListener;
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private int connectedCounter = 0;
|
||||
|
||||
private MmsRadio(Context context) {
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
this.context = context;
|
||||
this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:mms");
|
||||
this.wakeLock.setReferenceCounted(true);
|
||||
}
|
||||
|
||||
public synchronized void disconnect() {
|
||||
Log.i(TAG, "MMS Radio Disconnect Called...");
|
||||
wakeLock.release();
|
||||
connectedCounter--;
|
||||
|
||||
Log.i(TAG, "Reference count: " + connectedCounter);
|
||||
|
||||
if (connectedCounter == 0) {
|
||||
Log.i(TAG, "Turning off MMS radio...");
|
||||
try {
|
||||
final Method stopUsingNetworkFeatureMethod = connectivityManager.getClass().getMethod("stopUsingNetworkFeature", Integer.TYPE, String.class);
|
||||
stopUsingNetworkFeatureMethod.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
Log.w(TAG, nsme);
|
||||
} catch (IllegalAccessException iae) {
|
||||
Log.w(TAG, iae);
|
||||
} catch (InvocationTargetException ite) {
|
||||
Log.w(TAG, ite);
|
||||
}
|
||||
|
||||
if (connectivityListener != null) {
|
||||
Log.i(TAG, "Unregistering receiver...");
|
||||
context.unregisterReceiver(connectivityListener);
|
||||
connectivityListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void connect() throws MmsRadioException {
|
||||
int status;
|
||||
|
||||
try {
|
||||
final Method startUsingNetworkFeatureMethod = connectivityManager.getClass().getMethod("startUsingNetworkFeature", Integer.TYPE, String.class);
|
||||
status = (int)startUsingNetworkFeatureMethod.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
throw new MmsRadioException(nsme);
|
||||
} catch (IllegalAccessException iae) {
|
||||
throw new MmsRadioException(iae);
|
||||
} catch (InvocationTargetException ite) {
|
||||
throw new MmsRadioException(ite);
|
||||
}
|
||||
|
||||
Log.i(TAG, "startUsingNetworkFeature status: " + status);
|
||||
|
||||
if (status == APN_ALREADY_ACTIVE) {
|
||||
wakeLock.acquire();
|
||||
connectedCounter++;
|
||||
return;
|
||||
} else {
|
||||
wakeLock.acquire();
|
||||
connectedCounter++;
|
||||
|
||||
if (connectivityListener == null) {
|
||||
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
connectivityListener = new ConnectivityListener();
|
||||
context.registerReceiver(connectivityListener, filter);
|
||||
}
|
||||
|
||||
Util.wait(this, 30000);
|
||||
|
||||
if (!isConnected()) {
|
||||
Log.w(TAG, "Got back from connectivity wait, and not connected...");
|
||||
disconnect();
|
||||
throw new MmsRadioException("Unable to successfully enable MMS radio.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
NetworkInfo info = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
|
||||
Log.i(TAG, "Connected: " + info);
|
||||
|
||||
if ((info == null) || (info.getType() != TYPE_MOBILE_MMS) || !info.isConnected())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isConnectivityPossible() {
|
||||
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
|
||||
return networkInfo != null && networkInfo.isAvailable();
|
||||
}
|
||||
|
||||
private boolean isConnectivityFailure() {
|
||||
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
|
||||
return networkInfo == null || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED;
|
||||
}
|
||||
|
||||
private synchronized void issueConnectivityChange() {
|
||||
if (isConnected()) {
|
||||
Log.i(TAG, "Notifying connected...");
|
||||
notifyAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isConnected() && (isConnectivityFailure() || !isConnectivityPossible())) {
|
||||
Log.i(TAG, "Notifying not connected...");
|
||||
notifyAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private class ConnectivityListener extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Got connectivity change...");
|
||||
issueConnectivityChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MmsRadioException extends Throwable {
|
||||
public MmsRadioException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public MmsRadioException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MmsSendResult {
|
||||
|
||||
private final byte[] messageId;
|
||||
private final int responseStatus;
|
||||
|
||||
public MmsSendResult(byte[] messageId, int responseStatus) {
|
||||
this.messageId = messageId;
|
||||
this.responseStatus = responseStatus;
|
||||
}
|
||||
|
||||
public int getResponseStatus() {
|
||||
return responseStatus;
|
||||
}
|
||||
|
||||
public byte[] getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.HttpPostHC4;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.ByteArrayEntityHC4;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class OutgoingLegacyMmsConnection extends LegacyMmsConnection implements OutgoingMmsConnection {
|
||||
private final static String TAG = Log.tag(OutgoingLegacyMmsConnection.class);
|
||||
|
||||
public OutgoingLegacyMmsConnection(Context context) throws ApnUnavailableException {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private HttpUriRequest constructRequest(byte[] pduBytes, boolean useProxy)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
HttpPostHC4 request = new HttpPostHC4(apn.getMmsc());
|
||||
for (Header header : getBaseHeaders()) {
|
||||
request.addHeader(header);
|
||||
}
|
||||
|
||||
request.setEntity(new ByteArrayEntityHC4(pduBytes));
|
||||
if (useProxy) {
|
||||
HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort());
|
||||
request.setConfig(RequestConfig.custom().setProxy(proxy).build());
|
||||
}
|
||||
return request;
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new IOException(iae);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendNotificationReceived(byte[] pduBytes, boolean usingMmsRadio, boolean useProxyIfAvailable)
|
||||
throws IOException
|
||||
{
|
||||
sendBytes(pduBytes, usingMmsRadio, useProxyIfAvailable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SendConf send(@NonNull byte[] pduBytes, int subscriptionId) throws UndeliverableMessageException {
|
||||
try {
|
||||
MmsRadio radio = MmsRadio.getInstance(context);
|
||||
|
||||
if (isDirectConnect()) {
|
||||
Log.i(TAG, "Sending MMS directly without radio change...");
|
||||
try {
|
||||
return send(pduBytes, false, false);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sending MMS with radio change and proxy...");
|
||||
radio.connect();
|
||||
|
||||
try {
|
||||
try {
|
||||
return send(pduBytes, true, true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sending MMS with radio change and without proxy...");
|
||||
|
||||
try {
|
||||
return send(pduBytes, true, false);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
throw new UndeliverableMessageException(ioe);
|
||||
}
|
||||
} finally {
|
||||
radio.disconnect();
|
||||
}
|
||||
|
||||
} catch (MmsRadioException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new UndeliverableMessageException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private SendConf send(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
|
||||
byte[] response = sendBytes(pduBytes, useMmsRadio, useProxyIfAvailable);
|
||||
return (SendConf) new PduParser(response).parse();
|
||||
}
|
||||
|
||||
private byte[] sendBytes(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
|
||||
final boolean useProxy = useProxyIfAvailable && apn.hasProxy();
|
||||
final String targetHost = useProxy
|
||||
? apn.getProxy()
|
||||
: Uri.parse(apn.getMmsc()).getHost();
|
||||
|
||||
Log.i(TAG, "Sending MMS of length: " + pduBytes.length
|
||||
+ (useMmsRadio ? ", using mms radio" : "")
|
||||
+ (useProxy ? ", using proxy" : ""));
|
||||
|
||||
try {
|
||||
if (checkRouteToHost(context, targetHost, useMmsRadio)) {
|
||||
Log.i(TAG, "got successful route to host " + targetHost);
|
||||
byte[] response = execute(constructRequest(pduBytes, useProxy));
|
||||
if (response != null) return response;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
throw new IOException("Connection manager could not obtain route to host.");
|
||||
}
|
||||
|
||||
|
||||
public static boolean isConnectionPossible(Context context) {
|
||||
try {
|
||||
ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS);
|
||||
if (networkInfo == null) {
|
||||
Log.w(TAG, "MMS network info was null, unsupported by this device");
|
||||
return false;
|
||||
}
|
||||
|
||||
getApn(context);
|
||||
return true;
|
||||
} catch (ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.SmsManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.mms.service_alt.MmsConfig;
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.providers.MmsBodyProvider;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class OutgoingLollipopMmsConnection extends LollipopMmsConnection implements OutgoingMmsConnection {
|
||||
private static final String TAG = Log.tag(OutgoingLollipopMmsConnection.class);
|
||||
private static final String ACTION = OutgoingLollipopMmsConnection.class.getCanonicalName() + "MMS_SENT_ACTION";
|
||||
|
||||
private byte[] response;
|
||||
|
||||
public OutgoingLollipopMmsConnection(Context context) {
|
||||
super(context, ACTION);
|
||||
}
|
||||
|
||||
@TargetApi(22)
|
||||
@Override
|
||||
public synchronized void onResult(Context context, Intent intent) {
|
||||
if (VERSION.SDK_INT >= 22) {
|
||||
Log.i(TAG, "HTTP status: " + intent.getIntExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, -1));
|
||||
}
|
||||
|
||||
response = intent.getByteArrayExtra(SmsManager.EXTRA_MMS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable synchronized SendConf send(@NonNull byte[] pduBytes, int subscriptionId)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
beginTransaction();
|
||||
try {
|
||||
MmsBodyProvider.Pointer pointer = MmsBodyProvider.makeTemporaryPointer(getContext());
|
||||
StreamUtil.copy(new ByteArrayInputStream(pduBytes), pointer.getOutputStream());
|
||||
|
||||
SmsManager smsManager;
|
||||
|
||||
if (VERSION.SDK_INT >= 22 && subscriptionId != -1) {
|
||||
smsManager = SmsManager.getSmsManagerForSubscriptionId(subscriptionId);
|
||||
} else {
|
||||
smsManager = SmsManager.getDefault();
|
||||
}
|
||||
|
||||
Bundle configOverrides = new Bundle();
|
||||
configOverrides.putBoolean(SmsManager.MMS_CONFIG_GROUP_MMS_ENABLED, true);
|
||||
|
||||
MmsConfig mmsConfig = MmsConfigManager.getMmsConfig(getContext(), subscriptionId);
|
||||
|
||||
if (mmsConfig != null) {
|
||||
MmsConfig.Overridden overridden = new MmsConfig.Overridden(mmsConfig, new Bundle());
|
||||
configOverrides.putString(SmsManager.MMS_CONFIG_HTTP_PARAMS, overridden.getHttpParams());
|
||||
configOverrides.putInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE, overridden.getMaxMessageSize());
|
||||
}
|
||||
|
||||
smsManager.sendMultimediaMessage(getContext(),
|
||||
pointer.getUri(),
|
||||
null,
|
||||
configOverrides,
|
||||
getPendingIntent());
|
||||
|
||||
waitForResult();
|
||||
|
||||
Log.i(TAG, "MMS broadcast received and processed.");
|
||||
pointer.close();
|
||||
|
||||
if (response == null) {
|
||||
throw new UndeliverableMessageException("Null response.");
|
||||
}
|
||||
|
||||
return (SendConf) new PduParser(response).parse();
|
||||
} catch (IOException | TimeoutException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
} finally {
|
||||
endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
|
||||
public interface OutgoingMmsConnection {
|
||||
@Nullable
|
||||
SendConf send(@NonNull byte[] pduBytes, int subscriptionId) throws UndeliverableMessageException;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import com.google.android.mms.ContentType;
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.PduBody;
|
||||
import com.google.android.mms.pdu_alt.PduPart;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class PartParser {
|
||||
|
||||
private static final String TAG = Log.tag(PartParser.class);
|
||||
private static final List<String> DOCUMENT_TYPES = Arrays.asList("text/vcard", "text/x-vcard");
|
||||
|
||||
public static String getMessageText(PduBody body) {
|
||||
String bodyText = null;
|
||||
|
||||
for (int i=0;i<body.getPartsNum();i++) {
|
||||
if (isText(body.getPart(i)) && !isDocument(body.getPart(i))) {
|
||||
String partText;
|
||||
|
||||
try {
|
||||
String characterSet = CharacterSets.getMimeName(body.getPart(i).getCharset());
|
||||
|
||||
if (characterSet.equals(CharacterSets.MIMENAME_ANY_CHARSET))
|
||||
characterSet = CharacterSets.MIMENAME_UTF_8;
|
||||
|
||||
if (body.getPart(i).getData() != null) {
|
||||
partText = new String(body.getPart(i).getData(), characterSet);
|
||||
} else {
|
||||
partText = "";
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.w(TAG, e);
|
||||
partText = "Unsupported Encoding!";
|
||||
}
|
||||
|
||||
bodyText = (bodyText == null) ? partText : bodyText + " " + partText;
|
||||
}
|
||||
}
|
||||
|
||||
return bodyText;
|
||||
}
|
||||
|
||||
public static PduBody getSupportedMediaParts(PduBody body) {
|
||||
PduBody stripped = new PduBody();
|
||||
|
||||
for (int i=0;i<body.getPartsNum();i++) {
|
||||
if (isDisplayableMedia(body.getPart(i)) || isDocument(body.getPart(i))) {
|
||||
stripped.addPart(body.getPart(i));
|
||||
}
|
||||
}
|
||||
|
||||
return stripped;
|
||||
}
|
||||
|
||||
public static int getSupportedMediaPartCount(PduBody body) {
|
||||
int partCount = 0;
|
||||
|
||||
for (int i=0;i<body.getPartsNum();i++) {
|
||||
if (isDisplayableMedia(body.getPart(i)) || isDocument(body.getPart(i))) {
|
||||
partCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return partCount;
|
||||
}
|
||||
|
||||
public static boolean isImage(PduPart part) {
|
||||
return ContentType.isImageType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isAudio(PduPart part) {
|
||||
return ContentType.isAudioType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isVideo(PduPart part) {
|
||||
return ContentType.isVideoType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isText(PduPart part) {
|
||||
return ContentType.isTextType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isDisplayableMedia(PduPart part) {
|
||||
return isImage(part) || isAudio(part) || isVideo(part);
|
||||
}
|
||||
|
||||
public static boolean isDocument(PduPart part) {
|
||||
return DOCUMENT_TYPES.contains(Util.toIsoString(part.getContentType()).toLowerCase());
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class MmsPreferencesActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
assert getSupportActionBar() != null;
|
||||
this.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Fragment fragment = new MmsPreferencesFragment();
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
fragmentTransaction.replace(android.R.id.content, fragment);
|
||||
fragmentTransaction.commit();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.CustomDefaultPreference;
|
||||
import org.thoughtcrime.securesms.database.ApnDatabase;
|
||||
import org.thoughtcrime.securesms.mms.LegacyMmsConnection;
|
||||
import org.thoughtcrime.securesms.util.TelephonyUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class MmsPreferencesFragment extends CorrectedPreferenceFragment {
|
||||
|
||||
private static final String TAG = Log.tag(MmsPreferencesFragment.class);
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences_manual_mms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
new LoadApnDefaultsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private class LoadApnDefaultsTask extends AsyncTask<Void, Void, LegacyMmsConnection.Apn> {
|
||||
|
||||
@Override
|
||||
protected LegacyMmsConnection.Apn doInBackground(Void... params) {
|
||||
try {
|
||||
Context context = getActivity();
|
||||
|
||||
if (context != null) {
|
||||
return ApnDatabase.getInstance(context)
|
||||
.getDefaultApnParameters(TelephonyUtil.getMccMnc(context),
|
||||
TelephonyUtil.getApn(context));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(LegacyMmsConnection.Apn apnDefaults) {
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_HOST_PREF))
|
||||
.setValidator(new CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.UriValidator())
|
||||
.setDefaultValue(apnDefaults.getMmsc());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PROXY_HOST_PREF))
|
||||
.setValidator(new CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.HostnameValidator())
|
||||
.setDefaultValue(apnDefaults.getProxy());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PROXY_PORT_PREF))
|
||||
.setValidator(new CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.PortValidator())
|
||||
.setDefaultValue(apnDefaults.getPort());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_USERNAME_PREF))
|
||||
.setDefaultValue(apnDefaults.getPort());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_PASSWORD_PREF))
|
||||
.setDefaultValue(apnDefaults.getPassword());
|
||||
|
||||
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMS_USER_AGENT))
|
||||
.setDefaultValue(LegacyMmsConnection.USER_AGENT);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.providers;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public final class MmsBodyProvider extends BaseContentProvider {
|
||||
private static final String TAG = Log.tag(MmsBodyProvider.class);
|
||||
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".mms";
|
||||
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/mms";
|
||||
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
|
||||
private static final int SINGLE_ROW = 1;
|
||||
|
||||
private static final UriMatcher uriMatcher;
|
||||
|
||||
static {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI(CONTENT_AUTHORITY, "mms/#", SINGLE_ROW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private File getFile(Uri uri) {
|
||||
long id = Long.parseLong(uri.getPathSegments().get(1));
|
||||
return new File(getContext().getCacheDir(), id + ".mmsbody");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
Log.i(TAG, "openFile(" + uri + ", " + mode + ")");
|
||||
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case SINGLE_ROW:
|
||||
Log.i(TAG, "Fetching message body for a single row...");
|
||||
File tmpFile = getFile(uri);
|
||||
|
||||
final int fileMode;
|
||||
switch (mode) {
|
||||
case "w": fileMode = ParcelFileDescriptor.MODE_TRUNCATE |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_WRITE_ONLY; break;
|
||||
case "r": fileMode = ParcelFileDescriptor.MODE_READ_ONLY; break;
|
||||
default: throw new IllegalArgumentException("requested file mode unsupported");
|
||||
}
|
||||
|
||||
Log.i(TAG, "returning file " + tmpFile.getAbsolutePath());
|
||||
return ParcelFileDescriptor.open(tmpFile, fileMode);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Request for bad message.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String arg1, String[] arg2) {
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case SINGLE_ROW:
|
||||
return getFile(uri).delete() ? 1 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri arg0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri arg0, ContentValues arg1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
|
||||
return 0;
|
||||
}
|
||||
public static Pointer makeTemporaryPointer(Context context) {
|
||||
return new Pointer(context, ContentUris.withAppendedId(MmsBodyProvider.CONTENT_URI, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
public static class Pointer {
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
|
||||
public Pointer(Context context, Uri uri) {
|
||||
this.context = context;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws FileNotFoundException {
|
||||
return context.getContentResolver().openOutputStream(uri, "w");
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws FileNotFoundException {
|
||||
return context.getContentResolver().openInputStream(uri);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
context.getContentResolver().delete(uri, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
boolean isAudioAvailable = (recipient.isRegistered() || SignalStore.misc().getSmsExportPhase().allowSmsFeatures()) &&
|
||||
boolean isAudioAvailable = recipient.isRegistered() &&
|
||||
!recipient.isGroup() &&
|
||||
!recipient.isBlocked() &&
|
||||
!recipient.isSelf() &&
|
||||
|
||||
@@ -83,7 +83,6 @@ public final class GroupLinkBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
MultiselectForwardFragment.showBottomSheet(
|
||||
getParentFragmentManager(),
|
||||
new MultiselectForwardFragmentArgs(
|
||||
true,
|
||||
Collections.singletonList(new MultiShareArgs.Builder()
|
||||
.withDraftText(groupLink.getUrl())
|
||||
.build()),
|
||||
|
||||
@@ -368,7 +368,7 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
||||
}
|
||||
|
||||
public void showErrorDialog(Context context, String msg, DialogInterface.OnClickListener positiveButtonListener) {
|
||||
new MaterialAlertDialogBuilder(context).setMessage(msg).setPositiveButton(R.string.ok, positiveButtonListener).show();
|
||||
new MaterialAlertDialogBuilder(context).setMessage(msg).setPositiveButton(android.R.string.ok, positiveButtonListener).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Intent;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLDecoder;
|
||||
|
||||
public class QuickResponseService extends IntentService {
|
||||
|
||||
private static final String TAG = Log.tag(QuickResponseService.class);
|
||||
|
||||
public QuickResponseService() {
|
||||
super("QuickResponseService");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
if (!TelephonyManager.ACTION_RESPOND_VIA_MESSAGE.equals(intent.getAction())) {
|
||||
Log.w(TAG, "Received unknown intent: " + intent.getAction());
|
||||
return;
|
||||
}
|
||||
|
||||
if (KeyCachingService.isLocked(this)) {
|
||||
Log.w(TAG, "Got quick response request when locked...");
|
||||
Toast.makeText(this, R.string.QuickResponseService_quick_response_unavailable_when_Signal_is_locked, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Rfc5724Uri uri = new Rfc5724Uri(intent.getDataString());
|
||||
String content = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
String number = uri.getPath();
|
||||
|
||||
if (number.contains("%")){
|
||||
number = URLDecoder.decode(number);
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.external(this, number);
|
||||
|
||||
if (!TextUtils.isEmpty(content)) {
|
||||
MessageSender.send(this, OutgoingMessage.sms(recipient, content), -1, MessageSender.SendType.SIGNAL, null, null);
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
Toast.makeText(this, R.string.QuickResponseService_problem_sending_message, Toast.LENGTH_LONG).show();
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.telephony.SmsMessage;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSentJob;
|
||||
|
||||
public class SmsDeliveryListener extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = Log.tag(SmsDeliveryListener.class);
|
||||
|
||||
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
|
||||
public static final String DELIVERED_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DELIVERED_SMS_ACTION";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
long messageId = intent.getLongExtra("message_id", -1);
|
||||
int runAttempt = intent.getIntExtra("run_attempt", 0);
|
||||
boolean isMultipart = intent.getBooleanExtra("is_multipart", true);
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case SENT_SMS_ACTION:
|
||||
int result = getResultCode();
|
||||
|
||||
jobManager.add(new SmsSentJob(messageId, isMultipart, SENT_SMS_ACTION, result, runAttempt));
|
||||
break;
|
||||
case DELIVERED_SMS_ACTION:
|
||||
byte[] pdu = intent.getByteArrayExtra("pdu");
|
||||
|
||||
if (pdu == null) {
|
||||
Log.w(TAG, "No PDU in delivery receipt!");
|
||||
break;
|
||||
}
|
||||
|
||||
SmsMessage message = SmsMessage.createFromPdu(pdu);
|
||||
|
||||
if (message == null) {
|
||||
Log.w(TAG, "Delivery receipt failed to parse!");
|
||||
break;
|
||||
}
|
||||
|
||||
int status = message.getStatus();
|
||||
|
||||
Log.i(TAG, "Original status: " + status);
|
||||
|
||||
// Note: https://developer.android.com/reference/android/telephony/SmsMessage.html#getStatus()
|
||||
// " CDMA: For not interfering with status codes from GSM, the value is shifted to the bits 31-16"
|
||||
// Note: https://stackoverflow.com/a/33240109
|
||||
if ("3gpp2".equals(intent.getStringExtra("format"))) {
|
||||
Log.w(TAG, "Correcting for CDMA delivery receipt...");
|
||||
if (status >> 24 <= 0) status = MessageTable.Status.STATUS_COMPLETE;
|
||||
else if (status >> 24 == 2) status = MessageTable.Status.STATUS_PENDING;
|
||||
else if (status >> 24 == 3) status = MessageTable.Status.STATUS_FAILED;
|
||||
}
|
||||
|
||||
jobManager.add(new SmsSentJob(messageId, isMultipart, DELIVERED_SMS_ACTION, status, runAttempt));
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Unknown action: " + intent.getAction());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,6 @@ public final class ShakeToReport implements ShakeDetector.Listener {
|
||||
MultiselectForwardFragment.showFullScreen(
|
||||
activity.getSupportFragmentManager(),
|
||||
new MultiselectForwardFragmentArgs(
|
||||
true,
|
||||
Collections.singletonList(new MultiShareArgs.Builder()
|
||||
.withDraftText(url)
|
||||
.build()),
|
||||
|
||||
@@ -85,7 +85,6 @@ public final class MultiShareSender {
|
||||
public static MultiShareSendResultCollection sendSync(@NonNull MultiShareArgs multiShareArgs) {
|
||||
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getContactSearchKeys().size());
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
boolean isMmsEnabled = Util.isMmsCapable(context);
|
||||
String message = multiShareArgs.getDraftText();
|
||||
SlideDeck slideDeck;
|
||||
List<OutgoingMessage> storiesBatch = new LinkedList<>();
|
||||
@@ -111,8 +110,7 @@ public final class MultiShareSender {
|
||||
MessageSendType sendType = MessageSendType.SignalMessageSendType.INSTANCE;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
||||
List<Contact> contacts = multiShareArgs.getSharedContacts();
|
||||
boolean needsSplit = !sendType.usesSmsTransport() &&
|
||||
message != null &&
|
||||
boolean needsSplit = message != null &&
|
||||
message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
boolean hasMmsMedia = !multiShareArgs.getMedia().isEmpty() ||
|
||||
(multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) ||
|
||||
@@ -128,9 +126,9 @@ public final class MultiShareSender {
|
||||
MultiShareTimestampProvider sentTimestamp = recipient.isDistributionList() ? distributionListSentTimestamps : MultiShareTimestampProvider.create();
|
||||
boolean canSendAsTextStory = recipientSearchKey.isStory() && multiShareArgs.isValidForTextStoryGeneration();
|
||||
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent())) {
|
||||
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.MMS_NOT_ENABLED));
|
||||
} else if (hasMmsMedia && sendType.usesSmsTransport() || hasPushMedia && !sendType.usesSmsTransport() || canSendAsTextStory) {
|
||||
} else if (hasPushMedia || canSendAsTextStory) {
|
||||
sendMediaMessageOrCollectStoryToBatch(context,
|
||||
multiShareArgs,
|
||||
recipient,
|
||||
|
||||
@@ -6,13 +6,9 @@ import java.lang.UnsupportedOperationException
|
||||
|
||||
sealed class ResolvedShareData {
|
||||
|
||||
abstract val isMmsOrSmsSupported: Boolean
|
||||
|
||||
abstract fun toMultiShareArgs(): MultiShareArgs
|
||||
|
||||
data class Primitive(val text: CharSequence) : ResolvedShareData() {
|
||||
override val isMmsOrSmsSupported: Boolean = true
|
||||
|
||||
override fun toMultiShareArgs(): MultiShareArgs {
|
||||
return MultiShareArgs.Builder(setOf()).withDraftText(text.toString()).build()
|
||||
}
|
||||
@@ -21,8 +17,7 @@ sealed class ResolvedShareData {
|
||||
data class ExternalUri(
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
val text: CharSequence?,
|
||||
override val isMmsOrSmsSupported: Boolean
|
||||
val text: CharSequence?
|
||||
) : ResolvedShareData() {
|
||||
override fun toMultiShareArgs(): MultiShareArgs {
|
||||
return MultiShareArgs.Builder(setOf()).withDataUri(uri).withDataType(mimeType).withDraftText(text?.toString()).build()
|
||||
@@ -30,8 +25,7 @@ sealed class ResolvedShareData {
|
||||
}
|
||||
|
||||
data class Media(
|
||||
val media: List<org.thoughtcrime.securesms.mediasend.Media>,
|
||||
override val isMmsOrSmsSupported: Boolean
|
||||
val media: List<org.thoughtcrime.securesms.mediasend.Media>
|
||||
) : ResolvedShareData() {
|
||||
override fun toMultiShareArgs(): MultiShareArgs {
|
||||
return MultiShareArgs.Builder(setOf()).withMedia(media).build()
|
||||
@@ -39,7 +33,6 @@ sealed class ResolvedShareData {
|
||||
}
|
||||
|
||||
object Failure : ResolvedShareData() {
|
||||
override val isMmsOrSmsSupported: Boolean get() = throw UnsupportedOperationException()
|
||||
override fun toMultiShareArgs(): MultiShareArgs = throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +232,6 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C
|
||||
R.id.fragment_container,
|
||||
MultiselectForwardFragment.create(
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = resolvedShareData.isMmsOrSmsSupported,
|
||||
multiShareArgs = listOf(resolvedShareData.toMultiShareArgs()),
|
||||
title = getTitleFromExtras(),
|
||||
forceDisableAddMessage = true,
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.toKotlinPair
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.UriUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.Optional
|
||||
@@ -73,8 +64,7 @@ class ShareRepository(context: Context) {
|
||||
return ResolvedShareData.ExternalUri(
|
||||
uri = blobUri,
|
||||
mimeType = mimeType,
|
||||
text = multiShareExternal.text,
|
||||
isMmsOrSmsSupported = isMmsSupported(appContext, asUriAttachment(blobUri, mimeType, size))
|
||||
text = multiShareExternal.text
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,9 +121,7 @@ class ShareRepository(context: Context) {
|
||||
}.filterNotNull()
|
||||
|
||||
return if (media.isNotEmpty()) {
|
||||
val isMmsSupported = media.all { isMmsSupported(appContext, asUriAttachment(it.uri, it.mimeType, it.size)) }
|
||||
|
||||
ResolvedShareData.Media(media, isMmsSupported)
|
||||
ResolvedShareData.Media(media)
|
||||
} else {
|
||||
ResolvedShareData.Failure
|
||||
}
|
||||
@@ -179,21 +167,5 @@ class ShareRepository(context: Context) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun asUriAttachment(uri: Uri, mimeType: String, size: Long): UriAttachment {
|
||||
return UriAttachment(uri, mimeType, -1, size, null, false, false, false, false, null, null, null, null, null)
|
||||
}
|
||||
|
||||
private fun isMmsSupported(context: Context, attachment: Attachment): Boolean {
|
||||
val canReadPhoneState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
if (!Util.isDefaultSmsProvider(context) || !canReadPhoneState || !Util.isMmsCapable(context) || !SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val sendType: MessageSendType = MessageSendType.getFirstForTransport(MessageSendType.TransportType.SMS)
|
||||
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(sendType.simSubscriptionId ?: -1)
|
||||
return mmsConstraints.isSatisfied(context, attachment) || mmsConstraints.canResize(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,12 @@ import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentMarkUploadedJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileKeySendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDistributionListSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.IndividualSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.RemoteDeleteSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
@@ -536,10 +534,8 @@ public class MessageSender {
|
||||
sendDistributionList(context, recipient, messageId, Collections.emptySet(), uploadJobIds);
|
||||
} else if (sendType == SendType.SIGNAL && isPushMediaSend(context, recipient)) {
|
||||
sendMediaPush(context, recipient, messageId, uploadJobIds);
|
||||
} else if (sendType == SendType.MMS) {
|
||||
sendMms(context, messageId);
|
||||
} else {
|
||||
sendSms(recipient, messageId);
|
||||
Log.w(TAG, "Unknown send type!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,16 +572,6 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendMms(Context context, long messageId) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
MmsSendJob.enqueue(context, jobManager, messageId);
|
||||
}
|
||||
|
||||
private static void sendSms(Recipient recipient, long messageId) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
jobManager.add(new SmsSendJob(messageId, recipient));
|
||||
}
|
||||
|
||||
private static boolean isPushMediaSend(Context context, Recipient recipient) {
|
||||
if (!SignalStore.account().isRegistered()) {
|
||||
return false;
|
||||
|
||||
@@ -100,7 +100,6 @@ public final class StickerManagementActivity extends PassphraseRequiredActivity
|
||||
MultiselectForwardFragment.showBottomSheet(
|
||||
getSupportFragmentManager(),
|
||||
new MultiselectForwardFragmentArgs(
|
||||
true,
|
||||
Collections.singletonList(new MultiShareArgs.Builder()
|
||||
.withDraftText(StickerUrl.createShareLink(packId, packKey))
|
||||
.build()),
|
||||
|
||||
@@ -242,7 +242,6 @@ public final class StickerPackPreviewActivity extends PassphraseRequiredActivity
|
||||
MultiselectForwardFragment.showBottomSheet(
|
||||
getSupportFragmentManager(),
|
||||
new MultiselectForwardFragmentArgs(
|
||||
true,
|
||||
Collections.singletonList(new MultiShareArgs.Builder()
|
||||
.withDraftText(StickerUrl.createShareLink(packId, packKey))
|
||||
.build()),
|
||||
|
||||
@@ -153,7 +153,7 @@ public final class StorageSyncHelper {
|
||||
.setPinnedConversations(StorageSyncModels.localToRemotePinnedConversations(pinned))
|
||||
.setPreferContactAvatars(SignalStore.settings().isPreferSystemContactPhotos())
|
||||
.setPayments(SignalStore.paymentsValues().mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.paymentsValues().getPaymentsEntropy()).map(Entropy::getBytes).orElse(null))
|
||||
.setPrimarySendsSms(Util.isDefaultSmsProvider(context))
|
||||
.setPrimarySendsSms(false)
|
||||
.setUniversalExpireTimer(SignalStore.settings().getUniversalExpireTimer())
|
||||
.setDefaultReactions(SignalStore.emojiValues().getReactions())
|
||||
.setSubscriber(StorageSyncModels.localToRemoteSubscriber(SignalStore.donationsValues().getSubscriber()))
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.telephony.SmsMessage;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
public class SmsCharacterCalculator extends CharacterCalculator {
|
||||
|
||||
private static final String TAG = Log.tag(SmsCharacterCalculator.class);
|
||||
|
||||
@Override
|
||||
public CharacterState calculateCharacters(String messageBody) {
|
||||
int[] length;
|
||||
int messagesSpent;
|
||||
int charactersSpent;
|
||||
int charactersRemaining;
|
||||
|
||||
try {
|
||||
length = SmsMessage.calculateLength(messageBody, false);
|
||||
messagesSpent = length[0];
|
||||
charactersSpent = length[1];
|
||||
charactersRemaining = length[2];
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(TAG, e);
|
||||
messagesSpent = 1;
|
||||
charactersSpent = messageBody.length();
|
||||
charactersRemaining = 1000;
|
||||
} catch (RuntimeException e) {
|
||||
if (e instanceof SecurityException || e.getCause() instanceof SecurityException) {
|
||||
Log.e(TAG, "Security Exception", e);
|
||||
messagesSpent = 1;
|
||||
charactersSpent = messageBody.length();
|
||||
charactersRemaining = 1000;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
int maxMessageSize;
|
||||
|
||||
if (messagesSpent > 0) {
|
||||
maxMessageSize = (charactersSpent + charactersRemaining) / messagesSpent;
|
||||
} else {
|
||||
maxMessageSize = (charactersSpent + charactersRemaining);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.app.role.RoleManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.provider.Telephony;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public final class SmsUtil {
|
||||
|
||||
private SmsUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be used with {@code startActivityForResult}
|
||||
*/
|
||||
public static @NonNull Intent getSmsRoleIntent(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
RoleManager roleManager = ContextCompat.getSystemService(context, RoleManager.class);
|
||||
return roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS);
|
||||
} else {
|
||||
Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT);
|
||||
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,6 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.provider.Telephony;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -38,8 +35,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
@@ -50,11 +45,10 @@ import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -143,10 +137,6 @@ public class Util {
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
public static boolean isEmpty(EncodedStringValue[] value) {
|
||||
return value == null || value.length == 0;
|
||||
}
|
||||
|
||||
public static boolean isEmpty(ComposeText value) {
|
||||
return value == null || value.getText() == null || TextUtils.isEmpty(value.getTextTrimmed());
|
||||
}
|
||||
@@ -198,27 +188,15 @@ public class Util {
|
||||
}
|
||||
|
||||
public static @NonNull String toIsoString(byte[] bytes) {
|
||||
try {
|
||||
return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError("ISO_8859_1 must be supported!");
|
||||
}
|
||||
return new String(bytes, StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
public static byte[] toIsoBytes(String isoString) {
|
||||
try {
|
||||
return isoString.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError("ISO_8859_1 must be supported!");
|
||||
}
|
||||
return isoString.getBytes(StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
public static byte[] toUtf8Bytes(String utf8String) {
|
||||
try {
|
||||
return utf8String.getBytes(CharacterSets.MIMENAME_UTF_8);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError("UTF_8 must be supported!");
|
||||
}
|
||||
return utf8String.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static void wait(Object lock, long timeout) {
|
||||
@@ -231,7 +209,6 @@ public class Util {
|
||||
|
||||
@RequiresPermission(anyOf = {
|
||||
android.Manifest.permission.READ_PHONE_STATE,
|
||||
android.Manifest.permission.READ_SMS,
|
||||
android.Manifest.permission.READ_PHONE_NUMBERS
|
||||
})
|
||||
@SuppressLint("MissingPermission")
|
||||
@@ -328,11 +305,6 @@ public class Util {
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public static boolean isDefaultSmsProvider(Context context){
|
||||
return context.getPackageName().equals(Telephony.Sms.getDefaultSmsPackage(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* The app version.
|
||||
* <p>
|
||||
@@ -394,10 +366,6 @@ public class Util {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isMmsCapable(Context context) {
|
||||
return (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) || OutgoingLegacyMmsConnection.isConnectionPossible(context);
|
||||
}
|
||||
|
||||
public static <T> T getRandomElement(T[] elements) {
|
||||
return elements[new SecureRandom().nextInt(elements.length)];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user