From ab6ab5c625e24752a7b4d03f53919c364f769012 Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Thu, 24 Sep 2020 14:48:51 +1000 Subject: [PATCH 01/18] Use WorkManager for background poll task. --- AndroidManifest.xml | 9 +- build.gradle | 5 + .../securesms/ApplicationContext.java | 4 +- .../securesms/jobmanager/BootReceiver.java | 17 --- .../securesms/jobmanager/Job.java | 4 + .../migration/WorkManagerFactoryMappings.java | 2 - .../thoughtcrime/securesms/jobs/BaseJob.java | 5 + .../securesms/jobs/JobManagerFactories.java | 2 - .../securesms/loki/api/BackgroundPollJob.kt | 84 ------------- .../loki/api/BackgroundPollListener.kt | 36 ------ .../loki/api/BackgroundPollWorker.kt | 113 ++++++++++++++++++ .../securesms/loki/api/ClosedGroupPoller.kt | 2 +- .../securesms/loki/api/PublicChatPoller.kt | 4 +- 13 files changed, 134 insertions(+), 153 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/BootReceiver.java delete mode 100644 src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt delete mode 100644 src/org/thoughtcrime/securesms/loki/api/BackgroundPollListener.kt create mode 100644 src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 13dbb534ab..40c5a414b5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -682,7 +682,7 @@ @@ -700,13 +700,6 @@ - - - - - WorkManager + * API instead. */ public abstract class Job { diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java index 1449ace743..5f82cae0c6 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java @@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.jobs.SmsSentJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.jobs.UpdateApkJob; -import org.thoughtcrime.securesms.loki.api.BackgroundPollJob; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; @@ -59,7 +58,6 @@ public class WorkManagerFactoryMappings { put(AttachmentDownloadJob.class.getName(), AttachmentDownloadJob.KEY); put(AttachmentUploadJob.class.getName(), AttachmentUploadJob.KEY); put(AvatarDownloadJob.class.getName(), AvatarDownloadJob.KEY); - put(BackgroundPollJob.class.getName(), BackgroundPollJob.KEY); put(CleanPreKeysJob.class.getName(), CleanPreKeysJob.KEY); put(ClosedGroupUpdateMessageSendJob.class.getName(), ClosedGroupUpdateMessageSendJob.KEY); put(CreateSignedPreKeyJob.class.getName(), CreateSignedPreKeyJob.KEY); diff --git a/src/org/thoughtcrime/securesms/jobs/BaseJob.java b/src/org/thoughtcrime/securesms/jobs/BaseJob.java index 73053b0479..7e8852cd55 100644 --- a/src/org/thoughtcrime/securesms/jobs/BaseJob.java +++ b/src/org/thoughtcrime/securesms/jobs/BaseJob.java @@ -6,6 +6,11 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobLogger; import org.thoughtcrime.securesms.logging.Log; +/** + * @deprecated + * use WorkManager + * API instead. + */ public abstract class BaseJob extends Job { private static final String TAG = BaseJob.class.getSimpleName(); diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 6f3c34e310..111f7968e7 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -14,7 +14,6 @@ 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 org.thoughtcrime.securesms.loki.api.BackgroundPollJob; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; @@ -32,7 +31,6 @@ public final class JobManagerFactories { put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory()); - put(BackgroundPollJob.KEY, new BackgroundPollJob.Factory()); put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); put(ClosedGroupUpdateMessageSendJob.KEY, new ClosedGroupUpdateMessageSendJob.Factory()); put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory()); diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt deleted file mode 100644 index 1cf97b3c64..0000000000 --- a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt +++ /dev/null @@ -1,84 +0,0 @@ -package org.thoughtcrime.securesms.loki.api - -import android.content.Context -import kotlinx.coroutines.awaitAll -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.all -import nl.komponents.kovenant.functional.map -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.dependencies.InjectableType -import org.thoughtcrime.securesms.jobmanager.Data -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint -import org.thoughtcrime.securesms.jobs.BaseJob -import org.thoughtcrime.securesms.jobs.PushContentReceiveJob -import org.thoughtcrime.securesms.jobs.RotateCertificateJob -import org.thoughtcrime.securesms.logging.Log -import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.signalservice.api.SignalServiceAccountManager -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException -import org.whispersystems.signalservice.loki.api.SnodeAPI -import java.io.IOException -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -class BackgroundPollJob private constructor(parameters: Parameters) : BaseJob(parameters) { - - companion object { - const val KEY = "BackgroundPollJob" - } - - constructor(context: Context) : this(Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build()) { - setContext(context) - } - - override fun serialize(): Data { - return Data.EMPTY - } - - override fun getFactoryKey(): String { return KEY } - - public override fun onRun() { - try { - Log.d("Loki", "Performing background poll.") - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val promises = mutableListOf>() - val promise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes -> - envelopes.forEach { - PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) - } - } - promises.add(promise) - promises.addAll(ClosedGroupPoller.shared.pollOnce()) - val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value } - for (openGroup in openGroups) { - val poller = PublicChatPoller(context, openGroup) - poller.stop() - promises.add(poller.pollForNewMessages()) - } - all(promises).get() - } catch (exception: Exception) { - Log.d("Loki", "Background poll failed due to error: $exception.") - } - } - - public override fun onShouldRetry(e: Exception): Boolean { - return false - } - - override fun onCanceled() { } - - class Factory : Job.Factory { - - override fun create(parameters: Parameters, data: Data): BackgroundPollJob { - return BackgroundPollJob(parameters) - } - } -} diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollListener.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollListener.kt deleted file mode 100644 index da17528b4f..0000000000 --- a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollListener.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.thoughtcrime.securesms.loki.api - -import android.content.Context -import android.content.Intent -import nl.komponents.kovenant.functional.map -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.jobs.PushContentReceiveJob -import org.thoughtcrime.securesms.service.PersistentAlarmManagerListener -import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope -import org.whispersystems.signalservice.loki.api.SnodeAPI -import java.util.concurrent.TimeUnit - -class BackgroundPollListener : PersistentAlarmManagerListener() { - - companion object { - private val pollInterval = TimeUnit.MINUTES.toMillis(15) - - @JvmStatic - fun schedule(context: Context) { - BackgroundPollListener().onReceive(context, Intent()) - } - } - - override fun getNextScheduledExecutionTime(context: Context): Long { - return TextSecurePreferences.getBackgroundPollTime(context) - } - - override fun onAlarm(context: Context, scheduledTime: Long): Long { - ApplicationContext.getInstance(context).jobManager.add(BackgroundPollJob(context)) - val nextTime = System.currentTimeMillis() + pollInterval - TextSecurePreferences.setBackgroundPollTime(context, nextTime) - return nextTime - } -} diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt new file mode 100644 index 0000000000..66ce90905d --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt @@ -0,0 +1,113 @@ +package org.thoughtcrime.securesms.loki.api + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.work.* +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.all +import nl.komponents.kovenant.functional.map +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.jobs.PushContentReceiveJob +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope +import org.whispersystems.signalservice.loki.api.SnodeAPI +import java.util.concurrent.TimeUnit + +class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { + + companion object { + const val TAG = "BackgroundPollWorker" + + private const val RETRY_ATTEMPTS = 3 + + @JvmStatic + fun scheduleInstant(context: Context) { + val workRequest = OneTimeWorkRequestBuilder() + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + + WorkManager + .getInstance(context) + .enqueue(workRequest) + } + + @JvmStatic + fun schedulePeriodic(context: Context) { + Log.v(TAG, "Scheduling periodic work.") + val workRequest = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .build() + + WorkManager + .getInstance(context) + .enqueueUniquePeriodicWork( + TAG, + ExistingPeriodicWorkPolicy.KEEP, + workRequest + ) + } + } + + override fun doWork(): Result { + if (TextSecurePreferences.getLocalNumber(context) == null) { + Log.v(TAG, "Background poll is canceled due to the Session user is not set up yet.") + return Result.failure() + } + + try { + Log.v(TAG, "Performing background poll.") + val promises = mutableListOf>() + + // Private chats + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val privateChatsPromise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes -> + envelopes.forEach { + PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) + } + } + promises.add(privateChatsPromise) + + // Closed groups + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + ClosedGroupPoller.configureIfNeeded(context, sskDatabase) + promises.addAll(ClosedGroupPoller.shared.pollOnce()) + + // Open Groups + val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value } + for (openGroup in openGroups) { + val poller = PublicChatPoller(context, openGroup) + promises.add(poller.pollForNewMessages()) + } + + // Wait till all the promises get resolved + all(promises).get() + + return Result.success() + } catch (exception: Exception) { + Log.v(TAG, "Background poll failed due to error: ${exception.message}.", exception) + + return if (runAttemptCount < RETRY_ATTEMPTS) Result.retry() else Result.failure() + } + } + + class BootBroadcastReceiver: BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + Log.v(TAG, "Boot broadcast caught.") + BackgroundPollWorker.scheduleInstant(context) + BackgroundPollWorker.schedulePeriodic(context) + } + } + } +} diff --git a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt index 77b973f57b..4f1a1d1930 100644 --- a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt @@ -17,7 +17,7 @@ import org.whispersystems.signalservice.loki.utilities.getRandomElementOrNull class ClosedGroupPoller private constructor(private val context: Context, private val database: SharedSenderKeysDatabase) { private var isPolling = false - private val handler = Handler() + private val handler: Handler by lazy { Handler() } private val task = object : Runnable { diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt index 374f8b68c6..cc2ef6facb 100644 --- a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.api import android.content.Context import android.os.Handler import android.util.Log +import androidx.annotation.WorkerThread import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map @@ -30,9 +31,10 @@ import org.whispersystems.signalservice.loki.api.opengroups.PublicChatMessage import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol import java.security.MessageDigest import java.util.* +import java.util.concurrent.CompletableFuture class PublicChatPoller(private val context: Context, private val group: PublicChat) { - private val handler = Handler() + private val handler by lazy { Handler() } private var hasStarted = false private var isPollOngoing = false public var isCaughtUp = false From f9cec47c696e37e6833b8f63658b5afb1c14acdd Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Thu, 24 Sep 2020 15:35:42 +1000 Subject: [PATCH 02/18] Use custom work manager database name to avoid collision with the recent Android API. --- .../securesms/jobmanager/migration/WorkManagerDatabase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java index 81226fc976..f3910fc1f7 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java @@ -24,7 +24,7 @@ final class WorkManagerDatabase extends SQLiteOpenHelper { private static final String TAG = WorkManagerDatabase.class.getSimpleName(); - static final String DB_NAME = "androidx.work.workdb"; + static final String DB_NAME = "session.workdb"; WorkManagerDatabase(@NonNull Context context) { super(context, DB_NAME, null, 5); From 4fe6c8acfde8461a43e7e17a80b9edd2518849bd Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Wed, 28 Oct 2020 15:46:05 +1100 Subject: [PATCH 03/18] Work manager migration code removed. --- .../securesms/jobmanager/JobManager.java | 6 -- .../migration/WorkManagerDatabase.java | 101 ------------------ .../migration/WorkManagerMigrator.java | 45 -------- 3 files changed, 152 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java delete mode 100644 src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java diff --git a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java index 636faa04fb..dce43d6aff 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/src/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -7,7 +7,6 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; -import org.thoughtcrime.securesms.jobmanager.migration.WorkManagerMigrator; import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.Debouncer; @@ -52,11 +51,6 @@ public class JobManager implements ConstraintObserver.Notifier { this::onEmptyQueue); executor.execute(() -> { - if (WorkManagerMigrator.needsMigration(application)) { - Log.i(TAG, "Detected an old WorkManager database. Migrating."); - WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer()); - } - jobController.init(); for (int i = 0; i < jobRunners.length; i++) { diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java deleted file mode 100644 index f3910fc1f7..0000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerDatabase.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migration; - -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.jobmanager.Data; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec; -import org.thoughtcrime.securesms.logging.Log; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -final class WorkManagerDatabase extends SQLiteOpenHelper { - - private static final String TAG = WorkManagerDatabase.class.getSimpleName(); - - static final String DB_NAME = "session.workdb"; - - WorkManagerDatabase(@NonNull Context context) { - super(context, DB_NAME, null, 5); - } - - @Override - public void onCreate(SQLiteDatabase db) { - throw new UnsupportedOperationException("We should never be creating this database, only migrating an existing one!"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // There's a chance that a user who hasn't upgraded in > 6 months could hit this onUpgrade path, - // but we don't use any of the columns that were added in any migrations they could hit, so we - // can ignore this. - Log.w(TAG, "Hit onUpgrade path from " + oldVersion + " to " + newVersion); - } - - @NonNull List getAllJobs(@NonNull Data.Serializer dataSerializer) { - SQLiteDatabase db = getReadableDatabase(); - String[] columns = new String[] { "id", "worker_class_name", "input", "required_network_type"}; - List fullSpecs = new LinkedList<>(); - - try (Cursor cursor = db.query("WorkSpec", columns, null, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - String factoryName = WorkManagerFactoryMappings.getFactoryKey(cursor.getString(cursor.getColumnIndexOrThrow("worker_class_name"))); - - if (factoryName != null) { - String id = cursor.getString(cursor.getColumnIndexOrThrow("id")); - byte[] data = cursor.getBlob(cursor.getColumnIndexOrThrow("input")); - - List constraints = new LinkedList<>(); - JobSpec jobSpec = new JobSpec(id, - factoryName, - getQueueKey(id), - System.currentTimeMillis(), - 0, - 0, - Job.Parameters.UNLIMITED, - TimeUnit.SECONDS.toMillis(30), - TimeUnit.DAYS.toMillis(1), - Job.Parameters.UNLIMITED, - dataSerializer.serialize(DataMigrator.convert(data)), - false); - - - - if (cursor.getInt(cursor.getColumnIndexOrThrow("required_network_type")) != 0) { - constraints.add(new ConstraintSpec(id, NetworkConstraint.KEY)); - } - - fullSpecs.add(new FullSpec(jobSpec, constraints, Collections.emptyList())); - } else { - Log.w(TAG, "Failed to find a matching factory for worker class: " + factoryName); - } - } - } - - return fullSpecs; - } - - private @Nullable String getQueueKey(@NonNull String jobId) { - String query = "work_spec_id = ?"; - String[] args = new String[] { jobId }; - - try (Cursor cursor = getReadableDatabase().query("WorkName", null, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndexOrThrow("name")); - } - } - - return null; - } -} diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java deleted file mode 100644 index ae7df8eb9a..0000000000 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerMigrator.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.thoughtcrime.securesms.jobmanager.migration; - -import android.annotation.SuppressLint; -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; - -import org.thoughtcrime.securesms.jobmanager.Data; -import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec; -import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; -import org.thoughtcrime.securesms.logging.Log; - -import java.util.List; - -public class WorkManagerMigrator { - - private static final String TAG = Log.tag(WorkManagerMigrator.class); - - @SuppressLint("DefaultLocale") - @WorkerThread - public static synchronized void migrate(@NonNull Context context, - @NonNull JobStorage jobStorage, - @NonNull Data.Serializer dataSerializer) - { - long startTime = System.currentTimeMillis(); - Log.i(TAG, "Beginning WorkManager migration."); - - WorkManagerDatabase database = new WorkManagerDatabase(context); - List fullSpecs = database.getAllJobs(dataSerializer); - - for (FullSpec fullSpec : fullSpecs) { - Log.i(TAG, String.format("Migrating job with key '%s' and %d constraint(s).", fullSpec.getJobSpec().getFactoryKey(), fullSpec.getConstraintSpecs().size())); - } - - jobStorage.insertJobs(fullSpecs); - - context.deleteDatabase(WorkManagerDatabase.DB_NAME); - Log.i(TAG, String.format("WorkManager migration finished. Migrated %d job(s) in %d ms.", fullSpecs.size(), System.currentTimeMillis() - startTime)); - } - - @WorkerThread - public static synchronized boolean needsMigration(@NonNull Context context) { - return context.getDatabasePath(WorkManagerDatabase.DB_NAME).exists(); - } -} From d178eefd98938fcc994a6ac678a2e8c20b8b3d02 Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Wed, 28 Oct 2020 17:29:23 +1100 Subject: [PATCH 04/18] Clean up any previously scheduled background poll jobs. --- .../database/helpers/SQLCipherOpenHelper.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index cae4f03c09..d57c258717 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -93,8 +93,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV14_BACKUP_FILES = 35; private static final int lokiV15 = 36; private static final int lokiV16 = 37; + private static final int lokiV17_CLEAR_BG_POLL_JOBS = 38; - private static final int DATABASE_VERSION = lokiV16; + private static final int DATABASE_VERSION = lokiV17_CLEAR_BG_POLL_JOBS; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -633,11 +634,17 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV15) { db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand()); } - + if (oldVersion < lokiV16) { db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand()); } + if (oldVersion < lokiV17_CLEAR_BG_POLL_JOBS) { + // BackgroundPollJob was replaced with BackgroundPollWorker. Clear all the scheduled job records. + db.execSQL("DELETE FROM job_spec WHERE factory_key = 'BackgroundPollJob'"); + db.execSQL("DELETE FROM constraint_spec WHERE factory_key = 'BackgroundPollJob'"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); From 5ad7fcf95494f56f7e79ab1fe8590390d70c4c34 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Fri, 6 Nov 2020 14:49:23 +1100 Subject: [PATCH 05/18] rename whitelisted function --- .../securesms/conversation/ConversationActivity.java | 2 +- .../thoughtcrime/securesms/jobs/PushDecryptJob.java | 2 +- .../securesms/linkpreview/LinkPreviewUtil.java | 12 +++++------- .../securesms/net/ContentProxySafetyInterceptor.java | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 465de91e5f..d9f463844e 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2370,7 +2370,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity recipient.getAddress().isEmail() || inputPanel.getQuote().isPresent() || linkPreviewViewModel.hasLinkPreview() || - LinkPreviewUtil.isWhitelistedMediaUrl(message) || // Loki - Send GIFs as media messages + LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages needsSplit; Log.i(TAG, "isManual Selection: " + sendButton.isManualSelection()); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 84476b944e..8801ce4aa1 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -1368,7 +1368,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Optional title = Optional.fromNullable(preview.getTitle()); boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent(); boolean presentInBody = url.isPresent() && Stream.of(LinkPreviewUtil.findWhitelistedUrls(message)).map(Link::getUrl).collect(Collectors.toSet()).contains(url.get()); - boolean validDomain = url.isPresent() && LinkPreviewUtil.isWhitelistedLinkUrl(url.get()); + boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidLinkUrl(url.get()); if (hasContent && presentInBody && validDomain) { LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), thumbnail); diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index 9139f67ca0..c9ca8e3b6c 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -38,14 +38,14 @@ public final class LinkPreviewUtil { return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class)) .map(span -> new Link(span.getURL(), spannable.getSpanStart(span))) - .filter(link -> isWhitelistedLinkUrl(link.getUrl())) + .filter(link -> isValidLinkUrl(link.getUrl())) .toList(); } /** - * @return True if the host is present in the link whitelist. + * @return True if the host is valid. */ - public static boolean isWhitelistedLinkUrl(@Nullable String linkUrl) { + public static boolean isValidLinkUrl(@Nullable String linkUrl) { if (linkUrl == null) return false; if (StickerUrl.isValidShareLink(linkUrl)) return true; @@ -53,21 +53,19 @@ public final class LinkPreviewUtil { return url != null && !TextUtils.isEmpty(url.scheme()) && "https".equals(url.scheme()) && - LinkPreviewDomains.LINKS.contains(url.host()) && isLegalUrl(linkUrl); } /** - * @return True if the top-level domain is present in the media whitelist. + * @return True if the top-level domain is valid. */ - public static boolean isWhitelistedMediaUrl(@Nullable String mediaUrl) { + public static boolean isValidMediaUrl(@Nullable String mediaUrl) { if (mediaUrl == null) return false; HttpUrl url = HttpUrl.parse(mediaUrl); return url != null && !TextUtils.isEmpty(url.scheme()) && "https".equals(url.scheme()) && - LinkPreviewDomains.IMAGES.contains(url.topPrivateDomain()) && isLegalUrl(mediaUrl); } diff --git a/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java b/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java index 992500b8c1..dcf6097738 100644 --- a/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java +++ b/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java @@ -53,6 +53,6 @@ public class ContentProxySafetyInterceptor implements Interceptor { } private static boolean isWhitelisted(@Nullable String url) { - return LinkPreviewUtil.isWhitelistedLinkUrl(url) || LinkPreviewUtil.isWhitelistedMediaUrl(url); + return LinkPreviewUtil.isValidLinkUrl(url) || LinkPreviewUtil.isValidMediaUrl(url); } } From 283e7d62dd78aa7eb4e995a2ea9932ff5462f1a5 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Fri, 6 Nov 2020 14:49:48 +1100 Subject: [PATCH 06/18] remove unused white list --- .../linkpreview/LinkPreviewDomains.java | 50 ------------------- .../securesms/net/ContentProxySelector.java | 7 +-- 2 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/linkpreview/LinkPreviewDomains.java diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewDomains.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewDomains.java deleted file mode 100644 index 64baa6d42a..0000000000 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewDomains.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.thoughtcrime.securesms.linkpreview; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -public class LinkPreviewDomains { - public static final String STICKERS = "signal.org"; - - public static final Set LINKS = new HashSet<>(Arrays.asList( - // YouTube - "youtube.com", - "www.youtube.com", - "m.youtube.com", - "youtu.be", - // Reddit - "reddit.com", - "www.reddit.com", - "m.reddit.com", - // Imgur - "imgur.com", - "www.imgur.com", - "m.imgur.com", - // Instagram - "instagram.com", - "www.instagram.com", - "m.instagram.com", - // Pinterest - "pinterest.com", - "www.pinterest.com", - "pin.it", - // Giphy - "giphy.com", - "media.giphy.com", - "media1.giphy.com", - "media2.giphy.com", - "media3.giphy.com", - "gph.is" - )); - - public static final Set IMAGES = new HashSet<>(Arrays.asList( - "ytimg.com", - "cdninstagram.com", - "fbcdn.net", - "redd.it", - "imgur.com", - "pinimg.com", - "giphy.com" - )); -} diff --git a/src/org/thoughtcrime/securesms/net/ContentProxySelector.java b/src/org/thoughtcrime/securesms/net/ContentProxySelector.java index e370042af4..29418f34e3 100644 --- a/src/org/thoughtcrime/securesms/net/ContentProxySelector.java +++ b/src/org/thoughtcrime/securesms/net/ContentProxySelector.java @@ -1,13 +1,9 @@ package org.thoughtcrime.securesms.net; -import android.os.AsyncTask; - -import org.thoughtcrime.securesms.linkpreview.LinkPreviewDomains; import org.thoughtcrime.securesms.logging.Log; import network.loki.messenger.BuildConfig; -import org.thoughtcrime.securesms.util.Util; import java.io.IOException; import java.net.InetSocketAddress; @@ -27,8 +23,7 @@ public class ContentProxySelector extends ProxySelector { private static final Set WHITELISTED_DOMAINS = new HashSet<>(); static { - WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.LINKS); - WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.IMAGES); + WHITELISTED_DOMAINS.add("giphy.com"); } private final List CONTENT = new ArrayList(1) {{ From d669e8a0b10544495bd064a2eaef0b720a587698 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Fri, 6 Nov 2020 14:50:00 +1100 Subject: [PATCH 07/18] enable link preview for any site --- .../linkpreview/LinkPreviewRepository.java | 90 ++++++++++++------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index fc931c0545..5401a992a1 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -2,13 +2,16 @@ package org.thoughtcrime.securesms.linkpreview; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.text.Html; import android.text.TextUtils; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.FutureTarget; +import com.google.android.gms.common.util.IOUtils; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.Attachment; @@ -37,6 +40,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifes import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -73,7 +77,7 @@ public class LinkPreviewRepository implements InjectableType { RequestController getLinkPreview(@NonNull Context context, @NonNull String url, @NonNull Callback> callback) { CompositeRequestController compositeController = new CompositeRequestController(); - if (!LinkPreviewUtil.isWhitelistedLinkUrl(url)) { + if (!LinkPreviewUtil.isValidLinkUrl(url)) { Log.w(TAG, "Tried to get a link preview for a non-whitelisted domain."); callback.onComplete(Optional.absent()); return compositeController; @@ -137,7 +141,7 @@ public class LinkPreviewRepository implements InjectableType { Optional title = getProperty(body, "title"); Optional imageUrl = getProperty(body, "image"); - if (imageUrl.isPresent() && !LinkPreviewUtil.isWhitelistedMediaUrl(imageUrl.get())) { + if (imageUrl.isPresent() && !LinkPreviewUtil.isValidMediaUrl(imageUrl.get())) { Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping."); imageUrl = Optional.absent(); } @@ -150,48 +154,34 @@ public class LinkPreviewRepository implements InjectableType { } private @NonNull RequestController fetchThumbnail(@NonNull Context context, @NonNull String imageUrl, @NonNull Callback> callback) { - FutureTarget bitmapFuture = GlideApp.with(context).asBitmap() - .load(new ChunkedImageUrl(imageUrl)) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerInside() - .submit(1024, 1024); - - RequestController controller = () -> bitmapFuture.cancel(false); + Call call = client.newCall(new Request.Builder().url(imageUrl).build()); + CallRequestController controller = new CallRequestController(call); SignalExecutors.UNBOUNDED.execute(() -> { try { - Bitmap bitmap = bitmapFuture.get(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); - - byte[] bytes = baos.toByteArray(); - Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(); - Optional thumbnail = Optional.of(new UriAttachment(uri, - uri, - MediaUtil.IMAGE_JPEG, - AttachmentDatabase.TRANSFER_PROGRESS_STARTED, - bytes.length, - bitmap.getWidth(), - bitmap.getHeight(), - null, - null, - false, - false, - null, - null)); + Response response = call.execute(); + if (!response.isSuccessful() || response.body() == null) { + return; + } + + InputStream bodyStream = response.body().byteStream(); + controller.setStream(bodyStream); + + byte[] data = IOUtils.readInputStreamFully(bodyStream); + Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + Optional thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaUtil.IMAGE_JPEG); + + if (bitmap != null) bitmap.recycle(); callback.onComplete(thumbnail); - } catch (CancellationException | ExecutionException | InterruptedException e) { + } catch (IOException e) { + Log.w(TAG, "Exception during link preview image retrieval.", e); controller.cancel(); callback.onComplete(Optional.absent()); - } finally { - bitmapFuture.cancel(false); } }); - return () -> bitmapFuture.cancel(true); + return controller; } private @NonNull Optional getProperty(@NonNull String searchText, @NonNull String property) { @@ -266,6 +256,38 @@ public class LinkPreviewRepository implements InjectableType { return () -> Log.i(TAG, "Cancelled sticker pack link preview fetch -- no effect."); } + private static Optional bitmapToAttachment(@Nullable Bitmap bitmap, + @NonNull Bitmap.CompressFormat format, + @NonNull String contentType) + { + if (bitmap == null) { + return Optional.absent(); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + bitmap.compress(format, 80, baos); + + byte[] bytes = baos.toByteArray(); + Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(); + + return Optional.of(new UriAttachment(uri, + uri, + contentType, + AttachmentDatabase.TRANSFER_PROGRESS_STARTED, + bytes.length, + bitmap.getWidth(), + bitmap.getHeight(), + null, + null, + false, + false, + null, + null)); + + } + + private static class Metadata { private final Optional title; private final Optional imageUrl; From aabfa5817ce926d6b2427e5fdafc0339fd88f1e2 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Fri, 6 Nov 2020 16:57:00 +1100 Subject: [PATCH 08/18] fix a UI bug of link preview --- .../thoughtcrime/securesms/conversation/ConversationItem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index e98ab75a43..612d9d3455 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -500,7 +500,7 @@ public class ConversationItem extends TapJackingProofLinearLayout private void adjustMarginsIfNeeded(MessageRecord messageRecord) { LinearLayout.LayoutParams bodyTextLayoutParams = (LinearLayout.LayoutParams)bodyText.getLayoutParams(); bodyTextLayoutParams.topMargin = 0; - if (hasOnlyThumbnail(messageRecord)) { + if (hasOnlyThumbnail(messageRecord) || hasLinkPreview(messageRecord)) { int topPadding = 0; if (groupSenderHolder.getVisibility() == VISIBLE) { topPadding = (int)getResources().getDimension(R.dimen.medium_spacing); From 9f495b1ab8fb443ab39f727c160f2ef551f0b118 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Mon, 16 Nov 2020 17:00:00 +1100 Subject: [PATCH 09/18] ignore case for the key "location" in headers --- .../securesms/net/ContentProxySafetyInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java b/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java index dcf6097738..1e89618aee 100644 --- a/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java +++ b/src/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java @@ -31,7 +31,7 @@ public class ContentProxySafetyInterceptor implements Interceptor { Response response = chain.proceed(chain.request()); if (response.isRedirect()) { - if (isWhitelisted(response.header("Location"))) { + if (isWhitelisted(response.header("location")) || isWhitelisted(response.header("Location"))) { return response; } else { Log.w(TAG, "Tried to redirect to a non-whitelisted domain!"); From ad016731e4d9ec9a0b6b5d26e8a8d1a111391f22 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Mon, 16 Nov 2020 17:00:30 +1100 Subject: [PATCH 10/18] more general regex for matching the image and title in a raw html --- .../securesms/linkpreview/LinkPreviewRepository.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 5401a992a1..9dde7e0fea 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -187,10 +187,16 @@ public class LinkPreviewRepository implements InjectableType { private @NonNull Optional getProperty(@NonNull String searchText, @NonNull String property) { Pattern pattern = Pattern.compile("<\\s*meta\\s+property\\s*=\\s*\"\\s*og:" + property + "\\s*\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); Matcher matcher = pattern.matcher(searchText); + if (matcher.find()) { + String text = Html.fromHtml(matcher.group(1)).toString(); + if (!TextUtils.isEmpty(text)) { return Optional.of(text); } + } + pattern = Pattern.compile("<\\s*" + property + "[^>]*>(.*?)<\\s*/" + property + "[^>]*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + matcher = pattern.matcher(searchText); if (matcher.find()) { String text = Html.fromHtml(matcher.group(1)).toString(); - return TextUtils.isEmpty(text) ? Optional.absent() : Optional.of(text); + if (!TextUtils.isEmpty(text)) { return Optional.of(text); } } return Optional.absent(); From d6c5892a158905c8ef1e1d4fc66f15b016fdebab Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 18 Nov 2020 15:17:38 +1100 Subject: [PATCH 11/18] improve link preview to support more sites --- .../linkpreview/LinkPreviewRepository.java | 31 +--- .../linkpreview/LinkPreviewUtil.java | 143 +++++++++++++++++- .../securesms/util/DateUtils.java | 36 +++++ 3 files changed, 183 insertions(+), 27 deletions(-) diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 9dde7e0fea..a849a5e775 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -10,7 +10,6 @@ import android.text.Html; import android.text.TextUtils; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.request.FutureTarget; import com.google.android.gms.common.util.IOUtils; import org.thoughtcrime.securesms.ApplicationContext; @@ -18,7 +17,6 @@ import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.net.CallRequestController; @@ -37,6 +35,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest; import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo; +import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.OpenGraph; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -116,7 +115,8 @@ public class LinkPreviewRepository implements InjectableType { } private @NonNull RequestController fetchMetadata(@NonNull String url, Callback callback) { - Call call = client.newCall(new Request.Builder().url(url).cacheControl(NO_CACHE).build()); + Call call = client.newCall(new Request.Builder().url(url).removeHeader("User-Agent").addHeader("User-Agent", + "WhatsApp").cacheControl(NO_CACHE).build()); call.enqueue(new okhttp3.Callback() { @Override @@ -138,8 +138,9 @@ public class LinkPreviewRepository implements InjectableType { } String body = response.body().string(); - Optional title = getProperty(body, "title"); - Optional imageUrl = getProperty(body, "image"); + OpenGraph openGraph = LinkPreviewUtil.parseOpenGraphFields(body); + Optional title = openGraph.getTitle(); + Optional imageUrl = openGraph.getImageUrl(); if (imageUrl.isPresent() && !LinkPreviewUtil.isValidMediaUrl(imageUrl.get())) { Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping."); @@ -161,6 +162,8 @@ public class LinkPreviewRepository implements InjectableType { try { Response response = call.execute(); if (!response.isSuccessful() || response.body() == null) { + controller.cancel(); + callback.onComplete(Optional.absent()); return; } @@ -184,24 +187,6 @@ public class LinkPreviewRepository implements InjectableType { return controller; } - private @NonNull Optional getProperty(@NonNull String searchText, @NonNull String property) { - Pattern pattern = Pattern.compile("<\\s*meta\\s+property\\s*=\\s*\"\\s*og:" + property + "\\s*\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - Matcher matcher = pattern.matcher(searchText); - if (matcher.find()) { - String text = Html.fromHtml(matcher.group(1)).toString(); - if (!TextUtils.isEmpty(text)) { return Optional.of(text); } - } - - pattern = Pattern.compile("<\\s*" + property + "[^>]*>(.*?)<\\s*/" + property + "[^>]*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - matcher = pattern.matcher(searchText); - if (matcher.find()) { - String text = Html.fromHtml(matcher.group(1)).toString(); - if (!TextUtils.isEmpty(text)) { return Optional.of(text); } - } - - return Optional.absent(); - } - private RequestController fetchStickerPackLinkPreview(@NonNull Context context, @NonNull String packUrl, @NonNull Callback> callback) diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index c9ca8e3b6c..f3eb3c9046 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.linkpreview; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import android.annotation.SuppressLint; +import android.text.Html; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.URLSpan; @@ -10,9 +13,14 @@ import android.text.util.Linkify; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.stickers.StickerUrl; +import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libsignal.util.guava.Optional; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -20,10 +28,15 @@ import okhttp3.HttpUrl; public final class LinkPreviewUtil { - private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$"); - private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$"); - private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$"); - private static final Pattern STICKER_URL_PATTERN = Pattern.compile("^.*#pack_id=(.*)&pack_key=(.*)$"); + private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$"); + private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$"); + private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$"); + private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>"); + private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>"); + private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\""); + private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>"); + private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>"); + private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\""); /** * @return All whitelisted URLs in the source text. @@ -82,4 +95,126 @@ public final class LinkPreviewUtil { return false; } } + + public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) { + return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString()); + } + + static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) { + if (html == null) { + return new OpenGraph(Collections.emptyMap(), null, null); + } + + Map openGraphTags = new HashMap<>(); + Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html); + + while (openGraphMatcher.find()) { + String tag = openGraphMatcher.group(); + String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null; + + if (property != null) { + Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); + if (contentMatcher.find() && contentMatcher.groupCount() > 0) { + String content = htmlDecoder.fromEncoded(contentMatcher.group(1)); + openGraphTags.put(property.toLowerCase(), content); + } + } + } + + Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html); + + while (articleMatcher.find()) { + String tag = articleMatcher.group(); + String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null; + + if (property != null) { + Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); + if (contentMatcher.find() && contentMatcher.groupCount() > 0) { + String content = htmlDecoder.fromEncoded(contentMatcher.group(1)); + openGraphTags.put(property.toLowerCase(), content); + } + } + } + + String htmlTitle = ""; + String faviconUrl = ""; + + Matcher titleMatcher = TITLE_PATTERN.matcher(html); + if (titleMatcher.find() && titleMatcher.groupCount() > 0) { + htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1)); + } + + Matcher faviconMatcher = FAVICON_PATTERN.matcher(html); + if (faviconMatcher.find()) { + Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group()); + if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) { + faviconUrl = faviconHrefMatcher.group(1); + } + } + + return new OpenGraph(openGraphTags, htmlTitle, faviconUrl); + } + + private static @Nullable String parseTopLevelDomain(@NonNull String domain) { + int periodIndex = domain.lastIndexOf("."); + + if (periodIndex >= 0 && periodIndex < domain.length() - 1) { + return domain.substring(periodIndex + 1); + } else { + return null; + } + } + + + public static final class OpenGraph { + + private final Map values; + + private final @Nullable String htmlTitle; + private final @Nullable String faviconUrl; + + private static final String KEY_TITLE = "title"; + private static final String KEY_DESCRIPTION_URL = "description"; + private static final String KEY_IMAGE_URL = "image"; + private static final String KEY_PUBLISHED_TIME_1 = "published_time"; + private static final String KEY_PUBLISHED_TIME_2 = "article:published_time"; + private static final String KEY_MODIFIED_TIME_1 = "modified_time"; + private static final String KEY_MODIFIED_TIME_2 = "article:modified_time"; + + public OpenGraph(@NonNull Map values, @Nullable String htmlTitle, @Nullable String faviconUrl) { + this.values = values; + this.htmlTitle = htmlTitle; + this.faviconUrl = faviconUrl; + } + + public @NonNull Optional getTitle() { + return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle)); + } + + public @NonNull Optional getImageUrl() { + return Optional.of(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl)); + } + + @SuppressLint("ObsoleteSdkInt") + public long getDate() { + return Stream.of(values.get(KEY_PUBLISHED_TIME_1), + values.get(KEY_PUBLISHED_TIME_2), + values.get(KEY_MODIFIED_TIME_1), + values.get(KEY_MODIFIED_TIME_2)) + .map(DateUtils::parseIso8601) + .filter(time -> time > 0) + .findFirst() + .orElse(0L); + } + + public @NonNull + Optional getDescription() { + return Optional.of(values.get(KEY_DESCRIPTION_URL)); + } + } + + public interface HtmlDecoder { + @NonNull String fromEncoded(@NonNull String html); + } + } diff --git a/src/org/thoughtcrime/securesms/util/DateUtils.java b/src/org/thoughtcrime/securesms/util/DateUtils.java index a7fc61fb4f..901aba7dc2 100644 --- a/src/org/thoughtcrime/securesms/util/DateUtils.java +++ b/src/org/thoughtcrime/securesms/util/DateUtils.java @@ -16,10 +16,17 @@ */ package org.thoughtcrime.securesms.util; +import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import android.os.Build; import android.text.format.DateFormat; +import org.thoughtcrime.securesms.logging.Log; + +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -142,4 +149,33 @@ public class DateUtils extends android.text.format.DateUtils { private static String getLocalizedPattern(String template, Locale locale) { return DateFormat.getBestDateTimePattern(locale, template); } + + /** + * e.g. 2020-09-04T19:17:51Z + * https://www.iso.org/iso-8601-date-and-time-format.html + * + * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. + * + * @return The timestamp if able to be parsed, otherwise -1. + */ + @SuppressLint("ObsoleteSdkInt") + public static long parseIso8601(@Nullable String date) { + SimpleDateFormat format; + if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); + } else { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + } + + if (date.isEmpty()) { + return -1; + } + + try { + return format.parse(date).getTime(); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse date.", e); + return -1; + } + } } From 53d0689cf7c4ab72502d219f7b748719c495728a Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Thu, 19 Nov 2020 16:58:11 +1100 Subject: [PATCH 12/18] make pattern case insensitive --- .../securesms/linkpreview/LinkPreviewUtil.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index f3eb3c9046..abe9018061 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -28,15 +28,15 @@ import okhttp3.HttpUrl; public final class LinkPreviewUtil { - private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$"); - private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$"); - private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$"); - private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>"); - private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>"); - private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\""); - private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>"); - private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>"); - private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\""); + private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE); + private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE); + private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE); + private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE); + private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE); + private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); + private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE); + private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE); + private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); /** * @return All whitelisted URLs in the source text. From 5e47d3b9acfbbc19c258d3fe983de0d69a56a4f2 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Thu, 19 Nov 2020 17:09:24 +1100 Subject: [PATCH 13/18] show link preview cancel button --- res/layout/link_preview.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/layout/link_preview.xml b/res/layout/link_preview.xml index 09d371485a..493f65a41f 100644 --- a/res/layout/link_preview.xml +++ b/res/layout/link_preview.xml @@ -79,11 +79,11 @@ android:layout_marginEnd="6dp" android:layout_marginTop="4dp" android:src="@drawable/ic_close_white_18dp" - android:tint="@color/gray70" - android:visibility="gone" + android:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:visibility="visible" /> + app:tint="@color/gray70" + tools:ignore="MissingPrefix" /> Date: Fri, 20 Nov 2020 11:41:50 +1100 Subject: [PATCH 14/18] fix link preview close button --- res/layout/link_preview.xml | 4 ++-- .../securesms/components/LinkPreviewView.java | 11 +++++++++++ .../securesms/conversation/ConversationItem.java | 5 ++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/res/layout/link_preview.xml b/res/layout/link_preview.xml index 493f65a41f..c4215b810a 100644 --- a/res/layout/link_preview.xml +++ b/res/layout/link_preview.xml @@ -79,11 +79,11 @@ android:layout_marginEnd="6dp" android:layout_marginTop="4dp" android:src="@drawable/ic_close_white_18dp" - android:visibility="visible" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:tint="@color/gray70" - tools:ignore="MissingPrefix" /> + tools:visibility="visible" /> Date: Fri, 20 Nov 2020 18:59:13 +1100 Subject: [PATCH 15/18] Conversation activity no longer pulls the public chat info directly. Public chat API partially refactored to avoid thread branching and to use Kotlin coroutines instead of kovenant futures. --- build.gradle | 2 + .../securesms/GroupCreateActivity.java | 2 +- .../RecipientPreferenceActivity.java | 4 +- .../securesms/components/QuoteView.java | 2 +- .../components/TypingStatusSender.java | 2 +- .../conversation/ConversationActivity.java | 30 ++++---- .../securesms/database/GroupDatabase.java | 26 ++++++- .../securesms/database/MmsDatabase.java | 6 +- .../securesms/database/SmsDatabase.java | 6 +- .../securesms/database/SmsMigrator.java | 4 +- .../securesms/database/ThreadDatabase.java | 6 +- .../loaders/BucketedThreadMediaLoader.java | 2 +- .../database/loaders/PagingMediaLoader.java | 2 +- .../database/loaders/ThreadMediaLoader.java | 2 +- .../securesms/groups/GroupManager.java | 28 +++++++- .../groups/GroupMessageProcessor.java | 4 +- .../securesms/jobs/PushDecryptJob.java | 18 ++--- .../activities/CreateClosedGroupActivity.kt | 2 +- .../securesms/loki/activities/HomeActivity.kt | 8 ++- .../loki/activities/JoinPublicChatActivity.kt | 27 ++++--- .../loki/api/BackgroundPollWorker.kt | 14 ++-- .../loki/api/PublicChatInfoUpdateWorker.kt | 55 ++++++++++++++ .../securesms/loki/api/PublicChatManager.kt | 29 +++++--- .../securesms/loki/api/PublicChatPoller.kt | 4 +- .../loki/database/LokiThreadDatabase.kt | 3 +- .../loki/protocol/ClosedGroupsProtocol.kt | 13 ++-- .../protocol/SessionManagementProtocol.kt | 6 +- .../protocol/SessionResetImplementation.kt | 2 +- .../protocol/shelved/SyncMessagesProtocol.kt | 2 + .../loki/utilities/NotificationUtilities.kt | 2 +- .../loki/utilities/OpenGroupUtilities.kt | 71 +++++++++++++++---- .../securesms/loki/views/UserView.kt | 5 +- .../securesms/recipients/Recipient.java | 4 ++ .../recipients/RecipientProvider.java | 8 +++ .../securesms/sms/MessageSender.java | 4 +- .../securesms/util/CommunicationActions.java | 2 +- .../securesms/util/IdentityUtil.java | 4 +- 37 files changed, 289 insertions(+), 122 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt diff --git a/build.gradle b/build.gradle index 05ce8a73b0..5b96847b45 100644 --- a/build.gradle +++ b/build.gradle @@ -89,7 +89,9 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' implementation "androidx.work:work-runtime-ktx:2.4.0" + implementation "androidx.core:core-ktx:1.3.2" implementation ("com.google.firebase:firebase-messaging:18.0.0") { exclude group: 'com.google.firebase', module: 'firebase-core' diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index 3aeb4111d7..2ed0d9b6eb 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -348,7 +348,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true, Collections.singletonList(local)); Recipient groupRecipient = Recipient.from(activity, Address.fromSerialized(groupId), true); - long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT); + long threadId = DatabaseFactory.getThreadDatabase(activity).getOrCreateThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT); return new GroupActionResult(groupRecipient, threadId); } diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java index 3604d40651..c446f8da73 100644 --- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java +++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java @@ -767,7 +767,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi .setBlocked(recipient, blocked); if (recipient.isGroupRecipient() && DatabaseFactory.getGroupDatabase(context).isActive(recipient.getAddress().toGroupString())) { - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, recipient); if (threadId != -1 && leaveMessage.isPresent()) { @@ -776,7 +776,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); String groupId = recipient.getAddress().toGroupString(); groupDatabase.setActive(groupId, false); - groupDatabase.remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); + groupDatabase.removeMember(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); } else { Log.w(TAG, "Failed to leave group. Can't block."); Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show(); diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index 112f522ce8..b01691fb22 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -197,7 +197,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener String quoteeDisplayName = author.toShortString(); - long threadID = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient); + long threadID = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(conversationRecipient); String senderHexEncodedPublicKey = author.getAddress().serialize(); PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID); if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) { diff --git a/src/org/thoughtcrime/securesms/components/TypingStatusSender.java b/src/org/thoughtcrime/securesms/components/TypingStatusSender.java index 55fdd513f7..d8b070d86f 100644 --- a/src/org/thoughtcrime/securesms/components/TypingStatusSender.java +++ b/src/org/thoughtcrime/securesms/components/TypingStatusSender.java @@ -90,7 +90,7 @@ public class TypingStatusSender { Set linkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(recipient.getAddress().serialize()); for (String device : linkedDevices) { Recipient deviceAsRecipient = Recipient.from(context, Address.fromSerialized(device), false); - long deviceThreadID = threadDatabase.getThreadIdFor(deviceAsRecipient); + long deviceThreadID = threadDatabase.getOrCreateThreadIdFor(deviceAsRecipient); ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted)); } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index d9f463844e..402da15c83 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -156,6 +156,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity; import org.thoughtcrime.securesms.loki.activities.HomeActivity; +import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabaseDelegate; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; @@ -163,6 +164,7 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol; import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; +import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities; import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView; import org.thoughtcrime.securesms.loki.views.ProfilePictureView; import org.thoughtcrime.securesms.loki.views.SessionRestoreBannerView; @@ -462,20 +464,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); if (publicChat != null) { - PublicChatAPI publicChatAPI = ApplicationContext.getInstance(this).getPublicChatAPI(); - publicChatAPI.getChannelInfo(publicChat.getChannel(), publicChat.getServer()).success(info -> { - String groupId = GroupUtil.getEncodedOpenGroupId(publicChat.getId().getBytes()); - - publicChatAPI.updateProfileIfNeeded( - publicChat.getChannel(), - publicChat.getServer(), - groupId, - info, - false); - - runOnUiThread(ConversationActivity.this::updateSubtitleTextView); - return Unit.INSTANCE; - }); + // Request open group info update and handle the successful result in #onOpenGroupInfoUpdated(). + PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel()); } View rootView = findViewById(R.id.rootView); @@ -1940,6 +1930,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity .show(TooltipPopup.POSITION_ABOVE); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) { + PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); + if (publicChat != null && + publicChat.getChannel() == event.getChannel() && + publicChat.getServer().equals(event.getUrl())) { + this.updateSubtitleTextView(); + } + } + private void initializeReceivers() { securityUpdateReceiver = new BroadcastReceiver() { @Override @@ -2095,7 +2095,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity long threadId = params[0]; if (drafts.size() > 0) { - if (threadId == -1) threadId = threadDatabase.getThreadIdFor(getRecipient(), thisDistributionType); + if (threadId == -1) threadId = threadDatabase.getOrCreateThreadIdFor(getRecipient(), thisDistributionType); draftDatabase.insertDrafts(threadId, drafts); threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this), diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index c5bd65e903..b2b9432d48 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -218,6 +218,18 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt notifyConversationListListeners(); } + public boolean delete(@NonNull String groupId) { + int result = databaseHelper.getWritableDatabase().delete(TABLE_NAME, GROUP_ID + " = ?", new String[]{groupId}); + + if (result > 0) { + Recipient.removeCached(Address.fromSerialized(groupId)); + notifyConversationListListeners(); + return true; + } else { + return false; + } + } + public void update(String groupId, String title, SignalServiceAttachmentPointer avatar) { ContentValues contentValues = new ContentValues(); if (title != null) contentValues.put(TITLE, title); @@ -262,7 +274,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt long avatarId; if (newValue != null) avatarId = Math.abs(new SecureRandom().nextLong()); - else avatarId = 0; + else avatarId = 0; ContentValues contentValues = new ContentValues(2); @@ -300,7 +312,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); } - public void remove(String groupId, Address source) { + public void removeMember(String groupId, Address source) { List
currentMembers = getCurrentMembers(groupId); currentMembers.remove(source); @@ -352,13 +364,21 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId}); } - public byte[] allocateGroupId() { byte[] groupId = new byte[16]; new SecureRandom().nextBytes(groupId); return groupId; } + public boolean hasGroup(@NonNull String groupId) { + try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery( + "SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1", + new String[]{groupId} + )) { + return cursor.getCount() > 0; + } + } + public static class Reader implements Closeable { private final Cursor cursor; diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index eccb3600cb..d01046adfa 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -321,10 +321,10 @@ public class MmsDatabase extends MessagingDatabase { private long getThreadIdFor(IncomingMediaMessage retrieved) throws RecipientFormattingException, MmsException { if (retrieved.getGroupId() != null) { Recipient groupRecipients = Recipient.from(context, retrieved.getGroupId(), true); - return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients); + return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipients); } else { Recipient sender = Recipient.from(context, retrieved.getFrom(), true); - return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(sender); + return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(sender); } } @@ -333,7 +333,7 @@ public class MmsDatabase extends MessagingDatabase { ? Util.toIsoString(notification.getFrom().getTextString()) : ""; Recipient recipient = Recipient.from(context, Address.fromExternal(context, fromString), false); - return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); } private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) { diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index ee4527d00b..6ebd0ec579 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -556,7 +556,7 @@ public class SmsDatabase extends MessagingDatabase { private @NonNull Pair insertCallLog(@NonNull Address address, long type, boolean unread) { Recipient recipient = Recipient.from(context, address, true); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); ContentValues values = new ContentValues(6); values.put(ADDRESS, address.serialize()); @@ -620,8 +620,8 @@ public class SmsDatabase extends MessagingDatabase { long threadId; - if (groupRecipient == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); - else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + if (groupRecipient == null) threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); + else threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient); ContentValues values = new ContentValues(6); values.put(ADDRESS, message.getSender().serialize()); diff --git a/src/org/thoughtcrime/securesms/database/SmsMigrator.java b/src/org/thoughtcrime/securesms/database/SmsMigrator.java index 5eaae3d178..66e83a36f5 100644 --- a/src/org/thoughtcrime/securesms/database/SmsMigrator.java +++ b/src/org/thoughtcrime/securesms/database/SmsMigrator.java @@ -209,7 +209,7 @@ public class SmsMigrator { if (ourRecipients != null) { if (ourRecipients.size() == 1) { - long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients.iterator().next()); + long ourThreadId = threadDatabase.getOrCreateThreadIdFor(ourRecipients.iterator().next()); migrateConversation(context, listener, progress, theirThreadId, ourThreadId); } else if (ourRecipients.size() > 1) { ourRecipients.add(Recipient.from(context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), true)); @@ -222,7 +222,7 @@ public class SmsMigrator { String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true, null); Recipient ourGroupRecipient = Recipient.from(context, Address.fromSerialized(ourGroupId), true); - long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); + long ourThreadId = threadDatabase.getOrCreateThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); migrateConversation(context, listener, progress, theirThreadId, ourThreadId); } diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index f58471b9ba..20d822f8ac 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -512,11 +512,11 @@ public class ThreadDatabase extends Database { } } - public long getThreadIdFor(Recipient recipient) { - return getThreadIdFor(recipient, DistributionTypes.DEFAULT); + public long getOrCreateThreadIdFor(Recipient recipient) { + return getOrCreateThreadIdFor(recipient, DistributionTypes.DEFAULT); } - public long getThreadIdFor(Recipient recipient, int distributionType) { + public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); String where = ADDRESS + " = ?"; String[] recipientsArg = new String[]{recipient.getAddress().serialize()}; diff --git a/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java b/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java index 6f50bd1616..4cb29cfc94 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java @@ -62,7 +62,7 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader> { @Nullable @Override public Pair loadInBackground() { - long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(recipient); Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId); while (cursor != null && cursor.moveToNext()) { diff --git a/src/org/thoughtcrime/securesms/database/loaders/ThreadMediaLoader.java b/src/org/thoughtcrime/securesms/database/loaders/ThreadMediaLoader.java index 0e8fc9a0b5..abcff10e1c 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/ThreadMediaLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/ThreadMediaLoader.java @@ -23,7 +23,7 @@ public class ThreadMediaLoader extends AbstractCursorLoader { @Override public Cursor getCursor() { - long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(Recipient.from(getContext(), address, true)); + long threadId = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(Recipient.from(getContext(), address, true)); if (gallery) return DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId); else return DatabaseFactory.getMediaDatabase(getContext()).getDocumentMediaForThread(threadId); diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index 694627f629..9ea420da54 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; @@ -89,7 +90,8 @@ public class GroupManager { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true); return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses); } else { - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor( + groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadId); } } @@ -127,10 +129,30 @@ public class GroupManager { groupDatabase.updateProfilePicture(groupId, avatarBytes); - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); + long threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor( + groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadID); } + public static boolean deleteGroup(@NonNull String groupId, + @NonNull Context context) + { + final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + final ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false); + + if (!groupDatabase.getGroup(groupId).isPresent()) { + return false; + } + + long threadId = threadDatabase.getThreadIdIfExistsFor(groupRecipient); + if (threadId != -1L) { + threadDatabase.deleteConversation(threadId); + } + + return groupDatabase.delete(groupId); + } + public static GroupActionResult updateGroup(@NonNull Context context, @NonNull String groupId, @NonNull Set members, @@ -154,7 +176,7 @@ public class GroupManager { return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses); } else { Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient); return new GroupActionResult(groupRecipient, threadId); } } diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 4b2549344d..80ec6d7ae8 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -233,7 +233,7 @@ public class GroupMessageProcessor { String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender()); if (masterDevice == null) { masterDevice = content.getSender(); } if (members.contains(Address.fromExternal(context, masterDevice))) { - database.remove(id, Address.fromExternal(context, masterDevice)); + database.removeMember(id, Address.fromExternal(context, masterDevice)); if (outgoing) database.setActive(id, false); return storeMessage(context, content, group, builder.build(), outgoing); @@ -260,7 +260,7 @@ public class GroupMessageProcessor { Address address = Address.fromExternal(context, GroupUtil.getEncodedId(group)); Recipient recipient = Recipient.from(context, address, false); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList()); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); mmsDatabase.markAsSent(messageId, true); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 8801ce4aa1..900ba496af 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -498,7 +498,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1); OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); if (!recipient.isGroupRecipient()) { // TODO: Handle session reset on sync messages @@ -808,7 +808,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (result.getMessageId() > -1) { ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient); + long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient); lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); } } @@ -822,7 +822,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { message.getTimestamp(), message.getMessage().getExpiresInSeconds() * 1000L); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null); database.markAsSent(messageId, true); @@ -864,7 +864,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { handleSynchronizeSentExpirationUpdate(message); } - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipients); database.beginTransaction(); @@ -995,7 +995,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (result.getMessageId() > -1) { ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient); + long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient); lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); } } @@ -1018,7 +1018,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { handleSynchronizeSentExpirationUpdate(message); } - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); boolean isGroup = recipient.getAddress().isGroup(); MessagingDatabase database; @@ -1102,7 +1102,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (canRecoverAutomatically(e)) { Recipient recipient = Recipient.from(context, Address.fromSerialized(sender), false); LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(context); - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); threadDB.addSessionRestoreDevice(threadID, sender); SessionManagementProtocol.startSessionReset(context, sender); } else { @@ -1249,7 +1249,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } else { // See if we need to redirect the message author = getMessageMasterDestination(content.getSender()); - threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(author); + threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(author); } if (threadId <= 0) { @@ -1459,7 +1459,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) { Recipient author = Recipient.from(context, Address.fromSerialized(sender), false); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(conversationRecipient); if (threadId > 0) { Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message."); diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index eb81812744..aff8e77528 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -125,7 +125,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> loader.fadeOut() isLoading = false - val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) + val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) if (!isFinishing) { openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) finish() diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 8a5cb4ae5c..e5f84a673e 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -26,13 +26,12 @@ import kotlinx.android.synthetic.main.activity_home.* import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob -import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet import org.thoughtcrime.securesms.loki.dialogs.LightThemeFeatureIntroBottomSheet import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet @@ -343,6 +342,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val recipient = thread.recipient val threadDB = DatabaseFactory.getThreadDatabase(this) val deleteThread = Runnable { + //TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager AsyncTask.execute { val publicChat = DatabaseFactory.getLokiThreadDatabase(this@HomeActivity).getPublicChat(threadID) if (publicChat != null) { @@ -351,6 +351,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server) ApplicationContext.getInstance(this@HomeActivity).publicChatAPI!!.leave(publicChat.channel, publicChat.server) + + //FIXME Group deletion should be synchronized with the related thread deletion. + val groupId = threadDB.getRecipientForThreadId(threadID)!!.address.serialize() + GroupManager.deleteGroup(groupId, this@HomeActivity) } threadDB.deleteConversation(threadID) ApplicationContext.getInstance(this@HomeActivity).messageNotifier.updateNotification(this@HomeActivity) diff --git a/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index 60b872883c..65ea1f3def 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -11,8 +11,12 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.lifecycle.lifecycleScope import kotlinx.android.synthetic.main.activity_join_public_chat.* import kotlinx.android.synthetic.main.fragment_enter_chat_url.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import network.loki.messenger.R import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi @@ -22,6 +26,7 @@ import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities +import java.lang.Exception class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { private val adapter = JoinPublicChatActivityAdapter(this) @@ -67,13 +72,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode } showLoader() val channel: Long = 1 - OpenGroupUtilities.addGroup(this, url, channel).success { + + lifecycleScope.launch(Dispatchers.IO) { + try { + OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel) + } catch (e: Exception) { + withContext(Dispatchers.Main) { + hideLoader() + Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() + } + return@launch + } SyncMessagesProtocol.syncAllOpenGroups(this@JoinPublicChatActivity) - }.successUi { - finish() - }.failUi { - hideLoader() - Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() + withContext(Dispatchers.Main) { finish() } } } // endregion @@ -123,13 +134,13 @@ class EnterChatURLFragment : Fragment() { } private fun joinPublicChatIfPossible() { - val inputMethodManager = context!!.getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager + val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0) var chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://") if (!chatURL.toLowerCase().startsWith("https")) { chatURL = "https://$chatURL" } - (activity!! as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL) + (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL) } } // endregion \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt index 66ce90905d..27489a285c 100644 --- a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt +++ b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt @@ -25,10 +25,9 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor @JvmStatic fun scheduleInstant(context: Context) { val workRequest = OneTimeWorkRequestBuilder() - .setConstraints( - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() + .setConstraints(Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() ) .build() @@ -41,10 +40,9 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor fun schedulePeriodic(context: Context) { Log.v(TAG, "Scheduling periodic work.") val workRequest = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) - .setConstraints( - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() + .setConstraints(Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() ) .build() diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt new file mode 100644 index 0000000000..e7b7b9be32 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.loki.api + +import android.content.Context +import androidx.work.* +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities +import org.whispersystems.signalservice.loki.api.opengroups.PublicChat + +/** + * Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager. + */ +class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { + + companion object { + const val TAG = "PublicChatInfoUpdateWorker" + + private const val DATA_KEY_SERVER_URL = "server_uRL" + private const val DATA_KEY_CHANNEL = "channel" + + @JvmStatic + fun scheduleInstant(context: Context, serverURL: String, channel: Long) { + val workRequest = OneTimeWorkRequestBuilder() + .setConstraints(Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .setInputData(workDataOf( + DATA_KEY_SERVER_URL to serverURL, + DATA_KEY_CHANNEL to channel + )) + .build() + + WorkManager + .getInstance(context) + .enqueue(workRequest) + } + } + + override fun doWork(): Result { + val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!! + val channel = inputData.getLong(DATA_KEY_CHANNEL, -1) + + val publicChatId = PublicChat.getId(channel, serverUrl) + + return try { + Log.v(TAG, "Updating open group info for $publicChatId.") + OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel) + Log.v(TAG, "Open group info was successfully updated for $publicChatId.") + Result.success() + } catch (e: Exception) { + Log.e(TAG, "Failed to update open group info for $publicChatId", e) + Result.failure() + } + } +} diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt index 6b710c5ea4..a4281704e5 100644 --- a/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.database.ContentObserver import android.graphics.Bitmap import android.text.TextUtils +import androidx.annotation.WorkerThread import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map @@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.loki.api.opengroups.PublicChatInfo import org.whispersystems.signalservice.loki.api.opengroups.PublicChat +import kotlin.jvm.Throws class PublicChatManager(private val context: Context) { private var chats = mutableMapOf() @@ -23,7 +25,7 @@ class PublicChatManager(private val context: Context) { private val observers = mutableMapOf() private var isPolling = false - public fun areAllCaughtUp():Boolean { + public fun areAllCaughtUp(): Boolean { var areAllCaughtUp = true refreshChatsAndPollers() for ((threadID, chat) in chats) { @@ -58,19 +60,24 @@ class PublicChatManager(private val context: Context) { isPolling = false } - public fun addChat(server: String, channel: Long): Promise { + //TODO Declare a specific type of checked exception instead of "Exception". + @WorkerThread + @Throws(java.lang.Exception::class) + public fun addChat(server: String, channel: Long): PublicChat { val groupChatAPI = ApplicationContext.getInstance(context).publicChatAPI - ?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!")) - return groupChatAPI.getAuthToken(server).bind { - groupChatAPI.getChannelInfo(channel, server) - }.map { - addChat(server, channel, it) - } + ?: throw IllegalStateException("LokiPublicChatAPI is not set!") + + // Ensure the auth token is acquired. + groupChatAPI.getAuthToken(server).get() + + val channelInfo = groupChatAPI.getChannelInfo(channel, server).get() + return addChat(server, channel, channelInfo) } + @WorkerThread public fun addChat(server: String, channel: Long, info: PublicChatInfo): PublicChat { val chat = PublicChat(channel, server, info.displayName, true) - var threadID = GroupManager.getOpenGroupThreadID(chat.id, context) + var threadID = GroupManager.getOpenGroupThreadID(chat.id, context) var profilePicture: Bitmap? = null // Create the group if we don't have one if (threadID < 0) { @@ -105,7 +112,7 @@ class PublicChatManager(private val context: Context) { private fun listenToThreadDeletion(threadID: Long) { if (threadID < 0 || observers[threadID] != null) { return } - val observer = createDeletionObserver(threadID, Runnable { + val observer = createDeletionObserver(threadID) { val chat = chats[threadID] // Reset last message cache @@ -119,7 +126,7 @@ class PublicChatManager(private val context: Context) { pollers.remove(threadID)?.stop() observers.remove(threadID) startPollersIfNeeded() - }) + } observers[threadID] = observer context.applicationContext.contentResolver.registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer) diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt index 2d682f07d8..e15cb90d77 100644 --- a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt @@ -193,8 +193,8 @@ class PublicChatPoller(private val context: Context, private val group: PublicCh val messageID = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID) var isDuplicate = false if (messageID != null) { - isDuplicate = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageID) > 0 - || DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) > 0 + isDuplicate = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageID) >= 0 + || DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) >= 0 } if (isDuplicate) { return } if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return } diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt index 5a4697b00c..5c5aadd0e3 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.loki.database import android.content.ContentValues import android.content.Context import android.database.Cursor -import android.util.Log import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory @@ -34,7 +33,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa override fun getThreadID(hexEncodedPublicKey: String): Long { val address = Address.fromSerialized(hexEncodedPublicKey) val recipient = Recipient.from(context, address, false) - return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient) } fun getThreadID(messageID: Long): Long { diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index bed24db122..73b071bc3c 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -25,7 +25,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType import org.whispersystems.signalservice.internal.push.SignalServiceProtos import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext -import org.whispersystems.signalservice.loki.api.SnodeAPI import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey @@ -82,7 +81,7 @@ object ClosedGroupsProtocol { // Add the group to the user's set of public keys to poll for DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) // Notify the user - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) @@ -166,7 +165,7 @@ object ClosedGroupsProtocol { if (isUserLeaving) { sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) groupDB.setActive(groupID, false) - groupDB.remove(groupID, Address.fromSerialized(userPublicKey)) + groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) } else { @@ -230,7 +229,7 @@ object ClosedGroupsProtocol { } // Notify the user val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) deferred.resolve(Unit) }.start() @@ -385,7 +384,7 @@ object ClosedGroupsProtocol { if (wasCurrentUserRemoved) { sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) groupDB.setActive(groupID, false) - groupDB.remove(groupID, Address.fromSerialized(userPublicKey)) + groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) } else { @@ -510,7 +509,7 @@ object ClosedGroupsProtocol { @JvmStatic fun leaveLegacyGroup(context: Context, recipient: Recipient): Boolean { if (!recipient.address.isClosedGroup) { return true } - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient) val message = GroupUtil.createGroupLeaveMessage(context, recipient).orNull() if (threadID < 0 || message == null) { return false } MessageSender.send(context, message, threadID, false, null) @@ -522,7 +521,7 @@ object ClosedGroupsProtocol { val groupDatabase = DatabaseFactory.getGroupDatabase(context) val groupID = recipient.address.toGroupString() groupDatabase.setActive(groupID, false) - groupDatabase.remove(groupID, Address.fromSerialized(userPublicKey)) + groupDatabase.removeMember(groupID, Address.fromSerialized(userPublicKey)) return true } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt index 073af39aff..93ae8337b9 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context -import android.os.AsyncTask import android.util.Log import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.IdentityKeyUtil @@ -11,7 +10,6 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.CleanPreKeysJob import org.thoughtcrime.securesms.loki.utilities.recipient -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage import org.thoughtcrime.securesms.sms.OutgoingTextMessage @@ -28,7 +26,7 @@ object SessionManagementProtocol { val recipient = recipient(context, publicKey) if (recipient.isGroupRecipient) { return } val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context) - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient) val devices = lokiThreadDB.getSessionRestoreDevices(threadID) for (device in devices) { val endSessionMessage = OutgoingEndSessionMessage(OutgoingTextMessage(recipient, "TERMINATE", 0, -1)) @@ -106,7 +104,7 @@ object SessionManagementProtocol { if (TextSecurePreferences.getRestorationTime(context) > errorTimestamp) { return ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(publicKey) } - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(masterDeviceAsRecipient) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(masterDeviceAsRecipient) DatabaseFactory.getLokiThreadDatabase(context).addSessionRestoreDevice(threadID, publicKey) } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt index b8e0fa8e87..9003918065 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt @@ -27,7 +27,7 @@ class SessionResetImplementation(private val context: Context) : SessionResetPro } val smsDB = DatabaseFactory.getSmsDatabase(context) val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient) val infoMessage = OutgoingTextMessage(recipient, "", 0, 0) val infoMessageID = smsDB.insertMessageOutbox(threadID, infoMessage, false, System.currentTimeMillis(), null) if (infoMessageID > -1) { diff --git a/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt index 487dfe7251..827e321f8a 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol.shelved import android.content.Context import android.util.Log +import androidx.annotation.WorkerThread import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData @@ -132,6 +133,7 @@ object SyncMessagesProtocol { } @JvmStatic + @WorkerThread fun handleOpenGroupSyncMessage(context: Context, content: SignalServiceContent, openGroups: List) { val userPublicKey = TextSecurePreferences.getLocalNumber(context) val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey) diff --git a/src/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt b/src/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt index 69a00115ec..2b98a4bc35 100644 --- a/src/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt @@ -6,7 +6,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.recipients.Recipient fun getOpenGroupDisplayName(recipient: Recipient, threadRecipient: Recipient, context: Context): String { - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(threadRecipient) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(threadRecipient) val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) val publicKey = recipient.address.toString() val displayName = if (publicChat != null) { diff --git a/src/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt b/src/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt index 4cf69ea315..fa23d46b31 100644 --- a/src/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt @@ -1,28 +1,42 @@ package org.thoughtcrime.securesms.loki.utilities import android.content.Context -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.then +import androidx.annotation.WorkerThread +import org.greenrobot.eventbus.EventBus import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.groups.GroupManager +import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.loki.api.opengroups.PublicChat +import java.lang.Exception +import java.lang.IllegalStateException +import kotlin.jvm.Throws +//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception. object OpenGroupUtilities { - @JvmStatic fun addGroup(context: Context, url: String, channel: Long): Promise { - // Check for an existing group - val groupID = PublicChat.getId(channel, url) - val threadID = GroupManager.getOpenGroupThreadID(groupID, context) - val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) - if (openGroup != null) { return Promise.of(openGroup) } - // Add the new group - val application = ApplicationContext.getInstance(context) - val displayName = TextSecurePreferences.getProfileName(context) - val lokiPublicChatAPI = application.publicChatAPI ?: throw Error("LokiPublicChatAPI is not initialized.") - return application.publicChatManager.addChat(url, channel).then { group -> + private const val TAG = "OpenGroupUtilities" + + @JvmStatic + @WorkerThread + @Throws(Exception::class) + fun addGroup(context: Context, url: String, channel: Long): PublicChat { + // Check for an existing group. + val groupID = PublicChat.getId(channel, url) + val threadID = GroupManager.getOpenGroupThreadID(groupID, context) + val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) + if (openGroup != null) return openGroup + + // Add the new group. + val application = ApplicationContext.getInstance(context) + val displayName = TextSecurePreferences.getProfileName(context) + val lokiPublicChatAPI = application.publicChatAPI + ?: throw IllegalStateException("LokiPublicChatAPI is not initialized.") + + val group = application.publicChatManager.addChat(url, channel) + DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(channel, url) DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(channel, url) lokiPublicChatAPI.getMessages(channel, url) @@ -31,7 +45,34 @@ object OpenGroupUtilities { val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(context) val profileUrl: String? = TextSecurePreferences.getProfilePictureURL(context) lokiPublicChatAPI.setProfilePicture(url, profileKey, profileUrl) - group + return group } - } + + /** + * Pulls the general public chat data from the server and updates related records. + * Fires [GroupInfoUpdatedEvent] on [EventBus] upon success. + * + * Consider using [org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker] for lazy approach. + */ + @JvmStatic + @WorkerThread + @Throws(Exception::class) + fun updateGroupInfo(context: Context, url: String, channel: Long) { + val publicChatAPI = ApplicationContext.getInstance(context).publicChatAPI + ?: throw IllegalStateException("Public chat API is not initialized!") + + // Check if open group has a related DB record. + val groupId = GroupUtil.getEncodedOpenGroupId(PublicChat.getId(channel, url).toByteArray()) + if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) { + throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId") + } + + val info = publicChatAPI.getChannelInfo(channel, url).get() + + publicChatAPI.updateProfileIfNeeded(channel, url, groupId, info, false) + + EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel)) + } + + data class GroupInfoUpdatedEvent(val url: String, val channel: Long) } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/views/UserView.kt b/src/org/thoughtcrime/securesms/loki/views/UserView.kt index 14614f512c..0f45a9bb0c 100644 --- a/src/org/thoughtcrime/securesms/loki/views/UserView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/UserView.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.loki.views import android.content.Context -import android.text.TextUtils import android.util.AttributeSet import android.view.LayoutInflater import android.view.View @@ -10,11 +9,9 @@ import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView import kotlinx.android.synthetic.main.view_user.view.* import network.loki.messenger.R import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.recipients.Recipient -import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager class UserView : LinearLayout { var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly @@ -63,7 +60,7 @@ class UserView : LinearLayout { return result ?: publicKey } } - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(user) + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(user) MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this val address = user.address.serialize() profilePictureView.glide = glide diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index 24c44b19ac..be90b012eb 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -121,6 +121,10 @@ public class Recipient implements RecipientModifiedListener { if (recipient.isPresent()) consumer.accept(recipient.get()); } + public static boolean removeCached(@NonNull Address address) { + return provider.removeCached(address); + } + Recipient(@NonNull Context context, @NonNull Address address, @Nullable Recipient stale, diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java index c1f80d8043..6d23221fdb 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java @@ -79,6 +79,10 @@ class RecipientProvider { return Optional.fromNullable(recipientCache.get(address)); } + boolean removeCached(@NonNull Address address) { + return recipientCache.remove(address); + } + private @NonNull Optional createPrefetchedRecipientDetails(@NonNull Context context, @NonNull Address address, @NonNull Optional settings, @NonNull Optional groupRecord) @@ -230,6 +234,10 @@ class RecipientProvider { cache.put(address, recipient); } + public synchronized boolean remove(Address address) { + return cache.remove(address) != null; + } + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index 3deb7565dd..c590947561 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -69,7 +69,7 @@ public class MessageSender { long allocatedThreadId; if (threadId == -1) { - allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); } else { allocatedThreadId = threadId; } @@ -94,7 +94,7 @@ public class MessageSender { long allocatedThreadId; if (threadId == -1) { - allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType()); + allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(message.getRecipient(), message.getDistributionType()); } else { allocatedThreadId = threadId; } diff --git a/src/org/thoughtcrime/securesms/util/CommunicationActions.java b/src/org/thoughtcrime/securesms/util/CommunicationActions.java index db66b955cf..c7fcf5c499 100644 --- a/src/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/src/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -64,7 +64,7 @@ public class CommunicationActions { new AsyncTask() { @Override protected Long doInBackground(Void... voids) { - return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); } @Override diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java index 38fa1c3472..c116009b3a 100644 --- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -88,7 +88,7 @@ public class IdentityUtil { smsDatabase.insertMessageInbox(incoming); } else { Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(group.getGroupId(), false)), true); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient); OutgoingTextMessage outgoing ; if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipient); @@ -112,7 +112,7 @@ public class IdentityUtil { if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipient); else outgoing = new OutgoingIdentityDefaultMessage(recipient); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); Log.i(TAG, "Inserting verified outbox..."); DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, outgoing, false, time, null); From a56fcd1439cb6d8f06b1bb930f39019be9132d1a Mon Sep 17 00:00:00 2001 From: Kacper Marcinkiewicz Date: Sat, 21 Nov 2020 20:15:32 +0100 Subject: [PATCH 16/18] Fixes/Improvements to the Polish translation --- res/values-pl/strings.xml | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index a94b98f7f1..158c3ba773 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -1103,7 +1103,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Rozmowy i multimedia Limit długości konwersacji Przytnij wszystkie konwersacje teraz - Przeskanuj wszystkie konwersacje i przytnij to określonej długości + Przeskanuj wszystkie konwersacje i przytnij do określonej długości Połączone urządzenia Jasny Ciemny @@ -1360,29 +1360,29 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Kontyntynuj Kopiuj nieprawidłowy URL - Skopiowane do schowka - Nie można połączyć urządzenia. - Kolejny - Dzielić + Skopiowano do schowka + Nie można podłączyć urządzenia. + Dalej + Udostępnij Nieprawidłowy identyfikator Session Anuluj Twój identyfikator Session - Twoja Session zaczyna się tutaj... + Twoja sesja zaczyna się tutaj... Utwórz identyfikator Session Kontynuuj swoją sesję Połącz z istniejącym kontem Twoje urządzenie zostało rozłączone pomyślnie - Jaka jest Session + Jaki jest Session To zdecentralizowana, szyfrowana aplikacja do przesyłania wiadomości Więc nie zbiera moich danych osobowych ani metadanych z mojej rozmowy? Jak to działa? Wykorzystując połączenie zaawansowanych anonimowych tras i technologii szyfrowania end-to-end. Znajomi nie pozwalają znajomym korzystać z zainfekowanych komunikatorów. Nie ma za co. Przywitaj się z identyfikatorem Session - Twój identyfikator Session to unikalny adres, za pomocą którego można się z Tobą kontaktować w Sesji. Bez połączenia z twoją prawdziwą tożsamością, identyfikator Session jest z założenia całkowicie anonimowy i prywatny. - Skopiowane do schowka + Twój identyfikator Session to unikalny adres, za pomocą którego można się z Tobą kontaktować w Session. Bez połączenia z twoją prawdziwą tożsamością, identyfikator Session jest z założenia całkowicie anonimowy i prywatny. + Skopiowano do schowka Przywróć swoje konto Wprowadź frazę odzyskiwania, która została Ci przekazana podczas rejestracji w celu przywrócenia konta. @@ -1398,11 +1398,11 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Wpisz swój identyfikator Session Wybierz swoją nazwę wyświetlaną - To będzie twoje imię, kiedy będziesz używać Sesji. Może to być twoje prawdziwe imię, alias lub cokolwiek innego, co lubisz. + To będzie twoje imię, kiedy będziesz używać Session. Może to być twoje prawdziwe imię, pseudonim lub cokolwiek innego, co lubisz. Wprowadź wyświetlaną nazwe Wybierz wyświetlaną nazwę Wybierz wyświetlaną nazwę, która składa się tylko z znaków az, AZ, 0–9 i _ - Wybierz krótszą nazwę wyświetlaną + Wybierz krótszą wyświetlaną nazwę Zalecana Wybierz opcję @@ -1416,35 +1416,35 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Twoja fraza odzyskiwania Poznaj swoją frazę odzyskiwania - Twoja fraza odzyskiwania jest kluczem głównym do identyfikatora Session - możesz go użyć do przywrócenia identyfikatora Session, jeśli stracisz dostęp do urządzenia. Przechowuj swoją frazę odzyskiwania w bezpiecznym miejscu i nikomu jej nie udostępniaj. + Twoja fraza odzyskiwania jest głównym kluczem do identyfikatora Session - możesz jej użyć do przywrócenia identyfikatora Session, jeśli stracisz dostęp do urządzenia. Przechowuj swoją frazę odzyskiwania w bezpiecznym miejscu i nikomu jej nie udostępniaj. Przytrzymaj, aby odsłonić Zabezpiecz swoje konto, zapisując frazę odzyskiwania - Stuknij i przytrzymaj zredagowane słowa, aby odsłonić frazę odzyskiwania, a następnie przechowuj ją bezpiecznie, aby zabezpieczyć identyfikator Session. + Stuknij i przytrzymaj zredagowane słowa, aby odsłonić frazę odzyskiwania, a następnie przechowuj ją w bezpiecznym miejscu, aby zabezpieczyć identyfikator Session. Pamiętaj, aby przechowywać frazę odzyskiwania w bezpiecznym miejscu Ścieżka - Sesja ukrywa Twój adres IP, odbijając wiadomości przez kilka węzłów usług w zdecentralizowanej sieci Session. Oto kraje, w których obecnie Twoje połączenie jest odbijane: + Session ukrywa Twój adres IP, odbijając wiadomości przez kilka węzłów usług w zdecentralizowanej sieci Session. Oto kraje, w których obecnie Twoje połączenie jest odbijane: ty Węzeł wejścia Węzeł serwisowy Miejsce docelowe - Ucz się więcej + Dowiedz się więcej - Nowa Session + Nowa sesja Wpisz identyfikator Session Skanowania QR code Zeskanuj kod QR użytkownika, aby rozpocząć sesję. Kody QR można znaleźć, dotykając ikony kodu QR w ustawieniach konta. Wprowadź identyfikator Session odbiorcy - Użytkownicy mogą udostępnić swój identyfikator Session, przechodząc do ustawień konta i stukając opcję Udostępnij identyfikator Session lub udostępniając kod QR. + Użytkownicy mogą udostępnić swój identyfikator Session, przechodząc do ustawień konta i klikając opcję Udostępnij identyfikator Session lub udostępniając kod QR. - Sesja wymaga dostępu do kamery, aby skanować kody QR + Session wymaga dostępu do kamery, aby skanować kody QR Udziel dostępu do kamery Nowa grupa zamknięta Wpisz nazwę grupy - Grupy zamknięte obsługują do 10 członków i zapewniają taką samą ochronę prywatności jak sesje jeden na jednego. + Grupy zamknięte obsługują do 10 członków i zapewniają taką samą ochronę prywatności jak sesje jeden do jednego. Nie masz jeszcze żadnych kontaktów Rozpocznij sesję Wpisz nazwę grupy @@ -1452,7 +1452,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Grupa zamknięta nie może mieć więcej niż 20 członków Jeden z członków Twojej grupy ma nieprawidłowy identyfikator Session - Dołącz do Open Group + Dołącz do otwartej grupy Nie można dołączyć do grupy Otwórz adres URL grupy Skanowania QR code @@ -1463,13 +1463,13 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Ustawienia Wprowadź wyświetlaną nazwe Wybierz wyświetlaną nazwę - Wybierz wyświetlaną nazwę, która składa się tylko z znaków az, AZ, 0–9 i _ - Wybierz krótszą nazwę wyświetlaną + Wybierz wyświetlaną nazwę, która składa się tylko ze znaków az, AZ, 0–9 i _ + Wybierz krótszą wyświetlaną nazwę Prywatność Powiadomienia Czaty Urządzenia - Zwrot odzyskiwania + Fraza odzyskiwania Wyczyść dane Powiadomienia @@ -1482,9 +1482,9 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Urządzenia Osiągnięto limit urządzeń - Obecnie nie można łączyć więcej niż jednego urządzenia. + Obecnie nie można podłączyć więcej niż jednego urządzenia. Nie można odłączyć urządzenia. - Twoje urządzenie zostało rozłączone pomyślnie + Twoje urządzenie zostało odłączone pomyślnie Nie można połączyć urządzenia. Nie podłączyłeś jeszcze żadnych urządzeń Połącz urządzenie (Beta) @@ -1502,7 +1502,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Pobierz Sesję na drugie urządzenie i dotknij Połącz z istniejącym kontem u dołu ekranu docelowego. Jeśli masz już konto na drugim urządzeniu, najpierw musisz je usunąć. Sprawdź, czy poniższe słowa odpowiadają słowom wyświetlanym na drugim urządzeniu. Poczekaj, aż łącze urządzenia zostanie utworzone. Może to potrwać do minuty. - Autoryzować + Autoryzuj Zmień nazwę Odłącz urządzenie @@ -1510,10 +1510,10 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Wpisz imię Twoja fraza odzyskiwania - To jest twoja fraza odzyskiwania. Dzięki niemu możesz przywrócić lub przenieść identyfikator Session na nowe urządzenie. + To jest twoja fraza odzyskiwania. Dzięki niej możesz przywrócić lub przenieść identyfikator Session na nowe urządzenie. Wyczyść wszystkie dane - Spowoduje to trwałe usunięcie wiadomości, Session i kontaktów. + Spowoduje to trwałe usunięcie wiadomości, sesji i kontaktów. Kod QR Wyświetl mój kod QR @@ -1524,8 +1524,8 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Udostępnij kod QR Czy chcesz przywrócić sesję za pomocą %s? - Oddalić - Przywracać + Anuluj + Przywróć Łączność Grupy zamknięte From 14a5ff05461e931ece42a660aa0759f0b8eb3878 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Mon, 23 Nov 2020 12:00:18 +1100 Subject: [PATCH 17/18] temporarily disable image with extension like '.ico' to make it the same iOS (just accept jpg, png and gif) --- .../securesms/linkpreview/LinkPreviewRepository.java | 5 +++++ .../securesms/linkpreview/LinkPreviewUtil.java | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index a849a5e775..9bc08332fb 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -147,6 +147,11 @@ public class LinkPreviewRepository implements InjectableType { imageUrl = Optional.absent(); } + if (imageUrl.isPresent() && !LinkPreviewUtil.isVaildMimeType(imageUrl.get())) { + Log.i(TAG, "Image URL was invalid mime type. Skipping."); + imageUrl = Optional.absent(); + } + callback.onComplete(new Metadata(title, imageUrl)); } }); diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index abe9018061..be7a586e7a 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -96,6 +97,15 @@ public final class LinkPreviewUtil { } } + public static boolean isVaildMimeType(@NonNull String url) { + String[] vaildMimeType = {"jpg", "png", "gif", "jpeg"}; + if (url.contains(".")) { + String extenstion = url.substring(url.lastIndexOf(".") + 1).toLowerCase(); + return Arrays.asList(vaildMimeType).contains(extenstion); + } + return true; + } + public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) { return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString()); } From 0ebb382edd6fce071400af4b921ef1090de6b6d7 Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Mon, 23 Nov 2020 16:59:44 +1100 Subject: [PATCH 18/18] Thread deletion cleanup. --- .../securesms/groups/GroupManager.java | 6 +- .../securesms/loki/activities/HomeActivity.kt | 89 ++++++++----------- .../securesms/loki/api/PublicChatManager.kt | 15 +++- 3 files changed, 50 insertions(+), 60 deletions(-) diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index 9ea420da54..fa0fb72a63 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups; import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -23,7 +24,6 @@ import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; @@ -134,8 +134,8 @@ public class GroupManager { return new GroupActionResult(groupRecipient, threadID); } - public static boolean deleteGroup(@NonNull String groupId, - @NonNull Context context) + public static boolean deleteGroup(@NonNull String groupId, + @NonNull Context context) { final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); final ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index e5f84a673e..f6e28336b4 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -9,7 +9,6 @@ import android.database.Cursor import android.net.Uri import android.os.AsyncTask import android.os.Bundle -import android.os.Handler import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan @@ -18,11 +17,15 @@ import android.view.View import android.widget.RelativeLayout import android.widget.Toast import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.activity_home.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity @@ -71,24 +74,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) - // Process any outstanding deletes - val threadDatabase = DatabaseFactory.getThreadDatabase(this) - val archivedConversationCount = threadDatabase.archivedConversationListCount - if (archivedConversationCount > 0) { - val archivedConversations = threadDatabase.archivedConversationList - archivedConversations.moveToFirst() - fun deleteThreadAtCurrentPosition() { - val threadID = archivedConversations.getLong(archivedConversations.getColumnIndex(ThreadDatabase.ID)) - AsyncTask.execute { - threadDatabase.deleteConversation(threadID) - (applicationContext as ApplicationContext).messageNotifier.updateNotification(this) - } - } - deleteThreadAtCurrentPosition() - while (archivedConversations.moveToNext()) { - deleteThreadAtCurrentPosition() - } - } // Double check that the long poller is up (applicationContext as ApplicationContext).startPollingIfNeeded() // Set content view @@ -341,58 +326,56 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val threadID = thread.threadId val recipient = thread.recipient val threadDB = DatabaseFactory.getThreadDatabase(this) - val deleteThread = Runnable { - //TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager - AsyncTask.execute { - val publicChat = DatabaseFactory.getLokiThreadDatabase(this@HomeActivity).getPublicChat(threadID) - if (publicChat != null) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(this@HomeActivity) - apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server) - apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) - apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server) - ApplicationContext.getInstance(this@HomeActivity).publicChatAPI!!.leave(publicChat.channel, publicChat.server) - - //FIXME Group deletion should be synchronized with the related thread deletion. - val groupId = threadDB.getRecipientForThreadId(threadID)!!.address.serialize() - GroupManager.deleteGroup(groupId, this@HomeActivity) - } - threadDB.deleteConversation(threadID) - ApplicationContext.getInstance(this@HomeActivity).messageNotifier.updateNotification(this@HomeActivity) - } - } val dialogMessage = if (recipient.isGroupRecipient) R.string.activity_home_leave_group_dialog_message else R.string.activity_home_delete_conversation_dialog_message val dialog = AlertDialog.Builder(this) dialog.setMessage(dialogMessage) - dialog.setPositiveButton(R.string.yes) { _, _ -> + dialog.setPositiveButton(R.string.yes) { _, _ -> lifecycleScope.launch(Dispatchers.Main) { + val context = this@HomeActivity as Context + val isClosedGroup = recipient.address.isClosedGroup // Send a leave group message if this is an active closed group - if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) { + if (isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) { var isSSKBasedClosedGroup: Boolean var groupPublicKey: String? try { groupPublicKey = ClosedGroupsProtocol.doubleDecodeGroupID(recipient.address.toString()).toHexString() - isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey) + isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey) } catch (e: IOException) { groupPublicKey = null isSSKBasedClosedGroup = false } if (isSSKBasedClosedGroup) { - ClosedGroupsProtocol.leave(this, groupPublicKey!!) - } else if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) { - Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() - return@setPositiveButton + ClosedGroupsProtocol.leave(context, groupPublicKey!!) + } else if (!ClosedGroupsProtocol.leaveLegacyGroup(context, recipient)) { + Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() + return@launch + } + } + + withContext(Dispatchers.IO) { + val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) + //TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager + if (publicChat != null) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server) + apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) + apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server) + + ApplicationContext.getInstance(context).publicChatAPI!! + .leave(publicChat.channel, publicChat.server) + + ApplicationContext.getInstance(context).publicChatManager + .removeChat(publicChat.server, publicChat.channel) + } else { + threadDB.deleteConversation(threadID) } + ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) } - // Archive the conversation and then delete it after 10 seconds (the case where the - // app was closed before the conversation could be deleted is handled in onCreate) - threadDB.archiveConversation(threadID) - val delay = if (isClosedGroup) 10000L else 1000L - val handler = Handler() - handler.postDelayed(deleteThread, delay) + // Notify the user val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message - Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show() - } + Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show() + }} dialog.setNegativeButton(R.string.no) { _, _ -> // Do nothing } diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt index a4281704e5..ca7352ec8d 100644 --- a/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt @@ -5,9 +5,6 @@ import android.database.ContentObserver import android.graphics.Bitmap import android.text.TextUtils import androidx.annotation.WorkerThread -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.functional.map import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseFactory @@ -96,11 +93,21 @@ class PublicChatManager(private val context: Context) { ApplicationContext.getInstance(context).publicChatAPI?.setDisplayName(displayName, server) } // Start polling - Util.runOnMain{ startPollersIfNeeded() } + Util.runOnMain { startPollersIfNeeded() } return chat } + public fun removeChat(server: String, channel: Long) { + val threadDB = DatabaseFactory.getThreadDatabase(context) + val groupId = PublicChat.getId(channel, server) + val threadId = GroupManager.getOpenGroupThreadID(groupId, context) + val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize() + GroupManager.deleteGroup(groupAddress, context) + + Util.runOnMain { startPollersIfNeeded() } + } + private fun refreshChatsAndPollers() { val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats() val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) }