Merge pull request #1164 from RyanRory/bluetooth-manager-crash

Fix crash & ANRs
This commit is contained in:
Morgan Pretty 2023-06-06 09:00:49 +10:00 committed by GitHub
commit 7f7ab63d15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 349 additions and 12061 deletions

View File

@ -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" />

View File

@ -55,7 +55,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;
@ -65,11 +64,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;
@ -82,7 +77,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;
@ -112,7 +106,6 @@ import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig;
/**
* Will be called once when the TextSecure process is created.
@ -132,7 +125,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;
@ -147,7 +139,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject LokiAPIDatabase lokiAPIDatabase;
@Inject Storage storage;
@Inject MessageDataProvider messageDataProvider;
@Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@ -166,10 +157,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);
}
@ -229,7 +216,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
initializeProfileManager();
initializePeriodicTasks();
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
initializeJobManager();
initializeWebRtc();
initializeBlobProvider();
resubmitProfilePictureIfNeeded();
@ -286,10 +272,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
LocaleParser.Companion.configure(new LocaleParseHelper());
}
public JobManager getJobManager() {
return jobManager;
}
public ExpiringMessageManager getExpiringMessageManager() {
return expiringMessageManager;
}
@ -352,16 +334,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);
}
@ -384,10 +356,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() {

View File

@ -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)
}
}

View File

@ -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);
}
}
}

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}
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")
}

View File

@ -576,7 +576,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() {
@ -972,7 +972,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()
@ -1026,7 +1026,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()
}
@ -1367,7 +1367,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

View File

@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import com.goterl.lazysodium.utils.KeyPair
import dagger.assisted.Assisted
@ -22,6 +24,7 @@ import org.session.libsignal.utilities.Log
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(
@ -78,17 +81,27 @@ 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)
// TODO: Remove in UserConfig branch
GlobalScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
}
}
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)
// TODO: Remove in UserConfig branch
GlobalScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
}
}

View File

@ -1,20 +1,25 @@
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.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()))
@ -37,5 +42,10 @@ class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
private fun unblock() {
DatabaseComponent.get(requireContext()).recipientDatabase().setBlocked(recipient, false)
dismiss()
// TODO: Remove in UserConfig branch
GlobalScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
}
}

View File

@ -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)));
}
}

View File

@ -60,6 +60,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
@ -1405,11 +1406,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),

View File

@ -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)

View File

@ -42,13 +42,12 @@ import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.KeyHelper
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
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
@ -70,13 +69,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return Profile(displayName, profileKey, profilePictureUrl)
}
override fun setUserProfilePictureURL(newValue: String) {
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
Recipient.from(context, it, false)
}
TextSecurePreferences.setProfilePictureURL(context, newValue)
RetrieveProfileAvatarJob(ourRecipient, newValue)
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newValue))
override fun setProfileAvatar(recipient: Recipient, profileAvatar: String?) {
val database = DatabaseComponent.get(context).recipientDatabase()
database.setProfileAvatar(recipient, profileAvatar)
}
override fun getOrGenerateRegistrationID(): Int {
@ -211,7 +206,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
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? {
@ -713,8 +708,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
recipientDatabase.setApproved(recipient, true)
threadDatabase.setHasSent(threadId, true)
}
if (contact.isBlocked == true) {
recipientDatabase.setBlocked(recipient, true)
val contactIsBlocked: Boolean? = contact.isBlocked
if (contactIsBlocked != null && recipient.isBlocked != contactIsBlocked) {
recipientDatabase.setBlocked(recipient, contactIsBlocked)
threadDatabase.deleteConversation(threadId)
}
}
@ -743,6 +740,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return mmsSmsDb.getConversationCount(threadID)
}
override fun deleteConversation(threadId: Long) {
val threadDB = DatabaseComponent.get(context).threadDatabase()
threadDB.deleteConversation(threadId)
}
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {

View File

@ -24,7 +24,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;
@ -284,9 +283,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());

View File

@ -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

View File

@ -86,10 +86,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)

View File

@ -495,6 +495,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setBlocked(thread.recipient, true)
// TODO: Remove in UserConfig branch
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@HomeActivity)
withContext(Dispatchers.Main) {
binding.recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss()
@ -511,6 +513,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
lifecycleScope.launch(Dispatchers.IO) {
recipientDatabase.setBlocked(thread.recipient, false)
// TODO: Remove in UserConfig branch
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@HomeActivity)
withContext(Dispatchers.Main) {
binding.recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss()

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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 {
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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));
}
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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)
}

View File

@ -1,144 +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, profileAvatar);
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()));
} finally {
if (downloadDestination != null) downloadDestination.delete();
}
if (recipient.isLocalNumber()) {
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
}
database.setProfileAvatar(recipient, profileAvatar);
}
@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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -27,7 +27,7 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity() {
AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.continue_2) { _, _ -> viewModel.unblock() }
.setPositiveButton(R.string.continue_2) { _, _ -> viewModel.unblock(this@BlockedContactsActivity) }
.setNegativeButton(R.string.cancel) { _, _ -> }
.show()
}

View File

@ -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
@ -60,9 +63,13 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage)
return _state
}
fun unblock() {
fun unblock(context: Context) {
storage.unblock(state.selectedItems)
_state.value = state.copy(selectedItems = emptySet())
// TODO: Remove in UserConfig branch
GlobalScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
}
fun select(selectedItem: Recipient, isSelected: Boolean) {

View File

@ -26,8 +26,10 @@ import nl.komponents.kovenant.all
import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper
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
@ -100,10 +102,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) {
@ -273,7 +277,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
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -2,11 +2,11 @@ package org.thoughtcrime.securesms.sskenvironment
import android.content.Context
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
class ProfileManager : SSKEnvironment.ProfileManagerProtocol {
@ -40,9 +40,8 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol {
}
override fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) {
val job = RetrieveProfileAvatarJob(recipient, profilePictureURL)
val jobManager = ApplicationContext.getInstance(context).jobManager
jobManager.add(job)
val job = RetrieveProfileAvatarJob(profilePictureURL, recipient.address)
JobQueue.shared.add(job)
val sessionID = recipient.address.serialize()
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()
var contact = contactDatabase.getContactWithSessionID(sessionID)

View File

@ -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)
}
}

View File

@ -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))

View File

@ -15,7 +15,7 @@ class SignalAudioHandler(looper: Looper) : Handler(looper) {
}
}
fun isOnHandler(): Boolean {
private fun isOnHandler(): Boolean {
return Looper.myLooper() == looper
}
}

View File

@ -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,34 +82,37 @@ 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)
savedAudioMode = androidAudioManager.mode
savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
hasWiredHeadset = androidAudioManager.isWiredHeadsetOn
signalBluetoothManager = SignalBluetoothManager(context, this, androidAudioManager, handler!!)
androidAudioManager.requestCallAudioFocus()
handler!!.post {
setMicrophoneMute(false)
savedAudioMode = androidAudioManager.mode
savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
hasWiredHeadset = androidAudioManager.isWiredHeadsetOn
audioDevices.clear()
androidAudioManager.requestCallAudioFocus()
signalBluetoothManager.start()
setMicrophoneMute(false)
updateAudioDeviceState()
audioDevices.clear()
wiredHeadsetReceiver = WiredHeadsetReceiver()
context.registerReceiver(wiredHeadsetReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG))
signalBluetoothManager!!.start()
state = State.PREINITIALIZED
updateAudioDeviceState()
Log.d(TAG, "Initialized")
}
}
wiredHeadsetReceiver = WiredHeadsetReceiver()
context.registerReceiver(wiredHeadsetReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG))
state = State.PREINITIALIZED
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
}
}
}
@ -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
}
}
incomingRinger.stop()
outgoingRinger.stop()
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) }
}
}

View 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>

View File

@ -5,5 +5,5 @@
<solid android:color="@color/profile_picture_background" />
<corners android:radius="38dp" />
<corners android:radius="40dp" />
</shape>

View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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>
@ -337,6 +338,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/classic_dark_3</item>
@ -413,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>
@ -500,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>
@ -581,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>

View File

@ -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));
}
}
}

View File

@ -37,7 +37,7 @@ interface StorageProtocol {
fun getUserPublicKey(): String?
fun getUserX25519KeyPair(): ECKeyPair
fun getUserProfile(): Profile
fun setUserProfilePictureURL(newProfilePicture: String)
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
// Signal
fun getOrGenerateRegistrationID(): Int
@ -159,6 +159,7 @@ interface StorageProtocol {
fun trimThread(threadID: Long, threadLimit: Int)
fun trimThreadBefore(threadID: Long, timestamp: Long)
fun getMessageCount(threadID: Long): Long
fun deleteConversation(threadId: Long)
// Contacts
fun getContactWithSessionID(sessionID: String): Contact?

View File

@ -125,6 +125,7 @@ class JobQueue : JobDelegate {
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> {
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,
)
allJobTypes.forEach { type ->
resumePendingJobs(type)

View File

@ -0,0 +1,105 @@
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 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
}
if (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))
} finally {
downloadDestination.delete()
}
if (recipient.isLocalNumber) {
setProfileAvatarId(context, SecureRandom().nextInt())
setProfilePictureURL(context, profileAvatar)
}
storage.setProfileAvatar(recipient, profileAvatar)
}
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)
}
}
}

View File

@ -52,19 +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
for (member in members) {
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
try {
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).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 +64,24 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
// Notify the user
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
for (member in members) {
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
try {
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).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
}
}
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Start polling

View File

@ -5,6 +5,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
@ -183,7 +184,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
profileManager.setProfileKey(context, recipient, message.profileKey)
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
storage.setUserProfilePictureURL(message.profilePicture!!)
JobQueue.shared.add(RetrieveProfileAvatarJob(message.profilePicture!!, recipient.address))
}
}
storage.addContacts(message.contacts)

View File

@ -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

View File

@ -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