mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-28 20:45:17 +00:00
Merge remote-tracking branch 'upstream/dev' into libsession-integration
# Conflicts: # app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java # app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt # app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt # app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt # app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java # app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt # app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt # app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt # libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt # libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt # libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
This commit is contained in:
commit
f63ad7e034
@ -159,8 +159,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 335
|
||||
def canonicalVersionName = "1.16.7"
|
||||
def canonicalVersionCode = 336
|
||||
def canonicalVersionName = "1.16.8"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -407,12 +407,6 @@
|
||||
<action android:name="network.loki.securesms.RESTART" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
@ -446,17 +440,9 @@
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.JobSchedulerScheduler$SystemService"
|
||||
android:enabled="@bool/enable_job_service"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
tools:targetApi="26" />
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.KeepAliveService"
|
||||
android:enabled="@bool/enable_alarm_manager" />
|
||||
<receiver
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.AlarmManagerScheduler$RetryReceiver"
|
||||
android:enabled="@bool/enable_alarm_manager" /> <!-- Probably don't need this one -->
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false" />
|
||||
|
@ -56,7 +56,6 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.database.Storage;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
@ -67,11 +66,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||
@ -84,7 +79,6 @@ import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
|
||||
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
|
||||
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
|
||||
@ -136,7 +130,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
private TypingStatusSender typingStatusSender;
|
||||
private JobManager jobManager;
|
||||
private ReadReceiptManager readReceiptManager;
|
||||
private ProfileManager profileManager;
|
||||
public MessageNotifier messageNotifier = null;
|
||||
@ -151,7 +144,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
@Inject LokiAPIDatabase lokiAPIDatabase;
|
||||
@Inject public Storage storage;
|
||||
@Inject MessageDataProvider messageDataProvider;
|
||||
@Inject JobDatabase jobDatabase;
|
||||
@Inject TextSecurePreferences textSecurePreferences;
|
||||
@Inject ConfigFactory configFactory;
|
||||
CallMessageProcessor callMessageProcessor;
|
||||
@ -171,10 +163,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
return (ApplicationContext) context.getApplicationContext();
|
||||
}
|
||||
|
||||
public TextSecurePreferences getPrefs() {
|
||||
return textSecurePreferences;
|
||||
}
|
||||
|
||||
public DatabaseComponent getDatabaseComponent() {
|
||||
return EntryPoints.get(getApplicationContext(), DatabaseComponent.class);
|
||||
}
|
||||
@ -245,7 +233,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
initializeProfileManager();
|
||||
initializePeriodicTasks();
|
||||
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
||||
initializeJobManager();
|
||||
initializeWebRtc();
|
||||
initializeBlobProvider();
|
||||
resubmitProfilePictureIfNeeded();
|
||||
@ -302,10 +289,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
LocaleParser.Companion.configure(new LocaleParseHelper());
|
||||
}
|
||||
|
||||
public JobManager getJobManager() {
|
||||
return jobManager;
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
@ -368,16 +351,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
|
||||
}
|
||||
|
||||
private void initializeJobManager() {
|
||||
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
|
||||
.setDataSerializer(new JsonDataSerializer())
|
||||
.setJobFactories(JobManagerFactories.getJobFactories(this))
|
||||
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
|
||||
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
|
||||
.setJobStorage(new FastJobStorage(jobDatabase))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void initializeExpiringMessageManager() {
|
||||
this.expiringMessageManager = new ExpiringMessageManager(this);
|
||||
}
|
||||
@ -400,10 +373,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
BackgroundPollWorker.schedulePeriodic(this);
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeWebRtc() {
|
||||
|
@ -1,14 +0,0 @@
|
||||
package org.thoughtcrime.securesms.backup
|
||||
|
||||
data class BackupEvent constructor(val type: Type, val count: Int, val exception: Exception?) {
|
||||
|
||||
enum class Type {
|
||||
PROGRESS, FINISHED
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun createProgress(count: Int) = BackupEvent(Type.PROGRESS, count, null)
|
||||
@JvmStatic fun createFinished() = BackupEvent(Type.FINISHED, 0, null)
|
||||
@JvmStatic fun createFinished(e: Exception?) = BackupEvent(Type.FINISHED, 0, e)
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
* Allows the getting and setting of the backup passphrase, which is stored encrypted on API >= 23.
|
||||
*/
|
||||
public class BackupPassphrase {
|
||||
|
||||
private static final String TAG = BackupPassphrase.class.getSimpleName();
|
||||
|
||||
public static @Nullable String get(@NonNull Context context) {
|
||||
String passphrase = TextSecurePreferences.getBackupPassphrase(context);
|
||||
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 23 || (passphrase == null && encryptedPassphrase == null)) {
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
if (encryptedPassphrase == null) {
|
||||
Log.i(TAG, "Migrating to encrypted passphrase.");
|
||||
set(context, passphrase);
|
||||
encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
|
||||
}
|
||||
|
||||
KeyStoreHelper.SealedData data = KeyStoreHelper.SealedData.fromString(encryptedPassphrase);
|
||||
return new String(KeyStoreHelper.unseal(data));
|
||||
}
|
||||
|
||||
public static void set(@NonNull Context context, @Nullable String passphrase) {
|
||||
if (passphrase == null || Build.VERSION.SDK_INT < 23) {
|
||||
TextSecurePreferences.setBackupPassphrase(context, passphrase);
|
||||
TextSecurePreferences.setEncryptedBackupPassphrase(context, null);
|
||||
} else {
|
||||
KeyStoreHelper.SealedData encryptedPassphrase = KeyStoreHelper.seal(passphrase.getBytes());
|
||||
TextSecurePreferences.setEncryptedBackupPassphrase(context, encryptedPassphrase.serialize());
|
||||
TextSecurePreferences.setBackupPassphrase(context, null);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package org.thoughtcrime.securesms.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.preference.PreferenceManager.getDefaultSharedPreferencesName
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_BOOLEAN
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_INT
|
||||
import java.util.*
|
||||
|
||||
object BackupPreferences {
|
||||
// region Backup related
|
||||
fun getBackupRecords(context: Context): List<BackupProtos.SharedPreference> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val prefsFileName: String
|
||||
prefsFileName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
getDefaultSharedPreferencesName(context)
|
||||
} else {
|
||||
context.packageName + "_preferences"
|
||||
}
|
||||
val prefList: LinkedList<BackupProtos.SharedPreference> = LinkedList<BackupProtos.SharedPreference>()
|
||||
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_REGISTRATION_ID_PREF)
|
||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_NUMBER_PREF)
|
||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_NAME_PREF)
|
||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_URL_PREF)
|
||||
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_ID_PREF)
|
||||
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_KEY_PREF)
|
||||
addBackupEntryBoolean(prefList, preferences, prefsFileName, TextSecurePreferences.IS_USING_FCM)
|
||||
return prefList
|
||||
}
|
||||
|
||||
private fun addBackupEntryString(
|
||||
outPrefList: MutableList<BackupProtos.SharedPreference>,
|
||||
prefs: SharedPreferences,
|
||||
prefFileName: String,
|
||||
prefKey: String,
|
||||
) {
|
||||
val value = prefs.getString(prefKey, null)
|
||||
if (value == null) {
|
||||
logBackupEntry(prefKey, false)
|
||||
return
|
||||
}
|
||||
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefFileName)
|
||||
.setKey(prefKey)
|
||||
.setValue(value)
|
||||
.build())
|
||||
logBackupEntry(prefKey, true)
|
||||
}
|
||||
|
||||
private fun addBackupEntryInt(
|
||||
outPrefList: MutableList<BackupProtos.SharedPreference>,
|
||||
prefs: SharedPreferences,
|
||||
prefFileName: String,
|
||||
prefKey: String,
|
||||
) {
|
||||
val value = prefs.getInt(prefKey, -1)
|
||||
if (value == -1) {
|
||||
logBackupEntry(prefKey, false)
|
||||
return
|
||||
}
|
||||
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefFileName)
|
||||
.setKey(PREF_PREFIX_TYPE_INT + prefKey) // The prefix denotes the type of the preference.
|
||||
.setValue(value.toString())
|
||||
.build())
|
||||
logBackupEntry(prefKey, true)
|
||||
}
|
||||
|
||||
private fun addBackupEntryBoolean(
|
||||
outPrefList: MutableList<BackupProtos.SharedPreference>,
|
||||
prefs: SharedPreferences,
|
||||
prefFileName: String,
|
||||
prefKey: String,
|
||||
) {
|
||||
if (!prefs.contains(prefKey)) {
|
||||
logBackupEntry(prefKey, false)
|
||||
return
|
||||
}
|
||||
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(prefFileName)
|
||||
.setKey(PREF_PREFIX_TYPE_BOOLEAN + prefKey) // The prefix denotes the type of the preference.
|
||||
.setValue(prefs.getBoolean(prefKey, false).toString())
|
||||
.build())
|
||||
logBackupEntry(prefKey, true)
|
||||
}
|
||||
|
||||
private fun logBackupEntry(prefName: String, wasIncluded: Boolean) {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Backup preference ")
|
||||
sb.append(if (wasIncluded) "+ " else "- ")
|
||||
sb.append('\"').append(prefName).append("\" ")
|
||||
if (!wasIncluded) {
|
||||
sb.append("(is empty and not included)")
|
||||
}
|
||||
Log.d("Loki", sb.toString())
|
||||
} // endregion
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,456 +0,0 @@
|
||||
package org.thoughtcrime.securesms.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.annimon.stream.function.Consumer
|
||||
import com.annimon.stream.function.Predicate
|
||||
import com.google.protobuf.ByteString
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.utilities.Conversions
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK
|
||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
||||
import org.session.libsignal.utilities.ByteUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Header
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.PushDatabase
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.Flushable
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.LinkedList
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object FullBackupExporter {
|
||||
private val TAG = FullBackupExporter::class.java.simpleName
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
fun export(context: Context,
|
||||
attachmentSecret: AttachmentSecret,
|
||||
input: SQLiteDatabase,
|
||||
fileUri: Uri,
|
||||
passphrase: String) {
|
||||
|
||||
val baseOutputStream = context.contentResolver.openOutputStream(fileUri)
|
||||
?: throw IOException("Cannot open an output stream for the file URI: $fileUri")
|
||||
|
||||
var count = 0
|
||||
try {
|
||||
BackupFrameOutputStream(baseOutputStream, passphrase).use { outputStream ->
|
||||
outputStream.writeDatabaseVersion(input.version)
|
||||
val tables = exportSchema(input, outputStream)
|
||||
for (table in tables) if (shouldExportTable(table)) {
|
||||
count = when (table) {
|
||||
SmsDatabase.TABLE_NAME, MmsDatabase.TABLE_NAME -> {
|
||||
exportTable(table, input, outputStream,
|
||||
{ cursor: Cursor ->
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0
|
||||
},
|
||||
null,
|
||||
count)
|
||||
}
|
||||
GroupReceiptDatabase.TABLE_NAME -> {
|
||||
exportTable(table, input, outputStream,
|
||||
{ cursor: Cursor ->
|
||||
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID)))
|
||||
},
|
||||
null,
|
||||
count)
|
||||
}
|
||||
AttachmentDatabase.TABLE_NAME -> {
|
||||
exportTable(table, input, outputStream,
|
||||
{ cursor: Cursor ->
|
||||
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)))
|
||||
},
|
||||
{ cursor: Cursor ->
|
||||
exportAttachment(attachmentSecret, cursor, outputStream)
|
||||
},
|
||||
count)
|
||||
}
|
||||
else -> {
|
||||
exportTable(table, input, outputStream, null, null, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (preference in BackupUtil.getBackupRecords(context)) {
|
||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||
outputStream.writePreferenceEntry(preference)
|
||||
}
|
||||
for (preference in BackupPreferences.getBackupRecords(context)) {
|
||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||
outputStream.writePreferenceEntry(preference)
|
||||
}
|
||||
for (avatar in AvatarHelper.getAvatarFiles(context)) {
|
||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||
outputStream.writeAvatar(avatar.name, FileInputStream(avatar), avatar.length())
|
||||
}
|
||||
outputStream.writeEnd()
|
||||
}
|
||||
EventBus.getDefault().post(BackupEvent.createFinished())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to make full backup.", e)
|
||||
EventBus.getDefault().post(BackupEvent.createFinished(e))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun shouldExportTable(table: String): Boolean {
|
||||
return table != PushDatabase.TABLE_NAME &&
|
||||
|
||||
table != LokiBackupFilesDatabase.TABLE_NAME &&
|
||||
table != LokiAPIDatabase.openGroupProfilePictureTable &&
|
||||
|
||||
table != JobDatabase.Jobs.TABLE_NAME &&
|
||||
table != JobDatabase.Constraints.TABLE_NAME &&
|
||||
table != JobDatabase.Dependencies.TABLE_NAME &&
|
||||
|
||||
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
|
||||
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
|
||||
!table.startsWith("sqlite_")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun exportSchema(input: SQLiteDatabase, outputStream: BackupFrameOutputStream): List<String> {
|
||||
val tables: MutableList<String> = LinkedList()
|
||||
input.rawQuery("SELECT sql, name, type FROM sqlite_master", null).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
val sql = cursor.getString(0)
|
||||
val name = cursor.getString(1)
|
||||
val type = cursor.getString(2)
|
||||
if (sql != null) {
|
||||
val isSmsFtsSecretTable = name != null && name != SearchDatabase.SMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME)
|
||||
val isMmsFtsSecretTable = name != null && name != SearchDatabase.MMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME)
|
||||
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
|
||||
if ("table" == type) {
|
||||
tables.add(name)
|
||||
}
|
||||
outputStream.writeSql(SqlStatement.newBuilder().setStatement(cursor.getString(0)).build())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tables
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun exportTable(table: String,
|
||||
input: SQLiteDatabase,
|
||||
outputStream: BackupFrameOutputStream,
|
||||
predicate: Predicate<Cursor>?,
|
||||
postProcess: Consumer<Cursor>?,
|
||||
count: Int): Int {
|
||||
var count = count
|
||||
val template = "INSERT INTO $table VALUES "
|
||||
input.rawQuery("SELECT * FROM $table", null).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||
if (predicate != null && !predicate.test(cursor)) continue
|
||||
|
||||
val statement = StringBuilder(template)
|
||||
val statementBuilder = SqlStatement.newBuilder()
|
||||
statement.append('(')
|
||||
for (i in 0 until cursor.columnCount) {
|
||||
statement.append('?')
|
||||
when (cursor.getType(i)) {
|
||||
Cursor.FIELD_TYPE_STRING -> {
|
||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||
.setStringParamter(cursor.getString(i)))
|
||||
}
|
||||
Cursor.FIELD_TYPE_FLOAT -> {
|
||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||
.setDoubleParameter(cursor.getDouble(i)))
|
||||
}
|
||||
Cursor.FIELD_TYPE_INTEGER -> {
|
||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||
.setIntegerParameter(cursor.getLong(i)))
|
||||
}
|
||||
Cursor.FIELD_TYPE_BLOB -> {
|
||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||
.setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))))
|
||||
}
|
||||
Cursor.FIELD_TYPE_NULL -> {
|
||||
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
|
||||
.setNullparameter(true))
|
||||
}
|
||||
else -> {
|
||||
throw AssertionError("unknown type?" + cursor.getType(i))
|
||||
}
|
||||
}
|
||||
if (i < cursor.columnCount - 1) {
|
||||
statement.append(',')
|
||||
}
|
||||
}
|
||||
statement.append(')')
|
||||
outputStream.writeSql(statementBuilder.setStatement(statement.toString()).build())
|
||||
postProcess?.accept(cursor)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private fun exportAttachment(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
|
||||
try {
|
||||
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID))
|
||||
val uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))
|
||||
var size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))
|
||||
val data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA))
|
||||
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM))
|
||||
if (!TextUtils.isEmpty(data) && size <= 0) {
|
||||
size = calculateVeryOldStreamLength(attachmentSecret, random, data)
|
||||
}
|
||||
if (!TextUtils.isEmpty(data) && size > 0) {
|
||||
val inputStream: InputStream = if (random != null && random.size == 32) {
|
||||
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
|
||||
} else {
|
||||
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
|
||||
}
|
||||
outputStream.writeAttachment(AttachmentId(rowId, uniqueId), inputStream, size)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
|
||||
var result: Long = 0
|
||||
val inputStream: InputStream = if (random != null && random.size == 32) {
|
||||
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
|
||||
} else {
|
||||
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
|
||||
}
|
||||
var read: Int
|
||||
val buffer = ByteArray(8192)
|
||||
while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
|
||||
result += read.toLong()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
|
||||
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
|
||||
val where = MmsSmsColumns.ID + " = ?"
|
||||
val args = arrayOf(mmsId.toString())
|
||||
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
|
||||
if (mmsCursor != null && mmsCursor.moveToFirst()) {
|
||||
return mmsCursor.getLong(0) == 0L
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private class BackupFrameOutputStream(outputStream: OutputStream, passphrase: String) : Closeable, Flushable {
|
||||
|
||||
private val outputStream: OutputStream
|
||||
private var cipher: Cipher
|
||||
private var mac: Mac
|
||||
private val cipherKey: ByteArray
|
||||
private val macKey: ByteArray
|
||||
private val iv: ByteArray
|
||||
|
||||
private var counter: Int = 0
|
||||
|
||||
init {
|
||||
try {
|
||||
val salt = Util.getSecretBytes(32)
|
||||
val key = BackupUtil.computeBackupKey(passphrase, salt)
|
||||
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
|
||||
val split = ByteUtil.split(derived, 32, 32)
|
||||
cipherKey = split[0]
|
||||
macKey = split[1]
|
||||
cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
mac = Mac.getInstance("HmacSHA256")
|
||||
this.outputStream = outputStream
|
||||
iv = Util.getSecretBytes(16)
|
||||
counter = Conversions.byteArrayToInt(iv)
|
||||
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
|
||||
val header = BackupFrame.newBuilder().setHeader(Header.newBuilder()
|
||||
.setIv(ByteString.copyFrom(iv))
|
||||
.setSalt(ByteString.copyFrom(salt)))
|
||||
.build().toByteArray()
|
||||
outputStream.write(Conversions.intToByteArray(header.size))
|
||||
outputStream.write(header)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is NoSuchAlgorithmException,
|
||||
is NoSuchPaddingException,
|
||||
is InvalidKeyException -> {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writeSql(statement: SqlStatement) {
|
||||
write(outputStream, BackupFrame.newBuilder().setStatement(statement).build())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writePreferenceEntry(preference: SharedPreference?) {
|
||||
write(outputStream, BackupFrame.newBuilder().setPreference(preference).build())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writeAvatar(avatarName: String, inputStream: InputStream, size: Long) {
|
||||
write(outputStream, BackupFrame.newBuilder()
|
||||
.setAvatar(Avatar.newBuilder()
|
||||
.setName(avatarName)
|
||||
.setLength(Util.toIntExact(size))
|
||||
.build())
|
||||
.build())
|
||||
writeStream(inputStream)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writeAttachment(attachmentId: AttachmentId, inputStream: InputStream, size: Long) {
|
||||
write(outputStream, BackupFrame.newBuilder()
|
||||
.setAttachment(Attachment.newBuilder()
|
||||
.setRowId(attachmentId.rowId)
|
||||
.setAttachmentId(attachmentId.uniqueId)
|
||||
.setLength(Util.toIntExact(size))
|
||||
.build())
|
||||
.build())
|
||||
writeStream(inputStream)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writeSticker(rowId: Long, inputStream: InputStream, size: Long) {
|
||||
write(outputStream, BackupFrame.newBuilder()
|
||||
.setSticker(Sticker.newBuilder()
|
||||
.setRowId(rowId)
|
||||
.setLength(Util.toIntExact(size))
|
||||
.build())
|
||||
.build())
|
||||
writeStream(inputStream)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writeDatabaseVersion(version: Int) {
|
||||
write(outputStream, BackupFrame.newBuilder()
|
||||
.setVersion(DatabaseVersion.newBuilder().setVersion(version))
|
||||
.build())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun writeEnd() {
|
||||
write(outputStream, BackupFrame.newBuilder().setEnd(true).build())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeStream(inputStream: InputStream) {
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++)
|
||||
val remainder = synchronized(CIPHER_LOCK) {
|
||||
cipher.init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
SecretKeySpec(cipherKey, "AES"),
|
||||
IvParameterSpec(iv)
|
||||
)
|
||||
mac.update(iv)
|
||||
val buffer = ByteArray(8192)
|
||||
var read: Int
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
val ciphertext = cipher.update(buffer, 0, read)
|
||||
if (ciphertext != null) {
|
||||
outputStream.write(ciphertext)
|
||||
mac.update(ciphertext)
|
||||
}
|
||||
}
|
||||
cipher.doFinal()
|
||||
}
|
||||
outputStream.write(remainder)
|
||||
mac.update(remainder)
|
||||
val attachmentDigest = mac.doFinal()
|
||||
outputStream.write(attachmentDigest, 0, 10)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is InvalidKeyException,
|
||||
is InvalidAlgorithmParameterException,
|
||||
is IllegalBlockSizeException,
|
||||
is BadPaddingException -> {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun write(out: OutputStream, frame: BackupFrame) {
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++)
|
||||
val frameCiphertext = synchronized(CIPHER_LOCK) {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
||||
cipher.doFinal(frame.toByteArray())
|
||||
}
|
||||
val frameMac = mac.doFinal(frameCiphertext)
|
||||
val length = Conversions.intToByteArray(frameCiphertext.size + 10)
|
||||
out.write(length)
|
||||
out.write(frameCiphertext)
|
||||
out.write(frameMac, 0, 10)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is InvalidKeyException,
|
||||
is InvalidAlgorithmParameterException,
|
||||
is IllegalBlockSizeException,
|
||||
is BadPaddingException -> {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun flush() {
|
||||
outputStream.flush()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
outputStream.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,361 +0,0 @@
|
||||
package org.thoughtcrime.securesms.backup
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Conversions
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK
|
||||
import org.session.libsignal.crypto.kdf.HKDFv3
|
||||
import org.session.libsignal.utilities.ByteUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object FullBackupImporter {
|
||||
/**
|
||||
* Because BackupProtos.SharedPreference was made only to serialize string values,
|
||||
* we use these 3-char prefixes to explicitly cast the values before inserting to a preference file.
|
||||
*/
|
||||
const val PREF_PREFIX_TYPE_INT = "i__"
|
||||
const val PREF_PREFIX_TYPE_BOOLEAN = "b__"
|
||||
|
||||
private val TAG = FullBackupImporter::class.java.simpleName
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
fun importFromUri(context: Context,
|
||||
attachmentSecret: AttachmentSecret,
|
||||
db: SQLiteDatabase,
|
||||
fileUri: Uri,
|
||||
passphrase: String) {
|
||||
|
||||
val baseInputStream = context.contentResolver.openInputStream(fileUri)
|
||||
?: throw IOException("Cannot open an input stream for the file URI: $fileUri")
|
||||
|
||||
var count = 0
|
||||
try {
|
||||
BackupRecordInputStream(baseInputStream, passphrase).use { inputStream ->
|
||||
db.beginTransaction()
|
||||
dropAllTables(db)
|
||||
var frame: BackupFrame
|
||||
while (!inputStream.readFrame().also { frame = it }.end) {
|
||||
if (count++ % 100 == 0) EventBus.getDefault().post(BackupEvent.createProgress(count))
|
||||
when {
|
||||
frame.hasVersion() -> processVersion(db, frame.version)
|
||||
frame.hasStatement() -> processStatement(db, frame.statement)
|
||||
frame.hasPreference() -> processPreference(context, frame.preference)
|
||||
frame.hasAttachment() -> processAttachment(context, attachmentSecret, db, frame.attachment, inputStream)
|
||||
frame.hasAvatar() -> processAvatar(context, frame.avatar, inputStream)
|
||||
}
|
||||
}
|
||||
trimEntriesForExpiredMessages(context, db)
|
||||
db.setTransactionSuccessful()
|
||||
}
|
||||
} finally {
|
||||
if (db.inTransaction()) {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
EventBus.getDefault().post(BackupEvent.createFinished())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processVersion(db: SQLiteDatabase, version: DatabaseVersion) {
|
||||
if (version.version > db.version) {
|
||||
throw DatabaseDowngradeException(db.version, version.version)
|
||||
}
|
||||
db.version = version.version
|
||||
}
|
||||
|
||||
private fun processStatement(db: SQLiteDatabase, statement: SqlStatement) {
|
||||
val isForSmsFtsSecretTable = statement.statement.contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_")
|
||||
val isForMmsFtsSecretTable = statement.statement.contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_")
|
||||
val isForSqliteSecretTable = statement.statement.toLowerCase(Locale.ENGLISH).startsWith("create table sqlite_")
|
||||
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForSqliteSecretTable) {
|
||||
Log.i(TAG, "Ignoring import for statement: " + statement.statement)
|
||||
return
|
||||
}
|
||||
val parameters: MutableList<Any?> = LinkedList()
|
||||
for (parameter in statement.parametersList) {
|
||||
when {
|
||||
parameter.hasStringParamter() -> parameters.add(parameter.stringParamter)
|
||||
parameter.hasDoubleParameter() -> parameters.add(parameter.doubleParameter)
|
||||
parameter.hasIntegerParameter() -> parameters.add(parameter.integerParameter)
|
||||
parameter.hasBlobParameter() -> parameters.add(parameter.blobParameter.toByteArray())
|
||||
parameter.hasNullparameter() -> parameters.add(null)
|
||||
}
|
||||
}
|
||||
if (parameters.size > 0) {
|
||||
db.execSQL(statement.statement, parameters.toTypedArray())
|
||||
} else {
|
||||
db.execSQL(statement.statement)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processAttachment(context: Context, attachmentSecret: AttachmentSecret,
|
||||
db: SQLiteDatabase, attachment: Attachment,
|
||||
inputStream: BackupRecordInputStream) {
|
||||
val partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE)
|
||||
val dataFile = File.createTempFile("part", ".mms", partsDirectory)
|
||||
val output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false)
|
||||
inputStream.readAttachmentTo(output.second, attachment.length)
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(AttachmentDatabase.DATA, dataFile.absolutePath)
|
||||
contentValues.put(AttachmentDatabase.THUMBNAIL, null as String?)
|
||||
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first)
|
||||
db.update(AttachmentDatabase.TABLE_NAME, contentValues,
|
||||
"${AttachmentDatabase.ROW_ID} = ? AND ${AttachmentDatabase.UNIQUE_ID} = ?",
|
||||
arrayOf(attachment.rowId.toString(), attachment.attachmentId.toString()))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processAvatar(context: Context, avatar: Avatar, inputStream: BackupRecordInputStream) {
|
||||
inputStream.readAttachmentTo(FileOutputStream(
|
||||
AvatarHelper.getAvatarFile(context, Address.fromExternal(context, avatar.name))), avatar.length)
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun processPreference(context: Context, preference: SharedPreference) {
|
||||
val preferences = context.getSharedPreferences(preference.file, 0)
|
||||
val key = preference.key
|
||||
val value = preference.value
|
||||
|
||||
// See the comment next to PREF_PREFIX_TYPE_* constants.
|
||||
when {
|
||||
key.startsWith(PREF_PREFIX_TYPE_INT) ->
|
||||
preferences.edit().putInt(
|
||||
key.substring(PREF_PREFIX_TYPE_INT.length),
|
||||
value.toInt()
|
||||
).commit()
|
||||
key.startsWith(PREF_PREFIX_TYPE_BOOLEAN) ->
|
||||
preferences.edit().putBoolean(
|
||||
key.substring(PREF_PREFIX_TYPE_BOOLEAN.length),
|
||||
value.toBoolean()
|
||||
).commit()
|
||||
else ->
|
||||
preferences.edit().putString(key, value).commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun dropAllTables(db: SQLiteDatabase) {
|
||||
db.rawQuery("SELECT name, type FROM sqlite_master", null).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
val name = cursor.getString(0)
|
||||
val type = cursor.getString(1)
|
||||
if ("table" == type && !name.startsWith("sqlite_")) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
|
||||
val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
|
||||
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
|
||||
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
|
||||
val where = AttachmentDatabase.MMS_ID + trimmedCondition
|
||||
db.query(AttachmentDatabase.TABLE_NAME, columns, where, null, null, null, null).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
DatabaseComponent.get(context).attachmentDatabase()
|
||||
.deleteAttachment(AttachmentId(cursor.getLong(0), cursor.getLong(1)))
|
||||
}
|
||||
}
|
||||
db.query(ThreadDatabase.TABLE_NAME, arrayOf(ThreadDatabase.ID),
|
||||
ThreadDatabase.EXPIRES_IN + " > 0", null, null, null, null).use { cursor ->
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
DatabaseComponent.get(context).threadDatabase().update(cursor.getLong(0), false, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BackupRecordInputStream : Closeable {
|
||||
private val inputStream: InputStream
|
||||
private val cipher: Cipher
|
||||
private val mac: Mac
|
||||
private val cipherKey: ByteArray
|
||||
private val macKey: ByteArray
|
||||
private val iv: ByteArray
|
||||
|
||||
private var counter = 0
|
||||
|
||||
@Throws(IOException::class)
|
||||
constructor(inputStream: InputStream, passphrase: String) : super() {
|
||||
try {
|
||||
this.inputStream = inputStream
|
||||
val headerLengthBytes = ByteArray(4)
|
||||
Util.readFully(this.inputStream, headerLengthBytes)
|
||||
val headerLength = Conversions.byteArrayToInt(headerLengthBytes)
|
||||
val headerFrame = ByteArray(headerLength)
|
||||
Util.readFully(this.inputStream, headerFrame)
|
||||
val frame = BackupFrame.parseFrom(headerFrame)
|
||||
if (!frame.hasHeader()) {
|
||||
throw IOException("Backup stream does not start with header!")
|
||||
}
|
||||
val header = frame.header
|
||||
iv = header.iv.toByteArray()
|
||||
if (iv.size != 16) {
|
||||
throw IOException("Invalid IV length!")
|
||||
}
|
||||
val key = BackupUtil.computeBackupKey(passphrase, if (header.hasSalt()) header.salt.toByteArray() else null)
|
||||
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
|
||||
val split = ByteUtil.split(derived, 32, 32)
|
||||
cipherKey = split[0]
|
||||
macKey = split[1]
|
||||
cipher = synchronized(CIPHER_LOCK) { Cipher.getInstance("AES/CTR/NoPadding") }
|
||||
mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
|
||||
counter = Conversions.byteArrayToInt(iv)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is NoSuchAlgorithmException,
|
||||
is NoSuchPaddingException,
|
||||
is InvalidKeyException -> {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readFrame(): BackupFrame {
|
||||
return readFrame(inputStream)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readAttachmentTo(out: OutputStream, length: Int) {
|
||||
var length = length
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++)
|
||||
val plaintext = synchronized(CIPHER_LOCK) {
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(cipherKey, "AES"),
|
||||
IvParameterSpec(iv)
|
||||
)
|
||||
mac.update(iv)
|
||||
val buffer = ByteArray(8192)
|
||||
while (length > 0) {
|
||||
val read = inputStream.read(buffer, 0, Math.min(buffer.size, length))
|
||||
if (read == -1) throw IOException("File ended early!")
|
||||
mac.update(buffer, 0, read)
|
||||
val plaintext = cipher.update(buffer, 0, read)
|
||||
if (plaintext != null) {
|
||||
out.write(plaintext, 0, plaintext.size)
|
||||
}
|
||||
length -= read
|
||||
}
|
||||
cipher.doFinal()
|
||||
}
|
||||
if (plaintext != null) {
|
||||
out.write(plaintext, 0, plaintext.size)
|
||||
}
|
||||
out.close()
|
||||
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
|
||||
val theirMac = ByteArray(10)
|
||||
try {
|
||||
Util.readFully(inputStream, theirMac)
|
||||
} catch (e: IOException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw IOException("Bad MAC")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is InvalidKeyException,
|
||||
is InvalidAlgorithmParameterException,
|
||||
is IllegalBlockSizeException,
|
||||
is BadPaddingException -> {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readFrame(`in`: InputStream?): BackupFrame {
|
||||
return try {
|
||||
val length = ByteArray(4)
|
||||
Util.readFully(`in`, length)
|
||||
val frame = ByteArray(Conversions.byteArrayToInt(length))
|
||||
Util.readFully(`in`, frame)
|
||||
val theirMac = ByteArray(10)
|
||||
System.arraycopy(frame, frame.size - 10, theirMac, 0, theirMac.size)
|
||||
mac.update(frame, 0, frame.size - 10)
|
||||
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw IOException("Bad MAC")
|
||||
}
|
||||
Conversions.intToByteArray(iv, 0, counter++)
|
||||
val plaintext = synchronized(CIPHER_LOCK) {
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
|
||||
cipher.doFinal(frame, 0, frame.size - 10)
|
||||
}
|
||||
BackupFrame.parseFrom(plaintext)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is InvalidKeyException,
|
||||
is InvalidAlgorithmParameterException,
|
||||
is IllegalBlockSizeException,
|
||||
is BadPaddingException -> {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
inputStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseDowngradeException internal constructor(currentVersion: Int, backupVersion: Int) :
|
||||
IOException("Tried to import a backup with version $backupVersion into a database with version $currentVersion")
|
||||
}
|
@ -656,7 +656,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
|
||||
binding?.blockedBanner?.isVisible = recipient.isBlocked
|
||||
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
|
||||
binding?.blockedBanner?.setOnClickListener { viewModel.unblock(this@ConversationActivityV2) }
|
||||
}
|
||||
|
||||
private fun setUpLinkPreviewObserver() {
|
||||
@ -1094,7 +1094,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
.setMessage(message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ ->
|
||||
viewModel.block()
|
||||
viewModel.block(this@ConversationActivityV2)
|
||||
if (deleteThread) {
|
||||
viewModel.deleteThread()
|
||||
finish()
|
||||
@ -1148,7 +1148,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
.setMessage(message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ConversationActivity_unblock) { _, _ ->
|
||||
viewModel.unblock()
|
||||
viewModel.unblock(this@ConversationActivityV2)
|
||||
}.show()
|
||||
}
|
||||
|
||||
@ -1489,7 +1489,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
override fun sendMessage() {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
if (recipient.isContactRecipient && recipient.isBlocked) {
|
||||
BlockedDialog(recipient).show(supportFragmentManager, "Blocked Dialog")
|
||||
BlockedDialog(recipient, this).show(supportFragmentManager, "Blocked Dialog")
|
||||
return
|
||||
}
|
||||
val binding = binding ?: return
|
||||
|
@ -81,6 +81,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
|
||||
private View dropdownAnchor;
|
||||
private LinearLayout conversationItem;
|
||||
private View conversationBubble;
|
||||
private TextView conversationTimestamp;
|
||||
private View backgroundView;
|
||||
private ConstraintLayout foregroundView;
|
||||
private EmojiImageView[] emojiViews;
|
||||
@ -116,6 +118,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
|
||||
dropdownAnchor = findViewById(R.id.dropdown_anchor);
|
||||
conversationItem = findViewById(R.id.conversation_item);
|
||||
conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble);
|
||||
conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
|
||||
@ -165,10 +169,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
|
||||
View conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble);
|
||||
conversationBubble.setLayoutParams(new LinearLayout.LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
|
||||
conversationBubble.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
|
||||
TextView conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
|
||||
conversationTimestamp.setText(DateUtils.getDisplayFormattedTimeSpanString(getContext(), Locale.getDefault(), messageRecord.getTimestamp()));
|
||||
|
||||
updateConversationTimestamp(messageRecord);
|
||||
@ -190,12 +192,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
}
|
||||
|
||||
private void updateConversationTimestamp(MessageRecord message) {
|
||||
View bubble = conversationItem.findViewById(R.id.conversation_item_bubble);
|
||||
View timestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
|
||||
conversationItem.removeAllViewsInLayout();
|
||||
conversationItem.addView(message.isOutgoing() ? timestamp : bubble);
|
||||
conversationItem.addView(message.isOutgoing() ? bubble : timestamp);
|
||||
conversationItem.requestLayout();
|
||||
if (message.isOutgoing()) conversationBubble.bringToFront();
|
||||
else conversationTimestamp.bringToFront();
|
||||
}
|
||||
|
||||
private void showAfterLayout(@NonNull MessageRecord messageRecord,
|
||||
@ -351,11 +349,14 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
|
||||
int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
|
||||
|
||||
conversationBubble.animate()
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale)
|
||||
.setDuration(revealDuration);
|
||||
|
||||
conversationItem.animate()
|
||||
.x(endX)
|
||||
.y(endY)
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale)
|
||||
.setDuration(revealDuration);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@ -25,6 +26,7 @@ import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.repository.ConversationRepository
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import java.util.UUID
|
||||
|
||||
class ConversationViewModel(
|
||||
@ -94,14 +96,14 @@ class ConversationViewModel(
|
||||
repository.inviteContacts(threadId, contacts)
|
||||
}
|
||||
|
||||
fun block() {
|
||||
fun block(context: Context) {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for block action")
|
||||
if (recipient.isContactRecipient) {
|
||||
repository.setBlocked(recipient, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun unblock() {
|
||||
fun unblock(context: Context) {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
|
||||
if (recipient.isContactRecipient) {
|
||||
repository.setBlocked(recipient, false)
|
||||
|
@ -1,11 +1,15 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogBlockedBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
@ -13,9 +17,10 @@ import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
|
||||
/** Shown upon sending a message to a user that's blocked. */
|
||||
class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
class BlockedDialog(private val recipient: Recipient, private val context: Context) : BaseDialog() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
|
@ -1,249 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class JobDatabase extends Database {
|
||||
|
||||
public static final String[] CREATE_TABLE = new String[] { Jobs.CREATE_TABLE,
|
||||
Constraints.CREATE_TABLE,
|
||||
Dependencies.CREATE_TABLE };
|
||||
|
||||
public static final class Jobs {
|
||||
public static final String TABLE_NAME = "job_spec";
|
||||
private static final String ID = "_id";
|
||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||
private static final String FACTORY_KEY = "factory_key";
|
||||
private static final String QUEUE_KEY = "queue_key";
|
||||
private static final String CREATE_TIME = "create_time";
|
||||
private static final String NEXT_RUN_ATTEMPT_TIME = "next_run_attempt_time";
|
||||
private static final String RUN_ATTEMPT = "run_attempt";
|
||||
private static final String MAX_ATTEMPTS = "max_attempts";
|
||||
private static final String MAX_BACKOFF = "max_backoff";
|
||||
private static final String MAX_INSTANCES = "max_instances";
|
||||
private static final String LIFESPAN = "lifespan";
|
||||
private static final String SERIALIZED_DATA = "serialized_data";
|
||||
private static final String IS_RUNNING = "is_running";
|
||||
|
||||
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
JOB_SPEC_ID + " TEXT UNIQUE, " +
|
||||
FACTORY_KEY + " TEXT, " +
|
||||
QUEUE_KEY + " TEXT, " +
|
||||
CREATE_TIME + " INTEGER, " +
|
||||
NEXT_RUN_ATTEMPT_TIME + " INTEGER, " +
|
||||
RUN_ATTEMPT + " INTEGER, " +
|
||||
MAX_ATTEMPTS + " INTEGER, " +
|
||||
MAX_BACKOFF + " INTEGER, " +
|
||||
MAX_INSTANCES + " INTEGER, " +
|
||||
LIFESPAN + " INTEGER, " +
|
||||
SERIALIZED_DATA + " TEXT, " +
|
||||
IS_RUNNING + " INTEGER)";
|
||||
}
|
||||
|
||||
public static final class Constraints {
|
||||
public static final String TABLE_NAME = "constraint_spec";
|
||||
private static final String ID = "_id";
|
||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||
private static final String FACTORY_KEY = "factory_key";
|
||||
|
||||
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
JOB_SPEC_ID + " TEXT, " +
|
||||
FACTORY_KEY + " TEXT, " +
|
||||
"UNIQUE(" + JOB_SPEC_ID + ", " + FACTORY_KEY + "))";
|
||||
}
|
||||
|
||||
public static final class Dependencies {
|
||||
public static final String TABLE_NAME = "dependency_spec";
|
||||
private static final String ID = "_id";
|
||||
private static final String JOB_SPEC_ID = "job_spec_id";
|
||||
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";
|
||||
|
||||
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
JOB_SPEC_ID + " TEXT, " +
|
||||
DEPENDS_ON_JOB_SPEC_ID + " TEXT, " +
|
||||
"UNIQUE(" + JOB_SPEC_ID + ", " + DEPENDS_ON_JOB_SPEC_ID + "))";
|
||||
}
|
||||
|
||||
|
||||
public JobDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (FullSpec fullSpec : fullSpecs) {
|
||||
insertJobSpec(db, fullSpec.getJobSpec());
|
||||
insertConstraintSpecs(db, fullSpec.getConstraintSpecs());
|
||||
insertDependencySpecs(db, fullSpec.getDependencySpecs());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
|
||||
List<JobSpec> jobs = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
jobs.add(jobSpecFromCursor(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0);
|
||||
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ id };
|
||||
|
||||
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.IS_RUNNING, isRunning ? 1 : 0);
|
||||
contentValues.put(Jobs.RUN_ATTEMPT, runAttempt);
|
||||
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, nextRunAttemptTime);
|
||||
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ id };
|
||||
|
||||
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
public synchronized void updateAllJobsToBePending() {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.IS_RUNNING, 0);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
|
||||
}
|
||||
|
||||
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (String jobId : jobIds) {
|
||||
String[] arg = new String[]{jobId};
|
||||
|
||||
db.delete(Jobs.TABLE_NAME, Jobs.JOB_SPEC_ID + " = ?", arg);
|
||||
db.delete(Constraints.TABLE_NAME, Constraints.JOB_SPEC_ID + " = ?", arg);
|
||||
db.delete(Dependencies.TABLE_NAME, Dependencies.JOB_SPEC_ID + " = ?", arg);
|
||||
db.delete(Dependencies.TABLE_NAME, Dependencies.DEPENDS_ON_JOB_SPEC_ID + " = ?", arg);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
|
||||
List<ConstraintSpec> constraints = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
constraints.add(constraintSpecFromCursor(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
public synchronized @NonNull List<DependencySpec> getAllDependencySpecs() {
|
||||
List<DependencySpec> dependencies = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
dependencies.add(dependencySpecFromCursor(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private void insertJobSpec(@NonNull SQLiteDatabase db, @NonNull JobSpec job) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
contentValues.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
contentValues.put(Jobs.QUEUE_KEY, job.getQueueKey());
|
||||
contentValues.put(Jobs.CREATE_TIME, job.getCreateTime());
|
||||
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||
contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||
contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||
contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||
contentValues.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
|
||||
contentValues.put(Jobs.LIFESPAN, job.getLifespan());
|
||||
contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
contentValues.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
|
||||
|
||||
db.insertWithOnConflict(Jobs.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
|
||||
private void insertConstraintSpecs(@NonNull SQLiteDatabase db, @NonNull List<ConstraintSpec> constraints) {
|
||||
for (ConstraintSpec constraintSpec : constraints) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
|
||||
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
|
||||
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertDependencySpecs(@NonNull SQLiteDatabase db, @NonNull List<DependencySpec> dependencies) {
|
||||
for (DependencySpec dependencySpec : dependencies) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
|
||||
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
|
||||
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull JobSpec jobSpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new JobSpec(cursor.getString(cursor.getColumnIndexOrThrow(Jobs.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.FACTORY_KEY)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.QUEUE_KEY)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.CREATE_TIME)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.NEXT_RUN_ATTEMPT_TIME)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.RUN_ATTEMPT)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1);
|
||||
}
|
||||
|
||||
private @NonNull ConstraintSpec constraintSpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new ConstraintSpec(cursor.getString(cursor.getColumnIndexOrThrow(Constraints.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)));
|
||||
}
|
||||
|
||||
private @NonNull DependencySpec dependencySpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new DependencySpec(cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)));
|
||||
}
|
||||
}
|
@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
||||
@ -1354,11 +1355,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor)
|
||||
val quoteText = retrievedQuote?.body
|
||||
val quoteMissing = retrievedQuote == null
|
||||
val attachments = get(context).attachmentDatabase().getAttachment(cursor)
|
||||
val quoteAttachments: List<Attachment?>? =
|
||||
Stream.of(attachments).filter { obj: DatabaseAttachment? -> obj!!.isQuote }
|
||||
val quoteDeck = (
|
||||
(retrievedQuote as? MmsMessageRecord)?.slideDeck ?:
|
||||
Stream.of(get(context).attachmentDatabase().getAttachment(cursor))
|
||||
.filter { obj: DatabaseAttachment? -> obj!!.isQuote }
|
||||
.toList()
|
||||
val quoteDeck = SlideDeck(context, quoteAttachments!!)
|
||||
.let { SlideDeck(context, it) }
|
||||
)
|
||||
return Quote(
|
||||
quoteId,
|
||||
fromExternal(context, quoteAuthor),
|
||||
|
@ -46,7 +46,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
databaseHelper.writableDatabase.delete(sessionJobTable, "${Companion.jobID} = ?", arrayOf( jobID ))
|
||||
}
|
||||
|
||||
fun getAllPendingJobs(type: String): Map<String, Job?> {
|
||||
fun getAllJobs(type: String): Map<String, Job?> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf( type )) { cursor ->
|
||||
val jobID = cursor.getString(jobID)
|
||||
|
@ -76,7 +76,6 @@ import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
@ -85,7 +84,8 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.ClosedGroupManager
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
||||
@ -182,6 +182,11 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
return Profile(displayName, profileKey, profilePictureUrl)
|
||||
}
|
||||
|
||||
override fun setProfileAvatar(recipient: Recipient, profileAvatar: String?) {
|
||||
val database = DatabaseComponent.get(context).recipientDatabase()
|
||||
database.setProfileAvatar(recipient, profileAvatar)
|
||||
}
|
||||
|
||||
override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) {
|
||||
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
|
||||
Recipient.from(context, it, false)
|
||||
@ -189,7 +194,10 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
ourRecipient.resolve().profileKey = newProfileKey
|
||||
TextSecurePreferences.setProfileKey(context, newProfileKey?.let { Base64.encodeBytes(it) })
|
||||
TextSecurePreferences.setProfilePictureURL(context, newProfilePicture)
|
||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newProfilePicture))
|
||||
|
||||
if (newProfileKey != null) {
|
||||
JobQueue.shared.add(RetrieveProfileAvatarJob(Base64.encodeBytes(newProfileKey), ourRecipient.address))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrGenerateRegistrationID(): Int {
|
||||
@ -363,7 +371,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
}
|
||||
|
||||
override fun getAllPendingJobs(type: String): Map<String, Job?> {
|
||||
return DatabaseComponent.get(context).sessionJobDatabase().getAllPendingJobs(type)
|
||||
return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(type)
|
||||
}
|
||||
|
||||
override fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? {
|
||||
@ -383,7 +391,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
}
|
||||
|
||||
override fun getConfigSyncJob(destination: Destination): Job? {
|
||||
return DatabaseComponent.get(context).sessionJobDatabase().getAllPendingJobs(ConfigurationSyncJob.KEY).values.firstOrNull {
|
||||
return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(ConfigurationSyncJob.KEY).values.firstOrNull {
|
||||
(it as? ConfigurationSyncJob)?.destination == destination
|
||||
}
|
||||
}
|
||||
@ -469,11 +477,11 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
|
||||
// clear picture if userPic is null
|
||||
TextSecurePreferences.setProfileKey(context, null)
|
||||
TextSecurePreferences.setProfileAvatarId(context, 0)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, null)
|
||||
recipientDatabase.setProfileAvatar(recipient, null)
|
||||
TextSecurePreferences.setProfileAvatarId(context, 0)
|
||||
TextSecurePreferences.setProfilePictureURL(context, null)
|
||||
|
||||
setUserProfilePicture(null, null)
|
||||
Recipient.removeCached(fromSerialized(userPublicKey))
|
||||
configFactory.user?.setPic(UserPic.DEFAULT)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
@ -1193,9 +1201,10 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
|
||||
setRecipientApproved(recipient, true)
|
||||
threadDatabase.setHasSent(threadId, true)
|
||||
}
|
||||
if (contact.isBlocked == true) {
|
||||
setBlocked(listOf(recipient), true, fromConfigUpdate = true)
|
||||
threadDatabase.deleteConversation(threadId)
|
||||
|
||||
val contactIsBlocked: Boolean? = contact.isBlocked
|
||||
if (contactIsBlocked != null && recipient.isBlocked != contactIsBlocked) {
|
||||
setBlocked(listOf(recipient), contactIsBlocked, fromConfigUpdate = true)
|
||||
}
|
||||
}
|
||||
if (contacts.isNotEmpty()) {
|
||||
|
@ -25,7 +25,6 @@ import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupMemberDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase;
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase;
|
||||
@ -288,9 +287,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
for (String sql : SearchDatabase.CREATE_TABLE) {
|
||||
db.execSQL(sql);
|
||||
}
|
||||
for (String sql : JobDatabase.CREATE_TABLE) {
|
||||
db.execSQL(sql);
|
||||
}
|
||||
db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand());
|
||||
|
@ -32,7 +32,6 @@ interface DatabaseComponent {
|
||||
fun recipientDatabase(): RecipientDatabase
|
||||
fun groupReceiptDatabase(): GroupReceiptDatabase
|
||||
fun searchDatabase(): SearchDatabase
|
||||
fun jobDatabase(): JobDatabase
|
||||
fun lokiAPIDatabase(): LokiAPIDatabase
|
||||
fun lokiMessageDatabase(): LokiMessageDatabase
|
||||
fun lokiThreadDatabase(): LokiThreadDatabase
|
||||
|
@ -85,10 +85,6 @@ object DatabaseModule {
|
||||
@Singleton
|
||||
fun searchDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = SearchDatabase(context,openHelper)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideJobDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = JobDatabase(context, openHelper)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLokiApiDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = LokiAPIDatabase(context,openHelper)
|
||||
|
@ -546,6 +546,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
storage.setBlocked(listOf(thread.recipient), true)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
@ -562,6 +563,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
storage.setBlocked(listOf(thread.recipient), false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
|
@ -1,69 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import network.loki.messenger.BuildConfig;
|
||||
|
||||
/**
|
||||
* Schedules tasks using the {@link AlarmManager}.
|
||||
*
|
||||
* Given that this scheduler is only used when {@link KeepAliveService} is also used (which keeps
|
||||
* all of the {@link ConstraintObserver}s running), this only needs to schedule future runs in
|
||||
* situations where all constraints are already met. Otherwise, the {@link ConstraintObserver}s will
|
||||
* trigger future runs when the constraints are met.
|
||||
*
|
||||
* For the same reason, this class also doesn't have to schedule jobs that don't have delays.
|
||||
*
|
||||
* Important: Only use on API < 26.
|
||||
*/
|
||||
public class AlarmManagerScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = AlarmManagerScheduler.class.getSimpleName();
|
||||
|
||||
private final Application application;
|
||||
|
||||
AlarmManagerScheduler(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
if (delay > 0 && Stream.of(constraints).allMatch(Constraint::isMet)) {
|
||||
setUniqueAlarm(application, System.currentTimeMillis() + delay);
|
||||
}
|
||||
}
|
||||
|
||||
private void setUniqueAlarm(@NonNull Context context, long time) {
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
Intent intent = new Intent(context, RetryReceiver.class);
|
||||
|
||||
intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString());
|
||||
alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
|
||||
|
||||
Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms.");
|
||||
}
|
||||
|
||||
public static class RetryReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Received an alarm to retry a job.");
|
||||
ApplicationContext.getInstance(context).getJobManager().wakeUp();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
class CompositeScheduler implements Scheduler {
|
||||
|
||||
private final List<Scheduler> schedulers;
|
||||
|
||||
CompositeScheduler(@NonNull Scheduler... schedulers) {
|
||||
this.schedulers = Arrays.asList(schedulers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
for (Scheduler scheduler : schedulers) {
|
||||
scheduler.schedule(delay, constraints);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ConstraintInstantiator {
|
||||
|
||||
private final Map<String, Constraint.Factory> constraintFactories;
|
||||
|
||||
ConstraintInstantiator(@NonNull Map<String, Constraint.Factory> constraintFactories) {
|
||||
this.constraintFactories = new HashMap<>(constraintFactories);
|
||||
}
|
||||
|
||||
public @NonNull Constraint instantiate(@NonNull String constraintFactoryKey) {
|
||||
if (constraintFactories.containsKey(constraintFactoryKey)) {
|
||||
return constraintFactories.get(constraintFactoryKey).create();
|
||||
} else {
|
||||
throw new IllegalStateException("Tried to instantiate a constraint with key '" + constraintFactoryKey + "', but no matching factory was found.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface ConstraintObserver {
|
||||
|
||||
void register(@NonNull Notifier notifier);
|
||||
|
||||
interface Notifier {
|
||||
void onConstraintMet(@NonNull String reason);
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
/**
|
||||
* Interface responsible for injecting dependencies into Jobs.
|
||||
*/
|
||||
public interface DependencyInjector {
|
||||
void injectDependencies(Object object);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
public interface ExecutorFactory {
|
||||
@NonNull ExecutorService newSingleThreadExecutor(@NonNull String name);
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Schedules future runs on an in-app handler. Intended to be used in combination with a persistent
|
||||
* {@link Scheduler} to improve responsiveness when the app is open.
|
||||
*
|
||||
* This should only schedule runs when all constraints are met. Because this only works when the
|
||||
* app is foregrounded, jobs that don't have their constraints met will be run when the relevant
|
||||
* {@link ConstraintObserver} is triggered.
|
||||
*
|
||||
* Similarly, this does not need to schedule retries with no delay, as this doesn't provide any
|
||||
* persistence, and other mechanisms will take care of that.
|
||||
*/
|
||||
class InAppScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = InAppScheduler.class.getSimpleName();
|
||||
|
||||
private final JobManager jobManager;
|
||||
private final Handler handler;
|
||||
|
||||
InAppScheduler(@NonNull JobManager jobManager) {
|
||||
HandlerThread handlerThread = new HandlerThread("InAppScheduler");
|
||||
handlerThread.start();
|
||||
|
||||
this.jobManager = jobManager;
|
||||
this.handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
if (delay > 0 && Stream.of(constraints).allMatch(Constraint::isMet)) {
|
||||
Log.i(TAG, "Scheduling a retry in " + delay + " ms.");
|
||||
handler.postDelayed(() -> {
|
||||
Log.i(TAG, "Triggering a job retry.");
|
||||
jobManager.wakeUp();
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,286 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A durable unit of work.
|
||||
*
|
||||
* Jobs have {@link Parameters} that describe the conditions upon when you'd like them to run, how
|
||||
* often they should be retried, and how long they should be retried for.
|
||||
*
|
||||
* Never rely on a specific instance of this class being run. It can be created and destroyed as the
|
||||
* job is retried. State that you want to save is persisted to a {@link Data} object in
|
||||
* {@link #serialize()}. Your job is then recreated using a {@link Factory} that you register in
|
||||
* {@link JobManager.Configuration.Builder#setJobFactories(Map)}, which is given the saved
|
||||
* {@link Data} bundle.
|
||||
*
|
||||
* @deprecated
|
||||
* use <a href="https://developer.android.com/reference/androidx/work/WorkManager">WorkManager</a>
|
||||
* API instead.
|
||||
*/
|
||||
public abstract class Job {
|
||||
|
||||
private static final String TAG = Log.tag(Job.class);
|
||||
|
||||
private final Parameters parameters;
|
||||
|
||||
private String id;
|
||||
private int runAttempt;
|
||||
private long nextRunAttemptTime;
|
||||
|
||||
protected Context context;
|
||||
|
||||
public Job(@NonNull Parameters parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public final String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public final @NonNull Parameters getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public final int getRunAttempt() {
|
||||
return runAttempt;
|
||||
}
|
||||
|
||||
public final long getNextRunAttemptTime() {
|
||||
return nextRunAttemptTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is already called by {@link JobController} during job submission, but if you ever run a
|
||||
* job without submitting it to the {@link JobManager}, then you'll need to invoke this yourself.
|
||||
*/
|
||||
public final void setContext(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
final void setId(@NonNull String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
final void setRunAttempt(int runAttempt) {
|
||||
this.runAttempt = runAttempt;
|
||||
}
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
final void setNextRunAttemptTime(long nextRunAttemptTime) {
|
||||
this.nextRunAttemptTime = nextRunAttemptTime;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
final void onSubmit() {
|
||||
Log.i(TAG, JobLogger.format(this, "onSubmit()"));
|
||||
onAdded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the job is first submitted to the {@link JobManager}.
|
||||
*/
|
||||
@WorkerThread
|
||||
public void onAdded() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a job has run and its determined that a retry is required.
|
||||
*/
|
||||
@WorkerThread
|
||||
public void onRetry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize your job state so that it can be recreated in the future.
|
||||
*/
|
||||
public abstract @NonNull Data serialize();
|
||||
|
||||
/**
|
||||
* Returns the key that can be used to find the relevant factory needed to create your job.
|
||||
*/
|
||||
public abstract @NonNull String getFactoryKey();
|
||||
|
||||
/**
|
||||
* Called to do your actual work.
|
||||
*/
|
||||
@WorkerThread
|
||||
public abstract @NonNull Result run();
|
||||
|
||||
/**
|
||||
* Called when your job has completely failed.
|
||||
*/
|
||||
@WorkerThread
|
||||
public abstract void onCanceled();
|
||||
|
||||
public interface Factory<T extends Job> {
|
||||
@NonNull T create(@NonNull Parameters parameters, @NonNull Data data);
|
||||
}
|
||||
|
||||
public enum Result {
|
||||
SUCCESS, FAILURE, RETRY
|
||||
}
|
||||
|
||||
public static final class Parameters {
|
||||
|
||||
public static final int IMMORTAL = -1;
|
||||
public static final int UNLIMITED = -1;
|
||||
|
||||
private final long createTime;
|
||||
private final long lifespan;
|
||||
private final int maxAttempts;
|
||||
private final long maxBackoff;
|
||||
private final int maxInstances;
|
||||
private final String queue;
|
||||
private final List<String> constraintKeys;
|
||||
|
||||
private Parameters(long createTime,
|
||||
long lifespan,
|
||||
int maxAttempts,
|
||||
long maxBackoff,
|
||||
int maxInstances,
|
||||
@Nullable String queue,
|
||||
@NonNull List<String> constraintKeys)
|
||||
{
|
||||
this.createTime = createTime;
|
||||
this.lifespan = lifespan;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.maxInstances = maxInstances;
|
||||
this.queue = queue;
|
||||
this.constraintKeys = constraintKeys;
|
||||
}
|
||||
|
||||
public long getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public long getLifespan() {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
public int getMaxAttempts() {
|
||||
return maxAttempts;
|
||||
}
|
||||
|
||||
public long getMaxBackoff() {
|
||||
return maxBackoff;
|
||||
}
|
||||
|
||||
public int getMaxInstances() {
|
||||
return maxInstances;
|
||||
}
|
||||
|
||||
public @Nullable String getQueue() {
|
||||
return queue;
|
||||
}
|
||||
|
||||
public List<String> getConstraintKeys() {
|
||||
return constraintKeys;
|
||||
}
|
||||
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private long createTime = System.currentTimeMillis();
|
||||
private long maxBackoff = TimeUnit.SECONDS.toMillis(30);
|
||||
private long lifespan = IMMORTAL;
|
||||
private int maxAttempts = 1;
|
||||
private int maxInstances = UNLIMITED;
|
||||
private String queue = null;
|
||||
private List<String> constraintKeys = new LinkedList<>();
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
Builder setCreateTime(long createTime) {
|
||||
this.createTime = createTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the amount of time this job is allowed to be retried. Defaults to {@link #IMMORTAL}.
|
||||
*/
|
||||
public @NonNull Builder setLifespan(long lifespan) {
|
||||
this.lifespan = lifespan;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of times you want to attempt this job. Defaults to 1.
|
||||
*/
|
||||
public @NonNull Builder setMaxAttempts(int maxAttempts) {
|
||||
this.maxAttempts = maxAttempts;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the longest amount of time to wait between retries. No guarantees that this will
|
||||
* be respected on API >= 26.
|
||||
*/
|
||||
public @NonNull Builder setMaxBackoff(long maxBackoff) {
|
||||
this.maxBackoff = maxBackoff;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of instances you'd want of this job at any given time. If
|
||||
* enqueueing this job would put it over that limit, it will be ignored.
|
||||
*
|
||||
* Duplicates are determined by two jobs having the same {@link Job#getFactoryKey()}.
|
||||
*
|
||||
* This property is ignored if the job is submitted as part of a {@link JobManager.Chain}.
|
||||
*
|
||||
* Defaults to {@link #UNLIMITED}.
|
||||
*/
|
||||
public @NonNull Builder setMaxInstances(int maxInstances) {
|
||||
this.maxInstances = maxInstances;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a string representing a queue. All jobs within the same queue are run in a
|
||||
* serialized fashion -- one after the other, in order of insertion. Failure of a job earlier
|
||||
* in the queue has no impact on the execution of jobs later in the queue.
|
||||
*/
|
||||
public @NonNull Builder setQueue(@Nullable String queue) {
|
||||
this.queue = queue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a constraint via the key that was used to register its factory in
|
||||
* {@link JobManager.Configuration)};
|
||||
*/
|
||||
public @NonNull Builder addConstraint(@NonNull String constraintKey) {
|
||||
constraintKeys.add(constraintKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set constraints via the key that was used to register its factory in
|
||||
* {@link JobManager.Configuration)};
|
||||
*/
|
||||
public @NonNull Builder setConstraints(@NonNull List<String> constraintKeys) {
|
||||
this.constraintKeys.clear();
|
||||
this.constraintKeys.addAll(constraintKeys);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Parameters build() {
|
||||
return new Parameters(createTime, lifespan, maxAttempts, maxBackoff, maxInstances, queue, constraintKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,354 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.session.libsession.utilities.Debouncer;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages the queue of jobs. This is the only class that should write to {@link JobStorage} to
|
||||
* ensure consistency.
|
||||
*/
|
||||
class JobController {
|
||||
|
||||
private static final String TAG = JobController.class.getSimpleName();
|
||||
|
||||
private final Application application;
|
||||
private final JobStorage jobStorage;
|
||||
private final JobInstantiator jobInstantiator;
|
||||
private final ConstraintInstantiator constraintInstantiator;
|
||||
private final Data.Serializer dataSerializer;
|
||||
private final Scheduler scheduler;
|
||||
private final Debouncer debouncer;
|
||||
private final Callback callback;
|
||||
private final Set<String> runningJobs;
|
||||
|
||||
JobController(@NonNull Application application,
|
||||
@NonNull JobStorage jobStorage,
|
||||
@NonNull JobInstantiator jobInstantiator,
|
||||
@NonNull ConstraintInstantiator constraintInstantiator,
|
||||
@NonNull Data.Serializer dataSerializer,
|
||||
@NonNull Scheduler scheduler,
|
||||
@NonNull Debouncer debouncer,
|
||||
@NonNull Callback callback)
|
||||
{
|
||||
this.application = application;
|
||||
this.jobStorage = jobStorage;
|
||||
this.jobInstantiator = jobInstantiator;
|
||||
this.constraintInstantiator = constraintInstantiator;
|
||||
this.dataSerializer = dataSerializer;
|
||||
this.scheduler = scheduler;
|
||||
this.debouncer = debouncer;
|
||||
this.callback = callback;
|
||||
this.runningJobs = new HashSet<>();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void init() {
|
||||
jobStorage.init();
|
||||
jobStorage.updateAllJobsToBePending();
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
synchronized void wakeUp() {
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void submitNewJobChain(@NonNull List<List<Job>> chain) {
|
||||
chain = Stream.of(chain).filterNot(List::isEmpty).toList();
|
||||
|
||||
if (chain.isEmpty()) {
|
||||
Log.w(TAG, "Tried to submit an empty job chain. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chainExceedsMaximumInstances(chain)) {
|
||||
Job solo = chain.get(0).get(0);
|
||||
Log.w(TAG, JobLogger.format(solo, "Already at the max instance count of " + solo.getParameters().getMaxInstances() + ". Skipping."));
|
||||
return;
|
||||
}
|
||||
|
||||
insertJobChain(chain);
|
||||
scheduleJobs(chain.get(0));
|
||||
triggerOnSubmit(chain);
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void onRetry(@NonNull Job job) {
|
||||
int nextRunAttempt = job.getRunAttempt() + 1;
|
||||
long nextRunAttemptTime = calculateNextRunAttemptTime(System.currentTimeMillis(), nextRunAttempt, job.getParameters().getMaxBackoff());
|
||||
|
||||
jobStorage.updateJobAfterRetry(job.getId(), false, nextRunAttempt, nextRunAttemptTime);
|
||||
|
||||
List<Constraint> constraints = Stream.of(jobStorage.getConstraintSpecs(job.getId()))
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
.map(constraintInstantiator::instantiate)
|
||||
.toList();
|
||||
|
||||
|
||||
long delay = Math.max(0, nextRunAttemptTime - System.currentTimeMillis());
|
||||
|
||||
Log.i(TAG, JobLogger.format(job, "Scheduling a retry in " + delay + " ms."));
|
||||
scheduler.schedule(delay, constraints);
|
||||
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
synchronized void onJobFinished(@NonNull Job job) {
|
||||
runningJobs.remove(job.getId());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void onSuccess(@NonNull Job job) {
|
||||
jobStorage.deleteJob(job.getId());
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of all dependent jobs that should also be failed.
|
||||
*/
|
||||
@WorkerThread
|
||||
synchronized @NonNull List<Job> onFailure(@NonNull Job job) {
|
||||
List<Job> dependents = Stream.of(jobStorage.getDependencySpecsThatDependOnJob(job.getId()))
|
||||
.map(DependencySpec::getJobId)
|
||||
.map(jobStorage::getJobSpec)
|
||||
.withoutNulls()
|
||||
.map(jobSpec -> {
|
||||
List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId());
|
||||
return createJob(jobSpec, constraintSpecs);
|
||||
})
|
||||
.toList();
|
||||
|
||||
List<Job> all = new ArrayList<>(dependents.size() + 1);
|
||||
all.add(job);
|
||||
all.addAll(dependents);
|
||||
|
||||
jobStorage.deleteJobs(Stream.of(all).map(Job::getId).toList());
|
||||
|
||||
return dependents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next job that is eligible for execution. To be 'eligible' means that the job:
|
||||
* - Has no dependencies
|
||||
* - Has no unmet constraints
|
||||
*
|
||||
* This method will block until a job is available.
|
||||
* When the job returned from this method has been run, you must call {@link #onJobFinished(Job)}.
|
||||
*/
|
||||
@WorkerThread
|
||||
synchronized @NonNull Job pullNextEligibleJobForExecution() {
|
||||
try {
|
||||
Job job;
|
||||
|
||||
while ((job = getNextEligibleJobForExecution()) == null) {
|
||||
if (runningJobs.isEmpty()) {
|
||||
debouncer.publish(callback::onEmpty);
|
||||
}
|
||||
|
||||
wait();
|
||||
}
|
||||
|
||||
jobStorage.updateJobRunningState(job.getId(), true);
|
||||
runningJobs.add(job.getId());
|
||||
|
||||
return job;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted.");
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string representing the state of the job queue. Intended for debugging.
|
||||
*/
|
||||
@WorkerThread
|
||||
synchronized @NonNull String getDebugInfo() {
|
||||
List<JobSpec> jobs = jobStorage.getAllJobSpecs();
|
||||
List<ConstraintSpec> constraints = jobStorage.getAllConstraintSpecs();
|
||||
List<DependencySpec> dependencies = jobStorage.getAllDependencySpecs();
|
||||
|
||||
StringBuilder info = new StringBuilder();
|
||||
|
||||
info.append("-- Jobs\n");
|
||||
if (!jobs.isEmpty()) {
|
||||
Stream.of(jobs).forEach(j -> info.append(j.toString()).append('\n'));
|
||||
} else {
|
||||
info.append("None\n");
|
||||
}
|
||||
|
||||
info.append("\n-- Constraints\n");
|
||||
if (!constraints.isEmpty()) {
|
||||
Stream.of(constraints).forEach(c -> info.append(c.toString()).append('\n'));
|
||||
} else {
|
||||
info.append("None\n");
|
||||
}
|
||||
|
||||
info.append("\n-- Dependencies\n");
|
||||
if (!dependencies.isEmpty()) {
|
||||
Stream.of(dependencies).forEach(d -> info.append(d.toString()).append('\n'));
|
||||
} else {
|
||||
info.append("None\n");
|
||||
}
|
||||
|
||||
return info.toString();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private boolean chainExceedsMaximumInstances(@NonNull List<List<Job>> chain) {
|
||||
if (chain.size() == 1 && chain.get(0).size() == 1) {
|
||||
Job solo = chain.get(0).get(0);
|
||||
|
||||
if (solo.getParameters().getMaxInstances() != Job.Parameters.UNLIMITED &&
|
||||
jobStorage.getJobInstanceCount(solo.getFactoryKey()) >= solo.getParameters().getMaxInstances())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void triggerOnSubmit(@NonNull List<List<Job>> chain) {
|
||||
Stream.of(chain)
|
||||
.forEach(list -> Stream.of(list).forEach(job -> {
|
||||
job.setContext(application);
|
||||
job.onSubmit();
|
||||
}));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void insertJobChain(@NonNull List<List<Job>> chain) {
|
||||
List<FullSpec> fullSpecs = new LinkedList<>();
|
||||
List<Job> dependsOn = Collections.emptyList();
|
||||
|
||||
for (List<Job> jobList : chain) {
|
||||
for (Job job : jobList) {
|
||||
fullSpecs.add(buildFullSpec(job, dependsOn));
|
||||
}
|
||||
dependsOn = jobList;
|
||||
}
|
||||
|
||||
jobStorage.insertJobs(fullSpecs);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull FullSpec buildFullSpec(@NonNull Job job, @NonNull List<Job> dependsOn) {
|
||||
String id = UUID.randomUUID().toString();
|
||||
|
||||
job.setId(id);
|
||||
job.setRunAttempt(0);
|
||||
|
||||
JobSpec jobSpec = new JobSpec(job.getId(),
|
||||
job.getFactoryKey(),
|
||||
job.getParameters().getQueue(),
|
||||
job.getParameters().getCreateTime(),
|
||||
job.getNextRunAttemptTime(),
|
||||
job.getRunAttempt(),
|
||||
job.getParameters().getMaxAttempts(),
|
||||
job.getParameters().getMaxBackoff(),
|
||||
job.getParameters().getLifespan(),
|
||||
job.getParameters().getMaxInstances(),
|
||||
dataSerializer.serialize(job.serialize()),
|
||||
false);
|
||||
|
||||
List<ConstraintSpec> constraintSpecs = Stream.of(job.getParameters().getConstraintKeys())
|
||||
.map(key -> new ConstraintSpec(jobSpec.getId(), key))
|
||||
.toList();
|
||||
|
||||
List<DependencySpec> dependencySpecs = Stream.of(dependsOn)
|
||||
.map(depends -> new DependencySpec(job.getId(), depends.getId()))
|
||||
.toList();
|
||||
|
||||
return new FullSpec(jobSpec, constraintSpecs, dependencySpecs);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void scheduleJobs(@NonNull List<Job> jobs) {
|
||||
for (Job job : jobs) {
|
||||
List<Constraint> constraints = Stream.of(job.getParameters().getConstraintKeys())
|
||||
.map(key -> new ConstraintSpec(job.getId(), key))
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
.map(constraintInstantiator::instantiate)
|
||||
.toList();
|
||||
|
||||
scheduler.schedule(0, constraints);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Job getNextEligibleJobForExecution() {
|
||||
List<JobSpec> jobSpecs = jobStorage.getPendingJobsWithNoDependenciesInCreatedOrder(System.currentTimeMillis());
|
||||
|
||||
for (JobSpec jobSpec : jobSpecs) {
|
||||
List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId());
|
||||
List<Constraint> constraints = Stream.of(constraintSpecs)
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
.map(constraintInstantiator::instantiate)
|
||||
.toList();
|
||||
|
||||
if (Stream.of(constraints).allMatch(Constraint::isMet)) {
|
||||
return createJob(jobSpec, constraintSpecs);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull Job createJob(@NonNull JobSpec jobSpec, @NonNull List<ConstraintSpec> constraintSpecs) {
|
||||
Job.Parameters parameters = buildJobParameters(jobSpec, constraintSpecs);
|
||||
Data data = dataSerializer.deserialize(jobSpec.getSerializedData());
|
||||
Job job = jobInstantiator.instantiate(jobSpec.getFactoryKey(), parameters, data);
|
||||
|
||||
job.setId(jobSpec.getId());
|
||||
job.setRunAttempt(jobSpec.getRunAttempt());
|
||||
job.setNextRunAttemptTime(jobSpec.getNextRunAttemptTime());
|
||||
job.setContext(application);
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
private @NonNull Job.Parameters buildJobParameters(@NonNull JobSpec jobSpec, @NonNull List<ConstraintSpec> constraintSpecs) {
|
||||
return new Job.Parameters.Builder()
|
||||
.setCreateTime(jobSpec.getCreateTime())
|
||||
.setLifespan(jobSpec.getLifespan())
|
||||
.setMaxAttempts(jobSpec.getMaxAttempts())
|
||||
.setQueue(jobSpec.getQueueKey())
|
||||
.setConstraints(Stream.of(constraintSpecs).map(ConstraintSpec::getFactoryKey).toList())
|
||||
.build();
|
||||
}
|
||||
|
||||
private long calculateNextRunAttemptTime(long currentTime, int nextAttempt, long maxBackoff) {
|
||||
int boundedAttempt = Math.min(nextAttempt, 30);
|
||||
long exponentialBackoff = (long) Math.pow(2, boundedAttempt) * 1000;
|
||||
long actualBackoff = Math.min(exponentialBackoff, maxBackoff);
|
||||
|
||||
return currentTime + actualBackoff;
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
void onEmpty();
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class JobInstantiator {
|
||||
|
||||
private final Map<String, Job.Factory> jobFactories;
|
||||
|
||||
JobInstantiator(@NonNull Map<String, Job.Factory> jobFactories) {
|
||||
this.jobFactories = new HashMap<>(jobFactories);
|
||||
}
|
||||
|
||||
public @NonNull Job instantiate(@NonNull String jobFactoryKey, @NonNull Job.Parameters parameters, @NonNull Data data) {
|
||||
if (jobFactories.containsKey(jobFactoryKey)) {
|
||||
return jobFactories.get(jobFactoryKey).create(parameters, data);
|
||||
} else {
|
||||
throw new IllegalStateException("Tried to instantiate a job with key '" + jobFactoryKey + "', but no matching factory was found.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
public class JobLogger {
|
||||
|
||||
public static String format(@NonNull Job job, @NonNull String event) {
|
||||
return format(job, "", event);
|
||||
}
|
||||
|
||||
public static String format(@NonNull Job job, @NonNull String extraTag, @NonNull String event) {
|
||||
String id = job.getId();
|
||||
String tag = TextUtils.isEmpty(extraTag) ? "" : "[" + extraTag + "]";
|
||||
long timeSinceSubmission = System.currentTimeMillis() - job.getParameters().getCreateTime();
|
||||
int runAttempt = job.getRunAttempt() + 1;
|
||||
String maxAttempts = job.getParameters().getMaxAttempts() == Job.Parameters.UNLIMITED ? "Unlimited"
|
||||
: String.valueOf(job.getParameters().getMaxAttempts());
|
||||
String lifespan = job.getParameters().getLifespan() == Job.Parameters.IMMORTAL ? "Immortal"
|
||||
: String.valueOf(job.getParameters().getLifespan()) + " ms";
|
||||
return String.format("[%s][%s]%s %s (Time Since Submission: %d ms, Lifespan: %s, Run Attempt: %d/%s)",
|
||||
id, job.getClass().getSimpleName(), tag, event, timeSinceSubmission, lifespan, runAttempt, maxAttempts);
|
||||
}
|
||||
}
|
@ -1,310 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.session.libsession.utilities.Debouncer;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Allows the scheduling of durable jobs that will be run as early as possible.
|
||||
*/
|
||||
public class JobManager implements ConstraintObserver.Notifier {
|
||||
|
||||
private static final String TAG = JobManager.class.getSimpleName();
|
||||
|
||||
private final ExecutorService executor;
|
||||
private final JobController jobController;
|
||||
private final JobRunner[] jobRunners;
|
||||
|
||||
private final Set<EmptyQueueListener> emptyQueueListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
public JobManager(@NonNull Application application, @NonNull Configuration configuration) {
|
||||
this.executor = configuration.getExecutorFactory().newSingleThreadExecutor("JobManager");
|
||||
this.jobRunners = new JobRunner[configuration.getJobThreadCount()];
|
||||
this.jobController = new JobController(application,
|
||||
configuration.getJobStorage(),
|
||||
configuration.getJobInstantiator(),
|
||||
configuration.getConstraintFactories(),
|
||||
configuration.getDataSerializer(),
|
||||
Build.VERSION.SDK_INT < 26 ? new AlarmManagerScheduler(application)
|
||||
: new CompositeScheduler(new InAppScheduler(this), new JobSchedulerScheduler(application)),
|
||||
new Debouncer(500),
|
||||
this::onEmptyQueue);
|
||||
|
||||
executor.execute(() -> {
|
||||
jobController.init();
|
||||
|
||||
for (int i = 0; i < jobRunners.length; i++) {
|
||||
jobRunners[i] = new JobRunner(application, i + 1, jobController);
|
||||
jobRunners[i].start();
|
||||
}
|
||||
|
||||
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
|
||||
constraintObserver.register(this);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
application.startService(new Intent(application, KeepAliveService.class));
|
||||
}
|
||||
|
||||
wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a single job to be run.
|
||||
*/
|
||||
public void add(@NonNull Job job) {
|
||||
new Chain(this, Collections.singletonList(job)).enqueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins the creation of a job chain with a single job.
|
||||
* @see Chain
|
||||
*/
|
||||
public Chain startChain(@NonNull Job job) {
|
||||
return new Chain(this, Collections.singletonList(job));
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins the creation of a job chain with a set of jobs that can be run in parallel.
|
||||
* @see Chain
|
||||
*/
|
||||
public Chain startChain(@NonNull List<? extends Job> jobs) {
|
||||
return new Chain(this, jobs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string representing the state of the job queue. Intended for debugging.
|
||||
*/
|
||||
public @NonNull String getDebugInfo() {
|
||||
Future<String> result = executor.submit(jobController::getDebugInfo);
|
||||
try {
|
||||
return result.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.w(TAG, "Failed to retrieve Job info.", e);
|
||||
return "Failed to retrieve Job info.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to that will be notified when the job queue has been drained.
|
||||
*/
|
||||
void addOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
||||
executor.execute(() -> {
|
||||
emptyQueueListeners.add(listener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener that was added via {@link #addOnEmptyQueueListener(EmptyQueueListener)}.
|
||||
*/
|
||||
void removeOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
||||
executor.execute(() -> {
|
||||
emptyQueueListeners.remove(listener);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConstraintMet(@NonNull String reason) {
|
||||
Log.i(TAG, "onConstraintMet(" + reason + ")");
|
||||
wakeUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pokes the system to take another pass at the job queue.
|
||||
*/
|
||||
void wakeUp() {
|
||||
executor.execute(jobController::wakeUp);
|
||||
}
|
||||
|
||||
private void enqueueChain(@NonNull Chain chain) {
|
||||
executor.execute(() -> {
|
||||
jobController.submitNewJobChain(chain.getJobListChain());
|
||||
wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
private void onEmptyQueue() {
|
||||
executor.execute(() -> {
|
||||
for (EmptyQueueListener listener : emptyQueueListeners) {
|
||||
listener.onQueueEmpty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface EmptyQueueListener {
|
||||
void onQueueEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows enqueuing work that depends on each other. Jobs that appear later in the chain will
|
||||
* only run after all jobs earlier in the chain have been completed. If a job fails, all jobs
|
||||
* that occur later in the chain will also be failed.
|
||||
*/
|
||||
public static class Chain {
|
||||
|
||||
private final JobManager jobManager;
|
||||
private final List<List<Job>> jobs;
|
||||
|
||||
private Chain(@NonNull JobManager jobManager, @NonNull List<? extends Job> jobs) {
|
||||
this.jobManager = jobManager;
|
||||
this.jobs = new LinkedList<>();
|
||||
|
||||
this.jobs.add(new ArrayList<>(jobs));
|
||||
}
|
||||
|
||||
public Chain then(@NonNull Job job) {
|
||||
return then(Collections.singletonList(job));
|
||||
}
|
||||
|
||||
public Chain then(@NonNull List<Job> jobs) {
|
||||
if (!jobs.isEmpty()) {
|
||||
this.jobs.add(new ArrayList<>(jobs));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void enqueue() {
|
||||
jobManager.enqueueChain(this);
|
||||
}
|
||||
|
||||
private List<List<Job>> getJobListChain() {
|
||||
return jobs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Configuration {
|
||||
|
||||
private final ExecutorFactory executorFactory;
|
||||
private final int jobThreadCount;
|
||||
private final JobInstantiator jobInstantiator;
|
||||
private final ConstraintInstantiator constraintInstantiator;
|
||||
private final List<ConstraintObserver> constraintObservers;
|
||||
private final Data.Serializer dataSerializer;
|
||||
private final JobStorage jobStorage;
|
||||
|
||||
private Configuration(int jobThreadCount,
|
||||
@NonNull ExecutorFactory executorFactory,
|
||||
@NonNull JobInstantiator jobInstantiator,
|
||||
@NonNull ConstraintInstantiator constraintInstantiator,
|
||||
@NonNull List<ConstraintObserver> constraintObservers,
|
||||
@NonNull Data.Serializer dataSerializer,
|
||||
@NonNull JobStorage jobStorage)
|
||||
{
|
||||
this.executorFactory = executorFactory;
|
||||
this.jobThreadCount = jobThreadCount;
|
||||
this.jobInstantiator = jobInstantiator;
|
||||
this.constraintInstantiator = constraintInstantiator;
|
||||
this.constraintObservers = constraintObservers;
|
||||
this.dataSerializer = dataSerializer;
|
||||
this.jobStorage = jobStorage;
|
||||
}
|
||||
|
||||
int getJobThreadCount() {
|
||||
return jobThreadCount;
|
||||
}
|
||||
|
||||
@NonNull ExecutorFactory getExecutorFactory() {
|
||||
return executorFactory;
|
||||
}
|
||||
|
||||
@NonNull JobInstantiator getJobInstantiator() {
|
||||
return jobInstantiator;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
ConstraintInstantiator getConstraintFactories() {
|
||||
return constraintInstantiator;
|
||||
}
|
||||
|
||||
@NonNull List<ConstraintObserver> getConstraintObservers() {
|
||||
return constraintObservers;
|
||||
}
|
||||
|
||||
@NonNull Data.Serializer getDataSerializer() {
|
||||
return dataSerializer;
|
||||
}
|
||||
|
||||
@NonNull JobStorage getJobStorage() {
|
||||
return jobStorage;
|
||||
}
|
||||
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private ExecutorFactory executorFactory = new DefaultExecutorFactory();
|
||||
private int jobThreadCount = 1;
|
||||
private Map<String, Job.Factory> jobFactories = new HashMap<>();
|
||||
private Map<String, Constraint.Factory> constraintFactories = new HashMap<>();
|
||||
private List<ConstraintObserver> constraintObservers = new ArrayList<>();
|
||||
private Data.Serializer dataSerializer = new JsonDataSerializer();
|
||||
private JobStorage jobStorage = null;
|
||||
|
||||
public @NonNull Builder setJobThreadCount(int jobThreadCount) {
|
||||
this.jobThreadCount = jobThreadCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setExecutorFactory(@NonNull ExecutorFactory executorFactory) {
|
||||
this.executorFactory = executorFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setJobFactories(@NonNull Map<String, Job.Factory> jobFactories) {
|
||||
this.jobFactories = jobFactories;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setConstraintFactories(@NonNull Map<String, Constraint.Factory> constraintFactories) {
|
||||
this.constraintFactories = constraintFactories;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setConstraintObservers(@NonNull List<ConstraintObserver> constraintObservers) {
|
||||
this.constraintObservers = constraintObservers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setDataSerializer(@NonNull Data.Serializer dataSerializer) {
|
||||
this.dataSerializer = dataSerializer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setJobStorage(@NonNull JobStorage jobStorage) {
|
||||
this.jobStorage = jobStorage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Configuration build() {
|
||||
return new Configuration(jobThreadCount,
|
||||
executorFactory,
|
||||
new JobInstantiator(jobFactories),
|
||||
new ConstraintInstantiator(constraintFactories),
|
||||
new ArrayList<>(constraintObservers),
|
||||
dataSerializer,
|
||||
jobStorage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.PowerManager;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.util.WakeLockUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class JobRunner extends Thread {
|
||||
|
||||
private static final String TAG = JobRunner.class.getSimpleName();
|
||||
|
||||
private static long WAKE_LOCK_TIMEOUT = TimeUnit.MINUTES.toMillis(10);
|
||||
|
||||
private final Application application;
|
||||
private final int id;
|
||||
private final JobController jobController;
|
||||
|
||||
JobRunner(@NonNull Application application, int id, @NonNull JobController jobController) {
|
||||
super("JobRunner-" + id);
|
||||
|
||||
this.application = application;
|
||||
this.id = id;
|
||||
this.jobController = jobController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void run() {
|
||||
while (true) {
|
||||
Job job = jobController.pullNextEligibleJobForExecution();
|
||||
Job.Result result = run(job);
|
||||
|
||||
jobController.onJobFinished(job);
|
||||
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
jobController.onSuccess(job);
|
||||
break;
|
||||
case RETRY:
|
||||
jobController.onRetry(job);
|
||||
job.onRetry();
|
||||
break;
|
||||
case FAILURE:
|
||||
List<Job> dependents = jobController.onFailure(job);
|
||||
job.onCanceled();
|
||||
Stream.of(dependents).forEach(Job::onCanceled);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Job.Result run(@NonNull Job job) {
|
||||
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Running job."));
|
||||
|
||||
if (isJobExpired(job)) {
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its lifespan."));
|
||||
return Job.Result.FAILURE;
|
||||
}
|
||||
|
||||
Job.Result result = null;
|
||||
PowerManager.WakeLock wakeLock = null;
|
||||
|
||||
try {
|
||||
wakeLock = WakeLockUtil.acquire(application, PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TIMEOUT, job.getId());
|
||||
result = job.run();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing due to an unexpected exception."), e);
|
||||
return Job.Result.FAILURE;
|
||||
} finally {
|
||||
if (wakeLock != null) {
|
||||
WakeLockUtil.release(wakeLock, job.getId());
|
||||
}
|
||||
}
|
||||
|
||||
printResult(job, result);
|
||||
|
||||
if (result == Job.Result.RETRY && job.getRunAttempt() + 1 >= job.getParameters().getMaxAttempts() &&
|
||||
job.getParameters().getMaxAttempts() != Job.Parameters.UNLIMITED)
|
||||
{
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Failing after surpassing its max number of attempts."));
|
||||
return Job.Result.FAILURE;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isJobExpired(@NonNull Job job) {
|
||||
long expirationTime = job.getParameters().getCreateTime() + job.getParameters().getLifespan();
|
||||
|
||||
if (expirationTime < 0) {
|
||||
expirationTime = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
return job.getParameters().getLifespan() != Job.Parameters.IMMORTAL && expirationTime <= System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void printResult(@NonNull Job job, @NonNull Job.Result result) {
|
||||
if (result == Job.Result.FAILURE) {
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Job failed."));
|
||||
} else {
|
||||
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result: " + result));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import android.app.job.JobParameters;
|
||||
import android.app.job.JobScheduler;
|
||||
import android.app.job.JobService;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RequiresApi(26)
|
||||
public class JobSchedulerScheduler implements Scheduler {
|
||||
|
||||
private static final String TAG = JobSchedulerScheduler.class.getSimpleName();
|
||||
|
||||
private static final String PREF_NAME = "JobSchedulerScheduler_prefs";
|
||||
private static final String PREF_NEXT_ID = "pref_next_id";
|
||||
|
||||
private static final int MAX_ID = 75;
|
||||
|
||||
private final Application application;
|
||||
|
||||
JobSchedulerScheduler(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
@Override
|
||||
public void schedule(long delay, @NonNull List<Constraint> constraints) {
|
||||
JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getNextId(), new ComponentName(application, SystemService.class))
|
||||
.setMinimumLatency(delay)
|
||||
.setPersisted(true);
|
||||
|
||||
for (Constraint constraint : constraints) {
|
||||
constraint.applyToJobInfo(jobInfoBuilder);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Scheduling a run in " + delay + " ms.");
|
||||
JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
|
||||
jobScheduler.schedule(jobInfoBuilder.build());
|
||||
}
|
||||
|
||||
private int getNextId() {
|
||||
SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||
int returnedId = prefs.getInt(PREF_NEXT_ID, 0);
|
||||
int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1;
|
||||
|
||||
prefs.edit().putInt(PREF_NEXT_ID, nextId).apply();
|
||||
|
||||
return returnedId;
|
||||
}
|
||||
|
||||
@RequiresApi(api = 26)
|
||||
public static class SystemService extends JobService {
|
||||
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
Log.d(TAG, "onStartJob()");
|
||||
|
||||
JobManager jobManager = ApplicationContext.getInstance(getApplicationContext()).getJobManager();
|
||||
|
||||
jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() {
|
||||
@Override
|
||||
public void onQueueEmpty() {
|
||||
jobManager.removeOnEmptyQueueListener(this);
|
||||
jobFinished(params, false);
|
||||
Log.d(TAG, "jobFinished()");
|
||||
}
|
||||
});
|
||||
|
||||
jobManager.wakeUp();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStopJob(JobParameters params) {
|
||||
Log.d(TAG, "onStopJob()");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Scheduler {
|
||||
void schedule(long delay, @NonNull List<Constraint> constraints);
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
import org.thoughtcrime.securesms.sms.TelephonyServiceState;
|
||||
|
||||
public class CellServiceConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "CellServiceConstraint";
|
||||
|
||||
private final Application application;
|
||||
|
||||
public CellServiceConstraint(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
TelephonyServiceState telephonyServiceState = new TelephonyServiceState();
|
||||
return telephonyServiceState.isConnected(application);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
}
|
||||
|
||||
public static final class Factory implements Constraint.Factory<CellServiceConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellServiceConstraint create() {
|
||||
return new CellServiceConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.ServiceState;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
public class CellServiceConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = CellServiceConstraintObserver.class.getSimpleName();
|
||||
|
||||
private Notifier notifier;
|
||||
|
||||
public CellServiceConstraintObserver(@NonNull Application application) {
|
||||
TelephonyManager telephonyManager = (TelephonyManager) application.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
ServiceStateListener serviceStateListener = new ServiceStateListener();
|
||||
|
||||
telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
private class ServiceStateListener extends PhoneStateListener {
|
||||
@Override
|
||||
public void onServiceStateChanged(ServiceState serviceState) {
|
||||
if (serviceState.getState() == ServiceState.STATE_IN_SERVICE && notifier != null) {
|
||||
notifier.onConstraintMet(REASON);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.ExecutorFactory;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class DefaultExecutorFactory implements ExecutorFactory {
|
||||
@Override
|
||||
public @NonNull ExecutorService newSingleThreadExecutor(@NonNull String name) {
|
||||
return Executors.newSingleThreadExecutor(r -> new Thread(r, name));
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
public class NetworkConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = NetworkConstraintObserver.class.getSimpleName();
|
||||
|
||||
private final Application application;
|
||||
|
||||
public NetworkConstraintObserver(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
application.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
NetworkConstraint constraint = new NetworkConstraint.Factory(application).create();
|
||||
|
||||
if (constraint.isMet()) {
|
||||
notifier.onConstraintMet(REASON);
|
||||
}
|
||||
}
|
||||
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
|
||||
public class NetworkOrCellServiceConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "NetworkOrCellServiceConstraint";
|
||||
|
||||
private final NetworkConstraint networkConstraint;
|
||||
private final CellServiceConstraint serviceConstraint;
|
||||
|
||||
public NetworkOrCellServiceConstraint(@NonNull Application application) {
|
||||
networkConstraint = new NetworkConstraint.Factory(application).create();
|
||||
serviceConstraint = new CellServiceConstraint.Factory(application).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
return networkConstraint.isMet() || serviceConstraint.isMet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
}
|
||||
|
||||
public static class Factory implements Constraint.Factory<NetworkOrCellServiceConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkOrCellServiceConstraint create() {
|
||||
return new NetworkOrCellServiceConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
public class SqlCipherMigrationConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "SqlCipherMigrationConstraint";
|
||||
|
||||
private final Application application;
|
||||
|
||||
private SqlCipherMigrationConstraint(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
return !TextSecurePreferences.getNeedsSqlCipherMigration(application);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
}
|
||||
|
||||
public static final class Factory implements Constraint.Factory<SqlCipherMigrationConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlCipherMigrationConstraint create() {
|
||||
return new SqlCipherMigrationConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
public class SqlCipherMigrationConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = SqlCipherMigrationConstraintObserver.class.getSimpleName();
|
||||
|
||||
private Notifier notifier;
|
||||
|
||||
public SqlCipherMigrationConstraintObserver() {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(SqlCipherNeedsMigrationEvent event) {
|
||||
if (notifier != null) notifier.onConstraintMet(REASON);
|
||||
}
|
||||
|
||||
public static class SqlCipherNeedsMigrationEvent {
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ConstraintSpec {
|
||||
|
||||
private final String jobSpecId;
|
||||
private final String factoryKey;
|
||||
|
||||
public ConstraintSpec(@NonNull String jobSpecId, @NonNull String factoryKey) {
|
||||
this.jobSpecId = jobSpecId;
|
||||
this.factoryKey = factoryKey;
|
||||
}
|
||||
|
||||
public String getJobSpecId() {
|
||||
return jobSpecId;
|
||||
}
|
||||
|
||||
public String getFactoryKey() {
|
||||
return factoryKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ConstraintSpec that = (ConstraintSpec) o;
|
||||
return Objects.equals(jobSpecId, that.jobSpecId) &&
|
||||
Objects.equals(factoryKey, that.factoryKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobSpecId, factoryKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("jobSpecId: %s | factoryKey: %s", jobSpecId, factoryKey);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class DependencySpec {
|
||||
|
||||
private final String jobId;
|
||||
private final String dependsOnJobId;
|
||||
|
||||
public DependencySpec(@NonNull String jobId, @NonNull String dependsOnJobId) {
|
||||
this.jobId = jobId;
|
||||
this.dependsOnJobId = dependsOnJobId;
|
||||
}
|
||||
|
||||
public @NonNull String getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
public @NonNull String getDependsOnJobId() {
|
||||
return dependsOnJobId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DependencySpec that = (DependencySpec) o;
|
||||
return Objects.equals(jobId, that.jobId) &&
|
||||
Objects.equals(dependsOnJobId, that.dependsOnJobId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobId, dependsOnJobId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("jobSpecId: %s | dependsOnJobSpecId: %s", jobId, dependsOnJobId);
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class FullSpec {
|
||||
|
||||
private final JobSpec jobSpec;
|
||||
private final List<ConstraintSpec> constraintSpecs;
|
||||
private final List<DependencySpec> dependencySpecs;
|
||||
|
||||
public FullSpec(@NonNull JobSpec jobSpec,
|
||||
@NonNull List<ConstraintSpec> constraintSpecs,
|
||||
@NonNull List<DependencySpec> dependencySpecs)
|
||||
{
|
||||
this.jobSpec = jobSpec;
|
||||
this.constraintSpecs = constraintSpecs;
|
||||
this.dependencySpecs = dependencySpecs;
|
||||
}
|
||||
|
||||
public @NonNull JobSpec getJobSpec() {
|
||||
return jobSpec;
|
||||
}
|
||||
|
||||
public @NonNull List<ConstraintSpec> getConstraintSpecs() {
|
||||
return constraintSpecs;
|
||||
}
|
||||
|
||||
public @NonNull List<DependencySpec> getDependencySpecs() {
|
||||
return dependencySpecs;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
FullSpec fullSpec = (FullSpec) o;
|
||||
return Objects.equals(jobSpec, fullSpec.jobSpec) &&
|
||||
Objects.equals(constraintSpecs, fullSpec.constraintSpecs) &&
|
||||
Objects.equals(dependencySpecs, fullSpec.dependencySpecs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobSpec, constraintSpecs, dependencySpecs);
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class JobSpec {
|
||||
|
||||
private final String id;
|
||||
private final String factoryKey;
|
||||
private final String queueKey;
|
||||
private final long createTime;
|
||||
private final long nextRunAttemptTime;
|
||||
private final int runAttempt;
|
||||
private final int maxAttempts;
|
||||
private final long maxBackoff;
|
||||
private final long lifespan;
|
||||
private final int maxInstances;
|
||||
private final String serializedData;
|
||||
private final boolean isRunning;
|
||||
|
||||
public JobSpec(@NonNull String id,
|
||||
@NonNull String factoryKey,
|
||||
@Nullable String queueKey,
|
||||
long createTime,
|
||||
long nextRunAttemptTime,
|
||||
int runAttempt,
|
||||
int maxAttempts,
|
||||
long maxBackoff,
|
||||
long lifespan,
|
||||
int maxInstances,
|
||||
@NonNull String serializedData,
|
||||
boolean isRunning)
|
||||
{
|
||||
this.id = id;
|
||||
this.factoryKey = factoryKey;
|
||||
this.queueKey = queueKey;
|
||||
this.createTime = createTime;
|
||||
this.nextRunAttemptTime = nextRunAttemptTime;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.runAttempt = runAttempt;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.lifespan = lifespan;
|
||||
this.maxInstances = maxInstances;
|
||||
this.serializedData = serializedData;
|
||||
this.isRunning = isRunning;
|
||||
}
|
||||
|
||||
public @NonNull String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public @NonNull String getFactoryKey() {
|
||||
return factoryKey;
|
||||
}
|
||||
|
||||
public @Nullable String getQueueKey() {
|
||||
return queueKey;
|
||||
}
|
||||
|
||||
public long getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public long getNextRunAttemptTime() {
|
||||
return nextRunAttemptTime;
|
||||
}
|
||||
|
||||
public int getRunAttempt() {
|
||||
return runAttempt;
|
||||
}
|
||||
|
||||
public int getMaxAttempts() {
|
||||
return maxAttempts;
|
||||
}
|
||||
|
||||
public long getMaxBackoff() {
|
||||
return maxBackoff;
|
||||
}
|
||||
|
||||
public int getMaxInstances() {
|
||||
return maxInstances;
|
||||
}
|
||||
|
||||
public long getLifespan() {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
public @NonNull String getSerializedData() {
|
||||
return serializedData;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
JobSpec jobSpec = (JobSpec) o;
|
||||
return createTime == jobSpec.createTime &&
|
||||
nextRunAttemptTime == jobSpec.nextRunAttemptTime &&
|
||||
runAttempt == jobSpec.runAttempt &&
|
||||
maxAttempts == jobSpec.maxAttempts &&
|
||||
maxBackoff == jobSpec.maxBackoff &&
|
||||
lifespan == jobSpec.lifespan &&
|
||||
maxInstances == jobSpec.maxInstances &&
|
||||
isRunning == jobSpec.isRunning &&
|
||||
Objects.equals(id, jobSpec.id) &&
|
||||
Objects.equals(factoryKey, jobSpec.factoryKey) &&
|
||||
Objects.equals(queueKey, jobSpec.queueKey) &&
|
||||
Objects.equals(serializedData, jobSpec.serializedData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, maxInstances, serializedData, isRunning);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("id: %s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | maxInstances: %d | lifespan: %d | isRunning: %b | data: %s",
|
||||
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, maxInstances, lifespan, isRunning, serializedData);
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.persistence;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface JobStorage {
|
||||
|
||||
@WorkerThread
|
||||
void init();
|
||||
|
||||
@WorkerThread
|
||||
void insertJobs(@NonNull List<FullSpec> fullSpecs);
|
||||
|
||||
@WorkerThread
|
||||
@Nullable JobSpec getJobSpec(@NonNull String id);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<JobSpec> getAllJobSpecs();
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<JobSpec> getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime);
|
||||
|
||||
@WorkerThread
|
||||
int getJobInstanceCount(@NonNull String factoryKey);
|
||||
|
||||
@WorkerThread
|
||||
void updateJobRunningState(@NonNull String id, boolean isRunning);
|
||||
|
||||
@WorkerThread
|
||||
void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime);
|
||||
|
||||
@WorkerThread
|
||||
void updateAllJobsToBePending();
|
||||
|
||||
@WorkerThread
|
||||
void deleteJob(@NonNull String id);
|
||||
|
||||
@WorkerThread
|
||||
void deleteJobs(@NonNull List<String> ids);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<ConstraintSpec> getConstraintSpecs(@NonNull String jobId);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<ConstraintSpec> getAllConstraintSpecs();
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<DependencySpec> getDependencySpecsThatDependOnJob(@NonNull String jobSpecId);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<DependencySpec> getAllDependencySpecs();
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.session.libsession.utilities.DownloadUtilities;
|
||||
import org.session.libsession.utilities.GroupRecord;
|
||||
import org.session.libsignal.exceptions.InvalidMessageException;
|
||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.streams.AttachmentCipherInputStream;
|
||||
import org.session.libsignal.utilities.Hex;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AvatarDownloadJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "AvatarDownloadJob";
|
||||
|
||||
private static final String TAG = AvatarDownloadJob.class.getSimpleName();
|
||||
|
||||
private static final int MAX_AVATAR_SIZE = 20 * 1024 * 1024;
|
||||
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private String groupId;
|
||||
|
||||
public AvatarDownloadJob(@NonNull String groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(10)
|
||||
.build(),
|
||||
groupId);
|
||||
}
|
||||
|
||||
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull String groupId) {
|
||||
super(parameters);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, groupId).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
GroupDatabase database = DatabaseComponent.get(context).groupDatabase();
|
||||
Optional<GroupRecord> record = database.getGroup(groupId);
|
||||
File attachment = null;
|
||||
|
||||
try {
|
||||
if (record.isPresent()) {
|
||||
long avatarId = record.get().getAvatarId();
|
||||
String contentType = record.get().getAvatarContentType();
|
||||
byte[] key = record.get().getAvatarKey();
|
||||
String relay = record.get().getRelay();
|
||||
Optional<byte[]> digest = Optional.fromNullable(record.get().getAvatarDigest());
|
||||
Optional<String> fileName = Optional.absent();
|
||||
String url = record.get().getUrl();
|
||||
|
||||
if (avatarId == -1 || key == null || url.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (digest.isPresent()) {
|
||||
Log.i(TAG, "Downloading group avatar with digest: " + Hex.toString(digest.get()));
|
||||
}
|
||||
|
||||
attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
|
||||
attachment.deleteOnExit();
|
||||
|
||||
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url);
|
||||
|
||||
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
|
||||
DownloadUtilities.downloadFile(attachment, pointer.getUrl());
|
||||
|
||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
InputStream inputStream;
|
||||
if (!pointer.getDigest().isPresent()) {
|
||||
inputStream = new FileInputStream(attachment);
|
||||
} else {
|
||||
inputStream = AttachmentCipherInputStream.createForAttachment(attachment, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
|
||||
}
|
||||
|
||||
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
|
||||
|
||||
database.updateProfilePicture(groupId, avatar);
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
} finally {
|
||||
if (attachment != null)
|
||||
attachment.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
if (exception instanceof IOException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<AvatarDownloadJob> {
|
||||
@Override
|
||||
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new AvatarDownloadJob(parameters, data.getString(KEY_GROUP_ID));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use <a href="https://developer.android.com/reference/androidx/work/WorkManager">WorkManager</a>
|
||||
* API instead.
|
||||
*/
|
||||
public abstract class BaseJob extends Job {
|
||||
|
||||
private static final String TAG = BaseJob.class.getSimpleName();
|
||||
|
||||
public BaseJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Result run() {
|
||||
try {
|
||||
onRun();
|
||||
return Result.SUCCESS;
|
||||
} catch (Exception e) {
|
||||
if (onShouldRetry(e)) {
|
||||
Log.i(TAG, JobLogger.format(this, "Encountered a retryable exception."), e);
|
||||
return Result.RETRY;
|
||||
} else {
|
||||
Log.w(TAG, JobLogger.format(this, "Encountered a failing exception."), e);
|
||||
return Result.FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void onRun() throws Exception;
|
||||
|
||||
protected abstract boolean onShouldRetry(@NonNull Exception e);
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FastJobStorage implements JobStorage {
|
||||
|
||||
private final JobDatabase jobDatabase;
|
||||
|
||||
private final List<JobSpec> jobs;
|
||||
private final Map<String, List<ConstraintSpec>> constraintsByJobId;
|
||||
private final Map<String, List<DependencySpec>> dependenciesByJobId;
|
||||
|
||||
public FastJobStorage(@NonNull JobDatabase jobDatabase) {
|
||||
this.jobDatabase = jobDatabase;
|
||||
this.jobs = new ArrayList<>();
|
||||
this.constraintsByJobId = new HashMap<>();
|
||||
this.dependenciesByJobId = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void init() {
|
||||
List<JobSpec> jobSpecs = jobDatabase.getAllJobSpecs();
|
||||
List<ConstraintSpec> constraintSpecs = jobDatabase.getAllConstraintSpecs();
|
||||
List<DependencySpec> dependencySpecs = jobDatabase.getAllDependencySpecs();
|
||||
|
||||
jobs.addAll(jobSpecs);
|
||||
|
||||
for (ConstraintSpec constraintSpec: constraintSpecs) {
|
||||
List<ConstraintSpec> jobConstraints = Util.getOrDefault(constraintsByJobId, constraintSpec.getJobSpecId(), new LinkedList<>());
|
||||
jobConstraints.add(constraintSpec);
|
||||
constraintsByJobId.put(constraintSpec.getJobSpecId(), jobConstraints);
|
||||
}
|
||||
|
||||
for (DependencySpec dependencySpec : dependencySpecs) {
|
||||
List<DependencySpec> jobDependencies = Util.getOrDefault(dependenciesByJobId, dependencySpec.getJobId(), new LinkedList<>());
|
||||
jobDependencies.add(dependencySpec);
|
||||
dependenciesByJobId.put(dependencySpec.getJobId(), jobDependencies);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||
jobDatabase.insertJobs(fullSpecs);
|
||||
|
||||
for (FullSpec fullSpec : fullSpecs) {
|
||||
jobs.add(fullSpec.getJobSpec());
|
||||
constraintsByJobId.put(fullSpec.getJobSpec().getId(), fullSpec.getConstraintSpecs());
|
||||
dependenciesByJobId.put(fullSpec.getJobSpec().getId(), fullSpec.getDependencySpecs());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized @Nullable JobSpec getJobSpec(@NonNull String id) {
|
||||
for (JobSpec jobSpec : jobs) {
|
||||
if (jobSpec.getId().equals(id)) {
|
||||
return jobSpec;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
|
||||
return new ArrayList<>(jobs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized @NonNull List<JobSpec> getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime) {
|
||||
return Stream.of(jobs)
|
||||
.filter(j -> JobManagerFactories.hasFactoryForKey(j.getFactoryKey()))
|
||||
.filterNot(JobSpec::isRunning)
|
||||
.filter(this::firstInQueue)
|
||||
.filter(j -> !dependenciesByJobId.containsKey(j.getId()) || dependenciesByJobId.get(j.getId()).isEmpty())
|
||||
.filter(j -> j.getNextRunAttemptTime() <= currentTime)
|
||||
.sorted((j1, j2) -> Long.compare(j1.getCreateTime(), j2.getCreateTime()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean firstInQueue(@NonNull JobSpec job) {
|
||||
if (job.getQueueKey() == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Stream.of(jobs)
|
||||
.filter(j -> Util.equals(j.getQueueKey(), job.getQueueKey()))
|
||||
.sorted((j1, j2) -> Long.compare(j1.getCreateTime(), j2.getCreateTime()))
|
||||
.toList()
|
||||
.get(0)
|
||||
.equals(job);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getJobInstanceCount(@NonNull String factoryKey) {
|
||||
return (int) Stream.of(jobs)
|
||||
.filter(j -> j.getFactoryKey().equals(factoryKey))
|
||||
.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) {
|
||||
jobDatabase.updateJobRunningState(id, isRunning);
|
||||
|
||||
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
JobSpec existing = iter.next();
|
||||
if (existing.getId().equals(id)) {
|
||||
JobSpec updated = new JobSpec(existing.getId(),
|
||||
existing.getFactoryKey(),
|
||||
existing.getQueueKey(),
|
||||
existing.getCreateTime(),
|
||||
existing.getNextRunAttemptTime(),
|
||||
existing.getRunAttempt(),
|
||||
existing.getMaxAttempts(),
|
||||
existing.getMaxBackoff(),
|
||||
existing.getLifespan(),
|
||||
existing.getMaxInstances(),
|
||||
existing.getSerializedData(),
|
||||
isRunning);
|
||||
iter.set(updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) {
|
||||
jobDatabase.updateJobAfterRetry(id, isRunning, runAttempt, nextRunAttemptTime);
|
||||
|
||||
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
JobSpec existing = iter.next();
|
||||
if (existing.getId().equals(id)) {
|
||||
JobSpec updated = new JobSpec(existing.getId(),
|
||||
existing.getFactoryKey(),
|
||||
existing.getQueueKey(),
|
||||
existing.getCreateTime(),
|
||||
nextRunAttemptTime,
|
||||
runAttempt,
|
||||
existing.getMaxAttempts(),
|
||||
existing.getMaxBackoff(),
|
||||
existing.getLifespan(),
|
||||
existing.getMaxInstances(),
|
||||
existing.getSerializedData(),
|
||||
isRunning);
|
||||
iter.set(updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateAllJobsToBePending() {
|
||||
jobDatabase.updateAllJobsToBePending();
|
||||
|
||||
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
JobSpec existing = iter.next();
|
||||
JobSpec updated = new JobSpec(existing.getId(),
|
||||
existing.getFactoryKey(),
|
||||
existing.getQueueKey(),
|
||||
existing.getCreateTime(),
|
||||
existing.getNextRunAttemptTime(),
|
||||
existing.getRunAttempt(),
|
||||
existing.getMaxAttempts(),
|
||||
existing.getMaxBackoff(),
|
||||
existing.getLifespan(),
|
||||
existing.getMaxInstances(),
|
||||
existing.getSerializedData(),
|
||||
false);
|
||||
iter.set(updated);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void deleteJob(@NonNull String jobId) {
|
||||
deleteJobs(Collections.singletonList(jobId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
|
||||
jobDatabase.deleteJobs(jobIds);
|
||||
|
||||
Set<String> deleteIds = new HashSet<>(jobIds);
|
||||
|
||||
Iterator<JobSpec> jobIter = jobs.iterator();
|
||||
while (jobIter.hasNext()) {
|
||||
if (deleteIds.contains(jobIter.next().getId())) {
|
||||
jobIter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
for (String jobId : jobIds) {
|
||||
constraintsByJobId.remove(jobId);
|
||||
dependenciesByJobId.remove(jobId);
|
||||
|
||||
for (Map.Entry<String, List<DependencySpec>> entry : dependenciesByJobId.entrySet()) {
|
||||
Iterator<DependencySpec> depedencyIter = entry.getValue().iterator();
|
||||
|
||||
while (depedencyIter.hasNext()) {
|
||||
if (depedencyIter.next().getDependsOnJobId().equals(jobId)) {
|
||||
depedencyIter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized @NonNull List<ConstraintSpec> getConstraintSpecs(@NonNull String jobId) {
|
||||
return Util.getOrDefault(constraintsByJobId, jobId, new LinkedList<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
|
||||
return Stream.of(constraintsByJobId)
|
||||
.map(Map.Entry::getValue)
|
||||
.flatMap(Stream::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized @NonNull List<DependencySpec> getDependencySpecsThatDependOnJob(@NonNull String jobSpecId) {
|
||||
return Stream.of(dependenciesByJobId.entrySet())
|
||||
.map(Map.Entry::getValue)
|
||||
.flatMap(Stream::of)
|
||||
.filter(j -> j.getDependsOnJobId().equals(jobSpecId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<DependencySpec> getAllDependencySpecs() {
|
||||
return Stream.of(dependenciesByJobId)
|
||||
.map(Map.Entry::getValue)
|
||||
.flatMap(Stream::of)
|
||||
.toList();
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class JobManagerFactories {
|
||||
|
||||
private static Collection<String> factoryKeys = new ArrayList<>();
|
||||
|
||||
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
|
||||
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
|
||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
|
||||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
|
||||
}};
|
||||
factoryKeys.addAll(factoryHashMap.keySet());
|
||||
return factoryHashMap;
|
||||
}
|
||||
|
||||
public static Map<String, Constraint.Factory> getConstraintFactories(@NonNull Application application) {
|
||||
return new HashMap<String, Constraint.Factory>() {{
|
||||
put(CellServiceConstraint.KEY, new CellServiceConstraint.Factory(application));
|
||||
put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application));
|
||||
put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application));
|
||||
put(SqlCipherMigrationConstraint.KEY, new SqlCipherMigrationConstraint.Factory(application));
|
||||
}};
|
||||
}
|
||||
|
||||
public static List<ConstraintObserver> getConstraintObservers(@NonNull Application application) {
|
||||
return Arrays.asList(new CellServiceConstraintObserver(application),
|
||||
new NetworkConstraintObserver(application),
|
||||
new SqlCipherMigrationConstraintObserver());
|
||||
}
|
||||
|
||||
public static boolean hasFactoryForKey(String factoryKey) {
|
||||
return factoryKeys.contains(factoryKey);
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.session.libsignal.utilities.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.database.BackupFileRecord;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class LocalBackupJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "LocalBackupJob";
|
||||
|
||||
private static final String TAG = LocalBackupJob.class.getSimpleName();
|
||||
|
||||
public LocalBackupJob() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("__LOCAL_BACKUP__")
|
||||
.setMaxInstances(1)
|
||||
.setMaxAttempts(3)
|
||||
.build());
|
||||
}
|
||||
|
||||
private LocalBackupJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws NoExternalStorageException, IOException {
|
||||
Log.i(TAG, "Executing backup job...");
|
||||
|
||||
GenericForegroundService.startForegroundTask(context,
|
||||
context.getString(R.string.LocalBackupJob_creating_backup),
|
||||
NotificationChannels.BACKUPS,
|
||||
R.drawable.ic_launcher_foreground);
|
||||
|
||||
// TODO: Maybe create a new backup icon like ic_signal_backup?
|
||||
|
||||
try {
|
||||
BackupFileRecord record = BackupUtil.createBackupFile(context);
|
||||
BackupUtil.deleteAllBackupFiles(context, Collections.singletonList(record));
|
||||
|
||||
} finally {
|
||||
GenericForegroundService.stopForegroundTask(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<LocalBackupJob> {
|
||||
@Override
|
||||
public @NonNull LocalBackupJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new LocalBackupJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.os.Build
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.utilities.DecodedAudio
|
||||
import org.session.libsession.utilities.InputStreamMediaDataSource
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobs.PrepareAttachmentAudioExtrasJob.AudioExtrasUpdatedEvent
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Decodes the audio content of the related attachment entry
|
||||
* and caches the result with [DatabaseAttachmentAudioExtras] data.
|
||||
*
|
||||
* It only process attachments with "audio" mime types.
|
||||
*
|
||||
* Due to [DecodedAudio] implementation limitations, it only works for API 23+.
|
||||
* For any lower targets fake data will be generated.
|
||||
*
|
||||
* You can subscribe to [AudioExtrasUpdatedEvent] to be notified about the successful result.
|
||||
*/
|
||||
//TODO AC: Rewrite to WorkManager API when
|
||||
// https://github.com/loki-project/session-android/pull/354 is merged.
|
||||
class PrepareAttachmentAudioExtrasJob : BaseJob {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AttachAudioExtrasJob"
|
||||
|
||||
const val KEY = "PrepareAttachmentAudioExtrasJob"
|
||||
const val DATA_ATTACH_ID = "attachment_id"
|
||||
|
||||
const val VISUAL_RMS_FRAMES = 32 // The amount of values to be computed for the visualization.
|
||||
}
|
||||
|
||||
private val attachmentId: AttachmentId
|
||||
|
||||
constructor(attachmentId: AttachmentId) : this(Parameters.Builder()
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build(),
|
||||
attachmentId)
|
||||
|
||||
private constructor(parameters: Parameters, attachmentId: AttachmentId) : super(parameters) {
|
||||
this.attachmentId = attachmentId
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder().putParcelable(DATA_ATTACH_ID, attachmentId).build();
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String { return KEY
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCanceled() { }
|
||||
|
||||
override fun onRun() {
|
||||
Log.v(TAG, "Processing attachment: $attachmentId")
|
||||
|
||||
val attachDb = DatabaseComponent.get(context).attachmentDatabase()
|
||||
val attachment = attachDb.getAttachment(attachmentId)
|
||||
|
||||
if (attachment == null) {
|
||||
throw IllegalStateException("Cannot find attachment with the ID $attachmentId")
|
||||
}
|
||||
if (!attachment.contentType.startsWith("audio/")) {
|
||||
throw IllegalStateException("Attachment $attachmentId is not of audio type.")
|
||||
}
|
||||
|
||||
// Check if the audio extras already exist.
|
||||
if (attachDb.getAttachmentAudioExtras(attachmentId) != null) return
|
||||
|
||||
fun extractAttachmentRandomSeed(attachment: Attachment): Int {
|
||||
return when {
|
||||
attachment.digest != null -> attachment.digest!!.sum()
|
||||
attachment.fileName != null -> attachment.fileName.hashCode()
|
||||
else -> attachment.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
fun generateFakeRms(seed: Int, frames: Int = VISUAL_RMS_FRAMES): ByteArray {
|
||||
return ByteArray(frames).apply { Random(seed.toLong()).nextBytes(this) }
|
||||
}
|
||||
|
||||
var rmsValues: ByteArray
|
||||
var totalDurationMs: Long = DatabaseAttachmentAudioExtras.DURATION_UNDEFINED
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// Due to API version incompatibility, we just display some random waveform for older API.
|
||||
rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment))
|
||||
} else {
|
||||
try {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val decodedAudio = PartAuthority.getAttachmentStream(context, attachment.dataUri!!).use {
|
||||
DecodedAudio.create(InputStreamMediaDataSource(it))
|
||||
}
|
||||
rmsValues = decodedAudio.calculateRms(VISUAL_RMS_FRAMES)
|
||||
totalDurationMs = (decodedAudio.totalDuration / 1000.0).toLong()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to decode sample values for the audio attachment \"${attachment.fileName}\".", e)
|
||||
rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment))
|
||||
}
|
||||
}
|
||||
|
||||
attachDb.setAttachmentAudioExtras(DatabaseAttachmentAudioExtras(
|
||||
attachmentId,
|
||||
rmsValues,
|
||||
totalDurationMs
|
||||
))
|
||||
|
||||
EventBus.getDefault().post(AudioExtrasUpdatedEvent(attachmentId))
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<PrepareAttachmentAudioExtrasJob> {
|
||||
override fun create(parameters: Parameters, data: Data): PrepareAttachmentAudioExtrasJob {
|
||||
return PrepareAttachmentAudioExtrasJob(parameters, data.getParcelable(DATA_ATTACH_ID, AttachmentId.CREATOR))
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets dispatched once the audio extras have been updated. */
|
||||
data class AudioExtrasUpdatedEvent(val attachmentId: AttachmentId)
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.app.Application;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.avatars.AvatarHelper;
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.DownloadUtilities;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsignal.exceptions.PushNetworkException;
|
||||
import org.session.libsignal.streams.ProfileCipherInputStream;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "RetrieveProfileAvatarJob";
|
||||
|
||||
private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName();
|
||||
|
||||
private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
private static final String KEY_PROFILE_AVATAR = "profile_avatar";
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
|
||||
|
||||
private String profileAvatar;
|
||||
private Recipient recipient;
|
||||
|
||||
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.setMaxAttempts(2)
|
||||
.setMaxInstances(1)
|
||||
.build(),
|
||||
recipient,
|
||||
profileAvatar);
|
||||
}
|
||||
|
||||
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) {
|
||||
super(parameters);
|
||||
this.recipient = recipient;
|
||||
this.profileAvatar = profileAvatar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder()
|
||||
.putString(KEY_PROFILE_AVATAR, profileAvatar)
|
||||
.putString(KEY_ADDRESS, recipient.getAddress().serialize())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
RecipientDatabase database = DatabaseComponent.get(context).recipientDatabase();
|
||||
byte[] profileKey = recipient.resolve().getProfileKey();
|
||||
|
||||
if (profileKey == null || (profileKey.length != 32 && profileKey.length != 16)) {
|
||||
Log.w(TAG, "Recipient profile key is gone!");
|
||||
return;
|
||||
}
|
||||
|
||||
// if (AvatarHelper.avatarFileExists(context, recipient.resolve().getAddress()) && Util.equals(profileAvatar, recipient.resolve().getProfileAvatar())) {
|
||||
// Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar);
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (TextUtils.isEmpty(profileAvatar)) {
|
||||
Log.w(TAG, "Removing profile avatar for: " + recipient.getAddress().serialize());
|
||||
AvatarHelper.delete(context, recipient.getAddress());
|
||||
database.setProfileAvatar(recipient, null);
|
||||
return;
|
||||
}
|
||||
|
||||
File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
|
||||
|
||||
try {
|
||||
DownloadUtilities.downloadFile(downloadDestination, profileAvatar);
|
||||
InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey);
|
||||
File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
|
||||
|
||||
Util.copy(avatarStream, new FileOutputStream(decryptDestination));
|
||||
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress()));
|
||||
if (recipient.isLocalNumber()) {
|
||||
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
|
||||
}
|
||||
database.setProfileAvatar(recipient, profileAvatar);
|
||||
} catch (Exception e) {
|
||||
Log.e("Loki", "Failed to download profile avatar", e);
|
||||
} finally {
|
||||
if (downloadDestination != null) downloadDestination.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
if (e instanceof PushNetworkException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RetrieveProfileAvatarJob> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RetrieveProfileAvatarJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new RetrieveProfileAvatarJob(parameters,
|
||||
Recipient.from(application, Address.fromSerialized(data.getString(KEY_ADDRESS)), true),
|
||||
data.getString(KEY_PROFILE_AVATAR));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,271 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkReadyListener;
|
||||
import org.session.libsession.utilities.FileUtils;
|
||||
import org.session.libsignal.utilities.Hex;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class UpdateApkJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "UpdateApkJob";
|
||||
|
||||
private static final String TAG = UpdateApkJob.class.getSimpleName();
|
||||
|
||||
public UpdateApkJob() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("UpdateApkJob")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(3)
|
||||
.build());
|
||||
}
|
||||
|
||||
private UpdateApkJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException, PackageManager.NameNotFoundException {
|
||||
if (!BuildConfig.PLAY_STORE_DISABLED) return;
|
||||
|
||||
Log.i(TAG, "Checking for APK update...");
|
||||
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
Request request = new Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build();
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("Bad response: " + response.message());
|
||||
}
|
||||
|
||||
UpdateDescriptor updateDescriptor = JsonUtil.fromJson(response.body().string(), UpdateDescriptor.class);
|
||||
byte[] digest = Hex.fromStringCondensed(updateDescriptor.getDigest());
|
||||
|
||||
Log.i(TAG, "Got descriptor: " + updateDescriptor);
|
||||
|
||||
if (updateDescriptor.getVersionCode() > getVersionCode()) {
|
||||
DownloadStatus downloadStatus = getDownloadStatus(updateDescriptor.getUrl(), digest);
|
||||
|
||||
Log.i(TAG, "Download status: " + downloadStatus.getStatus());
|
||||
|
||||
if (downloadStatus.getStatus() == DownloadStatus.Status.COMPLETE) {
|
||||
Log.i(TAG, "Download status complete, notifying...");
|
||||
handleDownloadNotify(downloadStatus.getDownloadId());
|
||||
} else if (downloadStatus.getStatus() == DownloadStatus.Status.MISSING) {
|
||||
Log.i(TAG, "Download status missing, starting download...");
|
||||
handleDownloadStart(updateDescriptor.getUrl(), updateDescriptor.getVersionName(), digest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof IOException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "Update check failed");
|
||||
}
|
||||
|
||||
private int getVersionCode() throws PackageManager.NameNotFoundException {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
|
||||
|
||||
return packageInfo.versionCode;
|
||||
}
|
||||
|
||||
private DownloadStatus getDownloadStatus(String uri, byte[] theirDigest) {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
|
||||
query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING | DownloadManager.STATUS_SUCCESSFUL);
|
||||
|
||||
long pendingDownloadId = TextSecurePreferences.getUpdateApkDownloadId(context);
|
||||
byte[] pendingDigest = getPendingDigest(context);
|
||||
Cursor cursor = downloadManager.query(query);
|
||||
|
||||
try {
|
||||
DownloadStatus status = new DownloadStatus(DownloadStatus.Status.MISSING, -1);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int jobStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
|
||||
String jobRemoteUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI));
|
||||
long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
|
||||
byte[] digest = getDigestForDownloadId(downloadId);
|
||||
|
||||
if (jobRemoteUri != null && jobRemoteUri.equals(uri) && downloadId == pendingDownloadId) {
|
||||
|
||||
if (jobStatus == DownloadManager.STATUS_SUCCESSFUL &&
|
||||
digest != null && pendingDigest != null &&
|
||||
MessageDigest.isEqual(pendingDigest, theirDigest) &&
|
||||
MessageDigest.isEqual(digest, theirDigest))
|
||||
{
|
||||
return new DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId);
|
||||
} else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) {
|
||||
status = new DownloadStatus(DownloadStatus.Status.PENDING, downloadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownloadStart(String uri, String versionName, byte[] digest) {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(uri));
|
||||
|
||||
downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
|
||||
downloadRequest.setTitle("Downloading Signal update");
|
||||
downloadRequest.setDescription("Downloading Signal " + versionName);
|
||||
downloadRequest.setVisibleInDownloadsUi(false);
|
||||
downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk");
|
||||
downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
|
||||
|
||||
long downloadId = downloadManager.enqueue(downloadRequest);
|
||||
TextSecurePreferences.setUpdateApkDownloadId(context, downloadId);
|
||||
TextSecurePreferences.setUpdateApkDigest(context, Hex.toStringCondensed(digest));
|
||||
}
|
||||
|
||||
private void handleDownloadNotify(long downloadId) {
|
||||
Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
|
||||
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
|
||||
|
||||
new UpdateApkReadyListener().onReceive(context, intent);
|
||||
}
|
||||
|
||||
private @Nullable byte[] getDigestForDownloadId(long downloadId) {
|
||||
try {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor());
|
||||
byte[] digest = FileUtils.getFileDigest(fin);
|
||||
|
||||
fin.close();
|
||||
|
||||
return digest;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable byte[] getPendingDigest(Context context) {
|
||||
try {
|
||||
String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context);
|
||||
|
||||
if (encodedDigest == null) return null;
|
||||
|
||||
return Hex.fromStringCondensed(encodedDigest);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpdateDescriptor {
|
||||
@JsonProperty
|
||||
private int versionCode;
|
||||
|
||||
@JsonProperty
|
||||
private String versionName;
|
||||
|
||||
@JsonProperty
|
||||
private String url;
|
||||
|
||||
@JsonProperty
|
||||
private String sha256sum;
|
||||
|
||||
|
||||
public int getVersionCode() {
|
||||
return versionCode;
|
||||
}
|
||||
|
||||
public String getVersionName() {
|
||||
return versionName;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public @NonNull String toString() {
|
||||
return "[" + versionCode + ", " + versionName + ", " + url + "]";
|
||||
}
|
||||
|
||||
public String getDigest() {
|
||||
return sha256sum;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DownloadStatus {
|
||||
enum Status {
|
||||
PENDING,
|
||||
COMPLETE,
|
||||
MISSING
|
||||
}
|
||||
|
||||
private final Status status;
|
||||
private final long downloadId;
|
||||
|
||||
DownloadStatus(Status status, long downloadId) {
|
||||
this.status = status;
|
||||
this.downloadId = downloadId;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public long getDownloadId() {
|
||||
return downloadId;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<UpdateApkJob> {
|
||||
@Override
|
||||
public @NonNull UpdateApkJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new UpdateApkJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,8 +7,10 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.copper.flow.observeQuery
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
@ -21,6 +23,7 @@ import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.adapter.SelectableItem
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -33,8 +33,10 @@ import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||
import org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
@ -115,10 +117,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
private fun setupProfilePictureView(view: ProfilePictureView) {
|
||||
view.glide = glide
|
||||
view.publicKey = hexEncodedPublicKey
|
||||
view.displayName = getDisplayName()
|
||||
view.isLarge = true
|
||||
view.update()
|
||||
view.apply {
|
||||
publicKey = hexEncodedPublicKey
|
||||
displayName = getDisplayName()
|
||||
isLarge = true
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@ -222,8 +226,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
if (profilePicture != null) {
|
||||
promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
|
||||
} else {
|
||||
TextSecurePreferences.setLastProfilePictureUpload(this, System.currentTimeMillis())
|
||||
TextSecurePreferences.setProfilePictureURL(this, null)
|
||||
MessagingModuleConfiguration.shared.storage.clearUserPic()
|
||||
}
|
||||
}
|
||||
val compoundPromise = all(promises)
|
||||
@ -284,14 +287,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun deleteProfilePicture() {
|
||||
MessagingModuleConfiguration.shared.storage.clearUserPic()
|
||||
with (binding.profilePictureView.root) {
|
||||
recycle()
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEditProfilePictureUI() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.activity_settings_set_display_picture)
|
||||
@ -306,7 +301,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
}
|
||||
.show().apply {
|
||||
findViewById<ProfilePictureView>(R.id.profile_picture_view)?.let(::setupProfilePictureView)
|
||||
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
|
||||
?.also(::setupProfilePictureView)
|
||||
|
||||
val pictureIcon = findViewById<View>(R.id.ic_pictures)
|
||||
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
|
||||
|
||||
val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
|
||||
|
||||
profilePic?.isVisible = photoSet
|
||||
pictureIcon?.isVisible = !photoSet
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalBackupListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getNextBackupTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
if (TextSecurePreferences.isBackupEnabled(context)) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new LocalBackupJob());
|
||||
}
|
||||
|
||||
long nextTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setNextBackupTime(context, nextTime);
|
||||
|
||||
return nextTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
if (TextSecurePreferences.isBackupEnabled(context)) {
|
||||
new LocalBackupListener().onReceive(context, new Intent());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class UpdateApkRefreshListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final String TAG = UpdateApkRefreshListener.class.getSimpleName();
|
||||
|
||||
private static final long INTERVAL = TimeUnit.HOURS.toMillis(6);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getUpdateApkRefreshTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
Log.i(TAG, "onAlarm...");
|
||||
|
||||
if (scheduledTime != 0 && BuildConfig.PLAY_STORE_DISABLED) {
|
||||
Log.i(TAG, "Queueing APK update job...");
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new UpdateApkJob());
|
||||
}
|
||||
|
||||
long newTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setUpdateApkRefreshTime(context, newTime);
|
||||
|
||||
return newTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
new UpdateApkRefreshListener().onReceive(context, new Intent());
|
||||
}
|
||||
|
||||
}
|
@ -799,6 +799,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
||||
wantsToAnswerReceiver?.let { receiver ->
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
||||
}
|
||||
callManager.shutDownAudioManager()
|
||||
powerButtonReceiver = null
|
||||
wiredHeadsetStateReceiver = null
|
||||
networkChangedReceiver = null
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import network.loki.messenger.libsession_util.util.UserPic
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
@ -11,8 +12,8 @@ import org.session.libsignal.utilities.IdPrefix
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
|
||||
|
||||
class ProfileManager(private val context: Context, private val configFactory: ConfigFactory) : SSKEnvironment.ProfileManagerProtocol {
|
||||
|
||||
@ -55,13 +56,9 @@ class ProfileManager(private val context: Context, private val configFactory: Co
|
||||
profilePictureURL: String?,
|
||||
profileKey: ByteArray?
|
||||
) {
|
||||
// New API
|
||||
val job = RetrieveProfileAvatarJob(profilePictureURL, recipient.address)
|
||||
JobQueue.shared.add(job)
|
||||
val sessionID = recipient.address.serialize()
|
||||
// Old API
|
||||
val database = DatabaseComponent.get(context).recipientDatabase()
|
||||
database.setProfileKey(recipient, profileKey)
|
||||
if (recipient.isLocalNumber) return
|
||||
|
||||
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
|
||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||
if (contact == null) contact = Contact(sessionID)
|
||||
@ -71,13 +68,9 @@ class ProfileManager(private val context: Context, private val configFactory: Co
|
||||
contact.profilePictureURL = profilePictureURL
|
||||
contactDatabase.setContact(contact)
|
||||
}
|
||||
val job = RetrieveProfileAvatarJob(recipient, profilePictureURL)
|
||||
val jobManager = ApplicationContext.getInstance(context).jobManager
|
||||
jobManager.add(job)
|
||||
contactUpdatedInternal(contact)
|
||||
}
|
||||
|
||||
|
||||
override fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) {
|
||||
val database = DatabaseComponent.get(context).recipientDatabase()
|
||||
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
|
||||
|
@ -1,313 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.R
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.ByteUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent
|
||||
import org.thoughtcrime.securesms.backup.BackupPassphrase
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
||||
import org.thoughtcrime.securesms.backup.FullBackupExporter
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.BackupFileRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object BackupUtil {
|
||||
private const val MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences"
|
||||
private const val TAG = "BackupUtil"
|
||||
const val BACKUP_FILE_MIME_TYPE = "application/session-backup"
|
||||
const val BACKUP_PASSPHRASE_LENGTH = 30
|
||||
|
||||
fun getBackupRecords(context: Context): List<SharedPreference> {
|
||||
val prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME
|
||||
val preferences = context.getSharedPreferences(prefName, 0)
|
||||
val prefList = LinkedList<SharedPreference>()
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, null))
|
||||
.build())
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, null))
|
||||
.build())
|
||||
if (preferences.contains(IdentityKeyUtil.ED25519_PUBLIC_KEY)) {
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.ED25519_PUBLIC_KEY)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.ED25519_PUBLIC_KEY, null))
|
||||
.build())
|
||||
}
|
||||
if (preferences.contains(IdentityKeyUtil.ED25519_SECRET_KEY)) {
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.ED25519_SECRET_KEY)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.ED25519_SECRET_KEY, null))
|
||||
.build())
|
||||
}
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.LOKI_SEED)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.LOKI_SEED, null))
|
||||
.build())
|
||||
return prefList
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLastBackupTimeString(context: Context, locale: Locale): String {
|
||||
val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime()
|
||||
if (timestamp == null) {
|
||||
return context.getString(R.string.BackupUtil_never)
|
||||
}
|
||||
return DateUtils.getDisplayFormattedTimeSpanString(context, locale, timestamp.time)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLastBackup(context: Context): BackupFileRecord? {
|
||||
return DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFile()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun generateBackupPassphrase(): Array<String> {
|
||||
val random = ByteArray(BACKUP_PASSPHRASE_LENGTH).also { SecureRandom().nextBytes(it) }
|
||||
return Array(6) { i ->
|
||||
String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun validateDirAccess(context: Context, dirUri: Uri): Boolean {
|
||||
val hasWritePermission = context.contentResolver.persistedUriPermissions.any {
|
||||
it.isWritePermission && it.uri == dirUri
|
||||
}
|
||||
if (!hasWritePermission) return false
|
||||
|
||||
val document = DocumentFile.fromTreeUri(context, dirUri)
|
||||
if (document == null || !document.exists()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getBackupDirUri(context: Context): Uri? {
|
||||
val dirUriString = TextSecurePreferences.getBackupSaveDir(context) ?: return null
|
||||
return Uri.parse(dirUriString)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setBackupDirUri(context: Context, uriString: String?) {
|
||||
TextSecurePreferences.setBackupSaveDir(context, uriString)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The selected backup directory if it's valid (exists, is writable).
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getSelectedBackupDirIfValid(context: Context): Uri? {
|
||||
val dirUri = getBackupDirUri(context)
|
||||
|
||||
if (dirUri == null) {
|
||||
Log.v(TAG, "The backup dir wasn't selected yet.")
|
||||
return null
|
||||
}
|
||||
if (!validateDirAccess(context, dirUri)) {
|
||||
Log.v(TAG, "Cannot validate the access to the dir $dirUri.")
|
||||
return null
|
||||
}
|
||||
|
||||
return dirUri;
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
fun createBackupFile(context: Context): BackupFileRecord {
|
||||
val backupPassword = BackupPassphrase.get(context)
|
||||
?: throw IOException("Backup password is null")
|
||||
|
||||
val dirUri = getSelectedBackupDirIfValid(context)
|
||||
?: throw IOException("Backup save directory is not selected or invalid")
|
||||
|
||||
val date = Date()
|
||||
val timestamp = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(date)
|
||||
val fileName = String.format("session-%s.backup", timestamp)
|
||||
|
||||
val fileUri = DocumentsContract.createDocument(
|
||||
context.contentResolver,
|
||||
DocumentFile.fromTreeUri(context, dirUri)!!.uri,
|
||||
BACKUP_FILE_MIME_TYPE,
|
||||
fileName)
|
||||
|
||||
if (fileUri == null) {
|
||||
Toast.makeText(context, "Cannot create writable file in the dir $dirUri", Toast.LENGTH_LONG).show()
|
||||
throw IOException("Cannot create writable file in the dir $dirUri")
|
||||
}
|
||||
|
||||
try {
|
||||
FullBackupExporter.export(context,
|
||||
AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret,
|
||||
DatabaseComponent.get(context).openHelper().readableDatabase,
|
||||
fileUri,
|
||||
backupPassword)
|
||||
} catch (e: Exception) {
|
||||
// Delete the backup file on any error.
|
||||
DocumentsContract.deleteDocument(context.contentResolver, fileUri)
|
||||
throw e
|
||||
}
|
||||
|
||||
//TODO Use real file size.
|
||||
val record = DatabaseComponent.get(context).lokiBackupFilesDatabase()
|
||||
.insertBackupFile(BackupFileRecord(fileUri, -1, date))
|
||||
|
||||
Log.v(TAG, "A backup file was created: $fileUri")
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun deleteAllBackupFiles(context: Context, except: Collection<BackupFileRecord>? = null) {
|
||||
val db = DatabaseComponent.get(context).lokiBackupFilesDatabase()
|
||||
db.getBackupFiles().iterator().forEach { record ->
|
||||
if (except != null && except.contains(record)) return@forEach
|
||||
|
||||
// Try to delete the related file. The operation may fail in many cases
|
||||
// (the user moved/deleted the file, revoked the write permission, etc), so that's OK.
|
||||
try {
|
||||
val result = DocumentsContract.deleteDocument(context.contentResolver, record.uri)
|
||||
if (!result) {
|
||||
Log.w(TAG, "Failed to delete backup file: ${record.uri}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to delete backup file: ${record.uri}", e)
|
||||
}
|
||||
|
||||
db.deleteBackupFile(record)
|
||||
|
||||
Log.v(TAG, "Backup file was deleted: ${record.uri}")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun computeBackupKey(passphrase: String, salt: ByteArray?): ByteArray {
|
||||
return try {
|
||||
EventBus.getDefault().post(BackupEvent.createProgress(0))
|
||||
val digest = MessageDigest.getInstance("SHA-512")
|
||||
val input = passphrase.replace(" ", "").toByteArray()
|
||||
var hash: ByteArray = input
|
||||
if (salt != null) digest.update(salt)
|
||||
for (i in 0..249999) {
|
||||
if (i % 1000 == 0) EventBus.getDefault().post(BackupEvent.createProgress(0))
|
||||
digest.update(hash)
|
||||
hash = digest.digest(input)
|
||||
}
|
||||
ByteUtil.trim(hash, 32)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An utility class to help perform backup directory selection requests.
|
||||
*
|
||||
* An instance of this class should be created per an [Activity] or [Fragment]
|
||||
* and [onActivityResult] should be called appropriately.
|
||||
*/
|
||||
class BackupDirSelector(private val contextProvider: ContextProvider) {
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE_SAVE_DIR = 7844
|
||||
}
|
||||
|
||||
private val context: Context get() = contextProvider.getContext()
|
||||
|
||||
private var listener: Listener? = null
|
||||
|
||||
constructor(activity: Activity) :
|
||||
this(ActivityContextProvider(activity))
|
||||
|
||||
constructor(fragment: Fragment) :
|
||||
this(FragmentContextProvider(fragment))
|
||||
|
||||
/**
|
||||
* Performs ACTION_OPEN_DOCUMENT_TREE intent to select backup directory URI.
|
||||
* If the directory is already selected and valid, the request will be skipped.
|
||||
* @param force if true, the previous selection is ignored and the user is requested to select another directory.
|
||||
* @param onSelectedListener an optional action to perform once the directory is selected.
|
||||
*/
|
||||
fun selectBackupDir(force: Boolean, onSelectedListener: Listener? = null) {
|
||||
if (!force) {
|
||||
val dirUri = BackupUtil.getSelectedBackupDirIfValid(context)
|
||||
if (dirUri != null && onSelectedListener != null) {
|
||||
onSelectedListener.onBackupDirSelected(dirUri)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Let user pick the dir.
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
|
||||
// Request read/write permission grant for the dir.
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
|
||||
// Set the default dir.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val dirUri = BackupUtil.getBackupDirUri(context)
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, dirUri
|
||||
?: Uri.fromFile(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)))
|
||||
}
|
||||
|
||||
if (onSelectedListener != null) {
|
||||
this.listener = onSelectedListener
|
||||
}
|
||||
|
||||
contextProvider.startActivityForResult(intent, REQUEST_CODE_SAVE_DIR)
|
||||
}
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode != REQUEST_CODE_SAVE_DIR) return
|
||||
|
||||
if (resultCode == Activity.RESULT_OK && data != null && data.data != null) {
|
||||
// Acquire persistent access permissions for the file selected.
|
||||
val persistentFlags: Int = data.flags and
|
||||
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
context.contentResolver.takePersistableUriPermission(data.data!!, persistentFlags)
|
||||
|
||||
BackupUtil.setBackupDirUri(context, data.dataString)
|
||||
|
||||
listener?.onBackupDirSelected(data.data!!)
|
||||
}
|
||||
|
||||
listener = null
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface Listener {
|
||||
fun onBackupDirSelected(uri: Uri)
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.util.Size
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DimenRes
|
||||
@ -15,6 +16,7 @@ import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun View.contains(point: PointF): Boolean {
|
||||
return hitRect.contains(point.x.toInt(), point.y.toInt())
|
||||
@ -68,8 +70,23 @@ fun View.hideKeyboard() {
|
||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
}
|
||||
|
||||
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap =
|
||||
Bitmap.createBitmap(width, height, config).applyCanvas {
|
||||
|
||||
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888, longestWidth: Int = 2000): Bitmap {
|
||||
val size = Size(measuredWidth, measuredHeight).coerceAtMost(longestWidth)
|
||||
val scale = size.width / measuredWidth.toFloat()
|
||||
|
||||
return Bitmap.createBitmap(size.width, size.height, config).applyCanvas {
|
||||
scale(scale, scale)
|
||||
translate(-scrollX.toFloat(), -scrollY.toFloat())
|
||||
draw(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun Size.coerceAtMost(longestWidth: Int): Size =
|
||||
(width.toFloat() / height).let { aspect ->
|
||||
if (aspect > 1) {
|
||||
width.coerceAtMost(longestWidth).let { Size(it, (it / aspect).roundToInt()) }
|
||||
} else {
|
||||
height.coerceAtMost(longestWidth).let { Size((it * aspect).roundToInt(), it) }
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
||||
peerConnectionObservers.remove(listener)
|
||||
}
|
||||
|
||||
fun shutDownAudioManager() {
|
||||
signalAudioManager.shutdown()
|
||||
}
|
||||
|
||||
private val _audioEvents = MutableStateFlow(AudioEnabled(false))
|
||||
val audioEvents = _audioEvents.asSharedFlow()
|
||||
private val _videoEvents = MutableStateFlow(VideoEnabled(false))
|
||||
|
@ -15,7 +15,7 @@ class SignalAudioHandler(looper: Looper) : Handler(looper) {
|
||||
}
|
||||
}
|
||||
|
||||
fun isOnHandler(): Boolean {
|
||||
private fun isOnHandler(): Boolean {
|
||||
return Looper.myLooper() == looper
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import android.media.SoundPool
|
||||
import android.os.HandlerThread
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalBluetoothManager.State as BState
|
||||
|
||||
@ -32,10 +33,10 @@ class SignalAudioManager(private val context: Context,
|
||||
private val eventListener: EventListener?,
|
||||
private val androidAudioManager: AudioManagerCompat) {
|
||||
|
||||
private var commandAndControlThread: HandlerThread? = HandlerThread("call-audio").apply { start() }
|
||||
private var handler: SignalAudioHandler? = null
|
||||
private var commandAndControlThread: HandlerThread? = HandlerThread("call-audio", ThreadUtils.PRIORITY_IMPORTANT_BACKGROUND_THREAD).apply { start() }
|
||||
private var handler: SignalAudioHandler = SignalAudioHandler(commandAndControlThread!!.looper)
|
||||
|
||||
private var signalBluetoothManager: SignalBluetoothManager? = null
|
||||
private var signalBluetoothManager: SignalBluetoothManager = SignalBluetoothManager(context, this, androidAudioManager, handler)
|
||||
|
||||
private var state: State = State.UNINITIALIZED
|
||||
|
||||
@ -62,12 +63,9 @@ class SignalAudioManager(private val context: Context,
|
||||
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
|
||||
|
||||
fun handleCommand(command: AudioManagerCommand) {
|
||||
if (command == AudioManagerCommand.Initialize) {
|
||||
initialize()
|
||||
return
|
||||
}
|
||||
handler?.post {
|
||||
handler.post {
|
||||
when (command) {
|
||||
is AudioManagerCommand.Initialize -> initialize()
|
||||
is AudioManagerCommand.UpdateAudioDeviceState -> updateAudioDeviceState()
|
||||
is AudioManagerCommand.Start -> start()
|
||||
is AudioManagerCommand.Stop -> stop(command.playDisconnect)
|
||||
@ -84,13 +82,6 @@ class SignalAudioManager(private val context: Context,
|
||||
Log.i(TAG, "Initializing audio manager state: $state")
|
||||
|
||||
if (state == State.UNINITIALIZED) {
|
||||
commandAndControlThread = HandlerThread("call-audio").apply { start() }
|
||||
handler = SignalAudioHandler(commandAndControlThread!!.looper)
|
||||
|
||||
signalBluetoothManager = SignalBluetoothManager(context, this, androidAudioManager, handler!!)
|
||||
|
||||
handler!!.post {
|
||||
|
||||
savedAudioMode = androidAudioManager.mode
|
||||
savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn
|
||||
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
|
||||
@ -102,7 +93,7 @@ class SignalAudioManager(private val context: Context,
|
||||
|
||||
audioDevices.clear()
|
||||
|
||||
signalBluetoothManager!!.start()
|
||||
signalBluetoothManager.start()
|
||||
|
||||
updateAudioDeviceState()
|
||||
|
||||
@ -114,6 +105,16 @@ class SignalAudioManager(private val context: Context,
|
||||
Log.d(TAG, "Initialized")
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
handler.post {
|
||||
stop(false)
|
||||
if (commandAndControlThread != null) {
|
||||
Log.i(TAG, "Shutting down command and control")
|
||||
commandAndControlThread?.quitSafely()
|
||||
commandAndControlThread = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
@ -138,23 +139,11 @@ class SignalAudioManager(private val context: Context,
|
||||
|
||||
private fun stop(playDisconnect: Boolean) {
|
||||
Log.d(TAG, "Stopping. state: $state")
|
||||
if (state == State.UNINITIALIZED) {
|
||||
Log.i(TAG, "Trying to stop AudioManager in incorrect state: $state")
|
||||
return
|
||||
}
|
||||
|
||||
handler?.post {
|
||||
incomingRinger.stop()
|
||||
outgoingRinger.stop()
|
||||
stop(false)
|
||||
if (commandAndControlThread != null) {
|
||||
Log.i(TAG, "Shutting down command and control")
|
||||
commandAndControlThread?.quitSafely()
|
||||
commandAndControlThread = null
|
||||
}
|
||||
}
|
||||
|
||||
if (playDisconnect) {
|
||||
if (playDisconnect && state != State.UNINITIALIZED) {
|
||||
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
|
||||
soundPool.play(disconnectedSoundId, volume, volume, 0, 0, 1.0f)
|
||||
}
|
||||
@ -170,7 +159,7 @@ class SignalAudioManager(private val context: Context,
|
||||
}
|
||||
wiredHeadsetReceiver = null
|
||||
|
||||
signalBluetoothManager?.stop()
|
||||
signalBluetoothManager.stop()
|
||||
|
||||
setSpeakerphoneOn(savedIsSpeakerPhoneOn)
|
||||
setMicrophoneMute(savedIsMicrophoneMute)
|
||||
@ -183,25 +172,25 @@ class SignalAudioManager(private val context: Context,
|
||||
}
|
||||
|
||||
private fun updateAudioDeviceState() {
|
||||
handler!!.assertHandlerThread()
|
||||
handler.assertHandlerThread()
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"updateAudioDeviceState(): " +
|
||||
"wired: $hasWiredHeadset " +
|
||||
"bt: ${signalBluetoothManager!!.state} " +
|
||||
"bt: ${signalBluetoothManager.state} " +
|
||||
"available: $audioDevices " +
|
||||
"selected: $selectedAudioDevice " +
|
||||
"userSelected: $userSelectedAudioDevice"
|
||||
)
|
||||
|
||||
if (signalBluetoothManager!!.state.shouldUpdate()) {
|
||||
signalBluetoothManager!!.updateDevice()
|
||||
if (signalBluetoothManager.state.shouldUpdate()) {
|
||||
signalBluetoothManager.updateDevice()
|
||||
}
|
||||
|
||||
val newAudioDevices = mutableSetOf(AudioDevice.SPEAKER_PHONE)
|
||||
|
||||
if (signalBluetoothManager!!.state.hasDevice()) {
|
||||
if (signalBluetoothManager.state.hasDevice()) {
|
||||
newAudioDevices += AudioDevice.BLUETOOTH
|
||||
}
|
||||
|
||||
@ -217,7 +206,7 @@ class SignalAudioManager(private val context: Context,
|
||||
var audioDeviceSetUpdated = audioDevices != newAudioDevices
|
||||
audioDevices = newAudioDevices
|
||||
|
||||
if (signalBluetoothManager!!.state == BState.UNAVAILABLE && userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
|
||||
if (signalBluetoothManager.state == BState.UNAVAILABLE && userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
|
||||
userSelectedAudioDevice = AudioDevice.NONE
|
||||
}
|
||||
|
||||
@ -230,7 +219,7 @@ class SignalAudioManager(private val context: Context,
|
||||
userSelectedAudioDevice = AudioDevice.NONE
|
||||
}
|
||||
|
||||
val btState = signalBluetoothManager!!.state
|
||||
val btState = signalBluetoothManager.state
|
||||
val needBluetoothAudioStart = btState == BState.AVAILABLE &&
|
||||
(userSelectedAudioDevice == AudioDevice.NONE || userSelectedAudioDevice == AudioDevice.BLUETOOTH || autoSwitchToBluetooth)
|
||||
|
||||
@ -238,27 +227,27 @@ class SignalAudioManager(private val context: Context,
|
||||
(userSelectedAudioDevice != AudioDevice.NONE && userSelectedAudioDevice != AudioDevice.BLUETOOTH)
|
||||
|
||||
if (btState.hasDevice()) {
|
||||
Log.i(TAG, "Need bluetooth audio: state: ${signalBluetoothManager!!.state} start: $needBluetoothAudioStart stop: $needBluetoothAudioStop")
|
||||
Log.i(TAG, "Need bluetooth audio: state: ${signalBluetoothManager.state} start: $needBluetoothAudioStart stop: $needBluetoothAudioStop")
|
||||
}
|
||||
|
||||
if (needBluetoothAudioStop) {
|
||||
signalBluetoothManager!!.stopScoAudio()
|
||||
signalBluetoothManager!!.updateDevice()
|
||||
signalBluetoothManager.stopScoAudio()
|
||||
signalBluetoothManager.updateDevice()
|
||||
}
|
||||
|
||||
if (!autoSwitchToBluetooth && signalBluetoothManager!!.state == BState.UNAVAILABLE) {
|
||||
if (!autoSwitchToBluetooth && signalBluetoothManager.state == BState.UNAVAILABLE) {
|
||||
autoSwitchToBluetooth = true
|
||||
}
|
||||
|
||||
if (needBluetoothAudioStart && !needBluetoothAudioStop) {
|
||||
if (!signalBluetoothManager!!.startScoAudio()) {
|
||||
if (!needBluetoothAudioStop && needBluetoothAudioStart) {
|
||||
if (!signalBluetoothManager.startScoAudio()) {
|
||||
Log.e(TAG,"Failed to start sco audio")
|
||||
audioDevices.remove(AudioDevice.BLUETOOTH)
|
||||
audioDeviceSetUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (autoSwitchToBluetooth && signalBluetoothManager!!.state == BState.CONNECTED) {
|
||||
if (autoSwitchToBluetooth && signalBluetoothManager.state == BState.CONNECTED) {
|
||||
userSelectedAudioDevice = AudioDevice.BLUETOOTH
|
||||
autoSwitchToBluetooth = false
|
||||
}
|
||||
@ -373,7 +362,7 @@ class SignalAudioManager(private val context: Context,
|
||||
val pluggedIn = intent.getIntExtra("state", 0) == 1
|
||||
val hasMic = intent.getIntExtra("microphone", 0) == 1
|
||||
|
||||
handler?.post { onWiredHeadsetChange(pluggedIn, hasMic) }
|
||||
handler.post { onWiredHeadsetChange(pluggedIn, hasMic) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="?attr/dialog_background_color" />
|
||||
|
||||
<corners android:radius="?dialogCornerRadius" />
|
||||
|
||||
</shape>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/default_dialog_background"
|
||||
android:inset="@dimen/medium_spacing">
|
||||
</inset>
|
18
app/src/main/res/drawable/ic_pictures.xml
Normal file
18
app/src/main/res/drawable/ic_pictures.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="44dp"
|
||||
android:height="35dp"
|
||||
android:viewportWidth="44"
|
||||
android:viewportHeight="35">
|
||||
<path
|
||||
android:pathData="M35.45,23.4L27.129,17.313C27.074,17.269 27.007,17.245 26.937,17.245C26.867,17.245 26.799,17.269 26.744,17.313L19.283,23.49C19.226,23.532 19.156,23.555 19.084,23.555C19.013,23.555 18.943,23.532 18.885,23.49L15.135,20.433C15.082,20.393 15.017,20.371 14.949,20.371C14.882,20.371 14.817,20.393 14.763,20.433L4.606,27.65C4.567,27.681 4.535,27.72 4.513,27.764C4.49,27.808 4.478,27.857 4.477,27.907V30.244C4.481,30.437 4.559,30.621 4.695,30.758C4.832,30.895 5.016,30.973 5.209,30.976H34.847C35.041,30.976 35.227,30.899 35.364,30.762C35.501,30.624 35.579,30.438 35.579,30.244V23.644C35.58,23.595 35.569,23.548 35.546,23.505C35.524,23.462 35.491,23.426 35.45,23.4Z"
|
||||
android:fillColor="#A1A2A1"/>
|
||||
<path
|
||||
android:pathData="M11.63,18.25C13.226,18.25 14.519,16.957 14.519,15.361C14.519,13.765 13.226,12.472 11.63,12.472C10.034,12.472 8.741,13.765 8.741,15.361C8.741,16.957 10.034,18.25 11.63,18.25Z"
|
||||
android:fillColor="#A1A2A1"/>
|
||||
<path
|
||||
android:pathData="M40.895,4.125L8.728,0.375C8.306,0.324 7.879,0.356 7.47,0.471C7.062,0.587 6.68,0.782 6.348,1.046C6.016,1.31 5.739,1.638 5.535,2.01C5.331,2.382 5.202,2.791 5.158,3.213L5.004,4.6H7.572L7.7,3.509C7.711,3.425 7.738,3.345 7.78,3.272C7.822,3.199 7.878,3.136 7.944,3.085C8.054,2.997 8.189,2.947 8.33,2.944H8.394L22.686,4.6H36.221C37.011,4.605 37.789,4.791 38.495,5.145C39.201,5.499 39.815,6.012 40.291,6.642H40.599C40.768,6.661 40.922,6.746 41.028,6.879C41.133,7.011 41.183,7.18 41.165,7.348L41.075,8.08C41.254,8.597 41.349,9.139 41.357,9.685V27.997L43.72,7.682C43.814,6.836 43.57,5.988 43.04,5.321C42.511,4.655 41.74,4.225 40.895,4.125Z"
|
||||
android:fillColor="#A1A2A1"/>
|
||||
<path
|
||||
android:pathData="M36.221,34.803H3.835C2.984,34.803 2.167,34.464 1.565,33.862C0.963,33.26 0.625,32.444 0.625,31.592V9.762C0.625,8.911 0.963,8.094 1.565,7.492C2.167,6.89 2.984,6.552 3.835,6.552H36.221C37.072,6.552 37.889,6.89 38.491,7.492C39.093,8.094 39.431,8.911 39.431,9.762V31.592C39.431,32.444 39.093,33.26 38.491,33.862C37.889,34.464 37.072,34.803 36.221,34.803ZM3.835,9.095C3.665,9.095 3.502,9.162 3.381,9.283C3.261,9.403 3.193,9.566 3.193,9.737V31.567C3.193,31.737 3.261,31.9 3.381,32.021C3.502,32.141 3.665,32.209 3.835,32.209H36.221C36.391,32.209 36.554,32.141 36.675,32.021C36.795,31.9 36.863,31.737 36.863,31.567V9.737C36.863,9.566 36.795,9.403 36.675,9.283C36.554,9.162 36.391,9.095 36.221,9.095H3.835Z"
|
||||
android:fillColor="#A1A2A1"/>
|
||||
</vector>
|
@ -5,5 +5,5 @@
|
||||
|
||||
<solid android:color="@color/profile_picture_background" />
|
||||
|
||||
<corners android:radius="38dp" />
|
||||
<corners android:radius="40dp" />
|
||||
</shape>
|
@ -2,9 +2,49 @@
|
||||
<FrameLayout
|
||||
android:orientation="vertical"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_gravity="center"
|
||||
android:id="@+id/ic_pictures"
|
||||
android:background="@drawable/circle_tintable"
|
||||
android:backgroundTint="@color/classic_dark_3"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_width="@dimen/large_profile_picture_size"
|
||||
android:layout_height="@dimen/large_profile_picture_size">
|
||||
|
||||
<ImageView
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/ic_pictures"/>
|
||||
|
||||
<!-- TODO: Add this back when we build the custom modal which allows tapping on the image to select a replacement-->
|
||||
<!-- <LinearLayout-->
|
||||
<!-- android:layout_gravity="bottom|end"-->
|
||||
<!-- android:gravity="center"-->
|
||||
<!-- android:background="@drawable/circle_tintable"-->
|
||||
<!-- android:backgroundTint="?attr/accentColor"-->
|
||||
<!-- android:paddingTop="1dp"-->
|
||||
<!-- android:paddingLeft="1dp"-->
|
||||
<!-- android:layout_width="24dp"-->
|
||||
<!-- android:layout_height="24dp"-->
|
||||
<!-- tools:backgroundTint="@color/accent_green">-->
|
||||
<!-- <View-->
|
||||
<!-- android:background="@drawable/ic_plus"-->
|
||||
<!-- android:backgroundTint="@color/black"-->
|
||||
<!-- android:layout_width="12dp"-->
|
||||
<!-- android:layout_height="12dp"-->
|
||||
<!-- />-->
|
||||
<!-- </LinearLayout>-->
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:layout_margin="30dp"
|
||||
android:id="@+id/profile_picture_view"
|
||||
@ -12,6 +52,7 @@
|
||||
android:layout_width="@dimen/large_profile_picture_size"
|
||||
android:layout_height="@dimen/large_profile_picture_size"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:contentDescription="@string/AccessibilityId_profile_picture" />
|
||||
android:contentDescription="@string/AccessibilityId_profile_picture"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
@ -92,7 +92,8 @@
|
||||
<item quantity="one">Ez végelegesen törölni fogja a kiválasztott üzenetet.</item>
|
||||
<item quantity="other">Ez véglegesen törölni fogja mind a(z) %1$d db kiválasztott üzenetet.</item>
|
||||
</plurals>
|
||||
<string name="ConversationFragment_ban_selected_user">Tiltja ezt a felhasználót?</string>
|
||||
<string name="ConversationActivity_call_title">Hívás engedély szükséges</string>
|
||||
<string name="ConversationFragment_ban_selected_user">Tiltod ezt a felhasználót?</string>
|
||||
<string name="ConversationFragment_save_to_sd_card">Mentés tárolóra?</string>
|
||||
<plurals name="ConversationFragment_saving_n_media_to_storage_warning">
|
||||
<item quantity="one">A média mentése a tárolóra lehetővé teszi bármelyik másik alkalmazásnak a készülékeden, hogy hozzáférjen.\n\nFolytatod?</item>
|
||||
@ -206,7 +207,7 @@
|
||||
<string name="ThreadRecord_called_you">Hívott téged</string>
|
||||
<string name="ThreadRecord_missed_call">Nem fogadott hívás</string>
|
||||
<string name="ThreadRecord_media_message">Média üzenet</string>
|
||||
<string name="ThreadRecord_s_is_on_signal">%s a Session-on van!</string>
|
||||
<string name="ThreadRecord_s_is_on_signal">%s elérhető a Session-on!</string>
|
||||
<string name="ThreadRecord_disappearing_messages_disabled">Eltűnő üzenetek letiltva</string>
|
||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Eltűnő üzenet ideje beállítva erre: %s</string>
|
||||
<string name="ThreadRecord_s_took_a_screenshot">%s készített egy képernyőképet.</string>
|
||||
@ -236,7 +237,7 @@
|
||||
<string name="MediaPreviewActivity_you">Te</string>
|
||||
<string name="MediaPreviewActivity_unssuported_media_type">Nem támogatott médiatípus</string>
|
||||
<string name="MediaPreviewActivity_draft">Piszkozat</string>
|
||||
<string name="MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">A Session- nek szüksége van fájlhozzáférési engedélyekhez, hogy menteni tudjon külső tárhelyre, de ezt az engedélyt megtagadták. Kérjük, hogy a készüléke beállításaiban engedélyezze a \"fájlok és média\" opciót a Session számára.</string>
|
||||
<string name="MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">A Session- nek szüksége van fájlhozzáférési engedélyekhez, hogy menteni tudjon külső tárhelyre. Kérlek, hogy a rendszerbeállításokban engedélyezd a \"fájlok és média\" opciót a Session számára.</string>
|
||||
<string name="MediaPreviewActivity_unable_to_write_to_external_storage_without_permission">Nem lehet engedély nélkül menteni a külső tárolóra</string>
|
||||
<string name="MediaPreviewActivity_media_delete_confirmation_title">Törlöd az üzenetet?</string>
|
||||
<string name="MediaPreviewActivity_media_delete_confirmation_message">Ez véglegesen törölni fogja ezt az üzenetet.</string>
|
||||
@ -250,8 +251,8 @@
|
||||
<string name="MessageNotifier_mark_all_as_read">Összes megjelölése olvasottként</string>
|
||||
<string name="MessageNotifier_mark_read">Olvasottnak jelöl</string>
|
||||
<string name="MessageNotifier_reply">Válasz</string>
|
||||
<string name="MessageNotifier_pending_signal_messages">Függő Session üzenetek</string>
|
||||
<string name="MessageNotifier_you_have_pending_signal_messages">Függő Session üzeneteid vannak, koppints a megnyitáshoz és letöltéshez</string>
|
||||
<string name="MessageNotifier_pending_signal_messages">Függőben lévő Session üzenetek</string>
|
||||
<string name="MessageNotifier_you_have_pending_signal_messages">Függőben lévő Session üzeneteid vannak, koppints a megnyitáshoz és letöltéshez</string>
|
||||
<string name="MessageNotifier_contact_message">%1$s %2$s</string>
|
||||
<string name="MessageNotifier_unknown_contact_message">Névjegy</string>
|
||||
<!-- Notification Channels -->
|
||||
@ -275,7 +276,7 @@
|
||||
<!-- ShortcutLauncherActivity -->
|
||||
<string name="ShortcutLauncherActivity_invalid_shortcut">Érvénytelen parancsikon</string>
|
||||
<!-- SingleRecipientNotificationBuilder -->
|
||||
<string name="SingleRecipientNotificationBuilder_signal">Ülés</string>
|
||||
<string name="SingleRecipientNotificationBuilder_signal">Session</string>
|
||||
<string name="SingleRecipientNotificationBuilder_new_message">Új üzenet</string>
|
||||
<!-- TransferControlView -->
|
||||
<plurals name="TransferControlView_n_items">
|
||||
@ -530,17 +531,17 @@
|
||||
<string name="share">Megosztás</string>
|
||||
<string name="invalid_session_id">Érvénytelen Session azonosító</string>
|
||||
<string name="cancel">Mégse</string>
|
||||
<string name="your_session_id">Az ön Session azonosítója</string>
|
||||
<string name="activity_landing_title_2">Az ön Session-ja itt kezdődik...</string>
|
||||
<string name="your_session_id">A session azonosítód</string>
|
||||
<string name="activity_landing_title_2">A Session itt kezdődik...</string>
|
||||
<string name="activity_landing_register_button_title">Session azonosító létrehozása</string>
|
||||
<string name="activity_landing_restore_button_title">Folytassa az ülését</string>
|
||||
<string name="activity_landing_restore_button_title">Session azonosító helyreállítása</string>
|
||||
<string name="view_fake_chat_bubble_1">Mi az a Session?</string>
|
||||
<string name="view_fake_chat_bubble_2">Ez egy decentralizált, titkosított üzenetküldő alkalmazás</string>
|
||||
<string name="view_fake_chat_bubble_3">Tehát nem gyűjti a személyes adataimat vagy a beszélgetés metaadatait? Hogyan működik?</string>
|
||||
<string name="view_fake_chat_bubble_4">Fejlett névtelen útválasztási és end-to-end titkosítási technológiák kombinációjának használata.</string>
|
||||
<string name="view_fake_chat_bubble_5">A barátok nem engedik, hogy a barátok kompromittált hírnököket használjanak. Szívesen.</string>
|
||||
<string name="view_fake_chat_bubble_4">Fejlett anonim útválasztási és végponttól-végpontig titkosítási technológiák használatával.</string>
|
||||
<string name="view_fake_chat_bubble_5">A barátként nem engedhetem, hogy megbízhatatlan üzenetküldő appokat használj. Szívesen.</string>
|
||||
<string name="activity_register_title">Ismerd meg a Session ID-d</string>
|
||||
<string name="activity_register_explanation">Az üles azonosító az az egyedi cím, amelyet az emberek használhatnak, hogy kapcsolatba lépjenek Önnel az Ülés során. Mivel nincs kapcsolat a valódi személyazonosságával, az Ülés azonosító teljesen névtelen, és privát.</string>
|
||||
<string name="activity_register_explanation">A Session azonosító az az egyedi cím, amelyet az emberek használhatnak, hogy kapcsolatba lépjenek Önnel a Sessionon. Mivel nincs kapcsolat a valódi személyazonosságával, az Session azonosító teljesen névtelen, és privát.</string>
|
||||
<string name="activity_restore_title">Fiók visszaállítása</string>
|
||||
<string name="activity_restore_explanation">Írja be azt a helyreállítási kifejezést, amelyet a fiók visszaállításához regisztrálásakor kapott.</string>
|
||||
<string name="activity_restore_seed_edit_text_hint">Írja be a helyreállítási kifejezést</string>
|
||||
@ -552,7 +553,7 @@
|
||||
<string name="activity_pn_mode_recommended_option_tag">Ajánlott</string>
|
||||
<string name="activity_pn_mode_no_option_picked_dialog_title">Kérjük, válasszon egy lehetőséget</string>
|
||||
<string name="activity_home_empty_state_message">Még nincsenek névjegyei</string>
|
||||
<string name="activity_home_empty_state_button_title">Indítson el egy ülést</string>
|
||||
<string name="activity_home_empty_state_button_title">Indítson el egy beszélgetést</string>
|
||||
<string name="activity_home_leave_group_dialog_message">Biztosan elhagyja ezt a csoportot?</string>
|
||||
<string name="activity_home_leaving_group_failed_message">"Nem sikerült kilépni a csoportból"</string>
|
||||
<string name="activity_home_delete_conversation_dialog_message">Biztosan törli ezt a beszélgetést?</string>
|
||||
@ -573,8 +574,8 @@
|
||||
<string name="activity_path_destination_row_title">Célállomás</string>
|
||||
<string name="activity_path_learn_more_button_title">Tudj meg többet</string>
|
||||
<string name="activity_path_resolving_progress">Feloldás...</string>
|
||||
<string name="activity_create_private_chat_title">Új Ülés</string>
|
||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Adja meg az Ülés azonosítóját</string>
|
||||
<string name="activity_create_private_chat_title">Új Session</string>
|
||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Adja meg a Session azonosítóját</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_tab_title">QR kód beolvasása</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_explanation">A beszélgetés elindításához olvassa be a felhasználó QR kódját. A QR kód a fiókbeállításokban található a QR kód ikonra koppintva.</string>
|
||||
<string name="fragment_enter_public_key_edit_text_hint">Írja be Session azonosítóját vagy ONS nevét</string>
|
||||
|
@ -92,7 +92,8 @@
|
||||
<item quantity="one">Ez végelegesen törölni fogja a kiválasztott üzenetet.</item>
|
||||
<item quantity="other">Ez véglegesen törölni fogja mind a(z) %1$d db kiválasztott üzenetet.</item>
|
||||
</plurals>
|
||||
<string name="ConversationFragment_ban_selected_user">Tiltja ezt a felhasználót?</string>
|
||||
<string name="ConversationActivity_call_title">Hívás engedély szükséges</string>
|
||||
<string name="ConversationFragment_ban_selected_user">Tiltod ezt a felhasználót?</string>
|
||||
<string name="ConversationFragment_save_to_sd_card">Mentés tárolóra?</string>
|
||||
<plurals name="ConversationFragment_saving_n_media_to_storage_warning">
|
||||
<item quantity="one">A média mentése a tárolóra lehetővé teszi bármelyik másik alkalmazásnak a készülékeden, hogy hozzáférjen.\n\nFolytatod?</item>
|
||||
@ -206,7 +207,7 @@
|
||||
<string name="ThreadRecord_called_you">Hívott téged</string>
|
||||
<string name="ThreadRecord_missed_call">Nem fogadott hívás</string>
|
||||
<string name="ThreadRecord_media_message">Média üzenet</string>
|
||||
<string name="ThreadRecord_s_is_on_signal">%s a Session-on van!</string>
|
||||
<string name="ThreadRecord_s_is_on_signal">%s elérhető a Session-on!</string>
|
||||
<string name="ThreadRecord_disappearing_messages_disabled">Eltűnő üzenetek letiltva</string>
|
||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Eltűnő üzenet ideje beállítva erre: %s</string>
|
||||
<string name="ThreadRecord_s_took_a_screenshot">%s készített egy képernyőképet.</string>
|
||||
@ -236,7 +237,7 @@
|
||||
<string name="MediaPreviewActivity_you">Te</string>
|
||||
<string name="MediaPreviewActivity_unssuported_media_type">Nem támogatott médiatípus</string>
|
||||
<string name="MediaPreviewActivity_draft">Piszkozat</string>
|
||||
<string name="MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">A Session- nek szüksége van fájlhozzáférési engedélyekhez, hogy menteni tudjon külső tárhelyre, de ezt az engedélyt megtagadták. Kérjük, hogy a készüléke beállításaiban engedélyezze a \"fájlok és média\" opciót a Session számára.</string>
|
||||
<string name="MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">A Session- nek szüksége van fájlhozzáférési engedélyekhez, hogy menteni tudjon külső tárhelyre. Kérlek, hogy a rendszerbeállításokban engedélyezd a \"fájlok és média\" opciót a Session számára.</string>
|
||||
<string name="MediaPreviewActivity_unable_to_write_to_external_storage_without_permission">Nem lehet engedély nélkül menteni a külső tárolóra</string>
|
||||
<string name="MediaPreviewActivity_media_delete_confirmation_title">Törlöd az üzenetet?</string>
|
||||
<string name="MediaPreviewActivity_media_delete_confirmation_message">Ez véglegesen törölni fogja ezt az üzenetet.</string>
|
||||
@ -250,8 +251,8 @@
|
||||
<string name="MessageNotifier_mark_all_as_read">Összes megjelölése olvasottként</string>
|
||||
<string name="MessageNotifier_mark_read">Olvasottnak jelöl</string>
|
||||
<string name="MessageNotifier_reply">Válasz</string>
|
||||
<string name="MessageNotifier_pending_signal_messages">Függő Session üzenetek</string>
|
||||
<string name="MessageNotifier_you_have_pending_signal_messages">Függő Session üzeneteid vannak, koppints a megnyitáshoz és letöltéshez</string>
|
||||
<string name="MessageNotifier_pending_signal_messages">Függőben lévő Session üzenetek</string>
|
||||
<string name="MessageNotifier_you_have_pending_signal_messages">Függőben lévő Session üzeneteid vannak, koppints a megnyitáshoz és letöltéshez</string>
|
||||
<string name="MessageNotifier_contact_message">%1$s %2$s</string>
|
||||
<string name="MessageNotifier_unknown_contact_message">Névjegy</string>
|
||||
<!-- Notification Channels -->
|
||||
@ -275,7 +276,7 @@
|
||||
<!-- ShortcutLauncherActivity -->
|
||||
<string name="ShortcutLauncherActivity_invalid_shortcut">Érvénytelen parancsikon</string>
|
||||
<!-- SingleRecipientNotificationBuilder -->
|
||||
<string name="SingleRecipientNotificationBuilder_signal">Ülés</string>
|
||||
<string name="SingleRecipientNotificationBuilder_signal">Session</string>
|
||||
<string name="SingleRecipientNotificationBuilder_new_message">Új üzenet</string>
|
||||
<!-- TransferControlView -->
|
||||
<plurals name="TransferControlView_n_items">
|
||||
@ -530,17 +531,17 @@
|
||||
<string name="share">Megosztás</string>
|
||||
<string name="invalid_session_id">Érvénytelen Session azonosító</string>
|
||||
<string name="cancel">Mégse</string>
|
||||
<string name="your_session_id">Az ön Session azonosítója</string>
|
||||
<string name="activity_landing_title_2">Az ön Session-ja itt kezdődik...</string>
|
||||
<string name="your_session_id">A session azonosítód</string>
|
||||
<string name="activity_landing_title_2">A Session itt kezdődik...</string>
|
||||
<string name="activity_landing_register_button_title">Session azonosító létrehozása</string>
|
||||
<string name="activity_landing_restore_button_title">Folytassa az ülését</string>
|
||||
<string name="activity_landing_restore_button_title">Session azonosító helyreállítása</string>
|
||||
<string name="view_fake_chat_bubble_1">Mi az a Session?</string>
|
||||
<string name="view_fake_chat_bubble_2">Ez egy decentralizált, titkosított üzenetküldő alkalmazás</string>
|
||||
<string name="view_fake_chat_bubble_3">Tehát nem gyűjti a személyes adataimat vagy a beszélgetés metaadatait? Hogyan működik?</string>
|
||||
<string name="view_fake_chat_bubble_4">Fejlett névtelen útválasztási és end-to-end titkosítási technológiák kombinációjának használata.</string>
|
||||
<string name="view_fake_chat_bubble_5">A barátok nem engedik, hogy a barátok kompromittált hírnököket használjanak. Szívesen.</string>
|
||||
<string name="view_fake_chat_bubble_4">Fejlett anonim útválasztási és végponttól-végpontig titkosítási technológiák használatával.</string>
|
||||
<string name="view_fake_chat_bubble_5">A barátként nem engedhetem, hogy megbízhatatlan üzenetküldő appokat használj. Szívesen.</string>
|
||||
<string name="activity_register_title">Ismerd meg a Session ID-d</string>
|
||||
<string name="activity_register_explanation">Az üles azonosító az az egyedi cím, amelyet az emberek használhatnak, hogy kapcsolatba lépjenek Önnel az Ülés során. Mivel nincs kapcsolat a valódi személyazonosságával, az Ülés azonosító teljesen névtelen, és privát.</string>
|
||||
<string name="activity_register_explanation">A Session azonosító az az egyedi cím, amelyet az emberek használhatnak, hogy kapcsolatba lépjenek Önnel a Sessionon. Mivel nincs kapcsolat a valódi személyazonosságával, az Session azonosító teljesen névtelen, és privát.</string>
|
||||
<string name="activity_restore_title">Fiók visszaállítása</string>
|
||||
<string name="activity_restore_explanation">Írja be azt a helyreállítási kifejezést, amelyet a fiók visszaállításához regisztrálásakor kapott.</string>
|
||||
<string name="activity_restore_seed_edit_text_hint">Írja be a helyreállítási kifejezést</string>
|
||||
@ -552,7 +553,7 @@
|
||||
<string name="activity_pn_mode_recommended_option_tag">Ajánlott</string>
|
||||
<string name="activity_pn_mode_no_option_picked_dialog_title">Kérjük, válasszon egy lehetőséget</string>
|
||||
<string name="activity_home_empty_state_message">Még nincsenek névjegyei</string>
|
||||
<string name="activity_home_empty_state_button_title">Indítson el egy ülést</string>
|
||||
<string name="activity_home_empty_state_button_title">Indítson el egy beszélgetést</string>
|
||||
<string name="activity_home_leave_group_dialog_message">Biztosan elhagyja ezt a csoportot?</string>
|
||||
<string name="activity_home_leaving_group_failed_message">"Nem sikerült kilépni a csoportból"</string>
|
||||
<string name="activity_home_delete_conversation_dialog_message">Biztosan törli ezt a beszélgetést?</string>
|
||||
@ -573,8 +574,8 @@
|
||||
<string name="activity_path_destination_row_title">Célállomás</string>
|
||||
<string name="activity_path_learn_more_button_title">Tudj meg többet</string>
|
||||
<string name="activity_path_resolving_progress">Feloldás...</string>
|
||||
<string name="activity_create_private_chat_title">Új Ülés</string>
|
||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Adja meg az Ülés azonosítóját</string>
|
||||
<string name="activity_create_private_chat_title">Új Session</string>
|
||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Adja meg a Session azonosítóját</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_tab_title">QR kód beolvasása</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_explanation">A beszélgetés elindításához olvassa be a felhasználó QR kódját. A QR kód a fiókbeállításokban található a QR kód ikonra koppintva.</string>
|
||||
<string name="fragment_enter_public_key_edit_text_hint">Írja be Session azonosítóját vagy ONS nevét</string>
|
||||
|
@ -28,6 +28,7 @@
|
||||
<attr name="ic_visibility_on" format="reference" />
|
||||
<attr name="ic_visibility_off" format="reference" />
|
||||
|
||||
<attr name="accentColor" format="reference|color"/>
|
||||
<attr name="prominentButtonColor" format="reference|color"/>
|
||||
<attr name="elementBorderColor" format="reference|color"/>
|
||||
<attr name="conversation_background" format="reference|color"/>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<dimen name="very_small_profile_picture_size">26dp</dimen>
|
||||
<dimen name="small_profile_picture_size">36dp</dimen>
|
||||
<dimen name="medium_profile_picture_size">46dp</dimen>
|
||||
<dimen name="large_profile_picture_size">76dp</dimen>
|
||||
<dimen name="large_profile_picture_size">80dp</dimen>
|
||||
<dimen name="conversation_view_status_indicator_size">14dp</dimen>
|
||||
<dimen name="border_thickness">1dp</dimen>
|
||||
<dimen name="new_conversation_button_collapsed_size">60dp</dimen>
|
||||
|
@ -23,14 +23,13 @@
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.Session.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
|
||||
<item name="android:background">@drawable/default_dialog_background</item>
|
||||
<item name="android:colorBackground">?attr/dialog_background_color</item>
|
||||
<item name="dialog_background_color">?colorPrimary</item>
|
||||
<item name="android:colorBackgroundFloating">?colorPrimary</item>
|
||||
<item name="backgroundTint">?colorPrimary</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:backgroundDimAmount">0.6</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowBackground">@null</item>
|
||||
<item name="textColorAlertDialogListItem">?android:textColorPrimary</item>
|
||||
</style>
|
||||
|
||||
|
@ -54,6 +54,7 @@
|
||||
<item name="menu_unpin_icon">@drawable/ic_outline_pin_off_24</item>
|
||||
<item name="menu_mark_all_as_read">@drawable/ic_outline_mark_chat_read_24</item>
|
||||
<item name="emoji_show_less_icon">@drawable/ic_chevron_up_light</item>
|
||||
<item name="accentColor">?colorAccent</item>
|
||||
<item name="prominentButtonColor">?colorAccent</item>
|
||||
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>
|
||||
<item name="attachment_document_icon_large">@drawable/ic_document_large_dark</item>
|
||||
@ -246,12 +247,8 @@
|
||||
<item name="windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.TextSecure.Dialog.Rationale" parent="Theme.AppCompat.DayNight.Dialog.Alert">
|
||||
<item name="android:windowBackground">@drawable/default_dialog_background</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.TextSecure.Dialog.MediaSendProgress" parent="@android:style/Theme.Dialog">
|
||||
<item name="android:background">@drawable/default_dialog_background</item>
|
||||
<item name="android:colorBackground">?attr/dialog_background_color</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
@ -303,7 +300,6 @@
|
||||
<item name="dividerHorizontal">?dividerVertical</item>
|
||||
<item name="message_received_background_color">#F2F2F2</item>
|
||||
<item name="colorAccent">@color/classic_accent</item>
|
||||
<item name="colorControlHighlight">?android:colorControlHighlight</item>
|
||||
<item name="tabStyle">@style/Widget.Session.TabLayout</item>
|
||||
</style>
|
||||
|
||||
@ -312,7 +308,6 @@
|
||||
<item name="dividerHorizontal">?dividerVertical</item>
|
||||
<item name="message_received_background_color">#F2F2F2</item>
|
||||
<item name="colorAccent">@color/ocean_accent</item>
|
||||
<item name="colorControlHighlight">?android:colorControlHighlight</item>
|
||||
<item name="tabStyle">@style/Widget.Session.TabLayout</item>
|
||||
</style>
|
||||
|
||||
@ -338,10 +333,12 @@
|
||||
<item name="colorSettingsBackground">@color/classic_dark_1</item>
|
||||
<item name="colorDividerBackground">@color/classic_dark_3</item>
|
||||
<item name="android:colorControlHighlight">@color/classic_dark_3</item>
|
||||
<item name="colorControlHighlight">@color/classic_dark_3</item>
|
||||
<item name="actionBarPopupTheme">@style/Dark.Popup</item>
|
||||
<item name="actionBarWidgetTheme">@null</item>
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
|
||||
<item name="accentColor">?colorAccent</item>
|
||||
<item name="prominentButtonColor">?colorAccent</item>
|
||||
<item name="elementBorderColor">@color/classic_dark_3</item>
|
||||
|
||||
@ -410,6 +407,7 @@
|
||||
<item name="colorSettingsBackground">@color/classic_light_5</item>
|
||||
<item name="colorDividerBackground">@color/classic_light_3</item>
|
||||
<item name="android:colorControlHighlight">@color/classic_light_3</item>
|
||||
<item name="colorControlHighlight">@color/classic_light_3</item>
|
||||
<item name="bottomSheetDialogTheme">@style/Classic.Light.BottomSheet</item>
|
||||
<item name="android:actionMenuTextColor">?android:textColorPrimary</item>
|
||||
<item name="popupTheme">?actionBarPopupTheme</item>
|
||||
@ -417,6 +415,7 @@
|
||||
<item name="actionBarWidgetTheme">@null</item>
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.ActionBar</item>
|
||||
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
|
||||
<item name="accentColor">?colorAccent</item>
|
||||
<item name="prominentButtonColor">?android:textColorPrimary</item>
|
||||
<item name="elementBorderColor">@color/classic_light_3</item>
|
||||
|
||||
@ -496,6 +495,7 @@
|
||||
<item name="colorSettingsBackground">@color/ocean_dark_1</item>
|
||||
<item name="colorDividerBackground">@color/ocean_dark_4</item>
|
||||
<item name="android:colorControlHighlight">@color/ocean_dark_4</item>
|
||||
<item name="colorControlHighlight">@color/ocean_dark_4</item>
|
||||
<item name="bottomSheetDialogTheme">@style/Ocean.Dark.BottomSheet</item>
|
||||
<item name="popupTheme">?actionBarPopupTheme</item>
|
||||
<item name="actionMenuTextColor">?android:textColorPrimary</item>
|
||||
@ -503,6 +503,7 @@
|
||||
<item name="actionBarWidgetTheme">@null</item>
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
|
||||
<item name="accentColor">?colorAccent</item>
|
||||
<item name="prominentButtonColor">?colorAccent</item>
|
||||
<item name="elementBorderColor">@color/ocean_dark_4</item>
|
||||
|
||||
@ -576,6 +577,7 @@
|
||||
<item name="colorSettingsBackground">@color/ocean_light_6</item>
|
||||
<item name="colorDividerBackground">@color/ocean_light_3</item>
|
||||
<item name="android:colorControlHighlight">@color/ocean_light_4</item>
|
||||
<item name="colorControlHighlight">@color/ocean_light_4</item>
|
||||
<item name="bottomSheetDialogTheme">@style/Ocean.Light.BottomSheet</item>
|
||||
<item name="actionBarPopupTheme">@style/Light.Popup</item>
|
||||
<item name="popupTheme">?actionBarPopupTheme</item>
|
||||
@ -583,6 +585,7 @@
|
||||
<item name="actionBarWidgetTheme">@null</item>
|
||||
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.ActionBar</item>
|
||||
<item name="actionBarStyle">@style/Widget.Session.ActionBar</item>
|
||||
<item name="accentColor">?colorAccent</item>
|
||||
<item name="prominentButtonColor">?android:textColorPrimary</item>
|
||||
<item name="elementBorderColor">@color/ocean_light_3</item>
|
||||
|
||||
|
@ -1,360 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.session.libsession.messaging.utilities.Data;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class FastJobStorageTest {
|
||||
|
||||
private static final JsonDataSerializer serializer = new JsonDataSerializer();
|
||||
private static final String EMPTY_DATA = serializer.serialize(Data.EMPTY);
|
||||
|
||||
@Test
|
||||
public void init_allStoredDataAvailable() {
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS));
|
||||
|
||||
subject.init();
|
||||
|
||||
DataSet1.assertJobsMatch(subject.getAllJobSpecs());
|
||||
DataSet1.assertConstraintsMatch(subject.getAllConstraintSpecs());
|
||||
DataSet1.assertDependenciesMatch(subject.getAllDependencySpecs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void insertJobs_writesToDatabase() {
|
||||
JobDatabase database = noopDatabase();
|
||||
FastJobStorage subject = new FastJobStorage(database);
|
||||
|
||||
subject.insertJobs(DataSet1.FULL_SPECS);
|
||||
|
||||
verify(database).insertJobs(DataSet1.FULL_SPECS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void insertJobs_dataCanBeFound() {
|
||||
FastJobStorage subject = new FastJobStorage(noopDatabase());
|
||||
|
||||
subject.insertJobs(DataSet1.FULL_SPECS);
|
||||
|
||||
DataSet1.assertJobsMatch(subject.getAllJobSpecs());
|
||||
DataSet1.assertConstraintsMatch(subject.getAllConstraintSpecs());
|
||||
DataSet1.assertDependenciesMatch(subject.getAllDependencySpecs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void insertJobs_individualJobCanBeFound() {
|
||||
FastJobStorage subject = new FastJobStorage(noopDatabase());
|
||||
|
||||
subject.insertJobs(DataSet1.FULL_SPECS);
|
||||
|
||||
assertEquals(DataSet1.JOB_1, subject.getJobSpec(DataSet1.JOB_1.getId()));
|
||||
assertEquals(DataSet1.JOB_2, subject.getJobSpec(DataSet1.JOB_2.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateAllJobsToBePending_writesToDatabase() {
|
||||
JobDatabase database = noopDatabase();
|
||||
FastJobStorage subject = new FastJobStorage(database);
|
||||
|
||||
subject.updateAllJobsToBePending();
|
||||
|
||||
verify(database).updateAllJobsToBePending();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateAllJobsToBePending_allArePending() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
|
||||
subject.init();
|
||||
subject.updateAllJobsToBePending();
|
||||
|
||||
assertFalse(subject.getJobSpec("1").isRunning());
|
||||
assertFalse(subject.getJobSpec("2").isRunning());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateJobRunningState_writesToDatabase() {
|
||||
JobDatabase database = noopDatabase();
|
||||
FastJobStorage subject = new FastJobStorage(database);
|
||||
|
||||
subject.updateJobRunningState("1", true);
|
||||
|
||||
verify(database).updateJobRunningState("1", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateJobRunningState_stateUpdated() {
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS));
|
||||
subject.init();
|
||||
|
||||
subject.updateJobRunningState(DataSet1.JOB_1.getId(), true);
|
||||
assertTrue(subject.getJobSpec(DataSet1.JOB_1.getId()).isRunning());
|
||||
|
||||
subject.updateJobRunningState(DataSet1.JOB_1.getId(), false);
|
||||
assertFalse(subject.getJobSpec(DataSet1.JOB_1.getId()).isRunning());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateJobAfterRetry_writesToDatabase() {
|
||||
JobDatabase database = noopDatabase();
|
||||
FastJobStorage subject = new FastJobStorage(database);
|
||||
|
||||
subject.updateJobAfterRetry("1", true, 1, 10);
|
||||
|
||||
verify(database).updateJobAfterRetry("1", true, 1, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateJobAfterRetry_stateUpdated() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 3, 30000, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
|
||||
subject.init();
|
||||
subject.updateJobAfterRetry("1", false, 1, 10);
|
||||
|
||||
JobSpec job = subject.getJobSpec("1");
|
||||
|
||||
assertNotNull(job);
|
||||
assertFalse(job.isRunning());
|
||||
assertEquals(1, job.getRunAttempt());
|
||||
assertEquals(10, job.getNextRunAttemptTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenEarlierItemInQueueInRunning() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(1).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenAllJobsAreRunning() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
subject.init();
|
||||
|
||||
assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenNextRunTimeIsAfterCurrentTime() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 10, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
subject.init();
|
||||
|
||||
assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(0).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenDependentOnAnotherJob() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.singletonList(new DependencySpec("2", "1")));
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
assertEquals(0, subject.getPendingJobsWithNoDependenciesInCreatedOrder(0).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_singleEligibleJob() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
subject.init();
|
||||
|
||||
assertEquals(1, subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_multipleEligibleJobs() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
assertEquals(2, subject.getPendingJobsWithNoDependenciesInCreatedOrder(10).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_singleEligibleJobInMixedList() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
List<JobSpec> jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10);
|
||||
|
||||
assertEquals(1, jobs.size());
|
||||
assertEquals("2", jobs.get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_firstItemInQueue() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", RetrieveProfileAvatarJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", RetrieveProfileAvatarJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
List<JobSpec> jobs = subject.getPendingJobsWithNoDependenciesInCreatedOrder(10);
|
||||
|
||||
assertEquals(1, jobs.size());
|
||||
assertEquals("1", jobs.get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteJobs_writesToDatabase() {
|
||||
JobDatabase database = noopDatabase();
|
||||
FastJobStorage subject = new FastJobStorage(database);
|
||||
List<String> ids = Arrays.asList("1", "2");
|
||||
|
||||
subject.deleteJobs(ids);
|
||||
|
||||
verify(database).deleteJobs(ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteJobs_deletesAllRelevantPieces() {
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(DataSet1.FULL_SPECS));
|
||||
|
||||
subject.init();
|
||||
subject.deleteJobs(Collections.singletonList("id1"));
|
||||
|
||||
List<JobSpec> jobs = subject.getAllJobSpecs();
|
||||
List<ConstraintSpec> constraints = subject.getAllConstraintSpecs();
|
||||
List<DependencySpec> dependencies = subject.getAllDependencySpecs();
|
||||
|
||||
assertEquals(1, jobs.size());
|
||||
assertEquals(DataSet1.JOB_2, jobs.get(0));
|
||||
assertEquals(1, constraints.size());
|
||||
assertEquals(DataSet1.CONSTRAINT_2, constraints.get(0));
|
||||
assertEquals(0, dependencies.size());
|
||||
}
|
||||
|
||||
|
||||
private JobDatabase noopDatabase() {
|
||||
JobDatabase database = mock(JobDatabase.class);
|
||||
|
||||
when(database.getAllJobSpecs()).thenReturn(Collections.emptyList());
|
||||
when(database.getAllConstraintSpecs()).thenReturn(Collections.emptyList());
|
||||
when(database.getAllDependencySpecs()).thenReturn(Collections.emptyList());
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
private JobDatabase fixedDataDatabase(List<FullSpec> fullSpecs) {
|
||||
JobDatabase database = mock(JobDatabase.class);
|
||||
|
||||
when(database.getAllJobSpecs()).thenReturn(Stream.of(fullSpecs).map(FullSpec::getJobSpec).toList());
|
||||
when(database.getAllConstraintSpecs()).thenReturn(Stream.of(fullSpecs).map(FullSpec::getConstraintSpecs).flatMap(Stream::of).toList());
|
||||
when(database.getAllDependencySpecs()).thenReturn(Stream.of(fullSpecs).map(FullSpec::getDependencySpecs).flatMap(Stream::of).toList());
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
private static final class DataSet1 {
|
||||
static final JobSpec JOB_1 = new JobSpec("id1", "f1", "q1", 1, 2, 3, 4, 5, 6, 7, EMPTY_DATA, false);
|
||||
static final JobSpec JOB_2 = new JobSpec("id2", "f2", "q2", 1, 2, 3, 4, 5, 6, 7, EMPTY_DATA, false);
|
||||
static final ConstraintSpec CONSTRAINT_1 = new ConstraintSpec("id1", "f1");
|
||||
static final ConstraintSpec CONSTRAINT_2 = new ConstraintSpec("id2", "f2");
|
||||
static final DependencySpec DEPENDENCY_2 = new DependencySpec("id2", "id1");
|
||||
static final FullSpec FULL_SPEC_1 = new FullSpec(JOB_1, Collections.singletonList(CONSTRAINT_1), Collections.emptyList());
|
||||
static final FullSpec FULL_SPEC_2 = new FullSpec(JOB_2, Collections.singletonList(CONSTRAINT_2), Collections.singletonList(DEPENDENCY_2));
|
||||
static final List<FullSpec> FULL_SPECS = Arrays.asList(FULL_SPEC_1, FULL_SPEC_2);
|
||||
|
||||
static void assertJobsMatch(@NonNull List<JobSpec> jobs) {
|
||||
assertEquals(jobs.size(), 2);
|
||||
assertTrue(jobs.contains(DataSet1.JOB_1));
|
||||
assertTrue(jobs.contains(DataSet1.JOB_1));
|
||||
}
|
||||
|
||||
static void assertConstraintsMatch(@NonNull List<ConstraintSpec> constraints) {
|
||||
assertEquals(constraints.size(), 2);
|
||||
assertTrue(constraints.contains(DataSet1.CONSTRAINT_1));
|
||||
assertTrue(constraints.contains(DataSet1.CONSTRAINT_2));
|
||||
}
|
||||
|
||||
static void assertDependenciesMatch(@NonNull List<DependencySpec> dependencies) {
|
||||
assertEquals(dependencies.size(), 1);
|
||||
assertTrue(dependencies.contains(DataSet1.DEPENDENCY_2));
|
||||
}
|
||||
}
|
||||
}
|
@ -40,6 +40,7 @@ interface StorageProtocol {
|
||||
fun getUserPublicKey(): String?
|
||||
fun getUserX25519KeyPair(): ECKeyPair
|
||||
fun getUserProfile(): Profile
|
||||
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
|
||||
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
|
||||
fun clearUserPic()
|
||||
// Signal
|
||||
|
@ -125,6 +125,7 @@ class JobQueue : JobDelegate {
|
||||
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob, is ConfigurationSyncJob -> {
|
||||
txQueue.send(job)
|
||||
}
|
||||
is RetrieveProfileAvatarJob,
|
||||
is AttachmentDownloadJob -> {
|
||||
mediaQueue.send(job)
|
||||
}
|
||||
@ -224,6 +225,7 @@ class JobQueue : JobDelegate {
|
||||
GroupAvatarDownloadJob.KEY,
|
||||
BackgroundGroupAddJob.KEY,
|
||||
OpenGroupDeleteJob.KEY,
|
||||
RetrieveProfileAvatarJob.KEY,
|
||||
ConfigurationSyncJob.KEY,
|
||||
)
|
||||
allJobTypes.forEach { type ->
|
||||
|
@ -0,0 +1,113 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import android.text.TextUtils
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.utilities.DownloadUtilities.downloadFile
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId
|
||||
import org.session.libsession.utilities.Util.copy
|
||||
import org.session.libsession.utilities.Util.equals
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.streams.ProfileCipherInputStream
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.security.SecureRandom
|
||||
|
||||
class RetrieveProfileAvatarJob(private val profileAvatar: String?, private val recipientAddress: Address): Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
override val maxFailureCount: Int = 0
|
||||
|
||||
companion object {
|
||||
val TAG = RetrieveProfileAvatarJob::class.simpleName
|
||||
val KEY: String = "RetrieveProfileAvatarJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private const val PROFILE_AVATAR_KEY = "profileAvatar"
|
||||
private const val RECEIPIENT_ADDRESS_KEY = "recipient"
|
||||
}
|
||||
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val recipient = Recipient.from(context, recipientAddress, true)
|
||||
val profileKey = recipient.resolve().profileKey
|
||||
|
||||
if (profileKey == null || (profileKey.size != 32 && profileKey.size != 16)) {
|
||||
Log.w(TAG, "Recipient profile key is gone!")
|
||||
return
|
||||
}
|
||||
|
||||
// Commit '78d1e9d' (fix: open group threads and avatar downloads) had this commented out so
|
||||
// it's now limited to just the current user case
|
||||
if (
|
||||
recipient.isLocalNumber &&
|
||||
AvatarHelper.avatarFileExists(context, recipient.resolve().address) &&
|
||||
equals(profileAvatar, recipient.resolve().profileAvatar)
|
||||
) {
|
||||
Log.w(TAG, "Already retrieved profile avatar: $profileAvatar")
|
||||
return
|
||||
}
|
||||
|
||||
if (profileAvatar.isNullOrEmpty()) {
|
||||
Log.w(TAG, "Removing profile avatar for: " + recipient.address.serialize())
|
||||
|
||||
if (recipient.isLocalNumber) {
|
||||
setProfileAvatarId(context, SecureRandom().nextInt())
|
||||
setProfilePictureURL(context, null)
|
||||
}
|
||||
|
||||
AvatarHelper.delete(context, recipient.address)
|
||||
storage.setProfileAvatar(recipient, null)
|
||||
return
|
||||
}
|
||||
|
||||
val downloadDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
|
||||
|
||||
try {
|
||||
downloadFile(downloadDestination, profileAvatar)
|
||||
val avatarStream: InputStream = ProfileCipherInputStream(FileInputStream(downloadDestination), profileKey)
|
||||
val decryptDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
|
||||
copy(avatarStream, FileOutputStream(decryptDestination))
|
||||
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.address))
|
||||
|
||||
if (recipient.isLocalNumber) {
|
||||
setProfileAvatarId(context, SecureRandom().nextInt())
|
||||
setProfilePictureURL(context, profileAvatar)
|
||||
}
|
||||
|
||||
storage.setProfileAvatar(recipient, profileAvatar)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Failed to download profile avatar", e)
|
||||
} finally {
|
||||
downloadDestination.delete()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putString(PROFILE_AVATAR_KEY, profileAvatar)
|
||||
.putString(RECEIPIENT_ADDRESS_KEY, recipientAddress.serialize())
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<RetrieveProfileAvatarJob> {
|
||||
override fun create(data: Data): RetrieveProfileAvatarJob {
|
||||
val profileAvatar = if (data.hasString(PROFILE_AVATAR_KEY)) { data.getString(PROFILE_AVATAR_KEY) } else { null }
|
||||
val recipientAddress = Address.fromSerialized(data.getString(RECEIPIENT_ADDRESS_KEY))
|
||||
return RetrieveProfileAvatarJob(profileAvatar, recipientAddress)
|
||||
}
|
||||
}
|
||||
}
|
@ -52,20 +52,10 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), SnodeAPI.nowWithOffset)
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, 0)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val ourPubKey = storage.getUserPublicKey()
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
try {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member), member == ourPubKey).get()
|
||||
} catch (e: Exception) {
|
||||
deferred.reject(e)
|
||||
return@queue
|
||||
}
|
||||
}
|
||||
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
@ -73,6 +63,30 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTime)
|
||||
// Create the thread
|
||||
storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
|
||||
|
||||
val ourPubKey = storage.getUserPublicKey()
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
try {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member), member == ourPubKey).get()
|
||||
} catch (e: Exception) {
|
||||
// We failed to properly create the group so delete it's associated data (in the past
|
||||
// we didn't create this data until the messages successfully sent but this resulted
|
||||
// in race conditions due to the `NEW` message sent to our own swarm)
|
||||
storage.removeClosedGroupPublicKey(groupPublicKey)
|
||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
storage.deleteConversation(threadID)
|
||||
deferred.reject(e)
|
||||
return@queue
|
||||
}
|
||||
}
|
||||
|
||||
// Add the group to the config now that it was successfully created
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
|
@ -6,6 +6,7 @@ import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
|
@ -79,7 +79,12 @@ class ClosedGroupPollerV2 {
|
||||
// reasonable fake time interval to use instead.
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val threadID = storage.getThreadId(groupID) ?: return
|
||||
val threadID = storage.getThreadId(groupID)
|
||||
if (threadID == null) {
|
||||
Log.d("Loki", "Stopping group poller due to missing thread for closed group: $groupPublicKey.")
|
||||
stopPolling(groupPublicKey)
|
||||
return
|
||||
}
|
||||
val lastUpdated = storage.getLastUpdated(threadID)
|
||||
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
|
||||
val minPollInterval = Companion.minPollInterval
|
||||
|
@ -1,13 +1,12 @@
|
||||
package org.session.libsignal.utilities
|
||||
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import android.os.Process
|
||||
import java.util.concurrent.*
|
||||
|
||||
object ThreadUtils {
|
||||
|
||||
const val PRIORITY_IMPORTANT_BACKGROUND_THREAD = Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE
|
||||
|
||||
val executorPool: ExecutorService = Executors.newCachedThreadPool()
|
||||
|
||||
@JvmStatic
|
||||
|
Loading…
Reference in New Issue
Block a user