Add sms export library and sample app.
|
@ -19,8 +19,8 @@ import org.greenrobot.eventbus.EventBus;
|
|||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
|
@ -50,7 +50,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
|
@ -79,22 +78,22 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
|
||||
private static final String TAG = Log.tag(FullBackupExporter.class);
|
||||
|
||||
private static final long DATABASE_VERSION_RECORD_COUNT = 1L;
|
||||
private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L;
|
||||
private static final long DATABASE_VERSION_RECORD_COUNT = 1L;
|
||||
private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L;
|
||||
private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L;
|
||||
private static final long FINAL_MESSAGE_COUNT = 1L;
|
||||
private static final long FINAL_MESSAGE_COUNT = 1L;
|
||||
|
||||
private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet(
|
||||
SignedPreKeyDatabase.TABLE_NAME,
|
||||
OneTimePreKeyDatabase.TABLE_NAME,
|
||||
SessionDatabase.TABLE_NAME,
|
||||
SearchDatabase.SMS_FTS_TABLE_NAME,
|
||||
SearchDatabase.MMS_FTS_TABLE_NAME,
|
||||
EmojiSearchDatabase.TABLE_NAME,
|
||||
SenderKeyDatabase.TABLE_NAME,
|
||||
SenderKeySharedDatabase.TABLE_NAME,
|
||||
PendingRetryReceiptDatabase.TABLE_NAME,
|
||||
AvatarPickerDatabase.TABLE_NAME
|
||||
SignedPreKeyDatabase.TABLE_NAME,
|
||||
OneTimePreKeyDatabase.TABLE_NAME,
|
||||
SessionDatabase.TABLE_NAME,
|
||||
SearchDatabase.SMS_FTS_TABLE_NAME,
|
||||
SearchDatabase.MMS_FTS_TABLE_NAME,
|
||||
EmojiSearchDatabase.TABLE_NAME,
|
||||
SenderKeyDatabase.TABLE_NAME,
|
||||
SenderKeySharedDatabase.TABLE_NAME,
|
||||
PendingRetryReceiptDatabase.TABLE_NAME,
|
||||
AvatarPickerDatabase.TABLE_NAME
|
||||
);
|
||||
|
||||
public static void export(@NonNull Context context,
|
||||
|
@ -315,7 +314,7 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
|
||||
statement.append('(');
|
||||
|
||||
for (int i=0;i<cursor.getColumnCount();i++) {
|
||||
for (int i = 0; i < cursor.getColumnCount(); i++) {
|
||||
statement.append('?');
|
||||
|
||||
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
|
||||
|
@ -329,10 +328,10 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
|
||||
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setNullparameter(true));
|
||||
} else {
|
||||
throw new AssertionError("unknown type?" + cursor.getType(i));
|
||||
throw new AssertionError("unknown type?" + cursor.getType(i));
|
||||
}
|
||||
|
||||
if (i < cursor.getColumnCount()-1) {
|
||||
if (i < cursor.getColumnCount() - 1) {
|
||||
statement.append(',');
|
||||
}
|
||||
}
|
||||
|
@ -375,7 +374,7 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
InputStream inputStream;
|
||||
|
||||
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
||||
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
|
||||
|
@ -390,8 +389,8 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
|
||||
private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) {
|
||||
try {
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
|
||||
|
||||
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH));
|
||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
|
||||
|
@ -410,13 +409,13 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
}
|
||||
|
||||
private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException {
|
||||
long result = 0;
|
||||
long result = 0;
|
||||
InputStream inputStream;
|
||||
|
||||
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
||||
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
|
||||
|
||||
int read;
|
||||
int read;
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
||||
|
@ -479,7 +478,7 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
|
||||
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
|
||||
}
|
||||
|
||||
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
|
||||
|
@ -509,7 +508,7 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
}
|
||||
|
||||
private static boolean isForNonExpiringMmsMessage(@NonNull SQLiteDatabase db, long mmsId) {
|
||||
String[] columns = new String[] { MmsDatabase.RECIPIENT_ID, MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
|
||||
String[] columns = new String[] { MmsDatabase.RECIPIENT_ID, MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE };
|
||||
String where = MmsDatabase.ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(mmsId) };
|
||||
|
||||
|
@ -528,8 +527,8 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
private final byte[] cipherKey;
|
||||
private final byte[] macKey;
|
||||
private final byte[] cipherKey;
|
||||
private final byte[] macKey;
|
||||
|
||||
private byte[] iv;
|
||||
private int counter;
|
||||
|
@ -695,5 +694,6 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
boolean isCanceled();
|
||||
}
|
||||
|
||||
public static final class BackupCanceledException extends IOException { }
|
||||
public static final class BackupCanceledException extends IOException {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.contacts.paged
|
|||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.concurrent.safeBlockingGet
|
||||
import org.signal.core.util.logging.Log
|
||||
|
@ -10,7 +11,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil
|
||||
import org.thoughtcrime.securesms.util.Stopwatch
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.IdentityCheckResponse
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.signal.contacts.ContactLinkConfiguration
|
|||
import org.signal.contacts.SystemContactsRepository
|
||||
import org.signal.contacts.SystemContactsRepository.ContactIterator
|
||||
import org.signal.contacts.SystemContactsRepository.ContactPhoneDetails
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
|
@ -30,7 +31,6 @@ import org.thoughtcrime.securesms.registration.RegistrationUtil
|
|||
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Stopwatch
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
|
|
|
@ -25,10 +25,9 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
|||
import org.thoughtcrime.securesms.push.IasTrustStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.contacts.sync
|
|||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.contacts.SystemContactsRepository
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
|
@ -13,7 +14,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Stopwatch
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.services.CdsiV2Service
|
||||
import java.io.IOException
|
||||
|
|
|
@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
|
|
|
@ -106,7 +106,6 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
|
|||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
|
||||
import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
|
@ -179,7 +178,7 @@ import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
|||
import org.thoughtcrime.securesms.util.SignalProxyUtil;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TopToastPopup;
|
||||
|
|
|
@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
|
|
@ -153,7 +153,7 @@ import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
|||
import org.thoughtcrime.securesms.util.SignalProxyUtil;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
|
|
@ -8,6 +8,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase
|
|||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.getTableRowCount
|
||||
import org.signal.core.util.logging.Log
|
||||
|
@ -15,7 +16,6 @@ import org.thoughtcrime.securesms.crypto.DatabaseSecret
|
|||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
||||
import org.thoughtcrime.securesms.database.model.LogEntry
|
||||
import org.thoughtcrime.securesms.util.ByteUnit
|
||||
import org.thoughtcrime.securesms.util.Stopwatch
|
||||
import java.io.Closeable
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
|
|
|
@ -17,6 +17,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase
|
|||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireString
|
||||
import org.thoughtcrime.securesms.color.MaterialColor
|
||||
|
@ -40,7 +41,6 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
|||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FileUtils
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.Stopwatch
|
||||
import org.thoughtcrime.securesms.util.Triple
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
|
|
|
@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsA
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.payments.TransactionSubmissionResult;
|
|||
import org.thoughtcrime.securesms.payments.Wallet;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
|
||||
import java.util.Objects;
|
||||
|
|
|
@ -42,7 +42,7 @@ import org.thoughtcrime.securesms.util.Base64;
|
|||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
|
|
|
@ -33,9 +33,8 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult
|
|||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
||||
import org.thoughtcrime.securesms.storage.StoryDistributionListRecordProcessor;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
|
|
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
|
|||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
|
|
@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
|||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
|
|
@ -53,7 +53,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
|
|||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay;
|
||||
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import androidx.camera.core.ImageProxy;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
|
|
@ -11,13 +11,13 @@ import androidx.lifecycle.Transformations
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.Stopwatch
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
|
|
|
@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
|||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -26,7 +25,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
|
|
|
@ -7,7 +7,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import com.google.zxing.common.BitMatrix;
|
|||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
|
||||
public final class QrCode {
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.concurrent.FilteredExecutor;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewMod
|
|||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.os.Looper;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package org.signal.core.util
|
||||
|
||||
/**
|
||||
* A Result that allows for generic definitions of success/failure values.
|
||||
*/
|
||||
sealed class Result<out S, out F> {
|
||||
|
||||
data class Failure<out F>(val failure: F) : Result<Nothing, F>()
|
||||
data class Success<out S>(val success: S) : Result<S, Nothing>()
|
||||
|
||||
companion object {
|
||||
fun <S> success(value: S) = Success(value)
|
||||
fun <F> failure(value: F) = Failure(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an Result<S, F> to an Result<T, F>. Failure values will pass through, while
|
||||
* right values will be operated on by the parameter.
|
||||
*/
|
||||
fun <T> map(onSuccess: (S) -> T): Result<T, F> {
|
||||
return when (this) {
|
||||
is Failure -> this
|
||||
is Success -> success(onSuccess(success))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the caller to operate on the Result such that the correct function is applied
|
||||
* to the value it contains.
|
||||
*/
|
||||
fun <T> either(
|
||||
onSuccess: (S) -> T,
|
||||
onFailure: (F) -> T
|
||||
): T {
|
||||
return when (this) {
|
||||
is Success -> onSuccess(success)
|
||||
is Failure -> onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an Result<L, R> to an Result<L, T>. Failure values will pass through, while
|
||||
* right values will be operated on by the parameter.
|
||||
*
|
||||
* Note this is an extension method in order to make the generics happy.
|
||||
*/
|
||||
fun <T, S, F> Result<S, F>.flatMap(onSuccess: (S) -> Result<T, F>): Result<T, F> {
|
||||
return when (this) {
|
||||
is Result.Success -> onSuccess(success)
|
||||
is Result.Failure -> this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try is a specialization of Result where the Failure is fixed to Throwable.
|
||||
*/
|
||||
typealias Try<S> = Result<S, Throwable>
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
package org.signal.core.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
|
@ -48,6 +48,7 @@ dependencyResolutionManagement {
|
|||
alias('androidx-biometric').to('androidx.biometric:biometric:1.1.0')
|
||||
alias('androidx-sharetarget').to('androidx.sharetarget:sharetarget:1.1.0')
|
||||
alias('androidx-sqlite').to('androidx.sqlite:sqlite:2.1.0')
|
||||
alias('androidx-core-role').to('androidx.core:core-role:1.0.0')
|
||||
|
||||
// Material
|
||||
alias('material-material').to('com.google.android.material:material:1.5.0')
|
||||
|
|
|
@ -336,6 +336,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="b037fdfb267dc0141ab9f4e4e85daf87b175cf311248d54b501b58ec42345315" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.core" name="core-role" version="1.0.0">
|
||||
<artifact name="core-role-1.0.0.aar">
|
||||
<sha256 value="796378da90f5e74a7664ae650d62b904ee96444f87006a7972f97285b223370d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.cursoradapter" name="cursoradapter" version="1.0.0">
|
||||
<artifact name="cursoradapter-1.0.0.aar">
|
||||
<sha256 value="a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564" origin="Generated by Gradle"/>
|
||||
|
@ -2850,6 +2855,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="25434813eef31de5f7b2d11bb744b8434be3750e3010fc9803e54671076f071d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest" name="ktlint" version="0.42.1">
|
||||
<artifact name="ktlint-0.42.1.jar">
|
||||
<sha256 value="aafdc2c1e66746a3c383cd6fb94343f0b7a856c2cfbfd40ff4464c726618a9a7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-0.42.1.module">
|
||||
<sha256 value="f06ba76eb422ad7b7da5ccf048d06d54dc5261ef953393a9043abd4f958c6e29" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest" name="ktlint" version="0.43.2">
|
||||
<artifact name="ktlint-0.43.2.jar">
|
||||
<sha256 value="99ec69ef0628695c24dbbc2cc4b8d7c61a754697d624f5233fc65f43faf2d235" origin="Generated by Gradle"/>
|
||||
|
@ -2858,6 +2871,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="8bbdf6bc56cb12aa8ddea097e9ae862cde9a7c11bc32332dedda73241fb220dc" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-core" version="0.42.1">
|
||||
<artifact name="ktlint-core-0.42.1.jar">
|
||||
<sha256 value="a7bd968f4f408521e44a781594a2237df0199aab1ad2942c52bf8ad21e15dea4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-core-0.42.1.module">
|
||||
<sha256 value="6b1efb95887d9172d109df25afc2ef89fa0f09e4b230a47f56c57ad53bfb17ba" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-core" version="0.43.2">
|
||||
<artifact name="ktlint-core-0.43.2.jar">
|
||||
<sha256 value="401515a76b780a32ef9dfeaf69f77316934c4bb90f339488638311789eca7a1a" origin="Generated by Gradle"/>
|
||||
|
@ -2866,6 +2887,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="aa276dfa9dcfab2f0459c81e7f903712058230d0908d545cc4bc8674273a51d7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-baseline" version="0.42.1">
|
||||
<artifact name="ktlint-reporter-baseline-0.42.1.jar">
|
||||
<sha256 value="6a6de6072e3a8b7b96ef9b8486985889977500761ff37f0467689af9fcbc2843" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-reporter-baseline-0.42.1.module">
|
||||
<sha256 value="7476d04c105bfec627889c9f2807f524d26ab316dd57d42f7748db7ffbe8ad4f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-baseline" version="0.43.2">
|
||||
<artifact name="ktlint-reporter-baseline-0.43.2.jar">
|
||||
<sha256 value="733ee7e2cadb321d6597b3501c70c7da73117adaa0c6bc084dfc16c455d68806" origin="Generated by Gradle"/>
|
||||
|
@ -2874,6 +2903,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="3b6466c5813d2deb31a534ae694c41c36b93aec787eb2a8aff162a1288c63533" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-checkstyle" version="0.42.1">
|
||||
<artifact name="ktlint-reporter-checkstyle-0.42.1.jar">
|
||||
<sha256 value="dad0e9626f6cbfec9df70eb8100ba5ea62d421e5c179b9b0e1f69586b0ba1fa6" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-reporter-checkstyle-0.42.1.module">
|
||||
<sha256 value="017768838d4276018aaebe07a271f0022b5f3e66952bc2f0ceae202da4cb66be" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-checkstyle" version="0.43.2">
|
||||
<artifact name="ktlint-reporter-checkstyle-0.43.2.jar">
|
||||
<sha256 value="becafb4006b9f2e82c99749864a1a8de340ee84ac7271631a68981a44f51e808" origin="Generated by Gradle"/>
|
||||
|
@ -2882,6 +2919,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="3937057372b1cab189647a1e2fa25aa19cb5f72168ca663421b9e250b4e77d05" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-html" version="0.42.1">
|
||||
<artifact name="ktlint-reporter-html-0.42.1.jar">
|
||||
<sha256 value="ca2c35bf0f436434a6fd8a95a8e47321b62d02cb242a4989c17a5d5b27ecea74" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-reporter-html-0.42.1.module">
|
||||
<sha256 value="61fdc1ded68e730b76f269c94d1024484d565df629bfcd5eb45fd4ce05353def" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-html" version="0.43.2">
|
||||
<artifact name="ktlint-reporter-html-0.43.2.jar">
|
||||
<sha256 value="800392e150d3266e72ca53c6ccca3136d4e26445dd9216c6ac6cfc1ba3afafe5" origin="Generated by Gradle"/>
|
||||
|
@ -2890,6 +2935,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="432a6fbb008f1373d3e8bde4ab9d905620ff87fd9f3b50a5654b7717f0a3eaab" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-json" version="0.42.1">
|
||||
<artifact name="ktlint-reporter-json-0.42.1.jar">
|
||||
<sha256 value="d173003331b292dec16bcd5f898546cfcaf4c61c2214136808e21f222a1afd1c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-reporter-json-0.42.1.module">
|
||||
<sha256 value="3cd549d0c0bf07182cfe69bf6f1a7643473ec1669d1fca12194b2586f25525ed" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-json" version="0.43.2">
|
||||
<artifact name="ktlint-reporter-json-0.43.2.jar">
|
||||
<sha256 value="9d4a94190d96d671000a06a50c9d1ce111d0dcf629bef8b4f0221a9e3f3699a0" origin="Generated by Gradle"/>
|
||||
|
@ -2898,6 +2951,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="7e7be45882eb7abc67a62d12980018f2bb067d88d9947395a84ad678099b5179" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-plain" version="0.42.1">
|
||||
<artifact name="ktlint-reporter-plain-0.42.1.jar">
|
||||
<sha256 value="df673cd3e88e330e45dc37d58c2789b37b3ed8c3d2edcc4bd52cf719f2a7ee4c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-reporter-plain-0.42.1.module">
|
||||
<sha256 value="2afb405369eee884f7dcc1e17a2c5f37b1836d7de7ac506196c5b325584febe0" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-plain" version="0.43.2">
|
||||
<artifact name="ktlint-reporter-plain-0.43.2.jar">
|
||||
<sha256 value="1cab63f431ec4e9463df7a767f131ccfa8d76259c01fecc63a4c000063e8ee43" origin="Generated by Gradle"/>
|
||||
|
@ -2906,6 +2967,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="ea97899a3d8b6f8e18c7ae1a5d2f7147f976844f1bd2a51c27b7d8285d90a5ec" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-sarif" version="0.42.1">
|
||||
<artifact name="ktlint-reporter-sarif-0.42.1.jar">
|
||||
<sha256 value="13723186b353287cbdfd60ede056f25dbfb21a7a398be782ab64c9b4ef0ab593" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-reporter-sarif-0.42.1.module">
|
||||
<sha256 value="d480e84b60a747582cfe4e4b1608806511bc4cebe7c5c394920e842160c5cf7a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-reporter-sarif" version="0.43.2">
|
||||
<artifact name="ktlint-reporter-sarif-0.43.2.jar">
|
||||
<sha256 value="ed0046aaa4a2e4544197bfdccf88d472ef413a55ad05b6dc8aae41338e9d3748" origin="Generated by Gradle"/>
|
||||
|
@ -2914,6 +2983,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="7ff665bb3f0f36af38b80087c9a0067a9dff3c89b6a2c1c78a1f6e1455eb1d09" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-ruleset-experimental" version="0.42.1">
|
||||
<artifact name="ktlint-ruleset-experimental-0.42.1.jar">
|
||||
<sha256 value="9cdc257cba3d0568c553da9ebc90d0d8eda0743f150e2f0f9d3c60626165840d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-ruleset-experimental-0.42.1.module">
|
||||
<sha256 value="a3f839fb54c9443f60bde4518c69c65b3f5fa807deb4104f472c7ee22d6e2ae5" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-ruleset-experimental" version="0.43.2">
|
||||
<artifact name="ktlint-ruleset-experimental-0.43.2.jar">
|
||||
<sha256 value="d89e0edcdca0ae375c090565e323520ab5d424d82fd6ac6290ea986d360f0b11" origin="Generated by Gradle"/>
|
||||
|
@ -2922,6 +2999,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="2d85cd883fe88c4b5429f266de027afca9f9c53a4f49bf14822a4fdf4abeb67a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-ruleset-standard" version="0.42.1">
|
||||
<artifact name="ktlint-ruleset-standard-0.42.1.jar">
|
||||
<sha256 value="cd3a1f034a554a2e1877aead61a252f1eadc9adfed345edec0ce863dcff4e61c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-ruleset-standard-0.42.1.module">
|
||||
<sha256 value="cfb11e428ae3564249b96ebc08e5170596b3b3790250a9133782681f6b56a036" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-ruleset-standard" version="0.43.2">
|
||||
<artifact name="ktlint-ruleset-standard-0.43.2.jar">
|
||||
<sha256 value="6774dc9d42aa7c7fdd4a7f3732b56fdab99ba78ce0c4eb5159036525657d0014" origin="Generated by Gradle"/>
|
||||
|
@ -2930,6 +3015,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="7ce4e3721b8a6a2e0dd9607e8e5e5b337f5be4f9ed3f6a5dde9ff6d189355303" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-ruleset-test" version="0.42.1">
|
||||
<artifact name="ktlint-ruleset-test-0.42.1.jar">
|
||||
<sha256 value="0e9001347428a5be6b6b3a8bb322204259805e04b0d4bb6ed427d8a451db5097" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="ktlint-ruleset-test-0.42.1.module">
|
||||
<sha256 value="c0c9319daa040e6e3c0f4b8503138f9764dfdfc81672f4d2f7f9824cc4d7db39" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.pinterest.ktlint" name="ktlint-ruleset-test" version="0.43.2">
|
||||
<artifact name="ktlint-ruleset-test-0.43.2.jar">
|
||||
<sha256 value="7270c4d98b2cda268c25397a02b7dea0ab8cb923958cb3853121e0d9366ce797" origin="Generated by Gradle"/>
|
||||
|
@ -3095,6 +3188,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="c0e6e3d3bc67c819f3ea8fd3c755006d38e675433eaaaf2dab3b11d2fb4f2428" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.squareup.okhttp" name="okhttp" version="2.5.0">
|
||||
<artifact name="okhttp-2.5.0.jar">
|
||||
<sha256 value="1cc716e29539adcda677949508162796daffedb4794cbf947a6f65e696f0381c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.squareup.okhttp" name="okhttp" version="2.7.4">
|
||||
<artifact name="okhttp-2.7.4.jar">
|
||||
<sha256 value="c88be9af1509d5aeec9394a818c0fa08e26fad9d64ba134e6f977e0bb20cb114" origin="Generated by Gradle"/>
|
||||
|
@ -3103,6 +3201,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="1595d91482c30d2bdaacb2bf3a5d62659f1ecd3c22096124cc8a2c2356a9901e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.squareup.okhttp" name="okhttp-urlconnection" version="2.5.0">
|
||||
<artifact name="okhttp-urlconnection-2.5.0.jar">
|
||||
<sha256 value="79ec6f4e79e683105e87fe83278a531c693e538d30e3b9237000ce7c94fcb2cf" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.squareup.okhttp" name="parent" version="2.7.4">
|
||||
<artifact name="parent-2.7.4.pom">
|
||||
<sha256 value="b20bd74ba01b55b30d6b7d10b9373f2a324b1f3638ecda0275c93e454223c7c8" origin="Generated by Gradle"/>
|
||||
|
@ -3134,6 +3237,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="f1c10b1480d1ab75fb051a07b273e37cda2525e97e1607ee83e6833b70e1bfce" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.squareup.okio" name="okio" version="1.6.0">
|
||||
<artifact name="okio-1.6.0.jar">
|
||||
<sha256 value="114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.squareup.okio" name="okio" version="2.2.2">
|
||||
<artifact name="okio-2.2.2.jar">
|
||||
<sha256 value="e58c97406a6bb1138893750299ac63c6aa04b38b6b49eae1bfcad1a63ef9ba1b" origin="Generated by Gradle"/>
|
||||
|
@ -4602,6 +4710,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="2d10df04835f07b87aa81a0e7fec406d2c1ac11c54e7e08f08eb511afe5bb48f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="1.5.20">
|
||||
<artifact name="kotlin-compiler-embeddable-1.5.20.jar">
|
||||
<sha256 value="11d51087eb70b5abbad6fbf459a4349a0335916588000b5ecd990f01482e38ff" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="1.5.31">
|
||||
<artifact name="kotlin-compiler-embeddable-1.5.31.jar">
|
||||
<sha256 value="e39811a9e4c102e779c659eefe90b041c66ce87578c1bfdac07cf504d1551745" origin="Generated by Gradle"/>
|
||||
|
@ -4634,6 +4747,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="43c12720a4f276b35dc04396128e9b7fb1a8abd07a16cb409c6c0d6706a2316b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="1.5.20">
|
||||
<artifact name="kotlin-daemon-embeddable-1.5.20.jar">
|
||||
<sha256 value="5a2e1e6869d130d937b39c668ea6bca758ef8960d168847f6e13aa2a2add424a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="1.5.31">
|
||||
<artifact name="kotlin-daemon-embeddable-1.5.31.jar">
|
||||
<sha256 value="f61eaf89e5e3848631650b25cdfb66fe8cae0281a054d9d986716000a15ba8d6" origin="Generated by Gradle"/>
|
||||
|
@ -4738,6 +4856,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="909d0b8a326568c4db341f21b5f0e221c75c002896a4ea3b170aa5a1569a0e54" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.5.20">
|
||||
<artifact name="kotlin-reflect-1.5.20.jar">
|
||||
<sha256 value="fd6782d18bcc17ffa98221a1c34e4a42a7e3e6b4a4b72b474b5c82e14c8bab5a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.5.31">
|
||||
<artifact name="kotlin-reflect-1.5.31.jar">
|
||||
<sha256 value="6e0f5490e6b9649ddd2670534e4d3a03bd283c3358b8eef5d1304fd5f8a5a4fb" origin="Generated by Gradle"/>
|
||||
|
@ -4754,6 +4877,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="da71e1e3ddc2388d675641672e2f0316db9c9d212f946f026d4a0e6a1336a7f3" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="1.5.20">
|
||||
<artifact name="kotlin-script-runtime-1.5.20.jar">
|
||||
<sha256 value="e8a44d7195dc7ee4abb5cda5791e37aacd20b1b76378b13da109dd626536380f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="1.5.31">
|
||||
<artifact name="kotlin-script-runtime-1.5.31.jar">
|
||||
<sha256 value="24e450fee7645ed3590981dddccf397c0d9ebb725815c94c4f555cc3db2f9f96" origin="Generated by Gradle"/>
|
||||
|
@ -4859,6 +4987,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="305ad32461e1929a1cc32ba89861d9c41936a11189e6f486016eaf8b31c5bbcb" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.5.20">
|
||||
<artifact name="kotlin-stdlib-1.5.20.jar">
|
||||
<sha256 value="80cd79c26aac46d72d782de1ecb326061e93c6e688d994b48627ffd668ba63a8" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.5.31">
|
||||
<artifact name="kotlin-stdlib-1.5.31.jar">
|
||||
<sha256 value="4800ceacb2ec0bb9959a087154b8e35318ead1ea4eba32d4bb1b9734222a7e68" origin="Generated by Gradle"/>
|
||||
|
@ -4920,6 +5053,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="2ef683038382532fc14da37c50e1c4609bb76a7e6ff2e330f062c164217f4483" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.5.20">
|
||||
<artifact name="kotlin-stdlib-common-1.5.20.jar">
|
||||
<sha256 value="9819529804bf9296e3853acd5ae824df95d8f8c61309e7768b7cae5ca1361d36" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.5.31">
|
||||
<artifact name="kotlin-stdlib-common-1.5.31.jar">
|
||||
<sha256 value="dfa2a18e26b028388ee1968d199bf6f166f737ab7049c25a5e2da614404e22ad" origin="Generated by Gradle"/>
|
||||
|
@ -4960,6 +5098,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="064c379ad8b7e787ae8863c414bfc732814070c6841b525ba3627c1c333ceabb" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.5.20">
|
||||
<artifact name="kotlin-stdlib-jdk7-1.5.20.jar">
|
||||
<sha256 value="b110f6d20204303099af0d5f2c846ac60bc6ae5663ef5f22e726ca4627359d06" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.5.31">
|
||||
<artifact name="kotlin-stdlib-jdk7-1.5.31.jar">
|
||||
<sha256 value="a25bf47353ce899d843cbddee516d621a73473e7fba97f8d0301e7b4aed7c15f" origin="Generated by Gradle"/>
|
||||
|
@ -5000,6 +5143,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="e552e23edc0e7bc29f341645fb9327c82527e37dfe4bde13ba4a3af36de23fdb" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.5.20">
|
||||
<artifact name="kotlin-stdlib-jdk8-1.5.20.jar">
|
||||
<sha256 value="a7e9cffe569c43eb8f0fe3139978b0943fe92abcc513f7cf04544f2797f8d38a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.5.31">
|
||||
<artifact name="kotlin-stdlib-jdk8-1.5.31.jar">
|
||||
<sha256 value="b548f7767aacf029d2417e47440742bd6d3ebede19b60386e23554ce5c4c5fdc" origin="Generated by Gradle"/>
|
||||
|
|
|
@ -12,6 +12,8 @@ include ':device-transfer'
|
|||
include ':device-transfer-app'
|
||||
include ':image-editor'
|
||||
include ':image-editor-app'
|
||||
include ':sms-exporter'
|
||||
include ':sms-exporter-app'
|
||||
include ':donations'
|
||||
include ':donations-app'
|
||||
include ':spinner'
|
||||
|
@ -33,6 +35,9 @@ project(':libsignal-service').projectDir = file('libsignal/service')
|
|||
project(':image-editor').projectDir = file('image-editor/lib')
|
||||
project(':image-editor-app').projectDir = file('image-editor/app')
|
||||
|
||||
project(':sms-exporter').projectDir = file('sms-exporter/lib')
|
||||
project(':sms-exporter-app').projectDir = file('sms-exporter/app')
|
||||
|
||||
project(':donations').projectDir = file('donations/lib')
|
||||
project(':donations-app').projectDir = file('donations/app')
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
apply from: "$rootProject.projectDir/signalModuleApp.gradle"
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId "org.signal.smsexporter.app"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.androidx.core.role
|
||||
implementation libs.material.material
|
||||
implementation libs.rxjava3.rxjava
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
implementation project(':core-util')
|
||||
implementation project(':sms-exporter')
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.signal.smsexporter.app">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_SMS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SMS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Signal">
|
||||
|
||||
<service android:name=".TestSmsExportService" android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".SendResponseViaMessageService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="smsto" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".BroadcastSmsReceiver"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BROADCAST_SMS">
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.SMS_DELIVER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".BroadcastWapPushReceiver"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BROADCAST_WAP_PUSH">
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
|
||||
<data android:mimeType="application/vnd.wap.mms-message" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name="org.signal.smsexporter.app.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="smsto" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,35 @@
|
|||
package org.signal.smsexporter.app
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.util.Random
|
||||
|
||||
object BitmapGenerator {
|
||||
|
||||
private val colors = listOf(
|
||||
Color.BLACK,
|
||||
Color.BLUE,
|
||||
Color.GRAY,
|
||||
Color.GREEN,
|
||||
Color.RED,
|
||||
Color.CYAN
|
||||
)
|
||||
|
||||
fun getStream(): InputStream {
|
||||
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
|
||||
bitmap.applyCanvas {
|
||||
val random = Random()
|
||||
drawColor(colors[random.nextInt(colors.size - 1)])
|
||||
}
|
||||
|
||||
val out = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
|
||||
val data = out.toByteArray()
|
||||
|
||||
return ByteArrayInputStream(data)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.signal.smsexporter.app
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
class BroadcastSmsReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.signal.smsexporter.app
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
class BroadcastWapPushReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package org.signal.smsexporter.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.smsexporter.DefaultSmsHelper
|
||||
import org.signal.smsexporter.ReleaseSmsAppFailure
|
||||
import org.signal.smsexporter.SmsExportProgress
|
||||
import org.signal.smsexporter.SmsExportService
|
||||
|
||||
class MainActivity : AppCompatActivity(R.layout.main_activity) {
|
||||
|
||||
private lateinit var exportSmsButton: MaterialButton
|
||||
private lateinit var setAsDefaultSmsButton: MaterialButton
|
||||
private lateinit var clearDefaultSmsButton: MaterialButton
|
||||
private lateinit var exportStatus: TextView
|
||||
private lateinit var exportProgress: LinearProgressIndicator
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
exportSmsButton = findViewById(R.id.export_sms)
|
||||
setAsDefaultSmsButton = findViewById(R.id.set_as_default_sms)
|
||||
clearDefaultSmsButton = findViewById(R.id.clear_default_sms)
|
||||
exportStatus = findViewById(R.id.export_status)
|
||||
exportProgress = findViewById(R.id.export_progress)
|
||||
|
||||
disposables += SmsExportService.progressState.onBackpressureLatest().subscribeOn(Schedulers.computation()).observeOn(AndroidSchedulers.mainThread()).subscribe {
|
||||
when (it) {
|
||||
SmsExportProgress.Done -> {
|
||||
exportStatus.text = "Done"
|
||||
exportProgress.isVisible = true
|
||||
}
|
||||
is SmsExportProgress.InProgress -> {
|
||||
exportStatus.text = "$it"
|
||||
exportProgress.isVisible = true
|
||||
exportProgress.progress = it.progress
|
||||
exportProgress.max = it.total
|
||||
}
|
||||
SmsExportProgress.Init -> {
|
||||
exportStatus.text = "Init"
|
||||
exportProgress.isVisible = false
|
||||
}
|
||||
SmsExportProgress.Starting -> {
|
||||
exportStatus.text = "Starting"
|
||||
exportProgress.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setAsDefaultSmsButton.setOnClickListener {
|
||||
DefaultSmsHelper.becomeDefaultSms(this).either(
|
||||
onFailure = { onAppIsIneligableForDefaultSmsSelection() },
|
||||
onSuccess = this::onStartActivityForDefaultSmsSelection
|
||||
)
|
||||
}
|
||||
|
||||
clearDefaultSmsButton.setOnClickListener {
|
||||
DefaultSmsHelper.releaseDefaultSms(this).either(
|
||||
onFailure = {
|
||||
when (it) {
|
||||
ReleaseSmsAppFailure.APP_IS_INELIGABLE_TO_RELEASE_SMS_SELECTION -> onAppIsIneligableForReleaseSmsSelection()
|
||||
ReleaseSmsAppFailure.NO_METHOD_TO_RELEASE_SMS_AVIALABLE -> onNoMethodToReleaseSmsAvailable()
|
||||
}
|
||||
},
|
||||
onSuccess = this::onStartActivityForReleaseSmsSelection
|
||||
)
|
||||
}
|
||||
|
||||
exportSmsButton.setOnClickListener {
|
||||
exportSmsButton.isEnabled = false
|
||||
ContextCompat.startForegroundService(this, Intent(this, TestSmsExportService::class.java))
|
||||
}
|
||||
|
||||
presentButtonState()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
presentButtonState()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
private fun presentButtonState() {
|
||||
setAsDefaultSmsButton.isVisible = !DefaultSmsHelper.isDefaultSms(this)
|
||||
clearDefaultSmsButton.isVisible = DefaultSmsHelper.isDefaultSms(this)
|
||||
exportSmsButton.isVisible = DefaultSmsHelper.isDefaultSms(this)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
1 -> presentButtonState()
|
||||
2 -> presentButtonState()
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStartActivityForDefaultSmsSelection(intent: Intent) {
|
||||
startActivityForResult(intent, 1)
|
||||
}
|
||||
|
||||
private fun onAppIsIneligableForDefaultSmsSelection() {
|
||||
if (DefaultSmsHelper.isDefaultSms(this)) {
|
||||
Toast.makeText(this, "Already the SMS manager.", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "Cannot be SMS manager.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStartActivityForReleaseSmsSelection(intent: Intent) {
|
||||
startActivityForResult(intent, 2)
|
||||
}
|
||||
|
||||
private fun onAppIsIneligableForReleaseSmsSelection() {
|
||||
if (!DefaultSmsHelper.isDefaultSms(this)) {
|
||||
Toast.makeText(this, "Already not the SMS manager.", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "Cannot be SMS manager.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNoMethodToReleaseSmsAvailable() {
|
||||
Toast.makeText(this, "Cannot automatically release sms. Display manual instructions.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.signal.smsexporter.app
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
|
||||
class SendResponseViaMessageService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package org.signal.smsexporter.app
|
||||
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.smsexporter.ExportableMessage
|
||||
import org.signal.smsexporter.SmsExportService
|
||||
import org.signal.smsexporter.SmsExportState
|
||||
import java.io.InputStream
|
||||
|
||||
class TestSmsExportService : SmsExportService() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(TestSmsExportService::class.java)
|
||||
|
||||
private const val NOTIFICATION_ID = 1234
|
||||
private const val NOTIFICATION_CHANNEL_ID = "sms_export"
|
||||
|
||||
private const val startTime = 1659377120L
|
||||
}
|
||||
|
||||
override fun getNotification(progress: Int, total: Int): ExportNotification {
|
||||
ensureNotificationChannel()
|
||||
return ExportNotification(
|
||||
id = NOTIFICATION_ID,
|
||||
NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle("Test Exporter")
|
||||
.setProgress(total, progress, false)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExportState(exportableMessage: ExportableMessage): SmsExportState {
|
||||
return SmsExportState()
|
||||
}
|
||||
|
||||
override fun getUnexportedMessageCount(): Int {
|
||||
return 50
|
||||
}
|
||||
|
||||
override fun getUnexportedMessages(): Iterable<ExportableMessage> {
|
||||
return object : Iterable<ExportableMessage> {
|
||||
override fun iterator(): Iterator<ExportableMessage> {
|
||||
return ExportableMessageIterator(getUnexportedMessageCount())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageExportStarted(exportableMessage: ExportableMessage) {
|
||||
Log.d(TAG, "onMessageExportStarted() called with: exportableMessage = $exportableMessage")
|
||||
}
|
||||
|
||||
override fun onMessageExportSucceeded(exportableMessage: ExportableMessage) {
|
||||
Log.d(TAG, "onMessageExportSucceeded() called with: exportableMessage = $exportableMessage")
|
||||
}
|
||||
|
||||
override fun onMessageExportFailed(exportableMessage: ExportableMessage) {
|
||||
Log.d(TAG, "onMessageExportFailed() called with: exportableMessage = $exportableMessage")
|
||||
}
|
||||
|
||||
override fun onMessageIdCreated(exportableMessage: ExportableMessage, messageId: Long) {
|
||||
Log.d(TAG, "onMessageIdCreated() called with: exportableMessage = $exportableMessage, messageId = $messageId")
|
||||
}
|
||||
|
||||
override fun onAttachmentPartExportStarted(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) {
|
||||
Log.d(TAG, "onAttachmentPartExportStarted() called with: exportableMessage = $exportableMessage, attachment = $part")
|
||||
}
|
||||
|
||||
override fun onAttachmentPartExportSucceeded(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) {
|
||||
Log.d(TAG, "onAttachmentPartExportSucceeded() called with: exportableMessage = $exportableMessage, attachment = $part")
|
||||
}
|
||||
|
||||
override fun onAttachmentPartExportFailed(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) {
|
||||
Log.d(TAG, "onAttachmentPartExportFailed() called with: exportableMessage = $exportableMessage, attachment = $part")
|
||||
}
|
||||
|
||||
override fun onRecipientExportStarted(exportableMessage: ExportableMessage, recipient: String) {
|
||||
Log.d(TAG, "onRecipientExportStarted() called with: exportableMessage = $exportableMessage, recipient = $recipient")
|
||||
}
|
||||
|
||||
override fun onRecipientExportSucceeded(exportableMessage: ExportableMessage, recipient: String) {
|
||||
Log.d(TAG, "onRecipientExportSucceeded() called with: exportableMessage = $exportableMessage, recipient = $recipient")
|
||||
}
|
||||
|
||||
override fun onRecipientExportFailed(exportableMessage: ExportableMessage, recipient: String) {
|
||||
Log.d(TAG, "onRecipientExportFailed() called with: exportableMessage = $exportableMessage, recipient = $recipient")
|
||||
}
|
||||
|
||||
override fun getInputStream(part: ExportableMessage.Mms.Part): InputStream {
|
||||
return BitmapGenerator.getStream()
|
||||
}
|
||||
|
||||
override fun onAttachmentWrittenToDisk(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) {
|
||||
Log.d(TAG, "onAttachmentWrittenToDisk() called with: exportableMessage = $exportableMessage, attachment = $part")
|
||||
}
|
||||
|
||||
override fun onExportPassCompleted() {
|
||||
Log.d(TAG, "onExportPassCompleted() called")
|
||||
}
|
||||
|
||||
private fun ensureNotificationChannel() {
|
||||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
val channel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
|
||||
if (channel == null) {
|
||||
val newChannel = NotificationChannelCompat
|
||||
.Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName("misc")
|
||||
.build()
|
||||
|
||||
notificationManager.createNotificationChannel(newChannel)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExportableMessageIterator(private val size: Int) : Iterator<ExportableMessage> {
|
||||
private var emitted: Int = 0
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return emitted < size
|
||||
}
|
||||
|
||||
override fun next(): ExportableMessage {
|
||||
val message = if (emitted % 2 == 0) {
|
||||
getSmsMessage(emitted)
|
||||
} else {
|
||||
getMmsMessage(emitted)
|
||||
}
|
||||
|
||||
emitted++
|
||||
return message
|
||||
}
|
||||
|
||||
private fun getMmsMessage(it: Int): ExportableMessage.Mms {
|
||||
val me = "+15065550101"
|
||||
val addresses = setOf(me, "+15065550102", "+15065550121")
|
||||
val address = addresses.random()
|
||||
return ExportableMessage.Mms(
|
||||
id = "$it",
|
||||
addresses = addresses,
|
||||
dateSent = startTime + it - 1,
|
||||
dateReceived = startTime + it,
|
||||
isRead = true,
|
||||
isOutgoing = address == me,
|
||||
sender = address,
|
||||
parts = listOf(
|
||||
ExportableMessage.Mms.Part.Text("Hello, $it from $address"),
|
||||
ExportableMessage.Mms.Part.Stream("$it", "image/jpeg")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSmsMessage(it: Int): ExportableMessage.Sms {
|
||||
return ExportableMessage.Sms(
|
||||
id = it.toString(),
|
||||
address = "+15065550102",
|
||||
body = "Hello, World! $it",
|
||||
dateSent = startTime + it - 1,
|
||||
dateReceived = startTime + it,
|
||||
isRead = true,
|
||||
isOutgoing = it % 4 == 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/export_sms"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/export_sms"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/clear_default_sms"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/set_as_default_sms"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/set_as_default_sms_app"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/clear_default_sms"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clear_default_sms_app"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/export_sms" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/export_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/export_progress" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/export_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:max="100"
|
||||
tools:progress="50" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
Po Szerokość: | Wysokość: | Rozmiar: 1.4 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 982 B |
Po Szerokość: | Wysokość: | Rozmiar: 1.7 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.9 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 3.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 5.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 3.8 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 7.6 KiB |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#008577</color>
|
||||
<color name="colorPrimaryDark">#00574B</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||
<resources>
|
||||
<string name="app_name">Sms Exporter Test App</string>
|
||||
<string name="set_as_default_sms_app">Set as default SMS app</string>
|
||||
<string name="clear_default_sms_app">Clear default SMS app</string>
|
||||
<string name="export_sms">Export SMS</string>
|
||||
</resources>
|
|
@ -0,0 +1,9 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Signal" parent="Theme.MaterialComponents.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -0,0 +1,18 @@
|
|||
apply from: "$rootProject.projectDir/signalModule.gradle"
|
||||
|
||||
dependencies {
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
implementation project(':core-util')
|
||||
|
||||
coreLibraryDesugaring libs.android.tools.desugar
|
||||
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.annotation
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.androidx.core.role
|
||||
implementation libs.android.smsmms
|
||||
implementation libs.rxjava3.rxjava
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.signal.smsexporter">
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,13 @@
|
|||
package org.signal.smsexporter
|
||||
|
||||
enum class BecomeSmsAppFailure {
|
||||
/**
|
||||
* Already the default sms app
|
||||
*/
|
||||
ALREADY_DEFAULT_SMS,
|
||||
|
||||
/**
|
||||
* The system doesn't think we are allowed to become the sms app
|
||||
*/
|
||||
ROLE_IS_NOT_AVAILABLE
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package org.signal.smsexporter
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.smsexporter.internal.BecomeDefaultSmsUseCase
|
||||
import org.signal.smsexporter.internal.IsDefaultSms
|
||||
import org.signal.smsexporter.internal.ReleaseDefaultSmsUseCase
|
||||
|
||||
/**
|
||||
* Basic API for checking / becoming / releasing default SMS
|
||||
*/
|
||||
object DefaultSmsHelper {
|
||||
/**
|
||||
* Checks whether this app is currently the default SMS app
|
||||
*/
|
||||
fun isDefaultSms(context: Context) = IsDefaultSms.checkIsDefaultSms(context)
|
||||
|
||||
/**
|
||||
* Attempts to get an Intent which can be launched to become the default SMS app
|
||||
*/
|
||||
fun becomeDefaultSms(context: Context) = BecomeDefaultSmsUseCase.execute(context)
|
||||
|
||||
/**
|
||||
* Attempts to get an Intent which can be launched to relinquish the role of default SMS app
|
||||
*/
|
||||
fun releaseDefaultSms(context: Context) = ReleaseDefaultSmsUseCase.execute(context)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package org.signal.smsexporter
|
||||
|
||||
/**
|
||||
* Represents an exportable MMS or SMS message
|
||||
*/
|
||||
sealed interface ExportableMessage {
|
||||
|
||||
/**
|
||||
* An exportable SMS message
|
||||
*/
|
||||
data class Sms(
|
||||
val id: String,
|
||||
val address: String,
|
||||
val dateReceived: Long,
|
||||
val dateSent: Long,
|
||||
val isRead: Boolean,
|
||||
val isOutgoing: Boolean,
|
||||
val body: String
|
||||
) : ExportableMessage
|
||||
|
||||
/**
|
||||
* An exportable MMS message
|
||||
*/
|
||||
data class Mms(
|
||||
val id: String,
|
||||
val addresses: Set<String>,
|
||||
val dateReceived: Long,
|
||||
val dateSent: Long,
|
||||
val isRead: Boolean,
|
||||
val isOutgoing: Boolean,
|
||||
val parts: List<Part>,
|
||||
val sender: CharSequence
|
||||
) : ExportableMessage {
|
||||
/**
|
||||
* An attachment, attached to an MMS message
|
||||
*/
|
||||
sealed interface Part {
|
||||
|
||||
val contentType: String
|
||||
val contentId: String
|
||||
|
||||
data class Text(val text: String) : Part {
|
||||
override val contentType: String = "text/plain"
|
||||
override val contentId: String = "text"
|
||||
}
|
||||
data class Stream(
|
||||
val id: String,
|
||||
override val contentType: String
|
||||
) : Part {
|
||||
override val contentId: String = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.signal.smsexporter
|
||||
|
||||
enum class ReleaseSmsAppFailure {
|
||||
/**
|
||||
* Occurs when we are not the default sms app
|
||||
*/
|
||||
APP_IS_INELIGABLE_TO_RELEASE_SMS_SELECTION,
|
||||
|
||||
/**
|
||||
* No good way to release sms. Have to instruct user manually.
|
||||
*/
|
||||
NO_METHOD_TO_RELEASE_SMS_AVIALABLE
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.signal.smsexporter
|
||||
|
||||
/**
|
||||
* Expresses the current progress of SMS exporting.
|
||||
*/
|
||||
sealed class SmsExportProgress {
|
||||
/**
|
||||
* Have not started yet.
|
||||
*/
|
||||
object Init : SmsExportProgress()
|
||||
|
||||
/**
|
||||
* Starting up and about to start processing messages
|
||||
*/
|
||||
object Starting : SmsExportProgress()
|
||||
|
||||
/**
|
||||
* Processing messages
|
||||
*/
|
||||
data class InProgress(
|
||||
val progress: Int,
|
||||
val total: Int
|
||||
) : SmsExportProgress()
|
||||
|
||||
/**
|
||||
* All done.
|
||||
*/
|
||||
object Done : SmsExportProgress()
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
package org.signal.smsexporter
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.Try
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.smsexporter.internal.mms.ExportMmsMessagesUseCase
|
||||
import org.signal.smsexporter.internal.mms.ExportMmsPartsUseCase
|
||||
import org.signal.smsexporter.internal.mms.ExportMmsRecipientsUseCase
|
||||
import org.signal.smsexporter.internal.mms.GetOrCreateMmsThreadIdsUseCase
|
||||
import org.signal.smsexporter.internal.sms.ExportSmsMessagesUseCase
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Exports SMS and MMS messages to the system database.
|
||||
*/
|
||||
abstract class SmsExportService : Service() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SmsExportService::class.java)
|
||||
|
||||
/**
|
||||
* Progress state which can be listened to by interested components, such as fragments.
|
||||
*/
|
||||
val progressState: BehaviorProcessor<SmsExportProgress> = BehaviorProcessor.createDefault(SmsExportProgress.Init)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
private val threadCache: MutableMap<Set<String>, Long> = mutableMapOf()
|
||||
private var isStarted = false
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "Got start command in SMS Export Service")
|
||||
|
||||
startExport()
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startExport() {
|
||||
if (isStarted) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Running export...")
|
||||
|
||||
isStarted = true
|
||||
updateNotification(-1, -1)
|
||||
progressState.onNext(SmsExportProgress.Starting)
|
||||
|
||||
var progress = 0
|
||||
executor.execute {
|
||||
val totalCount = getUnexportedMessageCount()
|
||||
getUnexportedMessages().forEach { message ->
|
||||
val exportState = getExportState(message)
|
||||
if (exportState.progress != SmsExportState.Progress.COMPLETED) {
|
||||
when (message) {
|
||||
is ExportableMessage.Sms -> exportSms(exportState, message)
|
||||
is ExportableMessage.Mms -> exportMms(exportState, message)
|
||||
}
|
||||
|
||||
progress++
|
||||
updateNotification(progress, totalCount)
|
||||
progressState.onNext(SmsExportProgress.InProgress(progress, totalCount))
|
||||
}
|
||||
}
|
||||
|
||||
onExportPassCompleted()
|
||||
progressState.onNext(SmsExportProgress.Done)
|
||||
stopForeground(true)
|
||||
isStarted = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The executor that this service should do its work on.
|
||||
*/
|
||||
protected open val executor: Executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
/**
|
||||
* Produces the notification and notification id to display for this foreground service.
|
||||
* The progress and total represent how many messages we've processed, and how many total
|
||||
* we have to process. Failures and successes are both aggregated in this progress. You can
|
||||
* query for "failure" state *after* we signal completion of a run.
|
||||
*/
|
||||
protected abstract fun getNotification(progress: Int, total: Int): ExportNotification
|
||||
|
||||
/**
|
||||
* Gets the initial export state. This is only called once per message, before any processing
|
||||
* is done. It is used as a "known" state value, and via the onX methods below, it is up to the
|
||||
* application to properly update the underlying data structure when changes occur.
|
||||
*/
|
||||
protected abstract fun getExportState(exportableMessage: ExportableMessage): SmsExportState
|
||||
|
||||
/**
|
||||
* Gets the total number of messages to process. This is only used for the notification and
|
||||
* progress events.
|
||||
*/
|
||||
protected abstract fun getUnexportedMessageCount(): Int
|
||||
|
||||
/**
|
||||
* Gets an iterable of exportable messages.
|
||||
*/
|
||||
protected abstract fun getUnexportedMessages(): Iterable<ExportableMessage>
|
||||
|
||||
/**
|
||||
* We've started the export process for a given MMS / SMS message
|
||||
*/
|
||||
protected abstract fun onMessageExportStarted(exportableMessage: ExportableMessage)
|
||||
|
||||
/**
|
||||
* We've completely succeeded exporting a given MMS / SMS message
|
||||
*/
|
||||
protected abstract fun onMessageExportSucceeded(exportableMessage: ExportableMessage)
|
||||
|
||||
/**
|
||||
* We've failed to completely export a given MMS / SMS message
|
||||
*/
|
||||
protected abstract fun onMessageExportFailed(exportableMessage: ExportableMessage)
|
||||
|
||||
/**
|
||||
* We've written the message contents to the system database and were handed back an id.
|
||||
*/
|
||||
protected abstract fun onMessageIdCreated(exportableMessage: ExportableMessage, messageId: Long)
|
||||
|
||||
/**
|
||||
* We've begun trying to export a part row for an attachment for the given message
|
||||
*/
|
||||
protected abstract fun onAttachmentPartExportStarted(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part)
|
||||
|
||||
/**
|
||||
* We've successfully exported the attachment part for a given message
|
||||
*/
|
||||
protected abstract fun onAttachmentPartExportSucceeded(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part)
|
||||
|
||||
/**
|
||||
* We failed to export the attachment part for a given message.
|
||||
*/
|
||||
protected abstract fun onAttachmentPartExportFailed(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part)
|
||||
|
||||
/**
|
||||
* We've begun trying to export a recipient addr for a given message
|
||||
*/
|
||||
protected abstract fun onRecipientExportStarted(exportableMessage: ExportableMessage, recipient: String)
|
||||
|
||||
/**
|
||||
* We've successfully exported a recipient addr for a given message
|
||||
*/
|
||||
protected abstract fun onRecipientExportSucceeded(exportableMessage: ExportableMessage, recipient: String)
|
||||
|
||||
/**
|
||||
* We've failed to export a recipient addr for a given message
|
||||
*/
|
||||
protected abstract fun onRecipientExportFailed(exportableMessage: ExportableMessage, recipient: String)
|
||||
|
||||
/**
|
||||
* Gets the input stream for the given attachment, so that it might be written out to disk.
|
||||
*/
|
||||
protected abstract fun getInputStream(part: ExportableMessage.Mms.Part): InputStream
|
||||
|
||||
/**
|
||||
* Called after the attachment is successfully written to disk.
|
||||
*/
|
||||
protected abstract fun onAttachmentWrittenToDisk(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part)
|
||||
|
||||
/**
|
||||
* Called when an export pass completes. It is up to the implementation to determine whether
|
||||
* there are still messages to export.
|
||||
*/
|
||||
protected abstract fun onExportPassCompleted()
|
||||
|
||||
private fun updateNotification(progress: Int, total: Int) {
|
||||
val exportNotification = getNotification(progress, total)
|
||||
startForeground(exportNotification.id, exportNotification.notification)
|
||||
}
|
||||
|
||||
private fun exportSms(smsExportState: SmsExportState, sms: ExportableMessage.Sms) {
|
||||
onMessageExportStarted(sms)
|
||||
val mayAlreadyExist = smsExportState.progress == SmsExportState.Progress.STARTED
|
||||
ExportSmsMessagesUseCase.execute(this, sms, mayAlreadyExist).either(onSuccess = {
|
||||
onMessageExportSucceeded(sms)
|
||||
}, onFailure = {
|
||||
onMessageExportFailed(sms)
|
||||
})
|
||||
}
|
||||
|
||||
private fun exportMms(smsExportState: SmsExportState, mms: ExportableMessage.Mms) {
|
||||
onMessageExportStarted(mms)
|
||||
val threadIdOutput: GetOrCreateMmsThreadIdsUseCase.Output? = getThreadId(mms)
|
||||
val exportMmsOutput: ExportMmsMessagesUseCase.Output? = threadIdOutput?.let { exportMms(smsExportState, it) }
|
||||
val exportMmsPartsOutput: List<ExportMmsPartsUseCase.Output?>? = exportMmsOutput?.let { exportMmsParts(smsExportState, it) }
|
||||
val writeMmsPartsOutput: List<Result<Unit, Throwable>>? = exportMmsPartsOutput?.filterNotNull()?.map { writeAttachmentToDisk(smsExportState, it) }
|
||||
val exportMmsRecipients: List<Unit?>? = exportMmsOutput?.let { exportMmsRecipients(smsExportState, it) }
|
||||
|
||||
if (threadIdOutput != null &&
|
||||
exportMmsOutput != null &&
|
||||
exportMmsPartsOutput != null && !exportMmsPartsOutput.contains(null) &&
|
||||
writeMmsPartsOutput != null && writeMmsPartsOutput.all { it is Result.Success } &&
|
||||
exportMmsRecipients != null && !exportMmsRecipients.contains(null)
|
||||
) {
|
||||
onMessageExportSucceeded(mms)
|
||||
} else {
|
||||
onMessageExportFailed(mms)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreadId(mms: ExportableMessage.Mms): GetOrCreateMmsThreadIdsUseCase.Output? {
|
||||
return GetOrCreateMmsThreadIdsUseCase.execute(this, mms, threadCache).either(
|
||||
onSuccess = { output ->
|
||||
output
|
||||
},
|
||||
onFailure = {
|
||||
Log.w(TAG, "Failed to get thread id for export", it)
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun exportMms(smsExportState: SmsExportState, threadIdOutput: GetOrCreateMmsThreadIdsUseCase.Output): ExportMmsMessagesUseCase.Output? {
|
||||
return ExportMmsMessagesUseCase.execute(this, threadIdOutput, smsExportState.progress == SmsExportState.Progress.STARTED).either(
|
||||
onSuccess = {
|
||||
onMessageIdCreated(it.mms, it.messageId)
|
||||
it
|
||||
},
|
||||
onFailure = {
|
||||
Log.w(TAG, "Failed to export MMS into system database", it)
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun exportMmsParts(smsExportState: SmsExportState, exportMmsOutput: ExportMmsMessagesUseCase.Output): List<ExportMmsPartsUseCase.Output?> {
|
||||
val attachments = exportMmsOutput.mms.parts
|
||||
return if (attachments.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
attachments.filterNot { it.contentId in smsExportState.completedAttachments }.map { attachment ->
|
||||
onAttachmentPartExportStarted(exportMmsOutput.mms, attachment)
|
||||
ExportMmsPartsUseCase.execute(this, attachment, exportMmsOutput, smsExportState.startedAttachments.contains(attachment.contentId)).either(
|
||||
onSuccess = {
|
||||
onAttachmentPartExportSucceeded(exportMmsOutput.mms, attachment)
|
||||
it
|
||||
},
|
||||
onFailure = {
|
||||
onAttachmentPartExportFailed(exportMmsOutput.mms, attachment)
|
||||
Log.d(TAG, "Could not export MMS Part", it)
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportMmsRecipients(smsExportState: SmsExportState, exportMmsOutput: ExportMmsMessagesUseCase.Output): List<Unit?> {
|
||||
val recipients = exportMmsOutput.mms.addresses.map { it.toString() }.toSet()
|
||||
return if (recipients.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
recipients.filterNot { it in smsExportState.completedRecipients }.map { recipient ->
|
||||
onRecipientExportStarted(exportMmsOutput.mms, recipient)
|
||||
ExportMmsRecipientsUseCase.execute(this, exportMmsOutput.messageId, recipient, exportMmsOutput.mms.sender.toString(), smsExportState.startedRecipients.contains(recipient)).either(
|
||||
onSuccess = {
|
||||
onRecipientExportSucceeded(exportMmsOutput.mms, recipient)
|
||||
},
|
||||
onFailure = {
|
||||
onRecipientExportFailed(exportMmsOutput.mms, recipient)
|
||||
Log.w(TAG, "Failed to export MMS Recipient", it)
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeAttachmentToDisk(smsExportState: SmsExportState, output: ExportMmsPartsUseCase.Output): Try<Unit> {
|
||||
if (output.part.contentId in smsExportState.completedAttachments) {
|
||||
return Try.success(Unit)
|
||||
}
|
||||
|
||||
if (output.part is ExportableMessage.Mms.Part.Text) {
|
||||
onAttachmentWrittenToDisk(output.message, output.part)
|
||||
Try.success(Unit)
|
||||
}
|
||||
|
||||
return try {
|
||||
contentResolver.openOutputStream(output.uri)!!.use { out ->
|
||||
getInputStream(output.part).use {
|
||||
it.copyTo(out)
|
||||
}
|
||||
}
|
||||
onAttachmentWrittenToDisk(output.message, output.part)
|
||||
Try.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to write attachment to disk.", e)
|
||||
Try.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
data class ExportNotification(
|
||||
val id: Int,
|
||||
val notification: Notification
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.signal.smsexporter
|
||||
|
||||
/**
|
||||
* Describes the current "Export State" of a given message. This should be updated
|
||||
* by and persisted by the application whenever a state change occurs.
|
||||
*/
|
||||
data class SmsExportState(
|
||||
val messageId: Long = -1L,
|
||||
val startedRecipients: Set<String> = emptySet(),
|
||||
val completedRecipients: Set<String> = emptySet(),
|
||||
val startedAttachments: Set<String> = emptySet(),
|
||||
val completedAttachments: Set<String> = emptySet(),
|
||||
val copiedAttachments: Set<String> = emptySet(),
|
||||
val progress: Progress = Progress.INIT
|
||||
) {
|
||||
enum class Progress {
|
||||
INIT,
|
||||
STARTED,
|
||||
COMPLETED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package org.signal.smsexporter.internal
|
||||
|
||||
import android.app.role.RoleManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Telephony
|
||||
import androidx.core.role.RoleManagerCompat
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.smsexporter.BecomeSmsAppFailure
|
||||
|
||||
/**
|
||||
* Requests that this app becomes the default SMS app. The exact UX here is
|
||||
* API dependant.
|
||||
*
|
||||
* Returns an intent to fire for a result, or a Failure.
|
||||
*/
|
||||
internal object BecomeDefaultSmsUseCase {
|
||||
fun execute(context: Context): Result<Intent, BecomeSmsAppFailure> {
|
||||
return if (IsDefaultSms.checkIsDefaultSms(context)) {
|
||||
Result.failure(BecomeSmsAppFailure.ALREADY_DEFAULT_SMS)
|
||||
} else if (Build.VERSION.SDK_INT >= 29) {
|
||||
val roleManager = context.getSystemService(RoleManager::class.java)
|
||||
if (roleManager.isRoleAvailable(RoleManagerCompat.ROLE_SMS)) {
|
||||
Result.success(roleManager.createRequestRoleIntent(RoleManagerCompat.ROLE_SMS))
|
||||
} else {
|
||||
Result.failure(BecomeSmsAppFailure.ROLE_IS_NOT_AVAILABLE)
|
||||
}
|
||||
} else {
|
||||
Result.success(
|
||||
Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT)
|
||||
.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, context.packageName)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package org.signal.smsexporter.internal
|
||||
|
||||
import android.app.role.RoleManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Telephony
|
||||
import androidx.core.role.RoleManagerCompat
|
||||
|
||||
/**
|
||||
* Uses the appropriate service to check if we are the default sms
|
||||
*/
|
||||
internal object IsDefaultSms {
|
||||
fun checkIsDefaultSms(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= 29) {
|
||||
context.getSystemService(RoleManager::class.java).isRoleHeld(RoleManagerCompat.ROLE_SMS)
|
||||
} else {
|
||||
context.packageName == Telephony.Sms.getDefaultSmsPackage(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package org.signal.smsexporter.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.smsexporter.ReleaseSmsAppFailure
|
||||
|
||||
/**
|
||||
* Request to no longer be the default SMS app. This has a pretty bad UX, we need
|
||||
* to get the user to manually do it in settings. On API 24+ we can launch the default
|
||||
* app settings screen, whereas on 19 to 23, we can't. In this situation, we should
|
||||
* display some UX (perhaps based off API level) explaining to the user exactly what to
|
||||
* do.
|
||||
*
|
||||
* Returns the Intent to fire off, or a Failure.
|
||||
*/
|
||||
internal object ReleaseDefaultSmsUseCase {
|
||||
fun execute(context: Context): Result<Intent, ReleaseSmsAppFailure> {
|
||||
return if (!IsDefaultSms.checkIsDefaultSms(context)) {
|
||||
Result.failure(ReleaseSmsAppFailure.APP_IS_INELIGABLE_TO_RELEASE_SMS_SELECTION)
|
||||
} else if (Build.VERSION.SDK_INT >= 24) {
|
||||
Result.success(
|
||||
Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
|
||||
)
|
||||
} else {
|
||||
Result.failure(ReleaseSmsAppFailure.NO_METHOD_TO_RELEASE_SMS_AVIALABLE)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package org.signal.smsexporter.internal.mms
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.provider.Telephony
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import org.signal.core.util.Try
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.smsexporter.ExportableMessage
|
||||
|
||||
/**
|
||||
* Takes a list of messages and inserts them as a single batch. This includes
|
||||
* thread id get/create if necessary. The output is a list of (mms, message_id)
|
||||
*/
|
||||
internal object ExportMmsMessagesUseCase {
|
||||
|
||||
private val TAG = Log.tag(ExportMmsMessagesUseCase::class.java)
|
||||
|
||||
fun execute(
|
||||
context: Context,
|
||||
getOrCreateThreadOutput: GetOrCreateMmsThreadIdsUseCase.Output,
|
||||
checkForExistence: Boolean
|
||||
): Try<Output> {
|
||||
try {
|
||||
val (mms, threadId) = getOrCreateThreadOutput
|
||||
val transactionId = "signal:T${mms.id}"
|
||||
|
||||
if (checkForExistence) {
|
||||
Log.d(TAG, "Checking if the message is already in the database.")
|
||||
val messageId = isMessageAlreadyInDatabase(context, transactionId)
|
||||
if (messageId != -1L) {
|
||||
Log.d(TAG, "Message exists in database. Returning its id.")
|
||||
return Try.success(Output(mms, messageId))
|
||||
}
|
||||
}
|
||||
|
||||
val mmsContentValues = contentValuesOf(
|
||||
Telephony.Mms.THREAD_ID to threadId,
|
||||
Telephony.Mms.DATE to mms.dateReceived,
|
||||
Telephony.Mms.DATE_SENT to mms.dateSent,
|
||||
Telephony.Mms.MESSAGE_BOX to if (mms.isOutgoing) Telephony.Mms.MESSAGE_BOX_SENT else Telephony.Mms.MESSAGE_BOX_INBOX,
|
||||
Telephony.Mms.READ to if (mms.isRead) 1 else 0,
|
||||
Telephony.Mms.CONTENT_TYPE to "application/vnd.wap.multipart.related",
|
||||
Telephony.Mms.MESSAGE_TYPE to PduHeaders.MESSAGE_TYPE_SEND_REQ,
|
||||
Telephony.Mms.MMS_VERSION to PduHeaders.MMS_VERSION_1_3,
|
||||
Telephony.Mms.MESSAGE_CLASS to "personal",
|
||||
Telephony.Mms.PRIORITY to PduHeaders.PRIORITY_NORMAL,
|
||||
Telephony.Mms.TRANSACTION_ID to transactionId,
|
||||
Telephony.Mms.RESPONSE_STATUS to PduHeaders.RESPONSE_STATUS_OK
|
||||
)
|
||||
|
||||
val uri = context.contentResolver.insert(Telephony.Mms.CONTENT_URI, mmsContentValues)
|
||||
val newMessageId = ContentUris.parseId(uri!!)
|
||||
|
||||
return Try.success(Output(getOrCreateThreadOutput.mms, newMessageId))
|
||||
} catch (e: Exception) {
|
||||
return Try.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMessageAlreadyInDatabase(context: Context, transactionId: String): Long {
|
||||
return context.contentResolver.query(
|
||||
Telephony.Mms.CONTENT_URI,
|
||||
arrayOf("_id"),
|
||||
"${Telephony.Mms.TRANSACTION_ID} == ?",
|
||||
arrayOf(transactionId),
|
||||
null
|
||||
)?.use {
|
||||
it.moveToFirst()
|
||||
it.getLong(0)
|
||||
} ?: -1L
|
||||
}
|
||||
|
||||
data class Output(
|
||||
val mms: ExportableMessage.Mms,
|
||||
val messageId: Long
|
||||
)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package org.signal.smsexporter.internal.mms
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.Try
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.smsexporter.ExportableMessage
|
||||
|
||||
/**
|
||||
* Inserts the part objects for the given list of mms message insertion outputs. Returns a list
|
||||
* of attachments that can be enqueued for a disk write.
|
||||
*/
|
||||
internal object ExportMmsPartsUseCase {
|
||||
|
||||
private val TAG = Log.tag(ExportMmsPartsUseCase::class.java)
|
||||
|
||||
fun execute(context: Context, part: ExportableMessage.Mms.Part, output: ExportMmsMessagesUseCase.Output, checkForExistence: Boolean): Try<Output> {
|
||||
try {
|
||||
val (message, messageId) = output
|
||||
val contentId = "<signal:${part.contentId}>"
|
||||
val mmsPartUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("part").build()
|
||||
|
||||
if (checkForExistence) {
|
||||
Log.d(TAG, "Checking attachment that may already be present...")
|
||||
val partId: Long? = context.contentResolver.query(mmsPartUri, arrayOf(Telephony.Mms.Part._ID), "${Telephony.Mms.Part.CONTENT_ID} = ?", arrayOf(contentId), null)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getLong(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (partId != null) {
|
||||
Log.d(TAG, "Found attachment part that already exists.")
|
||||
return Try.success(
|
||||
Output(
|
||||
uri = ContentUris.withAppendedId(mmsPartUri, partId),
|
||||
part = part,
|
||||
message = message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val mmsPartContentValues = contentValuesOf(
|
||||
Telephony.Mms.Part.MSG_ID to messageId,
|
||||
Telephony.Mms.Part.CONTENT_TYPE to part.contentType,
|
||||
Telephony.Mms.Part.CONTENT_ID to contentId,
|
||||
Telephony.Mms.Part.TEXT to if (part is ExportableMessage.Mms.Part.Text) part.text else null
|
||||
)
|
||||
|
||||
val attachmentUri = context.contentResolver.insert(mmsPartUri, mmsPartContentValues)!!
|
||||
return Try.success(Output(attachmentUri, part, message))
|
||||
} catch (e: Exception) {
|
||||
return Try.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
data class Output(val uri: Uri, val part: ExportableMessage.Mms.Part, val message: ExportableMessage)
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package org.signal.smsexporter.internal.mms
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Telephony
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.google.android.mms.pdu_alt.CharacterSets
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import org.signal.core.util.Try
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.smsexporter.internal.sms.ExportSmsMessagesUseCase
|
||||
|
||||
/**
|
||||
* Inserts the recipients for each individual message in the insert mms output. Returns nothing.
|
||||
*/
|
||||
object ExportMmsRecipientsUseCase {
|
||||
|
||||
private val TAG = Log.tag(ExportSmsMessagesUseCase::class.java)
|
||||
|
||||
fun execute(context: Context, messageId: Long, recipient: String, sender: String, checkForExistence: Boolean): Try<Unit> {
|
||||
try {
|
||||
val addrUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("addr").build()
|
||||
|
||||
if (checkForExistence) {
|
||||
Log.d(TAG, "Checking for recipient that may have already been inserted...")
|
||||
val exists = context.contentResolver.query(addrUri, arrayOf("_id"), "${Telephony.Mms.Addr.ADDRESS} == ?", arrayOf(recipient), null)?.use {
|
||||
it.moveToFirst()
|
||||
} ?: false
|
||||
|
||||
if (exists) {
|
||||
Log.d(TAG, "Recipient was already inserted. Skipping.")
|
||||
return Try.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
val addrValues = contentValuesOf(
|
||||
Telephony.Mms.Addr.ADDRESS to recipient,
|
||||
Telephony.Mms.Addr.CHARSET to CharacterSets.DEFAULT_CHARSET,
|
||||
Telephony.Mms.Addr.TYPE to if (recipient == sender) PduHeaders.FROM else PduHeaders.TO,
|
||||
)
|
||||
|
||||
context.contentResolver.insert(addrUri, addrValues)
|
||||
|
||||
return Try.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
return Try.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.signal.smsexporter.internal.mms
|
||||
|
||||
import android.content.Context
|
||||
import com.klinker.android.send_message.Utils
|
||||
import org.signal.core.util.Try
|
||||
import org.signal.smsexporter.ExportableMessage
|
||||
|
||||
/**
|
||||
* Given a list of messages, gets or creates the threadIds for each different recipient set.
|
||||
* Returns a list of outputs that tie a given message to a thread id.
|
||||
*
|
||||
* This method will also filter out messages that do not have addresses.
|
||||
*/
|
||||
internal object GetOrCreateMmsThreadIdsUseCase {
|
||||
fun execute(
|
||||
context: Context,
|
||||
mms: ExportableMessage.Mms,
|
||||
threadCache: MutableMap<Set<String>, Long>
|
||||
): Try<Output> {
|
||||
return try {
|
||||
val recipients = getRecipientSet(mms)
|
||||
val threadId = getOrCreateThreadId(context, recipients, threadCache)
|
||||
|
||||
Try.success(Output(mms, threadId))
|
||||
} catch (e: Exception) {
|
||||
Try.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrCreateThreadId(context: Context, recipients: Set<String>, cache: MutableMap<Set<String>, Long>): Long {
|
||||
return if (cache.containsKey(recipients)) {
|
||||
cache[recipients]!!
|
||||
} else {
|
||||
val threadId = Utils.getOrCreateThreadId(context, recipients)
|
||||
cache[recipients] = threadId
|
||||
threadId
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRecipientSet(mms: ExportableMessage.Mms): Set<String> {
|
||||
val recipients = mms.addresses
|
||||
if (recipients.isEmpty()) {
|
||||
error("Expected non-empty recipient count.")
|
||||
}
|
||||
|
||||
return HashSet(recipients.map { it.toString() })
|
||||
}
|
||||
|
||||
data class Output(val mms: ExportableMessage.Mms, val threadId: Long)
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package org.signal.smsexporter.internal.sms
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Telephony
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.Try
|
||||
import org.signal.smsexporter.ExportableMessage
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* Given a list of Sms messages, export each one to the system SMS database
|
||||
* Returns nothing.
|
||||
*/
|
||||
internal object ExportSmsMessagesUseCase {
|
||||
fun execute(context: Context, sms: ExportableMessage.Sms, checkForExistence: Boolean): Try<Unit> {
|
||||
try {
|
||||
if (checkForExistence) {
|
||||
val exists = context.contentResolver.query(
|
||||
Telephony.Sms.CONTENT_URI,
|
||||
arrayOf("_id"),
|
||||
"${Telephony.Sms.ADDRESS} = ? AND ${Telephony.Sms.DATE_SENT} = ?",
|
||||
arrayOf(sms.address, sms.dateSent.toString()),
|
||||
null
|
||||
)?.use {
|
||||
it.count > 0
|
||||
} ?: false
|
||||
|
||||
if (exists) {
|
||||
return Try.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
val contentValues = contentValuesOf(
|
||||
Telephony.Sms.ADDRESS to sms.address,
|
||||
Telephony.Sms.BODY to sms.body,
|
||||
Telephony.Sms.DATE to sms.dateReceived,
|
||||
Telephony.Sms.DATE_SENT to sms.dateSent,
|
||||
Telephony.Sms.READ to if (sms.isRead) 1 else 0,
|
||||
Telephony.Sms.TYPE to if (sms.isOutgoing) Telephony.Sms.MESSAGE_TYPE_SENT else Telephony.Sms.MESSAGE_TYPE_INBOX
|
||||
)
|
||||
|
||||
context.contentResolver.insert(Telephony.Sms.CONTENT_URI, contentValues)
|
||||
|
||||
return Try.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
return Try.failure(e)
|
||||
}
|
||||
}
|
||||
}
|