From 2246a5d9ce01f52fefda7e51457f221f7d18782d Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Wed, 19 Apr 2023 23:07:36 +1000 Subject: [PATCH 001/112] feat: introduce new pns models and wire kotlinx serialization to apply in libsession --- app/build.gradle | 15 ------ .../securesms/ApplicationContext.java | 8 +-- ...nManager.kt => PushNotificationManager.kt} | 2 +- .../notifications/PushNotificationService.kt | 4 +- build.gradle | 6 ++- libsession/build.gradle | 2 + .../sending_receiving/notifications/Models.kt | 52 +++++++++++++++++++ .../notifications/PushNotificationAPI.kt | 4 +- 8 files changed, 67 insertions(+), 26 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/notifications/{LokiPushNotificationManager.kt => PushNotificationManager.kt} (99%) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt diff --git a/app/build.gradle b/app/build.gradle index b861220339..a2bb940922 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,18 +1,3 @@ -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath "com.android.tools.build:gradle:$gradlePluginVersion" - classpath files('libs/gradle-witness.jar') - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" - classpath "com.google.gms:google-services:$googleServicesVersion" - classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" - } -} - apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'witness' diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index a605a84922..2fc53dc244 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -76,7 +76,7 @@ import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.notifications.BackgroundPollWorker; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.FcmUtils; -import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager; +import org.thoughtcrime.securesms.notifications.PushNotificationManager; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -455,9 +455,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { if (TextSecurePreferences.isUsingFCM(this)) { - LokiPushNotificationManager.register(token, userPublicKey, this, force); + PushNotificationManager.register(token, userPublicKey, this, force); } else { - LokiPushNotificationManager.unregister(token, this); + PushNotificationManager.unregister(token, this); } }); @@ -536,7 +536,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO public void clearAllData(boolean isMigratingToV2KeyPair) { String token = TextSecurePreferences.getFCMToken(this); if (token != null && !token.isEmpty()) { - LokiPushNotificationManager.unregister(token, this); + PushNotificationManager.unregister(token, this); } if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) { firebaseInstanceIdJob.cancel(null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationManager.kt similarity index 99% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationManager.kt index adaec0e17a..5a53f7af22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationManager.kt @@ -15,7 +15,7 @@ import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.retryIfNeeded import org.thoughtcrime.securesms.dependencies.DatabaseComponent -object LokiPushNotificationManager { +object PushNotificationManager { private val maxRetryCount = 4 private val tokenExpirationInterval = 12 * 60 * 60 * 1000 diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index fc399d293e..b7a92e3b76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -18,7 +18,7 @@ class PushNotificationService : FirebaseMessagingService() { super.onNewToken(token) Log.d("Loki", "New FCM token: $token.") val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return - LokiPushNotificationManager.register(token, userPublicKey, this, false) + PushNotificationManager.register(token, userPublicKey, this, false) } override fun onMessageReceived(message: RemoteMessage) { @@ -53,6 +53,6 @@ class PushNotificationService : FirebaseMessagingService() { super.onDeletedMessages() val token = TextSecurePreferences.getFCMToken(this)!! val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return - LokiPushNotificationManager.register(token, userPublicKey, this, true) + PushNotificationManager.register(token, userPublicKey, this, true) } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7e7e14f00f..64a1fd4d02 100644 --- a/build.gradle +++ b/build.gradle @@ -5,9 +5,11 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:$gradlePluginVersion" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath "com.google.gms:google-services:$googleServicesVersion" classpath files('libs/gradle-witness.jar') + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" + classpath "com.google.gms:google-services:$googleServicesVersion" + classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" } } diff --git a/libsession/build.gradle b/libsession/build.gradle index dd8959958e..2241cfb3c5 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'kotlin-android' + id 'kotlinx-serialization' } android { @@ -41,6 +42,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" testImplementation "junit:junit:$junitVersion" testImplementation 'org.assertj:assertj-core:3.11.1' diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt new file mode 100644 index 0000000000..e37b5b0b60 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt @@ -0,0 +1,52 @@ +package org.session.libsession.messaging.sending_receiving.notifications + +import kotlinx.serialization.Serializable + +@Serializable +data class SubscriptionRequest( + /** the 33-byte account being subscribed to; typically a session ID */ + val pubkey: String, + /** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */ + val session_ed25519: String?, + /** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */ + val subkey_tag: String?, + /** array of integer namespaces to subscribe to, **must be sorted in ascending order** */ + val namespaces: List, + /** if provided and true then notifications will include the body of the message (as long as it isn't too large) */ + val data: Boolean, + /** the signature unix timestamp in seconds, not ms */ + val sig_ts: Long, + /** the 64-byte ed25519 signature */ + val signature: String, + /** the string identifying the notification service, "firebase" for android (currently) */ + val service: String, + /** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */ + val service_info: Map, + /** 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 via libsodium using this key. + * persist it on device */ + val enc_key: String +) + +@Serializable +data class SubscriptionResponse( + val error: Int?, + val message: String?, + val success: Boolean?, + val added: Boolean?, + val updated: Boolean?, +) { + companion object { + /** invalid values, missing reuqired arguments etc, details in message */ + const val UNPARSEABLE_ERROR = 1 + /** the "service" value is not active / valid */ + const val SERVICE_NOT_AVAILABLE = 2 + /** something getting wrong internally talking to the backend */ + const val SERVICE_TIMEOUT = 3 + /** other error processing the subscription (details in the message) */ + const val GENERIC_ERROR = 4 + } + fun isSuccess() = success == true && error == null + fun errorInfo() = if (success == false && error != null) { + true to message + } else false to null +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index f793cd6e4b..e49375babe 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -16,8 +16,8 @@ import org.session.libsignal.utilities.Log @SuppressLint("StaticFieldLeak") object PushNotificationAPI { val context = MessagingModuleConfiguration.shared.context - val server = "https://live.apns.getsession.org" - val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" + val server = "https://push.getsession.org" + val serverPublicKey: String = TODO("get the new server pubkey here") private val maxRetryCount = 4 private val tokenExpirationInterval = 12 * 60 * 60 * 1000 From 8d4f2445f28f1f9042f28c4e8fe87b3761e58486 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:12:38 +1000 Subject: [PATCH 002/112] feat: add support for firebase and split out google services as a dependency for only the play version of the app. Add support for requests in new pn server --- .gitignore | 2 +- app/build.gradle | 268 ++++++++--------- app/src/main/AndroidManifest.xml | 8 - .../securesms/ApplicationContext.java | 36 +-- .../securesms/crypto/IdentityKeyUtil.java | 1 + .../dependencies/InjectableType.java | 4 - .../securesms/dependencies/PushComponent.kt | 12 + .../securesms/home/HomeActivity.kt | 6 +- .../securesms/jobs/JobManagerFactories.java | 1 - .../securesms/jobs/UpdateApkJob.java | 271 ------------------ .../securesms/notifications/PushManager.kt | 6 + .../notifications/PushNotificationService.kt | 9 +- .../securesms/onboarding/PNModeActivity.kt | 2 +- .../NotificationsPreferenceFragment.java | 2 +- .../service/UpdateApkRefreshListener.java | 47 --- app/src/play/AndroidManifest.xml | 16 ++ .../securesms/notifications/FcmUtils.kt | 4 +- .../notifications/FirebasePushManager.kt | 142 +++++++++ .../notifications/FirebasePushModule.kt | 21 ++ .../sending_receiving/notifications/Models.kt | 27 +- .../notifications/PushNotificationAPI.kt | 6 +- 21 files changed, 381 insertions(+), 510 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/UpdateApkJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java create mode 100644 app/src/play/AndroidManifest.xml rename app/src/{main/java => play/kotlin}/org/thoughtcrime/securesms/notifications/FcmUtils.kt (90%) create mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt create mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt diff --git a/.gitignore b/.gitignore index 01ec4c41ce..1fe35a0e7b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ signing.properties ffpr *.sh pkcs11.password -play +app/play diff --git a/app/build.gradle b/app/build.gradle index a2bb940922..a8a869fc5a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,6 @@ apply plugin: 'kotlin-android' apply plugin: 'witness' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-parcelize' -apply plugin: 'com.google.gms.google-services' apply plugin: 'kotlinx-serialization' apply plugin: 'dagger.hilt.android.plugin' @@ -11,6 +10,140 @@ configurations.all { exclude module: "commons-logging" } +def canonicalVersionCode = 335 +def canonicalVersionName = "1.16.7" + +def postFixSize = 10 +def abiPostFix = ['armeabi-v7a' : 1, + 'arm64-v8a' : 2, + 'x86' : 3, + 'x86_64' : 4, + 'universal' : 5] + +android { + compileSdkVersion androidCompileSdkVersion + namespace 'network.loki.messenger' + useLibrary 'org.apache.http.legacy' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + packagingOptions { + exclude 'LICENSE.txt' + exclude 'LICENSE' + exclude 'NOTICE' + exclude 'asm-license.txt' + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE' + exclude 'META-INF/proguard/androidx-annotations.pro' + } + + splits { + abi { + enable true + reset() + include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + universalApk true + } + } + + defaultConfig { + versionCode canonicalVersionCode * postFixSize + versionName canonicalVersionName + + minSdkVersion androidMinimumSdkVersion + targetSdkVersion androidTargetSdkVersion + + multiDexEnabled = true + + vectorDrawables.useSupportLibrary = true + project.ext.set("archivesBaseName", "session") + + buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L" + buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" + buildConfigField "int", "CONTENT_PROXY_PORT", "443" + buildConfigField "String", "USER_AGENT", "\"OWA\"" + buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' + buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" + + resConfigs autoResConfig() + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + } + } + + sourceSets { + String sharedTestDir = 'src/sharedTest/java' + test.java.srcDirs += sharedTestDir + androidTest.java.srcDirs += sharedTestDir + } + + buildTypes { + release { + minifyEnabled false + } + debug { + minifyEnabled false + } + } + + flavorDimensions "distribution" + productFlavors { + play { + dimension "distribution" + apply plugin: 'com.google.gms.google-services' + ext.websiteUpdateUrl = "null" + buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" + buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" + } + + website { + dimension "distribution" + ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases" + buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" + buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" + } + } + + applicationVariants.all { variant -> + variant.outputs.each { output -> + def abiName = output.getFilter("ABI") ?: 'universal' + def postFix = abiPostFix.get(abiName, 0) + + if (postFix >= postFixSize) throw new AssertionError("postFix is too large") + output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk" + output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix + } + } + + lintOptions { + abortOnError true + baseline file("lint-baseline.xml") + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + buildFeatures { + dataBinding true + viewBinding true + } +} + dependencies { implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation 'androidx.recyclerview:recyclerview:1.2.1' @@ -34,7 +167,7 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.work:work-runtime-ktx:2.7.1" - implementation ("com.google.firebase:firebase-messaging:18.0.0") { + playImplementation ("com.google.firebase:firebase-messaging:18.0.0") { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -145,137 +278,6 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' } -def canonicalVersionCode = 335 -def canonicalVersionName = "1.16.7" - -def postFixSize = 10 -def abiPostFix = ['armeabi-v7a' : 1, - 'arm64-v8a' : 2, - 'x86' : 3, - 'x86_64' : 4, - 'universal' : 5] - -android { - compileSdkVersion androidCompileSdkVersion - namespace 'network.loki.messenger' - useLibrary 'org.apache.http.legacy' - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - packagingOptions { - exclude 'LICENSE.txt' - exclude 'LICENSE' - exclude 'NOTICE' - exclude 'asm-license.txt' - exclude 'META-INF/LICENSE' - exclude 'META-INF/NOTICE' - exclude 'META-INF/proguard/androidx-annotations.pro' - } - - splits { - abi { - enable true - reset() - include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - universalApk true - } - } - - defaultConfig { - versionCode canonicalVersionCode * postFixSize - versionName canonicalVersionName - - minSdkVersion androidMinimumSdkVersion - targetSdkVersion androidTargetSdkVersion - - multiDexEnabled = true - - vectorDrawables.useSupportLibrary = true - project.ext.set("archivesBaseName", "session") - - buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L" - buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" - buildConfigField "int", "CONTENT_PROXY_PORT", "443" - buildConfigField "String", "USER_AGENT", "\"OWA\"" - buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' - buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" - - resConfigs autoResConfig() - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - // The following argument makes the Android Test Orchestrator run its - // "pm clear" command after each test invocation. This command ensures - // that the app's state is completely cleared between tests. - testInstrumentationRunnerArguments clearPackageData: 'true' - testOptions { - execution 'ANDROIDX_TEST_ORCHESTRATOR' - } - } - - sourceSets { - String sharedTestDir = 'src/sharedTest/java' - test.java.srcDirs += sharedTestDir - androidTest.java.srcDirs += sharedTestDir - } - - buildTypes { - release { - minifyEnabled false - } - debug { - minifyEnabled false - } - } - - flavorDimensions "distribution" - productFlavors { - play { - ext.websiteUpdateUrl = "null" - buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" - buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" - } - - website { - ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases" - buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" - buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" - } - } - - applicationVariants.all { variant -> - variant.outputs.each { output -> - def abiName = output.getFilter("ABI") ?: 'universal' - def postFix = abiPostFix.get(abiName, 0) - - if (postFix >= postFixSize) throw new AssertionError("postFix is too large") - output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk" - output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix - } - } - - lintOptions { - abortOnError true - baseline file("lint-baseline.xml") - } - - testOptions { - unitTests { - includeAndroidResources = true - } - } - - buildFeatures { - dataBinding true - viewBinding true - } -} - static def getLastCommitTimestamp() { new ByteArrayOutputStream().withStream { os -> return os.toString() + "000" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6755addc03..7912ab9739 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -306,14 +306,6 @@ android:name="android.support.PARENT_ACTIVITY" android:value="org.thoughtcrime.securesms.home.HomeActivity" /> - - - - - { - if (!task.isSuccessful()) { - Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException()); - return Unit.INSTANCE; - } - String token = task.getResult().getToken(); - String userPublicKey = TextSecurePreferences.getLocalNumber(this); - if (userPublicKey == null) return Unit.INSTANCE; - - AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { - if (TextSecurePreferences.isUsingFCM(this)) { - PushNotificationManager.register(token, userPublicKey, this, force); - } else { - PushNotificationManager.unregister(token, this); - } - }); - - return Unit.INSTANCE; - }); + public void registerForPnIfNeeded(final Boolean force) { + pushManager.register(force); } private void setUpPollingIfNeeded() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java index a5333ef5d4..62aaf58f1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java @@ -52,6 +52,7 @@ public class IdentityKeyUtil { public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3"; public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key"; public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key"; + public static final String NOTIFICATION_KEY = "pref_notification_key"; public static final String LOKI_SEED = "loki_seed"; public static final String HAS_MIGRATED_KEY = "has_migrated_keys"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java deleted file mode 100644 index 033b3ef45a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/InjectableType.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.thoughtcrime.securesms.dependencies; - -public interface InjectableType { -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt new file mode 100644 index 0000000000..cd4e00416c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.dependencies + +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.thoughtcrime.securesms.notifications.PushManager + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface PushComponent { + fun providePushManager(): PushManager +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 9b1e01a54a..72df057985 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -1,11 +1,11 @@ package org.thoughtcrime.securesms.home import android.content.BroadcastReceiver +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.ClipData -import android.content.ClipboardManager import android.os.Bundle import android.text.SpannableString import android.widget.Toast @@ -199,7 +199,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), // update things based on TextSecurePrefs (profile info etc) // Set up remaining components if needed val application = ApplicationContext.getInstance(this@HomeActivity) - application.registerForFCMIfNeeded(false) + application.registerForPnIfNeeded(false) if (textSecurePreferences.getLocalNumber() != null) { OpenGroupManager.startPolling() JobQueue.shared.resumePendingJobs() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index ef73325f37..e660a774b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -31,7 +31,6 @@ public final class JobManagerFactories { put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory()); put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application)); - put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory()); }}; factoryKeys.addAll(factoryHashMap.keySet()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/UpdateApkJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/UpdateApkJob.java deleted file mode 100644 index 5b4ce8d13c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/UpdateApkJob.java +++ /dev/null @@ -1,271 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - - -import android.app.DownloadManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import org.session.libsession.messaging.utilities.Data; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.service.UpdateApkReadyListener; -import org.session.libsession.utilities.FileUtils; -import org.session.libsignal.utilities.Hex; -import org.session.libsignal.utilities.JsonUtil; -import org.session.libsession.utilities.TextSecurePreferences; - -import java.io.FileInputStream; -import java.io.IOException; -import java.security.MessageDigest; - -import network.loki.messenger.BuildConfig; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -public class UpdateApkJob extends BaseJob { - - public static final String KEY = "UpdateApkJob"; - - private static final String TAG = UpdateApkJob.class.getSimpleName(); - - public UpdateApkJob() { - this(new Job.Parameters.Builder() - .setQueue("UpdateApkJob") - .addConstraint(NetworkConstraint.KEY) - .setMaxAttempts(3) - .build()); - } - - private UpdateApkJob(@NonNull Job.Parameters parameters) { - super(parameters); - } - - @Override - public @NonNull - Data serialize() { - return Data.EMPTY; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException, PackageManager.NameNotFoundException { - if (!BuildConfig.PLAY_STORE_DISABLED) return; - - Log.i(TAG, "Checking for APK update..."); - - OkHttpClient client = new OkHttpClient(); - Request request = new Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build(); - - Response response = client.newCall(request).execute(); - - if (!response.isSuccessful()) { - throw new IOException("Bad response: " + response.message()); - } - - UpdateDescriptor updateDescriptor = JsonUtil.fromJson(response.body().string(), UpdateDescriptor.class); - byte[] digest = Hex.fromStringCondensed(updateDescriptor.getDigest()); - - Log.i(TAG, "Got descriptor: " + updateDescriptor); - - if (updateDescriptor.getVersionCode() > getVersionCode()) { - DownloadStatus downloadStatus = getDownloadStatus(updateDescriptor.getUrl(), digest); - - Log.i(TAG, "Download status: " + downloadStatus.getStatus()); - - if (downloadStatus.getStatus() == DownloadStatus.Status.COMPLETE) { - Log.i(TAG, "Download status complete, notifying..."); - handleDownloadNotify(downloadStatus.getDownloadId()); - } else if (downloadStatus.getStatus() == DownloadStatus.Status.MISSING) { - Log.i(TAG, "Download status missing, starting download..."); - handleDownloadStart(updateDescriptor.getUrl(), updateDescriptor.getVersionName(), digest); - } - } - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - return e instanceof IOException; - } - - @Override - public void onCanceled() { - Log.w(TAG, "Update check failed"); - } - - private int getVersionCode() throws PackageManager.NameNotFoundException { - PackageManager packageManager = context.getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); - - return packageInfo.versionCode; - } - - private DownloadStatus getDownloadStatus(String uri, byte[] theirDigest) { - DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Query query = new DownloadManager.Query(); - - query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING | DownloadManager.STATUS_SUCCESSFUL); - - long pendingDownloadId = TextSecurePreferences.getUpdateApkDownloadId(context); - byte[] pendingDigest = getPendingDigest(context); - Cursor cursor = downloadManager.query(query); - - try { - DownloadStatus status = new DownloadStatus(DownloadStatus.Status.MISSING, -1); - - while (cursor != null && cursor.moveToNext()) { - int jobStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)); - String jobRemoteUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI)); - long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)); - byte[] digest = getDigestForDownloadId(downloadId); - - if (jobRemoteUri != null && jobRemoteUri.equals(uri) && downloadId == pendingDownloadId) { - - if (jobStatus == DownloadManager.STATUS_SUCCESSFUL && - digest != null && pendingDigest != null && - MessageDigest.isEqual(pendingDigest, theirDigest) && - MessageDigest.isEqual(digest, theirDigest)) - { - return new DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId); - } else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) { - status = new DownloadStatus(DownloadStatus.Status.PENDING, downloadId); - } - } - } - - return status; - } finally { - if (cursor != null) cursor.close(); - } - } - - private void handleDownloadStart(String uri, String versionName, byte[] digest) { - DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(uri)); - - downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); - downloadRequest.setTitle("Downloading Signal update"); - downloadRequest.setDescription("Downloading Signal " + versionName); - downloadRequest.setVisibleInDownloadsUi(false); - downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk"); - downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN); - - long downloadId = downloadManager.enqueue(downloadRequest); - TextSecurePreferences.setUpdateApkDownloadId(context, downloadId); - TextSecurePreferences.setUpdateApkDigest(context, Hex.toStringCondensed(digest)); - } - - private void handleDownloadNotify(long downloadId) { - Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId); - - new UpdateApkReadyListener().onReceive(context, intent); - } - - private @Nullable byte[] getDigestForDownloadId(long downloadId) { - try { - DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor()); - byte[] digest = FileUtils.getFileDigest(fin); - - fin.close(); - - return digest; - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - private @Nullable byte[] getPendingDigest(Context context) { - try { - String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context); - - if (encodedDigest == null) return null; - - return Hex.fromStringCondensed(encodedDigest); - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - private static class UpdateDescriptor { - @JsonProperty - private int versionCode; - - @JsonProperty - private String versionName; - - @JsonProperty - private String url; - - @JsonProperty - private String sha256sum; - - - public int getVersionCode() { - return versionCode; - } - - public String getVersionName() { - return versionName; - } - - public String getUrl() { - return url; - } - - public @NonNull String toString() { - return "[" + versionCode + ", " + versionName + ", " + url + "]"; - } - - public String getDigest() { - return sha256sum; - } - } - - private static class DownloadStatus { - enum Status { - PENDING, - COMPLETE, - MISSING - } - - private final Status status; - private final long downloadId; - - DownloadStatus(Status status, long downloadId) { - this.status = status; - this.downloadId = downloadId; - } - - public Status getStatus() { - return status; - } - - public long getDownloadId() { - return downloadId; - } - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull UpdateApkJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new UpdateApkJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt new file mode 100644 index 0000000000..36aca1c912 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt @@ -0,0 +1,6 @@ +package org.thoughtcrime.securesms.notifications + +interface PushManager { + fun register(force: Boolean) + fun unregister(token: String) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index b7a92e3b76..6e4f8d104a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -4,6 +4,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters @@ -11,14 +12,18 @@ import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log +import javax.inject.Inject +@AndroidEntryPoint class PushNotificationService : FirebaseMessagingService() { + @Inject lateinit var pushManager: PushManager + override fun onNewToken(token: String) { super.onNewToken(token) Log.d("Loki", "New FCM token: $token.") - val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return - PushNotificationManager.register(token, userPublicKey, this, false) + TextSecurePreferences.getLocalNumber(this) ?: return + pushManager.register(true) } override fun onMessageReceived(message: RemoteMessage) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index 9cf9c3d049..e440ba21d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -160,7 +160,7 @@ class PNModeActivity : BaseActionBarActivity() { TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView)) val application = ApplicationContext.getInstance(this) application.startPollingIfNeeded() - application.registerForFCMIfNeeded(true) + application.registerForPnIfNeeded(true) val intent = Intent(this, HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK show(intent) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index 9ae78fc5cf..efc20aca4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -39,7 +39,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme this.findPreference(fcmKey) .setOnPreferenceChangeListener((preference, newValue) -> { TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue); - ApplicationContext.getInstance(getContext()).registerForFCMIfNeeded(true); + ApplicationContext.getInstance(getContext()).registerForPnIfNeeded(true); return true; }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java deleted file mode 100644 index 187713df95..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkRefreshListener.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.thoughtcrime.securesms.service; - - -import android.content.Context; -import android.content.Intent; -import org.session.libsignal.utilities.Log; - -import org.thoughtcrime.securesms.ApplicationContext; -import network.loki.messenger.BuildConfig; -import org.thoughtcrime.securesms.jobs.UpdateApkJob; -import org.session.libsession.utilities.TextSecurePreferences; - -import java.util.concurrent.TimeUnit; - -public class UpdateApkRefreshListener extends PersistentAlarmManagerListener { - - private static final String TAG = UpdateApkRefreshListener.class.getSimpleName(); - - private static final long INTERVAL = TimeUnit.HOURS.toMillis(6); - - @Override - protected long getNextScheduledExecutionTime(Context context) { - return TextSecurePreferences.getUpdateApkRefreshTime(context); - } - - @Override - protected long onAlarm(Context context, long scheduledTime) { - Log.i(TAG, "onAlarm..."); - - if (scheduledTime != 0 && BuildConfig.PLAY_STORE_DISABLED) { - Log.i(TAG, "Queueing APK update job..."); - ApplicationContext.getInstance(context) - .getJobManager() - .add(new UpdateApkJob()); - } - - long newTime = System.currentTimeMillis() + INTERVAL; - TextSecurePreferences.setUpdateApkRefreshTime(context, newTime); - - return newTime; - } - - public static void schedule(Context context) { - new UpdateApkRefreshListener().onReceive(context, new Intent()); - } - -} diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml new file mode 100644 index 0000000000..f5b54fd47e --- /dev/null +++ b/app/src/play/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt rename to app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt index 87a9efc0de..de3d14d430 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmUtils.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt @@ -13,7 +13,5 @@ fun getFcmInstanceId(body: (Task)->Unit): Job = MainScope().la // wait for task to complete while we are active } if (!isActive) return@launch // don't 'complete' task if we were canceled - withContext(Dispatchers.Main) { - body(task) - } + body(task) } \ No newline at end of file diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt new file mode 100644 index 0000000000..0865c25c90 --- /dev/null +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -0,0 +1,142 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.interfaces.AEAD +import com.goterl.lazysodium.interfaces.Sign +import com.goterl.lazysodium.utils.Key +import com.goterl.lazysodium.utils.KeyPair +import kotlinx.coroutines.Job +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.map +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.Version +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Namespace +import org.session.libsignal.utilities.retryIfNeeded +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.KeyPairUtilities + +class FirebasePushManager(private val context: Context, private val prefs: TextSecurePreferences): PushManager { + + companion object { + private const val maxRetryCount = 4 + private const val tokenExpirationInterval = 12 * 60 * 60 * 1000 + } + + private var firebaseInstanceIdJob: Job? = null + private val sodium = LazySodiumAndroid(SodiumAndroid()) + + private fun getOrCreateNotificationKey(): Key { + if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { + // generate the key and store it + val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) + IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) + } + return Key.fromHexString( + IdentityKeyUtil.retrieve( + context, + IdentityKeyUtil.NOTIFICATION_KEY + ) + ) + } + + override fun register(force: Boolean) { + val currentInstanceIdJob = firebaseInstanceIdJob + if (currentInstanceIdJob != null && currentInstanceIdJob.isActive && !force) return + + if (force && currentInstanceIdJob != null) { + currentInstanceIdJob.cancel(null) + } + + firebaseInstanceIdJob = getFcmInstanceId { task -> + // context in here is Dispatchers.IO + if (!task.isSuccessful) { + Log.w( + "Loki", + "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception + ) + return@getFcmInstanceId + } + val token: String = task.result?.token ?: return@getFcmInstanceId + val userPublicKey = getLocalNumber(context) ?: return@getFcmInstanceId + val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@getFcmInstanceId + if (prefs.isUsingFCM()) { + register(token, userPublicKey, userEdKey, force) + } else { + unregister(token) + } + } + } + + override fun unregister(token: String) { + TODO("Not yet implemented") + } + + fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { + val oldToken = TextSecurePreferences.getFCMToken(context) + val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) + if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } + + val pnKey = getOrCreateNotificationKey() + + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes) + val requestParameters = SubscriptionRequest ( + pubkey = publicKey, + session_ed25519 = userEd25519Key.publicKey.asHexString, + namespaces = listOf(Namespace.DEFAULT), + data = true, // only permit data subscription for now (?) + service = "firebase", + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + enc_key = pnKey.asHexString, + ) + + val url = "${PushNotificationAPI.server}/subscribe" + val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters)) + val request = Request.Builder().url(url).post(body) + retryIfNeeded(maxRetryCount) { + getResponseBody(request.build()).map { response -> + if (response.isSuccess()) { + TextSecurePreferences.setIsUsingFCM(context, true) + TextSecurePreferences.setFCMToken(context, token) + TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) + } else { + val (_, message) = response.errorInfo() + Log.d("Loki", "Couldn't register for FCM due to error: $message.") + } + }.fail { exception -> + Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.") + } + } + } + + private fun getResponseBody(request: Request): Promise { + return OnionRequestAPI.sendOnionRequest(request, + PushNotificationAPI.server, + PushNotificationAPI.serverPublicKey, Version.V4).map { response -> + Json.decodeFromStream(response.body!!.inputStream()) + } + } + + +} \ No newline at end of file diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt new file mode 100644 index 0000000000..983973d964 --- /dev/null +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt @@ -0,0 +1,21 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import org.session.libsession.utilities.TextSecurePreferences +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object FirebasePushModule { + @Provides + @Singleton + fun provideFirebasePushManager( + @ApplicationContext context: Context, + prefs: TextSecurePreferences, + ): PushManager = FirebasePushManager(context, prefs) +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt index e37b5b0b60..78b6cd4a13 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt @@ -1,7 +1,15 @@ package org.session.libsession.messaging.sending_receiving.notifications +import com.goterl.lazysodium.utils.Key +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +/** + * N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server. + * Changing the variable names will break how data is serialized/deserialized. + * If it's less than ideally named we can use [SerialName] + */ + @Serializable data class SubscriptionRequest( /** the 33-byte account being subscribed to; typically a session ID */ @@ -9,7 +17,7 @@ data class SubscriptionRequest( /** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */ val session_ed25519: String?, /** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */ - val subkey_tag: String?, + val subkey_tag: String? = null, /** array of integer namespaces to subscribe to, **must be sorted in ascending order** */ val namespaces: List, /** if provided and true then notifications will include the body of the message (as long as it isn't too large) */ @@ -46,7 +54,18 @@ data class SubscriptionResponse( const val GENERIC_ERROR = 4 } fun isSuccess() = success == true && error == null - fun errorInfo() = if (success == false && error != null) { - true to message - } else false to null + fun errorInfo() = if (success != true && error != null) { + error to message + } else null to null +} + +@Serializable +data class PushNotificationServerObject( + val enc_payload: String, + val spns: Int, +) { + fun decryptPayload(key: Key): Any { + + TODO() + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index e49375babe..cf037b019b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -9,15 +9,17 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.Version import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.retryIfNeeded @SuppressLint("StaticFieldLeak") object PushNotificationAPI { val context = MessagingModuleConfiguration.shared.context val server = "https://push.getsession.org" val serverPublicKey: String = TODO("get the new server pubkey here") + private val legacyServer = "https://live.apns.getsession.org" + private val legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" private val maxRetryCount = 4 private val tokenExpirationInterval = 12 * 60 * 60 * 1000 @@ -94,7 +96,7 @@ object PushNotificationAPI { val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response -> + OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2).map { response -> val code = response.info["code"] as? Int if (code == null || code == 0) { Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.") From 7762d534bbc8b54a13f3bdb0867e3f6cce73ba82 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:25:03 +1000 Subject: [PATCH 003/112] feat: add no op push manager for de-googled --- .../linkpreview/LinkPreviewRepository.java | 6 +++--- .../notifications/PushNotificationService.kt | 7 +++++++ .../securesms/notifications/NoOpPushManager.kt | 14 ++++++++++++++ .../securesms/notifications/NoOpPushModule.kt | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) rename app/src/{main/java => play/kotlin}/org/thoughtcrime/securesms/notifications/PushNotificationService.kt (88%) create mode 100644 app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt create mode 100644 app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 697f6718c2..6dcc928c99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.linkpreview; +import static org.session.libsession.utilities.Util.readFully; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -8,8 +10,6 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.gms.common.util.IOUtils; - import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment; @@ -148,7 +148,7 @@ public class LinkPreviewRepository { InputStream bodyStream = response.body().byteStream(); controller.setStream(bodyStream); - byte[] data = IOUtils.readInputStreamFully(bodyStream); + byte[] data = readFully(bodyStream); Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); Optional thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaTypes.IMAGE_JPEG); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt rename to app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 6e4f8d104a..6bf18131e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -28,6 +28,13 @@ class PushNotificationService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Log.d("Loki", "Received a push notification.") + if (message.data.containsKey("spns")) { + // assume this is the new push notification content + // deal with the enc payload (probably decrypting through the PushManager? + Log.d("Loki", "TODO: deal with the enc_payload\n${message.data["enc_payload"]}") + pushManager.decrypt(message.data) + return + } val base64EncodedData = message.data?.get("ENCRYPTED_DATA") val data = base64EncodedData?.let { Base64.decode(it) } if (data != null) { diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt new file mode 100644 index 0000000000..edbf7d710d --- /dev/null +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.notifications + +import org.session.libsignal.utilities.Log + +class NoOpPushManager: PushManager { + + override fun register(force: Boolean) { + Log.d("NoOpPushManager", "Push notifications not supported, not registering for push notifications") + } + + override fun unregister(token: String) { + Log.d("NoOpPushManager", "Push notifications not supported, not unregistering for push notifications") + } +} \ No newline at end of file diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt new file mode 100644 index 0000000000..1c8f2f936c --- /dev/null +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt @@ -0,0 +1,15 @@ +package org.thoughtcrime.securesms.notifications + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class NoOpPushModule { + @Provides + @Singleton + fun provideNoOpManager(): PushManager = NoOpPushManager() +} \ No newline at end of file From d2e80c3157c368d1cd9c7bc330a6bb21f295ec6b Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:17:18 +1100 Subject: [PATCH 004/112] feat: re-add bencode utility and fix tests to use bytearray instead of assuming utf-8 encoding for strings --- .../notifications/FirebasePushManager.kt | 36 ++++ .../notifications/FirebasePushModule.kt | 10 +- .../notifications/PushNotificationService.kt | 4 +- .../libsession/utilities/bencode/Bencode.kt | 169 ++++++++++++++++++ .../libsession/utilities/BencoderTest.kt | 107 +++++++++++ 5 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt create mode 100644 libsession/src/test/java/org/session/libsession/utilities/BencoderTest.kt diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 0865c25c90..39705c75d6 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -19,11 +19,16 @@ import okhttp3.RequestBody import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse +import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.Version import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber +import org.session.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeDict +import org.session.libsession.utilities.bencode.BencodeList +import org.session.libsession.utilities.bencode.BencodeString import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace @@ -55,6 +60,37 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS ) } + fun decrypt(encPayload: ByteArray) { + val encKey = getOrCreateNotificationKey() + val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val decrypted = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) + ?: return Log.e("Loki", "Failed to decrypt push notification") + val bencoded = Bencode.Decoder(decrypted) + val expectedList = (bencoded.decode() as? BencodeList) + ?: return Log.e("Loki", "Failed to decode bencoded list from payload") + + val (metadata, content) = expectedList.values + val metadataDict = (metadata as? BencodeDict)?.values + ?: return Log.e("Loki", "Failed to decode metadata dict") + + val push = """ + Push metadata received was: + @: ${metadataDict["@"]} + #: ${metadataDict["#"]} + n: ${metadataDict["n"]} + l: ${metadataDict["l"]} + B: ${metadataDict["B"]} + """.trimIndent() + + Log.d("Loki", "push") + + val contentBytes = (content as? BencodeString)?.value + ?: return Log.e("Loki", "Failed to decode content string") + + // TODO: something with contentBytes + } + override fun register(force: Boolean) { val currentInstanceIdJob = firebaseInstanceIdJob if (currentInstanceIdJob != null && currentInstanceIdJob.isActive && !force) return diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt index 983973d964..30045d6c25 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,5 +18,12 @@ object FirebasePushModule { fun provideFirebasePushManager( @ApplicationContext context: Context, prefs: TextSecurePreferences, - ): PushManager = FirebasePushManager(context, prefs) + ) = FirebasePushManager(context, prefs) +} + +@Module +@InstallIn(SingletonComponent::class) +abstract class FirebaseBindingModule { + @Binds + abstract fun bindPushManager(firebasePushManager: FirebasePushManager): PushManager } \ No newline at end of file diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 6bf18131e9..4c268ce180 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -17,7 +17,7 @@ import javax.inject.Inject @AndroidEntryPoint class PushNotificationService : FirebaseMessagingService() { - @Inject lateinit var pushManager: PushManager + @Inject lateinit var pushManager: FirebasePushManager override fun onNewToken(token: String) { super.onNewToken(token) @@ -32,7 +32,7 @@ class PushNotificationService : FirebaseMessagingService() { // assume this is the new push notification content // deal with the enc payload (probably decrypting through the PushManager? Log.d("Loki", "TODO: deal with the enc_payload\n${message.data["enc_payload"]}") - pushManager.decrypt(message.data) + pushManager.decrypt(Base64.decode(message.data["enc_payload"])) return } val base64EncodedData = message.data?.get("ENCRYPTED_DATA") diff --git a/libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt b/libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt new file mode 100644 index 0000000000..427e80691a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/bencode/Bencode.kt @@ -0,0 +1,169 @@ +package org.session.libsession.utilities.bencode + +import java.util.LinkedList + +object Bencode { + class Decoder(source: ByteArray) { + + private val iterator = LinkedList().apply { + addAll(source.asIterable()) + } + + /** + * Decode an element based on next marker assumed to be string/int/list/dict or return null + */ + fun decode(): BencodeElement? { + val result = when (iterator.peek()?.toInt()?.toChar()) { + in NUMBERS -> decodeString() + INT_INDICATOR -> decodeInt() + LIST_INDICATOR -> decodeList() + DICT_INDICATOR -> decodeDict() + else -> { + null + } + } + return result + } + + /** + * Decode a string element from iterator assumed to have structure `{length}:{data}` + */ + private fun decodeString(): BencodeString? { + val lengthStrings = buildString { + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) { + append(iterator.pop().toInt().toChar()) + } + } + iterator.pop() // drop `:` + val length = lengthStrings.toIntOrNull(10) ?: return null + val remaining = (0 until length).map { iterator.pop() }.toByteArray() + return BencodeString(remaining) + } + + /** + * Decode an int element from iterator assumed to have structure `i{int}e` + */ + private fun decodeInt(): BencodeElement? { + iterator.pop() // drop `i` + val intString = buildString { + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) { + append(iterator.pop().toInt().toChar()) + } + } + val asInt = intString.toIntOrNull(10) ?: return null + iterator.pop() // drop `e` + return BencodeInteger(asInt) + } + + /** + * Decode a list element from iterator assumed to have structure `l{data}e` + */ + private fun decodeList(): BencodeElement { + iterator.pop() // drop `l` + val listElements = mutableListOf() + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) { + decode()?.let { nextElement -> + listElements += nextElement + } + } + iterator.pop() // drop `e` + return BencodeList(listElements) + } + + /** + * Decode a dict element from iterator assumed to have structure `d{data}e` + */ + private fun decodeDict(): BencodeElement? { + iterator.pop() // drop `d` + val dictElements = mutableMapOf() + while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) { + val key = decodeString() ?: return null + val value = decode() ?: return null + dictElements += key.value.decodeToString() to value + } + iterator.pop() // drop `e` + return BencodeDict(dictElements) + } + + companion object { + private val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') + private const val INT_INDICATOR = 'i' + private const val LIST_INDICATOR = 'l' + private const val DICT_INDICATOR = 'd' + private const val END_INDICATOR = 'e' + private const val SEPARATOR = ':' + } + + } + +} + +sealed class BencodeElement { + abstract fun encode(): ByteArray +} + +fun String.bencode() = BencodeString(this.encodeToByteArray()) +fun Int.bencode() = BencodeInteger(this) + +data class BencodeString(val value: ByteArray): BencodeElement() { + override fun encode(): ByteArray = buildString { + append(value.size.toString()) + append(':') + }.toByteArray() + value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BencodeString + + if (!value.contentEquals(other.value)) return false + + return true + } + + override fun hashCode(): Int { + return value.contentHashCode() + } +} +data class BencodeInteger(val value: Int): BencodeElement() { + override fun encode(): ByteArray = buildString { + append('i') + append(value.toString()) + append('e') + }.toByteArray() +} +data class BencodeList(val values: List): BencodeElement() { + + constructor(vararg values: BencodeElement) : this(values.toList()) + + override fun encode(): ByteArray = "l".toByteArray() + + values.fold(byteArrayOf()) { array, element -> array + element.encode() } + + "e".toByteArray() +} +data class BencodeDict(val values: Map): BencodeElement() { + + constructor(vararg values: Pair) : this(values.toMap()) + + override fun encode(): ByteArray = "d".toByteArray() + + values.entries.fold(byteArrayOf()) { array, (key, value) -> + array + key.bencode().encode() + value.encode() + } + "e".toByteArray() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BencodeDict + + if (values != other.values) return false + + return true + } + + override fun hashCode(): Int { + return values.hashCode() + } + + +} \ No newline at end of file diff --git a/libsession/src/test/java/org/session/libsession/utilities/BencoderTest.kt b/libsession/src/test/java/org/session/libsession/utilities/BencoderTest.kt new file mode 100644 index 0000000000..d96fa6658f --- /dev/null +++ b/libsession/src/test/java/org/session/libsession/utilities/BencoderTest.kt @@ -0,0 +1,107 @@ +package org.session.libsession.utilities + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test +import org.session.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeDict +import org.session.libsession.utilities.bencode.BencodeInteger +import org.session.libsession.utilities.bencode.BencodeList +import org.session.libsession.utilities.bencode.bencode + +class BencoderTest { + + @Test + fun `it should decode a basic string`() { + val basicString = "5:howdy".toByteArray() + val bencoder = Bencode.Decoder(basicString) + val result = bencoder.decode() + assertEquals("howdy".bencode(), result) + } + + @Test + fun `it should decode a basic integer`() { + val basicInteger = "i3e".toByteArray() + val bencoder = Bencode.Decoder(basicInteger) + val result = bencoder.decode() + assertEquals(BencodeInteger(3), result) + } + + @Test + fun `it should decode a list of integers`() { + val basicIntList = "li1ei2ee".toByteArray() + val bencoder = Bencode.Decoder(basicIntList) + val result = bencoder.decode() + assertEquals( + BencodeList( + 1.bencode(), + 2.bencode() + ), + result + ) + } + + @Test + fun `it should decode a basic dict`() { + val basicDict = "d4:spaml1:a1:bee".toByteArray() + val bencoder = Bencode.Decoder(basicDict) + val result = bencoder.decode() + assertEquals( + BencodeDict( + "spam" to BencodeList( + "a".bencode(), + "b".bencode() + ) + ), + result + ) + } + + @Test + fun `it should encode a basic string`() { + val basicString = "5:howdy".toByteArray() + val element = "howdy".bencode() + assertArrayEquals(basicString, element.encode()) + } + + @Test + fun `it should encode a basic int`() { + val basicInt = "i3e".toByteArray() + val element = 3.bencode() + assertArrayEquals(basicInt, element.encode()) + } + + @Test + fun `it should encode a basic list`() { + val basicList = "li1ei2ee".toByteArray() + val element = BencodeList(1.bencode(),2.bencode()) + assertArrayEquals(basicList, element.encode()) + } + + @Test + fun `it should encode a basic dict`() { + val basicDict = "d4:spaml1:a1:bee".toByteArray() + val element = BencodeDict( + "spam" to BencodeList( + "a".bencode(), + "b".bencode() + ) + ) + assertArrayEquals(basicDict, element.encode()) + } + + @Test + fun `it should encode a more complex real world case`() { + val source = "d15:lastReadMessaged66:031122334455667788990011223344556677889900112233445566778899001122i1234568790e66:051122334455667788990011223344556677889900112233445566778899001122i1234568790ee5:seqNoi1ee".toByteArray() + val result = Bencode.Decoder(source).decode() + val expected = BencodeDict( + "lastReadMessage" to BencodeDict( + "051122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode(), + "031122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode() + ), + "seqNo" to BencodeInteger(1) + ) + assertEquals(expected, result) + } + +} \ No newline at end of file From 46acd7878d57f58f1a5c3ba19a5f1902afccbacd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 18 May 2023 18:03:00 -0300 Subject: [PATCH 005/112] New SPNS subscription and notifications Finishes the WIP for subscribing to push notifications and handling the new-style pushes we get. --- .../notifications/FirebasePushManager.kt | 46 +++++++++---------- .../notifications/PushNotificationService.kt | 20 ++++---- .../sending_receiving/notifications/Models.kt | 39 +++++++++++++--- .../notifications/PushNotificationAPI.kt | 2 +- .../messaging/utilities/SodiumUtilities.kt | 2 +- 5 files changed, 69 insertions(+), 40 deletions(-) diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 39705c75d6..12e62cee45 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -8,6 +8,7 @@ import com.goterl.lazysodium.interfaces.Sign import com.goterl.lazysodium.utils.Key import com.goterl.lazysodium.utils.KeyPair import kotlinx.coroutines.Job +import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -17,6 +18,7 @@ import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse import org.session.libsession.messaging.utilities.SodiumUtilities @@ -26,7 +28,6 @@ import org.session.libsession.snode.Version import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.bencode.Bencode -import org.session.libsession.utilities.bencode.BencodeDict import org.session.libsession.utilities.bencode.BencodeList import org.session.libsession.utilities.bencode.BencodeString import org.session.libsignal.utilities.Base64 @@ -60,35 +61,32 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS ) } - fun decrypt(encPayload: ByteArray) { + fun decrypt(encPayload: ByteArray): ByteArray? { val encKey = getOrCreateNotificationKey() val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() - val decrypted = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) - ?: return Log.e("Loki", "Failed to decrypt push notification") + val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) + ?: error("Failed to decrypt push notification") + val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() val bencoded = Bencode.Decoder(decrypted) - val expectedList = (bencoded.decode() as? BencodeList) - ?: return Log.e("Loki", "Failed to decode bencoded list from payload") + val expectedList = (bencoded.decode() as? BencodeList)?.values + ?: error("Failed to decode bencoded list from payload") - val (metadata, content) = expectedList.values - val metadataDict = (metadata as? BencodeDict)?.values - ?: return Log.e("Loki", "Failed to decode metadata dict") + val metadataJson = (expectedList[0] as? BencodeString)?.value + ?: error("no metadata") + val metadata:PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) - val push = """ - Push metadata received was: - @: ${metadataDict["@"]} - #: ${metadataDict["#"]} - n: ${metadataDict["n"]} - l: ${metadataDict["l"]} - B: ${metadataDict["B"]} - """.trimIndent() + val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null + // null content is valid only if we got a "data_too_long" flag + if (content == null) + check(metadata.data_too_long) { "missing message data, but no too-long flag" } + else + check(metadata.data_len == content.size) { "wrong message data size" } - Log.d("Loki", "push") + Log.d("Loki", + "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") - val contentBytes = (content as? BencodeString)?.value - ?: return Log.e("Loki", "Failed to decode content string") - - // TODO: something with contentBytes + return content } override fun register(force: Boolean) { @@ -158,10 +156,10 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) } else { val (_, message) = response.errorInfo() - Log.d("Loki", "Couldn't register for FCM due to error: $message.") + Log.e("Loki", "Couldn't register for FCM due to error: $message.") } }.fail { exception -> - Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.") + Log.e("Loki", "Couldn't register for FCM due to error: ${exception}.") } } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 4c268ce180..bb625bc540 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -28,15 +28,19 @@ class PushNotificationService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Log.d("Loki", "Received a push notification.") - if (message.data.containsKey("spns")) { - // assume this is the new push notification content - // deal with the enc payload (probably decrypting through the PushManager? - Log.d("Loki", "TODO: deal with the enc_payload\n${message.data["enc_payload"]}") - pushManager.decrypt(Base64.decode(message.data["enc_payload"])) - return + val data: ByteArray? = if (message.data.containsKey("spns")) { + // this is a v2 push notification + try { + pushManager.decrypt(Base64.decode(message.data["enc_payload"])) + } catch(e: Exception) { + Log.e("Loki", "Invalid push notification: ${e.message}") + return + } + } else { + // old v1 push notification; we still need this for receiving legacy closed group notifications + val base64EncodedData = message.data?.get("ENCRYPTED_DATA") + base64EncodedData?.let { Base64.decode(it) } } - val base64EncodedData = message.data?.get("ENCRYPTED_DATA") - val data = base64EncodedData?.let { Base64.decode(it) } if (data != null) { try { val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt index 78b6cd4a13..e7a0ad79a2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt @@ -7,7 +7,8 @@ import kotlinx.serialization.Serializable /** * N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server. * Changing the variable names will break how data is serialized/deserialized. - * If it's less than ideally named we can use [SerialName] + * If it's less than ideally named we can use [SerialName], such as for the push metadata which uses + * single-letter keys to be as compact as possible. */ @Serializable @@ -37,11 +38,11 @@ data class SubscriptionRequest( @Serializable data class SubscriptionResponse( - val error: Int?, - val message: String?, - val success: Boolean?, - val added: Boolean?, - val updated: Boolean?, + val error: Int? = null, + val message: String? = null, + val success: Boolean? = null, + val added: Boolean? = null, + val updated: Boolean? = null, ) { companion object { /** invalid values, missing reuqired arguments etc, details in message */ @@ -59,6 +60,32 @@ data class SubscriptionResponse( } else null to null } +@Serializable +data class PushNotificationMetadata( + /** Account ID (such as Session ID or closed group ID) where the message arrived **/ + @SerialName("@") + val account: String, + + /** The hash of the message in the swarm. */ + @SerialName("#") + val msg_hash: String, + + /** The swarm namespace in which this message arrived. */ + @SerialName("n") + val namespace: Int, + + /** The length of the message data. This is always included, even if the message content + * itself was too large to fit into the push notification. */ + @SerialName("l") + val data_len: Int, + + /** This will be true if the data was omitted because it was too long to fit in a push + * notification (around 2.5kB of raw data), in which case the push notification includes + * only this metadata but not the message content itself. */ + @SerialName("B") + val data_too_long : Boolean = false +) + @Serializable data class PushNotificationServerObject( val enc_payload: String, diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index cf037b019b..9016f30ebe 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -17,7 +17,7 @@ import org.session.libsignal.utilities.retryIfNeeded object PushNotificationAPI { val context = MessagingModuleConfiguration.shared.context val server = "https://push.getsession.org" - val serverPublicKey: String = TODO("get the new server pubkey here") + val serverPublicKey: String = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" private val legacyServer = "https://live.apns.getsession.org" private val legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" private val maxRetryCount = 4 diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt index 9a1de4f2d5..079caee235 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt @@ -205,7 +205,7 @@ object SodiumUtilities { } fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? { - val plaintextSize = ciphertext.size - AEAD.CHACHA20POLY1305_ABYTES + val plaintextSize = ciphertext.size - AEAD.XCHACHA20POLY1305_IETF_ABYTES val plaintext = ByteArray(plaintextSize) return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt( plaintext, From 95bb9ee4418e5eb718adb09d756bb25c9d268203 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Wed, 24 May 2023 11:56:38 +1000 Subject: [PATCH 006/112] refactor: remove some unnecessary code for legacy PN registration --- .../notifications/PushNotificationAPI.kt | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index 9016f30ebe..3548ab5816 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -61,27 +61,6 @@ object PushNotificationAPI { } fun register(token: String, publicKey: String, force: Boolean) { - val oldToken = TextSecurePreferences.getFCMToken(context) - val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) - if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } - val parameters = mapOf( "token" to token, "pubKey" to publicKey ) - val url = "$server/register" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response -> - val code = response.info["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setIsUsingFCM(context, true) - TextSecurePreferences.setFCMToken(context, token) - TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) - } else { - Log.d("Loki", "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.") - } - } // Subscribe to all closed groups val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() allClosedGroupPublicKeys.iterator().forEach { closedGroup -> @@ -92,7 +71,7 @@ object PushNotificationAPI { fun performOperation(operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) { if (!TextSecurePreferences.isUsingFCM(context)) { return } val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) - val url = "$server/${operation.rawValue}" + val url = "$legacyServer/${operation.rawValue}" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { From ba6eca2443f06291a3d91e8df90cd65338020f34 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 9 Jun 2023 09:52:11 +0930 Subject: [PATCH 007/112] Add FirebasePushManager#unregister --- .../securesms/notifications/PushManager.kt | 1 - .../notifications/FirebasePushManager.kt | 113 +++++++++++------- .../notifications/NoOpPushManager.kt | 6 +- .../sending_receiving/notifications/Models.kt | 26 ++++ .../utilities/TextSecurePreferences.kt | 2 +- 5 files changed, 100 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt index 36aca1c912..9d4ef23fe9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt @@ -2,5 +2,4 @@ package org.thoughtcrime.securesms.notifications interface PushManager { fun register(force: Boolean) - fun unregister(token: String) } \ No newline at end of file diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 12e62cee45..4f9a6292f3 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import com.google.android.gms.tasks.Task +import com.google.firebase.iid.InstanceIdResult import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.interfaces.AEAD @@ -21,6 +23,8 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI @@ -74,7 +78,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") - val metadata:PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) + val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null // null content is valid only if we got a "data_too_long" flag @@ -90,41 +94,71 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS } override fun register(force: Boolean) { - val currentInstanceIdJob = firebaseInstanceIdJob - if (currentInstanceIdJob != null && currentInstanceIdJob.isActive && !force) return - - if (force && currentInstanceIdJob != null) { - currentInstanceIdJob.cancel(null) + firebaseInstanceIdJob?.apply { + if (force) cancel() else if (isActive) return } - firebaseInstanceIdJob = getFcmInstanceId { task -> - // context in here is Dispatchers.IO - if (!task.isSuccessful) { - Log.w( - "Loki", - "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception - ) - return@getFcmInstanceId - } - val token: String = task.result?.token ?: return@getFcmInstanceId - val userPublicKey = getLocalNumber(context) ?: return@getFcmInstanceId - val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@getFcmInstanceId - if (prefs.isUsingFCM()) { - register(token, userPublicKey, userEdKey, force) - } else { - unregister(token) + firebaseInstanceIdJob = getFcmInstanceId { register(it, force) } + } + + private fun register(task: Task, force: Boolean) { + // context in here is Dispatchers.IO + if (!task.isSuccessful) { + Log.w( + "Loki", + "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception + ) + return + } + + val token: String = task.result?.token ?: return + val userPublicKey = getLocalNumber(context) ?: return + val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return + + when { + prefs.isUsingFCM() -> register(token, userPublicKey, userEdKey, force) + prefs.getFCMToken() != null -> unregister(token, userPublicKey, userEdKey) + } + } + + private fun unregister(token: String, userPublicKey: String, userEdKey: KeyPair) { + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) + + val requestParameters = UnsubscriptionRequest( + pubkey = userPublicKey, + session_ed25519 = userEdKey.publicKey.asHexString, + service = "firebase", + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + ) + + val url = "${PushNotificationAPI.server}/unsubscribe" + val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters)) + val request = Request.Builder().url(url).post(body) + retryIfNeeded(maxRetryCount) { + getResponseBody(request.build()).map { response -> + if (response.success == true) { + TextSecurePreferences.setIsUsingFCM(context, false) + TextSecurePreferences.setFCMToken(context, null) + Log.d("Loki", "Unsubscribe FCM success") + } else { + Log.e("Loki", "Couldn't unregister for FCM due to error: ${response.message}") + } + }.fail { exception -> + Log.e("Loki", "Couldn't unregister for FCM due to error: ${exception}.", exception) } } } - override fun unregister(token: String) { - TODO("Not yet implemented") - } - - fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { + private fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { val oldToken = TextSecurePreferences.getFCMToken(context) val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) - if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } + if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) return val pnKey = getOrCreateNotificationKey() @@ -133,7 +167,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray() val signature = ByteArray(Sign.BYTES) sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes) - val requestParameters = SubscriptionRequest ( + val requestParameters = SubscriptionRequest( pubkey = publicKey, session_ed25519 = userEd25519Key.publicKey.asHexString, namespaces = listOf(Namespace.DEFAULT), @@ -149,9 +183,8 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { response -> + getResponseBody(request.build()).map { response -> if (response.isSuccess()) { - TextSecurePreferences.setIsUsingFCM(context, true) TextSecurePreferences.setFCMToken(context, token) TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) } else { @@ -159,18 +192,16 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS Log.e("Loki", "Couldn't register for FCM due to error: $message.") } }.fail { exception -> - Log.e("Loki", "Couldn't register for FCM due to error: ${exception}.") + Log.e("Loki", "Couldn't register for FCM due to error: ${exception}.", exception) } } } - private fun getResponseBody(request: Request): Promise { - return OnionRequestAPI.sendOnionRequest(request, + private inline fun getResponseBody(request: Request): Promise = + OnionRequestAPI.sendOnionRequest( + request, PushNotificationAPI.server, - PushNotificationAPI.serverPublicKey, Version.V4).map { response -> - Json.decodeFromStream(response.body!!.inputStream()) - } - } - - -} \ No newline at end of file + PushNotificationAPI.serverPublicKey, + Version.V4 + ).map { response -> Json.decodeFromStream(response.body!!.inputStream()) } +} diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt index edbf7d710d..59f1b9df4b 100644 --- a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt @@ -7,8 +7,4 @@ class NoOpPushManager: PushManager { override fun register(force: Boolean) { Log.d("NoOpPushManager", "Push notifications not supported, not registering for push notifications") } - - override fun unregister(token: String) { - Log.d("NoOpPushManager", "Push notifications not supported, not unregistering for push notifications") - } -} \ No newline at end of file +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt index e7a0ad79a2..141202ef66 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt @@ -36,6 +36,24 @@ data class SubscriptionRequest( val enc_key: String ) +@Serializable +data class UnsubscriptionRequest( + /** the 33-byte account being subscribed to; typically a session ID */ + val pubkey: String, + /** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */ + val session_ed25519: String?, + /** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */ + val subkey_tag: String? = null, + /** the signature unix timestamp in seconds, not ms */ + val sig_ts: Long, + /** the 64-byte ed25519 signature */ + val signature: String, + /** the string identifying the notification service, "firebase" for android (currently) */ + val service: String, + /** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */ + val service_info: Map, +) + @Serializable data class SubscriptionResponse( val error: Int? = null, @@ -60,6 +78,14 @@ data class SubscriptionResponse( } else null to null } +@Serializable +data class UnsubscribeResponse( + val error: Int? = null, + val message: String? = null, + val success: Boolean? = null, + val removed: Boolean? = null, +) + @Serializable data class PushNotificationMetadata( /** Account ID (such as Session ID or closed group ID) where the message arrived **/ diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 1f05e4f135..b1ab1090f7 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -318,7 +318,7 @@ interface TextSecurePreferences { } @JvmStatic - fun setFCMToken(context: Context, value: String) { + fun setFCMToken(context: Context, value: String?) { setStringPreference(context, FCM_TOKEN, value) } From 3f6229f8412732deec3b531e5beeae12a7533c6d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 9 Jun 2023 14:50:45 +0930 Subject: [PATCH 008/112] Remove PushNotificationManager --- .../securesms/ApplicationContext.java | 6 +- .../securesms/notifications/PushManager.kt | 2 +- .../notifications/PushNotificationManager.kt | 121 ------------------ .../notifications/FirebasePushManager.kt | 6 +- .../notifications/PushNotificationService.kt | 13 +- .../notifications/NoOpPushManager.kt | 2 +- .../MessageSenderClosedGroupHandler.kt | 10 +- .../ReceivedMessageHandler.kt | 4 +- .../notifications/PushNotificationAPI.kt | 105 +++++++++------ 9 files changed, 87 insertions(+), 182 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationManager.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 57187235c2..885c716638 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -36,6 +36,7 @@ import org.session.libsession.avatars.AvatarHelper; import org.session.libsession.database.MessageDataProvider; import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI; import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2; import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.snode.SnodeModule; @@ -78,7 +79,6 @@ import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; import org.thoughtcrime.securesms.notifications.PushManager; -import org.thoughtcrime.securesms.notifications.PushNotificationManager; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -440,7 +440,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO private static class ProviderInitializationException extends RuntimeException { } public void registerForPnIfNeeded(final Boolean force) { - pushManager.register(force); + pushManager.refresh(force); } private void setUpPollingIfNeeded() { @@ -514,7 +514,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO public void clearAllData(boolean isMigratingToV2KeyPair) { String token = TextSecurePreferences.getFCMToken(this); if (token != null && !token.isEmpty()) { - PushNotificationManager.unregister(token, this); + PushNotificationAPI.unregister(token); } if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) { firebaseInstanceIdJob.cancel(null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt index 9d4ef23fe9..d094644c07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushManager.kt @@ -1,5 +1,5 @@ package org.thoughtcrime.securesms.notifications interface PushManager { - fun register(force: Boolean) + fun refresh(force: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationManager.kt deleted file mode 100644 index 5a53f7af22..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushNotificationManager.kt +++ /dev/null @@ -1,121 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import android.content.Context -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map -import okhttp3.MediaType -import okhttp3.Request -import okhttp3.RequestBody -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI -import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.Version -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.retryIfNeeded -import org.thoughtcrime.securesms.dependencies.DatabaseComponent - -object PushNotificationManager { - private val maxRetryCount = 4 - private val tokenExpirationInterval = 12 * 60 * 60 * 1000 - - private val server by lazy { - PushNotificationAPI.server - } - private val pnServerPublicKey by lazy { - PushNotificationAPI.serverPublicKey - } - - enum class ClosedGroupOperation { - Subscribe, Unsubscribe; - - val rawValue: String - get() { - return when (this) { - Subscribe -> "subscribe_closed_group" - Unsubscribe -> "unsubscribe_closed_group" - } - } - } - - @JvmStatic - fun unregister(token: String, context: Context) { - val parameters = mapOf( "token" to token ) - val url = "$server/unregister" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { json -> - val code = json["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setIsUsingFCM(context, false) - } else { - Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.") - } - } - // Unsubscribe from all closed groups - val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys() - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - allClosedGroupPublicKeys.iterator().forEach { closedGroup -> - performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) - } - } - - @JvmStatic - fun register(token: String, publicKey: String, context: Context, force: Boolean) { - val oldToken = TextSecurePreferences.getFCMToken(context) - val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) - if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } - val parameters = mapOf( "token" to token, "pubKey" to publicKey ) - val url = "$server/register" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { json -> - val code = json["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setIsUsingFCM(context, true) - TextSecurePreferences.setFCMToken(context, token) - TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) - } else { - Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.") - } - } - // Subscribe to all closed groups - val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys() - allClosedGroupPublicKeys.iterator().forEach { closedGroup -> - performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey) - } - } - - @JvmStatic - fun performOperation(context: Context, operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) { - if (!TextSecurePreferences.isUsingFCM(context)) { return } - val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) - val url = "$server/${operation.rawValue}" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { json -> - val code = json["code"] as? Int - if (code == null || code == 0) { - Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.") - } - } - } - - private fun getResponseBody(request: Request): Promise, Exception> { - return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response -> - JsonUtil.fromJson(response.body, Map::class.java) - } - } -} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 4f9a6292f3..c169f145c9 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -93,15 +93,15 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS return content } - override fun register(force: Boolean) { + override fun refresh(force: Boolean) { firebaseInstanceIdJob?.apply { if (force) cancel() else if (isActive) return } - firebaseInstanceIdJob = getFcmInstanceId { register(it, force) } + firebaseInstanceIdJob = getFcmInstanceId { refresh(it, force) } } - private fun register(task: Task, force: Boolean) { + private fun refresh(task: Task, force: Boolean) { // context in here is Dispatchers.IO if (!task.isSuccessful) { Log.w( diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index bb625bc540..0d9765d737 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 @@ -23,7 +24,7 @@ class PushNotificationService : FirebaseMessagingService() { super.onNewToken(token) Log.d("Loki", "New FCM token: $token.") TextSecurePreferences.getLocalNumber(this) ?: return - pushManager.register(true) + pushManager.refresh(true) } override fun onMessageReceived(message: RemoteMessage) { @@ -53,22 +54,18 @@ class PushNotificationService : FirebaseMessagingService() { Log.d("Loki", "Failed to decode data for message.") val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) - .setColor(this.getResources().getColor(network.loki.messenger.R.color.textsecure_primary)) + .setColor(resources.getColor(network.loki.messenger.R.color.textsecure_primary)) .setContentTitle("Session") .setContentText("You've got a new message.") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) - with(NotificationManagerCompat.from(this)) { - notify(11111, builder.build()) - } + NotificationManagerCompat.from(this).notify(11111, builder.build()) } } override fun onDeletedMessages() { Log.d("Loki", "Called onDeletedMessages.") super.onDeletedMessages() - val token = TextSecurePreferences.getFCMToken(this)!! - val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return - PushNotificationManager.register(token, userPublicKey, this, true) + PushNotificationAPI.register() } } \ No newline at end of file diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt index 59f1b9df4b..a817e59d5d 100644 --- a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt @@ -4,7 +4,7 @@ import org.session.libsignal.utilities.Log class NoOpPushManager: PushManager { - override fun register(force: Boolean) { + override fun refresh(force: Boolean) { Log.d("NoOpPushManager", "Push notifications not supported, not registering for push notifications") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index b8a903539f..39afec418b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -74,7 +74,7 @@ fun MessageSender.create(name: String, members: Collection): Promise, name: St // Update name if needed if (name != group.title) { setName(groupPublicKey, name) } // Add members if needed - val addedMembers = members - group.members.map { it.serialize() } - if (!addedMembers.isEmpty()) { addMembers(groupPublicKey, addedMembers) } + val addedMembers = members - group.members.map { it.serialize() }.toSet() + if (addedMembers.isNotEmpty()) { addMembers(groupPublicKey, addedMembers) } // Remove members if needed - val removedMembers = group.members.map { it.serialize() } - members + val removedMembers = group.members.map { it.serialize() } - members.toSet() if (removedMembers.isEmpty()) { removeMembers(groupPublicKey, removedMembers) } } @@ -195,7 +195,7 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List "subscribe_closed_group" - Unsubscribe -> "unsubscribe_closed_group" - } - } + private enum class ClosedGroupOperation(val rawValue: String) { + Subscribe("subscribe_closed_group"), + Unsubscribe("unsubscribe_closed_group"); } + fun register(token: String? = TextSecurePreferences.getFCMToken(context)) { + token?.let(::unregisterV1) + subscribeGroups() + } + + @JvmStatic fun unregister(token: String) { + unregisterV1(token) + unsubscribeGroups() + } + + private fun unregisterV1(token: String) { val parameters = mapOf( "token" to token ) val url = "$server/unregister" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) retryIfNeeded(maxRetryCount) { OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response -> - val code = response.info["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setIsUsingFCM(context, false) - } else { - Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.") + when (response.info["code"]) { + null, 0 -> Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"]}.") } }.fail { exception -> Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.") } } - // Unsubscribe from all closed groups - val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() - val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - allClosedGroupPublicKeys.iterator().forEach { closedGroup -> - performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) - } } - fun register(token: String, publicKey: String, force: Boolean) { - // Subscribe to all closed groups - val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() - allClosedGroupPublicKeys.iterator().forEach { closedGroup -> - performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey) - } + // Legacy Closed Groups + + fun subscribeGroup( + closedGroupPublicKey: String, + publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! + ) { + performGroupOperation(ClosedGroupOperation.Subscribe, closedGroupPublicKey, publicKey) } - fun performOperation(operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) { - if (!TextSecurePreferences.isUsingFCM(context)) { return } + private fun subscribeGroups( + closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), + publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! + ) { + performGroupOperations(ClosedGroupOperation.Subscribe, closedGroupPublicKeys, publicKey) + } + + fun unsubscribeGroup( + closedGroupPublicKey: String, + publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! + ) { + performGroupOperation(ClosedGroupOperation.Unsubscribe, closedGroupPublicKey, publicKey) + } + + private fun unsubscribeGroups( + closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), + publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! + ) { + performGroupOperations(ClosedGroupOperation.Unsubscribe, closedGroupPublicKeys, publicKey) + } + + private fun performGroupOperations( + operation: ClosedGroupOperation, + closedGroupPublicKeys: Collection, + publicKey: String + ) { + closedGroupPublicKeys.forEach { performGroupOperation(operation, it, publicKey) } + } + + private fun performGroupOperation( + operation: ClosedGroupOperation, + closedGroupPublicKey: String, + publicKey: String + ) { + if (!TextSecurePreferences.isUsingFCM(context)) return + val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) val url = "$legacyServer/${operation.rawValue}" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body) + retryIfNeeded(maxRetryCount) { OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2).map { response -> - val code = response.info["code"] as? Int - if (code == null || code == 0) { - Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.") + when (response.info["code"]) { + null, 0 -> Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"]}.") } }.fail { exception -> Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.") From 01d80ae54bcc095b976182c02c6504164731a5bb Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 9 Jun 2023 15:56:24 +0930 Subject: [PATCH 009/112] cleanup PushNotificationAPI --- .../notifications/FirebasePushManager.kt | 1 - .../ReceivedMessageHandler.kt | 2 +- .../notifications/MessageNotifier.kt | 2 +- .../notifications/PushNotificationAPI.kt | 24 ++++--------------- .../utilities/TextSecurePreferences.kt | 1 - 5 files changed, 6 insertions(+), 24 deletions(-) diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index c169f145c9..702d686122 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -143,7 +143,6 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS retryIfNeeded(maxRetryCount) { getResponseBody(request.build()).map { response -> if (response.success == true) { - TextSecurePreferences.setIsUsingFCM(context, false) TextSecurePreferences.setFCMToken(context, null) Log.d("Loki", "Unsubscribe FCM success") } else { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 0d24f630a0..7643f67b94 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -478,7 +478,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli // Set expiration timer storage.setExpirationTimer(groupID, expireTimer) // Notify the PN server - PushNotificationAPI.subscribeGroup(groupPublicKey, userPublicKey) + PushNotificationAPI.subscribeGroup(groupPublicKey) // Notify the user if (userPublicKey == sender && !groupExists) { val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt index 37480543b7..8de01ca53e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt @@ -14,4 +14,4 @@ interface MessageNotifier { fun updateNotification(context: Context, threadId: Long, signal: Boolean) fun updateNotification(context: Context, signal: Boolean, reminderCount: Int) fun clearReminder(context: Context) -} \ No newline at end of file +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index a7c260ab94..aff4987f8a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -59,38 +59,22 @@ object PushNotificationAPI { fun subscribeGroup( closedGroupPublicKey: String, publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) { - performGroupOperation(ClosedGroupOperation.Subscribe, closedGroupPublicKey, publicKey) - } + ) = performGroupOperation(ClosedGroupOperation.Subscribe, closedGroupPublicKey, publicKey) private fun subscribeGroups( closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) { - performGroupOperations(ClosedGroupOperation.Subscribe, closedGroupPublicKeys, publicKey) - } + ) = closedGroupPublicKeys.forEach { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) } fun unsubscribeGroup( closedGroupPublicKey: String, publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) { - performGroupOperation(ClosedGroupOperation.Unsubscribe, closedGroupPublicKey, publicKey) - } + ) = performGroupOperation(ClosedGroupOperation.Unsubscribe, closedGroupPublicKey, publicKey) private fun unsubscribeGroups( closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) { - performGroupOperations(ClosedGroupOperation.Unsubscribe, closedGroupPublicKeys, publicKey) - } - - private fun performGroupOperations( - operation: ClosedGroupOperation, - closedGroupPublicKeys: Collection, - publicKey: String - ) { - closedGroupPublicKeys.forEach { performGroupOperation(operation, it, publicKey) } - } + ) = closedGroupPublicKeys.forEach { performGroupOperation(ClosedGroupOperation.Unsubscribe, it, publicKey) } private fun performGroupOperation( operation: ClosedGroupOperation, diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index b1ab1090f7..9c13699dda 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -986,7 +986,6 @@ interface TextSecurePreferences { fun clearAll(context: Context) { getDefaultSharedPreferences(context).edit().clear().commit() } - } } From be4d742e8467f01e14833ad4481c5f51642f6a4d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 9 Jun 2023 18:10:06 +0930 Subject: [PATCH 010/112] Unregister v1 push --- .../notifications/FirebasePushManager.kt | 26 ++++++++++++------- .../notifications/PushNotificationAPI.kt | 26 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 702d686122..8015030795 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -41,6 +41,8 @@ import org.session.libsignal.utilities.retryIfNeeded import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.KeyPairUtilities +private const val TAG = "FirebasePushManager" + class FirebasePushManager(private val context: Context, private val prefs: TextSecurePreferences): PushManager { companion object { @@ -87,8 +89,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS else check(metadata.data_len == content.size) { "wrong message data size" } - Log.d("Loki", - "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") + Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") return content } @@ -102,11 +103,11 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS } private fun refresh(task: Task, force: Boolean) { + Log.d(TAG, "refresh") + // context in here is Dispatchers.IO if (!task.isSuccessful) { - Log.w( - "Loki", - "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception + Log.w(TAG, "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception ) return } @@ -143,18 +144,21 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS retryIfNeeded(maxRetryCount) { getResponseBody(request.build()).map { response -> if (response.success == true) { + Log.d(TAG, "Unsubscribe FCM success") TextSecurePreferences.setFCMToken(context, null) - Log.d("Loki", "Unsubscribe FCM success") + PushNotificationAPI.unregister(token) } else { - Log.e("Loki", "Couldn't unregister for FCM due to error: ${response.message}") + Log.e(TAG, "Couldn't unregister for FCM due to error: ${response.message}") } }.fail { exception -> - Log.e("Loki", "Couldn't unregister for FCM due to error: ${exception}.", exception) + Log.e(TAG, "Couldn't unregister for FCM due to error: ${exception}.", exception) } } } private fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { + Log.d(TAG, "register token: $token") + val oldToken = TextSecurePreferences.getFCMToken(context) val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) return @@ -184,14 +188,16 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS retryIfNeeded(maxRetryCount) { getResponseBody(request.build()).map { response -> if (response.isSuccess()) { + Log.d(TAG, "Success $token") TextSecurePreferences.setFCMToken(context, token) TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) + PushNotificationAPI.register(token) } else { val (_, message) = response.errorInfo() - Log.e("Loki", "Couldn't register for FCM due to error: $message.") + Log.e(TAG, "Couldn't register for FCM due to error: $message.") } }.fail { exception -> - Log.e("Loki", "Couldn't register for FCM due to error: ${exception}.", exception) + Log.e(TAG, "Couldn't register for FCM due to error: ${exception}.", exception) } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index aff4987f8a..d9e4293cf7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -15,6 +15,8 @@ import org.session.libsignal.utilities.retryIfNeeded @SuppressLint("StaticFieldLeak") object PushNotificationAPI { + private const val TAG = "PushNotificationAPI" + val context = MessagingModuleConfiguration.shared.context const val server = "https://push.getsession.org" const val serverPublicKey: String = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" @@ -28,28 +30,33 @@ object PushNotificationAPI { } fun register(token: String? = TextSecurePreferences.getFCMToken(context)) { + Log.d(TAG, "register: $token") + token?.let(::unregisterV1) subscribeGroups() } @JvmStatic fun unregister(token: String) { + Log.d(TAG, "unregister: $token") + unregisterV1(token) unsubscribeGroups() } private fun unregisterV1(token: String) { val parameters = mapOf( "token" to token ) - val url = "$server/unregister" + val url = "$legacyServer/unregister" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) + val request = Request.Builder().url(url).post(body).build() retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response -> + OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> when (response.info["code"]) { - null, 0 -> Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"]}.") + null, 0 -> Log.d(TAG, "Couldn't disable FCM due to error: ${response.info["message"]}.") + else -> Log.d(TAG, "unregisterV1 success token: $token") } }.fail { exception -> - Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.") + Log.d(TAG, "Couldn't disable FCM due to error: ${exception}.") } } } @@ -86,15 +93,16 @@ object PushNotificationAPI { val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) val url = "$legacyServer/${operation.rawValue}" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) + val request = Request.Builder().url(url).post(body).build() retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2).map { response -> + OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> when (response.info["code"]) { - null, 0 -> Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"]}.") + null, 0 -> Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${response.info["message"]}.") + else -> Log.d(TAG, "performGroupOperation success: ${operation.rawValue}") } }.fail { exception -> - Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.") + Log.d(TAG, "Couldn't ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.") } } } From b2a1b5fe4694e5a1e7d1e9c5962fa3fe30c3f96b Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 10 Jun 2023 11:44:43 +0930 Subject: [PATCH 011/112] Add token to FCM logs --- .../sending_receiving/notifications/PushNotificationAPI.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index d9e4293cf7..32bd17c8c2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -52,11 +52,11 @@ object PushNotificationAPI { retryIfNeeded(maxRetryCount) { OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> when (response.info["code"]) { - null, 0 -> Log.d(TAG, "Couldn't disable FCM due to error: ${response.info["message"]}.") + null, 0 -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${response.info["message"]}.") else -> Log.d(TAG, "unregisterV1 success token: $token") } }.fail { exception -> - Log.d(TAG, "Couldn't disable FCM due to error: ${exception}.") + Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") } } } From 288b70bb1467bab0d957c3cd0970b56eeefdca27 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 13 Jun 2023 10:21:52 +0930 Subject: [PATCH 012/112] Store legacy fcm token to reduce unregister api calls --- .../notifications/PushNotificationAPI.kt | 16 ++++++++++++---- .../utilities/TextSecurePreferences.kt | 13 ++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index 32bd17c8c2..9d36ff0013 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -32,7 +32,7 @@ object PushNotificationAPI { fun register(token: String? = TextSecurePreferences.getFCMToken(context)) { Log.d(TAG, "register: $token") - token?.let(::unregisterV1) + unregisterV1IfRequired() subscribeGroups() } @@ -40,11 +40,16 @@ object PushNotificationAPI { fun unregister(token: String) { Log.d(TAG, "unregister: $token") - unregisterV1(token) + unregisterV1IfRequired() unsubscribeGroups() } - private fun unregisterV1(token: String) { + /** + * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager. + */ + private fun unregisterV1IfRequired() { + val token = TextSecurePreferences.getLegacyFCMToken(context) ?: return + val parameters = mapOf( "token" to token ) val url = "$legacyServer/unregister" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) @@ -53,7 +58,10 @@ object PushNotificationAPI { OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> when (response.info["code"]) { null, 0 -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${response.info["message"]}.") - else -> Log.d(TAG, "unregisterV1 success token: $token") + else -> { + TextSecurePreferences.clearLegacyFCMToken(context) + Log.d(TAG, "unregisterV1 success token: $token") + } } }.fail { exception -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 9c13699dda..a1482d96b4 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -250,7 +250,8 @@ interface TextSecurePreferences { const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning" const val GIF_GRID_LAYOUT = "pref_gif_grid_layout" const val IS_USING_FCM = "pref_is_using_fcm" - const val FCM_TOKEN = "pref_fcm_token" + const val FCM_TOKEN_LEGACY = "pref_fcm_token" + const val FCM_TOKEN = "pref_fcm_token_2" const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2" const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time" const val CONFIGURATION_SYNCED = "pref_configuration_synced" @@ -312,6 +313,16 @@ interface TextSecurePreferences { setBooleanPreference(context, IS_USING_FCM, value) } + @JvmStatic + fun getLegacyFCMToken(context: Context): String? { + return getStringPreference(context, FCM_TOKEN_LEGACY, "") + } + + @JvmStatic + fun clearLegacyFCMToken(context: Context) { + removePreference(context, FCM_TOKEN_LEGACY) + } + @JvmStatic fun getFCMToken(context: Context): String? { return getStringPreference(context, FCM_TOKEN, "") From 153aa4ceaa0d305e1c24bc10269c8083421ca8ba Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 14 Jun 2023 18:46:28 +0930 Subject: [PATCH 013/112] Reinstate push v1 --- .../securesms/ApplicationContext.java | 9 +--- .../notifications/FirebasePushManager.kt | 7 ++- .../notifications/PushNotificationService.kt | 4 +- .../notifications/PushNotificationAPI.kt | 44 +++++++++++++------ .../utilities/TextSecurePreferences.kt | 5 +++ 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 885c716638..688e30d879 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -218,10 +218,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO broadcaster = new Broadcaster(this); LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase(); SnodeModule.Companion.configure(apiDB, broadcaster); - String userPublicKey = TextSecurePreferences.getLocalNumber(this); - if (userPublicKey != null) { - registerForPnIfNeeded(false); - } initializeExpiringMessageManager(); initializeTypingStatusRepository(); initializeTypingStatusSender(); @@ -512,10 +508,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } public void clearAllData(boolean isMigratingToV2KeyPair) { - String token = TextSecurePreferences.getFCMToken(this); - if (token != null && !token.isEmpty()) { - PushNotificationAPI.unregister(token); - } + PushNotificationAPI.unregister(); if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) { firebaseInstanceIdJob.cancel(null); } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 8015030795..108b10f147 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -146,7 +146,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS if (response.success == true) { Log.d(TAG, "Unsubscribe FCM success") TextSecurePreferences.setFCMToken(context, null) - PushNotificationAPI.unregister(token) + PushNotificationAPI.unregister() } else { Log.e(TAG, "Couldn't unregister for FCM due to error: ${response.message}") } @@ -161,7 +161,10 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val oldToken = TextSecurePreferences.getFCMToken(context) val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) - if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) return +// if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { +// Log.d(TAG, "not registering now... not forced or expired") +// return +// } val pnKey = getOrCreateNotificationKey() diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 0d9765d737..3e669dbf8c 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -24,7 +24,9 @@ class PushNotificationService : FirebaseMessagingService() { super.onNewToken(token) Log.d("Loki", "New FCM token: $token.") TextSecurePreferences.getLocalNumber(this) ?: return - pushManager.refresh(true) + if (TextSecurePreferences.getLocalNumber(this) != token) { + pushManager.refresh(true) + } } override fun onMessageReceived(message: RemoteMessage) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index 9d36ff0013..a2eaf353fd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -29,25 +29,43 @@ object PushNotificationAPI { Unsubscribe("unsubscribe_closed_group"); } - fun register(token: String? = TextSecurePreferences.getFCMToken(context)) { + fun register(token: String? = TextSecurePreferences.getLegacyFCMToken(context)) { Log.d(TAG, "register: $token") - unregisterV1IfRequired() + register(token, TextSecurePreferences.getLocalNumber(context)) subscribeGroups() } - @JvmStatic - fun unregister(token: String) { - Log.d(TAG, "unregister: $token") + fun register(token: String?, publicKey: String?) { + Log.d(TAG, "register($token)") - unregisterV1IfRequired() - unsubscribeGroups() + token ?: return + publicKey ?: return + val parameters = mapOf("token" to token, "pubKey" to publicKey) + val url = "$legacyServer/register" + val body = + RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body) + retryIfNeeded(maxRetryCount) { + OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2) + .map { response -> + val code = response.info["code"] as? Int + if (code != null && code != 0) { + TextSecurePreferences.setLegacyFCMToken(context, token) + } else { + Log.d(TAG, "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.") + } + }.fail { exception -> + Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") + } + } } /** * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager. */ - private fun unregisterV1IfRequired() { + @JvmStatic + fun unregister() { val token = TextSecurePreferences.getLegacyFCMToken(context) ?: return val parameters = mapOf( "token" to token ) @@ -56,17 +74,17 @@ object PushNotificationAPI { val request = Request.Builder().url(url).post(body).build() retryIfNeeded(maxRetryCount) { OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> + TextSecurePreferences.clearLegacyFCMToken(context) when (response.info["code"]) { null, 0 -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${response.info["message"]}.") - else -> { - TextSecurePreferences.clearLegacyFCMToken(context) - Log.d(TAG, "unregisterV1 success token: $token") - } + else -> Log.d(TAG, "unregisterV1 success token: $token") } }.fail { exception -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") } } + + unsubscribeGroups() } // Legacy Closed Groups @@ -110,7 +128,7 @@ object PushNotificationAPI { else -> Log.d(TAG, "performGroupOperation success: ${operation.rawValue}") } }.fail { exception -> - Log.d(TAG, "Couldn't ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.") + Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.") } } } diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index a1482d96b4..97fe9d3b49 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -318,6 +318,11 @@ interface TextSecurePreferences { return getStringPreference(context, FCM_TOKEN_LEGACY, "") } + @JvmStatic + fun setLegacyFCMToken(context: Context, token: String?) { + setStringPreference(context, FCM_TOKEN_LEGACY, token) + } + @JvmStatic fun clearLegacyFCMToken(context: Context) { removePreference(context, FCM_TOKEN_LEGACY) From 667af27bfb49164cdc5f7d7307b16d8fd7eaf7b3 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 16 Jun 2023 10:38:33 +0930 Subject: [PATCH 014/112] Utilise TokenManager and ExpiryManager --- .../securesms/ApplicationContext.java | 1 - .../notifications/FcmTokenManager.kt | 26 +++ .../securesms/notifications/ExpiryManager.kt | 25 +++ .../securesms/notifications/FcmUtils.kt | 7 +- .../notifications/FirebasePushManager.kt | 207 ++++++++++-------- .../notifications/FirebasePushModule.kt | 3 +- .../messaging/jobs/NotifyPNServerJob.kt | 18 +- .../sending_receiving/notifications/Models.kt | 48 ++-- .../notifications/PushNotificationAPI.kt | 104 ++++----- .../libsignal/utilities/PromiseUtilities.kt | 4 + .../session/libsignal/utilities/Retrying.kt | 1 + 11 files changed, 260 insertions(+), 184 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt create mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 688e30d879..e10a7a7dc7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -508,7 +508,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } public void clearAllData(boolean isMigratingToV2KeyPair) { - PushNotificationAPI.unregister(); if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) { firebaseInstanceIdJob.cancel(null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt new file mode 100644 index 0000000000..e1182a480b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import org.session.libsession.utilities.TextSecurePreferences + +class FcmTokenManager( + private val context: Context, + private val expiryManager: ExpiryManager +) { + val isUsingFCM get() = TextSecurePreferences.isUsingFCM(context) + + var fcmToken + get() = TextSecurePreferences.getFCMToken(context) + set(value) { + TextSecurePreferences.setFCMToken(context, value) + if (value != null) markTime() else clearTime() + } + + val requiresUnregister get() = fcmToken != null + + private fun clearTime() = expiryManager.clearTime() + private fun markTime() = expiryManager.markTime() + private fun isExpired() = expiryManager.isExpired() + + fun isInvalid(): Boolean = fcmToken == null || isExpired() +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt new file mode 100644 index 0000000000..36c986f0ea --- /dev/null +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt @@ -0,0 +1,25 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import org.session.libsession.utilities.TextSecurePreferences + +class ExpiryManager( + private val context: Context, + private val interval: Int = 12 * 60 * 60 * 1000 +) { + fun isExpired() = currentTime() > time + interval + + fun markTime() { + time = currentTime() + } + + fun clearTime() { + time = 0 + } + + private var time + get() = TextSecurePreferences.getLastFCMUploadTime(context) + set(value) = TextSecurePreferences.setLastFCMUploadTime(context, value) + + private fun currentTime() = System.currentTimeMillis() +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt index de3d14d430..af3f49a26a 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.notifications import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks import com.google.firebase.iid.FirebaseInstanceId import com.google.firebase.iid.InstanceIdResult import kotlinx.coroutines.* @@ -9,9 +10,7 @@ import kotlinx.coroutines.* fun getFcmInstanceId(body: (Task)->Unit): Job = MainScope().launch(Dispatchers.IO) { val task = FirebaseInstanceId.getInstance().instanceId - while (!task.isComplete && isActive) { - // wait for task to complete while we are active - } + Tasks.await(task) if (!isActive) return@launch // don't 'complete' task if we were canceled body(task) -} \ No newline at end of file +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 108b10f147..4a0d8307e5 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.notifications import android.content.Context -import com.google.android.gms.tasks.Task -import com.google.firebase.iid.InstanceIdResult import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.interfaces.AEAD @@ -15,12 +13,14 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.combine.and import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata +import org.session.libsession.messaging.sending_receiving.notifications.Response import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse @@ -29,7 +29,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.Version -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.bencode.Bencode import org.session.libsession.utilities.bencode.BencodeList @@ -37,19 +36,20 @@ import org.session.libsession.utilities.bencode.BencodeString import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace +import org.session.libsignal.utilities.emptyPromise import org.session.libsignal.utilities.retryIfNeeded import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.KeyPairUtilities private const val TAG = "FirebasePushManager" -class FirebasePushManager(private val context: Context, private val prefs: TextSecurePreferences): PushManager { +class FirebasePushManager (private val context: Context): PushManager { companion object { private const val maxRetryCount = 4 - private const val tokenExpirationInterval = 12 * 60 * 60 * 1000 } + private val tokenManager = FcmTokenManager(context, ExpiryManager(context)) private var firebaseInstanceIdJob: Job? = null private val sodium = LazySodiumAndroid(SodiumAndroid()) @@ -59,12 +59,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) } - return Key.fromHexString( - IdentityKeyUtil.retrieve( - context, - IdentityKeyUtil.NOTIFICATION_KEY - ) - ) + return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) } fun decrypt(encPayload: ByteArray): ByteArray? { @@ -78,8 +73,7 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS val expectedList = (bencoded.decode() as? BencodeList)?.values ?: error("Failed to decode bencoded list from payload") - val metadataJson = (expectedList[0] as? BencodeString)?.value - ?: error("no metadata") + val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null @@ -94,78 +88,81 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS return content } + @Synchronized override fun refresh(force: Boolean) { firebaseInstanceIdJob?.apply { if (force) cancel() else if (isActive) return } - firebaseInstanceIdJob = getFcmInstanceId { refresh(it, force) } - } - - private fun refresh(task: Task, force: Boolean) { - Log.d(TAG, "refresh") - - // context in here is Dispatchers.IO - if (!task.isSuccessful) { - Log.w(TAG, "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception - ) - return - } - - val token: String = task.result?.token ?: return - val userPublicKey = getLocalNumber(context) ?: return - val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return - - when { - prefs.isUsingFCM() -> register(token, userPublicKey, userEdKey, force) - prefs.getFCMToken() != null -> unregister(token, userPublicKey, userEdKey) - } - } - - private fun unregister(token: String, userPublicKey: String, userEdKey: KeyPair) { - val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s - // if we want to support passing namespace list, here is the place to do it - val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() - val signature = ByteArray(Sign.BYTES) - sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) - - val requestParameters = UnsubscriptionRequest( - pubkey = userPublicKey, - session_ed25519 = userEdKey.publicKey.asHexString, - service = "firebase", - sig_ts = timestamp, - signature = Base64.encodeBytes(signature), - service_info = mapOf("token" to token), - ) - - val url = "${PushNotificationAPI.server}/unsubscribe" - val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { response -> - if (response.success == true) { - Log.d(TAG, "Unsubscribe FCM success") - TextSecurePreferences.setFCMToken(context, null) - PushNotificationAPI.unregister() - } else { - Log.e(TAG, "Couldn't unregister for FCM due to error: ${response.message}") - } - }.fail { exception -> - Log.e(TAG, "Couldn't unregister for FCM due to error: ${exception}.", exception) + firebaseInstanceIdJob = getFcmInstanceId { task -> + when { + task.isSuccessful -> task.result?.token?.let { refresh(it, force).get() } + else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception) } } } - private fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List = listOf(Namespace.DEFAULT)) { - Log.d(TAG, "register token: $token") + private fun refresh(token: String, force: Boolean): Promise<*, Exception> { + val userPublicKey = getLocalNumber(context) ?: return emptyPromise() + val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() - val oldToken = TextSecurePreferences.getFCMToken(context) - val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) -// if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { -// Log.d(TAG, "not registering now... not forced or expired") -// return -// } + return when { + tokenManager.isUsingFCM -> register(force, token, userPublicKey, userEdKey) + tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey) + else -> emptyPromise() + } + } + /** + * Register for push notifications if: + * force is true + * there is no FCM Token + * FCM Token has expired + */ + private fun register( + force: Boolean, + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List = listOf(Namespace.DEFAULT) + ): Promise<*, Exception> = if (force || tokenManager.isInvalid()) { + register(token, publicKey, userEd25519Key, namespaces) + } else emptyPromise() + + /** + * Register for push notifications. + */ + private fun register( + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List = listOf(Namespace.DEFAULT) + ): Promise<*, Exception> = PushNotificationAPI.register(token) and getSubscription( + token, publicKey, userEd25519Key, namespaces + ) fail { + Log.e(TAG, "Couldn't register for FCM due to error: $it.", it) + } success { + tokenManager.fcmToken = token + } + + private fun unregister( + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise<*, Exception> = PushNotificationAPI.unregister() and getUnsubscription( + token, userPublicKey, userEdKey + ) fail { + Log.e(TAG, "Couldn't unregister for FCM due to error: ${it}.", it) + } success { + tokenManager.fcmToken = null + } + + private fun getSubscription( + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List + ): Promise { val pnKey = getOrCreateNotificationKey() val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s @@ -183,33 +180,51 @@ class FirebasePushManager(private val context: Context, private val prefs: TextS signature = Base64.encodeBytes(signature), service_info = mapOf("token" to token), enc_key = pnKey.asHexString, - ) + ).let(Json::encodeToString) - val url = "${PushNotificationAPI.server}/subscribe" - val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - getResponseBody(request.build()).map { response -> - if (response.isSuccess()) { - Log.d(TAG, "Success $token") - TextSecurePreferences.setFCMToken(context, token) - TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) - PushNotificationAPI.register(token) - } else { - val (_, message) = response.errorInfo() - Log.e(TAG, "Couldn't register for FCM due to error: $message.") - } - }.fail { exception -> - Log.e(TAG, "Couldn't register for FCM due to error: ${exception}.", exception) - } - } + return retryResponseBody("subscribe", requestParameters) } - private inline fun getResponseBody(request: Request): Promise = - OnionRequestAPI.sendOnionRequest( + private fun getUnsubscription( + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise { + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) + + val requestParameters = UnsubscriptionRequest( + pubkey = userPublicKey, + session_ed25519 = userEdKey.publicKey.asHexString, + service = "firebase", + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + ).let(Json::encodeToString) + + return retryResponseBody("unsubscribe", requestParameters) + } + + private inline fun retryResponseBody(path: String, requestParameters: String): Promise = + retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) } + + private inline fun getResponseBody(path: String, requestParameters: String): Promise { + val url = "${PushNotificationAPI.server}/$path" + val body = RequestBody.create(MediaType.get("application/json"), requestParameters) + val request = Request.Builder().url(url).post(body).build() + + return OnionRequestAPI.sendOnionRequest( request, PushNotificationAPI.server, PushNotificationAPI.serverPublicKey, Version.V4 - ).map { response -> Json.decodeFromStream(response.body!!.inputStream()) } + ).map { response -> + response.body!!.inputStream() + .let { Json.decodeFromStream(it) } + .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } + } + } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt index 30045d6c25..3f33016a02 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt @@ -17,8 +17,7 @@ object FirebasePushModule { @Singleton fun provideFirebasePushManager( @ApplicationContext context: Context, - prefs: TextSecurePreferences, - ) = FirebasePushManager(context, prefs) + ) = FirebasePushManager(context) } @Module diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 25fb2194c8..7bb2707a80 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -10,6 +10,7 @@ import okhttp3.RequestBody import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI.server import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.OnionRequestAPI @@ -33,18 +34,21 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { } override fun execute(dispatcherName: String) { - val server = PushNotificationAPI.server val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) val url = "${server}/notify" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) + val request = Request.Builder().url(url).post(body).build() retryIfNeeded(4) { - OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response -> - val code = response.info["code"] as? Int - if (code == null || code == 0) { - Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.") + OnionRequestAPI.sendOnionRequest( + request, + server, + PushNotificationAPI.serverPublicKey, + Version.V2 + ) success { response -> + when (response.info["code"]) { + null, 0 -> Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"]}.") } - }.fail { exception -> + } fail { exception -> Log.d("Loki", "Couldn't notify PN server due to error: $exception.") } }.success { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt index 141202ef66..ea2492d5af 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Models.kt @@ -54,37 +54,39 @@ data class UnsubscriptionRequest( val service_info: Map, ) +/** invalid values, missing reuqired arguments etc, details in message */ +private const val UNPARSEABLE_ERROR = 1 +/** the "service" value is not active / valid */ +private const val SERVICE_NOT_AVAILABLE = 2 +/** something getting wrong internally talking to the backend */ +private const val SERVICE_TIMEOUT = 3 +/** other error processing the subscription (details in the message) */ +private const val GENERIC_ERROR = 4 + @Serializable data class SubscriptionResponse( - val error: Int? = null, - val message: String? = null, - val success: Boolean? = null, + override val error: Int? = null, + override val message: String? = null, + override val success: Boolean? = null, val added: Boolean? = null, val updated: Boolean? = null, -) { - companion object { - /** invalid values, missing reuqired arguments etc, details in message */ - const val UNPARSEABLE_ERROR = 1 - /** the "service" value is not active / valid */ - const val SERVICE_NOT_AVAILABLE = 2 - /** something getting wrong internally talking to the backend */ - const val SERVICE_TIMEOUT = 3 - /** other error processing the subscription (details in the message) */ - const val GENERIC_ERROR = 4 - } - fun isSuccess() = success == true && error == null - fun errorInfo() = if (success != true && error != null) { - error to message - } else null to null -} +): Response @Serializable data class UnsubscribeResponse( - val error: Int? = null, - val message: String? = null, - val success: Boolean? = null, + override val error: Int? = null, + override val message: String? = null, + override val success: Boolean? = null, val removed: Boolean? = null, -) +): Response + +interface Response { + val error: Int? + val message: String? + val success: Boolean? + fun isSuccess() = success == true && error == null + fun isFailure() = !isSuccess() +} @Serializable data class PushNotificationMetadata( diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index a2eaf353fd..b39b30f090 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -1,7 +1,10 @@ package org.session.libsession.messaging.sending_receiving.notifications import android.annotation.SuppressLint +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.all import nl.komponents.kovenant.functional.map +import nl.komponents.kovenant.task import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody @@ -11,6 +14,7 @@ import org.session.libsession.snode.Version import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.emptyPromise import org.session.libsignal.utilities.retryIfNeeded @SuppressLint("StaticFieldLeak") @@ -29,62 +33,57 @@ object PushNotificationAPI { Unsubscribe("unsubscribe_closed_group"); } - fun register(token: String? = TextSecurePreferences.getLegacyFCMToken(context)) { - Log.d(TAG, "register: $token") - - register(token, TextSecurePreferences.getLocalNumber(context)) + fun register( + token: String? = TextSecurePreferences.getLegacyFCMToken(context) + ): Promise<*, Exception> = all( + register(token, TextSecurePreferences.getLocalNumber(context)), subscribeGroups() - } + ) - fun register(token: String?, publicKey: String?) { - Log.d(TAG, "register($token)") - - token ?: return - publicKey ?: return - val parameters = mapOf("token" to token, "pubKey" to publicKey) - val url = "$legacyServer/register" - val body = - RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) + fun register(token: String?, publicKey: String?): Promise = retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2) - .map { response -> - val code = response.info["code"] as? Int - if (code != null && code != 0) { - TextSecurePreferences.setLegacyFCMToken(context, token) - } else { - Log.d(TAG, "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.") - } - }.fail { exception -> + val parameters = mapOf("token" to token!!, "pubKey" to publicKey!!) + val url = "$legacyServer/register" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body).build() + + OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> + when (response.info["code"]) { + null, 0 -> throw Exception("error: ${response.info["message"]}.") + else -> TextSecurePreferences.setLegacyFCMToken(context, token) + } + } fail { exception -> Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") } } - } /** * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager. */ - @JvmStatic - fun unregister() { - val token = TextSecurePreferences.getLegacyFCMToken(context) ?: return + fun unregister(): Promise<*, Exception> { + val token = TextSecurePreferences.getLegacyFCMToken(context) ?: emptyPromise() - val parameters = mapOf( "token" to token ) - val url = "$legacyServer/unregister" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body).build() - retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> - TextSecurePreferences.clearLegacyFCMToken(context) - when (response.info["code"]) { - null, 0 -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${response.info["message"]}.") + return retryIfNeeded(maxRetryCount) { + val parameters = mapOf( "token" to token ) + val url = "$legacyServer/unregister" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body).build() + + OnionRequestAPI.sendOnionRequest( + request, + legacyServer, + legacyServerPublicKey, + Version.V2 + ) success { + when (it.info["code"]) { + null, 0 -> throw Exception("error: ${it.info["message"]}.") else -> Log.d(TAG, "unregisterV1 success token: $token") } - }.fail { exception -> - Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") + TextSecurePreferences.clearLegacyFCMToken(context) + } fail { + exception -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") } } - - unsubscribeGroups() } // Legacy Closed Groups @@ -97,7 +96,7 @@ object PushNotificationAPI { private fun subscribeGroups( closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = closedGroupPublicKeys.forEach { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) } + ) = closedGroupPublicKeys.map { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) }.let(::all) fun unsubscribeGroup( closedGroupPublicKey: String, @@ -107,27 +106,30 @@ object PushNotificationAPI { private fun unsubscribeGroups( closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = closedGroupPublicKeys.forEach { performGroupOperation(ClosedGroupOperation.Unsubscribe, it, publicKey) } + ) = closedGroupPublicKeys.map { performGroupOperation(ClosedGroupOperation.Unsubscribe, it, publicKey) }.let(::all) private fun performGroupOperation( operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String - ) { - if (!TextSecurePreferences.isUsingFCM(context)) return - + ): Promise<*, Exception> { val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) val url = "$legacyServer/${operation.rawValue}" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() - retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> - when (response.info["code"]) { - null, 0 -> Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${response.info["message"]}.") + return retryIfNeeded(maxRetryCount) { + OnionRequestAPI.sendOnionRequest( + request, + legacyServer, + legacyServerPublicKey, + Version.V2 + ) success { + when (it.info["code"]) { + null, 0 -> throw Exception("${it.info["message"]}") else -> Log.d(TAG, "performGroupOperation success: ${operation.rawValue}") } - }.fail { exception -> + } fail { exception -> Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.") } } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt index d6361289c5..8095b44597 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt @@ -3,8 +3,12 @@ package org.session.libsignal.utilities import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.task import java.util.concurrent.TimeoutException +fun emptyPromise() = EMPTY_PROMISE +private val EMPTY_PROMISE: Promise<*, java.lang.Exception> = task {} + fun Promise.get(defaultValue: V): V { return try { get() diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt index 9f1b1a3e82..28ed93b5a5 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt @@ -28,3 +28,4 @@ fun > retryIfNeeded(maxRetryCount: Int, retryInterv retryIfNeeded() return deferred.promise } + From 0e0ab9151ea1968e75814d7ad7cc01004a70e32c Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 20 Jun 2023 22:34:10 +0930 Subject: [PATCH 015/112] cleanup --- .../securesms/ApplicationContext.java | 1 - .../securesms/util/MockDataGenerator.kt | 3 - .../notifications/FirebasePushManager.kt | 21 ++++-- .../notifications/PushNotificationService.kt | 25 +++---- .../messaging/jobs/NotifyPNServerJob.kt | 7 +- .../MessageSenderClosedGroupHandler.kt | 4 +- .../ReceivedMessageHandler.kt | 6 +- ...ationAPI.kt => LegacyGroupsPushManager.kt} | 70 ++++++++----------- .../utilities/TextSecurePreferences.kt | 16 ----- 9 files changed, 65 insertions(+), 88 deletions(-) rename libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/{PushNotificationAPI.kt => LegacyGroupsPushManager.kt} (61%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 8f561f6314..8a2b0c178b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -36,7 +36,6 @@ import org.session.libsession.avatars.AvatarHelper; import org.session.libsession.database.MessageDataProvider; import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI; import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2; import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.snode.SnodeModule; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt index 10d507a538..b87f49da18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt @@ -7,8 +7,6 @@ import org.session.libsession.messaging.messages.signal.IncomingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI -import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.recipients.Recipient @@ -21,7 +19,6 @@ import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.groups.GroupManager import java.security.SecureRandom -import java.util.* import kotlin.random.asKotlinRandom object MockDataGenerator { diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 4a0d8307e5..65314d76d4 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -18,7 +18,7 @@ import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.messaging.sending_receiving.notifications.Response import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest @@ -63,6 +63,8 @@ class FirebasePushManager (private val context: Context): PushManager { } fun decrypt(encPayload: ByteArray): ByteArray? { + Log.d(TAG, "decrypt() called") + val encKey = getOrCreateNotificationKey() val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() @@ -90,19 +92,23 @@ class FirebasePushManager (private val context: Context): PushManager { @Synchronized override fun refresh(force: Boolean) { + Log.d(TAG, "refresh() called with: force = $force") + firebaseInstanceIdJob?.apply { if (force) cancel() else if (isActive) return } firebaseInstanceIdJob = getFcmInstanceId { task -> when { - task.isSuccessful -> task.result?.token?.let { refresh(it, force).get() } + task.isSuccessful -> try { task.result?.token?.let { refresh(it, force).get() } } catch(e: Exception) { Log.d(TAG, "refresh() failed", e) } else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception) } } } private fun refresh(token: String, force: Boolean): Promise<*, Exception> { + Log.d(TAG, "refresh() called with: token = $token, force = $force") + val userPublicKey = getLocalNumber(context) ?: return emptyPromise() val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() @@ -137,11 +143,12 @@ class FirebasePushManager (private val context: Context): PushManager { publicKey: String, userEd25519Key: KeyPair, namespaces: List = listOf(Namespace.DEFAULT) - ): Promise<*, Exception> = PushNotificationAPI.register(token) and getSubscription( + ): Promise<*, Exception> = LegacyGroupsPushManager.register(token, publicKey) and getSubscription( token, publicKey, userEd25519Key, namespaces ) fail { Log.e(TAG, "Couldn't register for FCM due to error: $it.", it) } success { + Log.d(TAG, "register() success!!") tokenManager.fcmToken = token } @@ -149,7 +156,7 @@ class FirebasePushManager (private val context: Context): PushManager { token: String, userPublicKey: String, userEdKey: KeyPair - ): Promise<*, Exception> = PushNotificationAPI.unregister() and getUnsubscription( + ): Promise<*, Exception> = LegacyGroupsPushManager.unregister() and getUnsubscription( token, userPublicKey, userEdKey ) fail { Log.e(TAG, "Couldn't unregister for FCM due to error: ${it}.", it) @@ -212,14 +219,14 @@ class FirebasePushManager (private val context: Context): PushManager { retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) } private inline fun getResponseBody(path: String, requestParameters: String): Promise { - val url = "${PushNotificationAPI.server}/$path" + val url = "${LegacyGroupsPushManager.server}/$path" val body = RequestBody.create(MediaType.get("application/json"), requestParameters) val request = Request.Builder().url(url).post(body).build() return OnionRequestAPI.sendOnionRequest( request, - PushNotificationAPI.server, - PushNotificationAPI.serverPublicKey, + LegacyGroupsPushManager.server, + LegacyGroupsPushManager.serverPublicKey, Version.V4 ).map { response -> response.body!!.inputStream() diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 3e669dbf8c..d09fd79913 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -8,13 +8,15 @@ import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import javax.inject.Inject +private const val TAG = "PushNotificationService" + @AndroidEntryPoint class PushNotificationService : FirebaseMessagingService() { @@ -22,27 +24,26 @@ class PushNotificationService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) - Log.d("Loki", "New FCM token: $token.") + Log.d(TAG, "New FCM token: $token.") TextSecurePreferences.getLocalNumber(this) ?: return - if (TextSecurePreferences.getLocalNumber(this) != token) { + if (TextSecurePreferences.getFCMToken(this) != token) { pushManager.refresh(true) } } override fun onMessageReceived(message: RemoteMessage) { - Log.d("Loki", "Received a push notification.") + Log.d(TAG, "Received a push notification.") val data: ByteArray? = if (message.data.containsKey("spns")) { // this is a v2 push notification try { pushManager.decrypt(Base64.decode(message.data["enc_payload"])) } catch(e: Exception) { - Log.e("Loki", "Invalid push notification: ${e.message}") + Log.e(TAG, "Invalid push notification: ${e.message}") return } } else { // old v1 push notification; we still need this for receiving legacy closed group notifications - val base64EncodedData = message.data?.get("ENCRYPTED_DATA") - base64EncodedData?.let { Base64.decode(it) } + message.data?.get("ENCRYPTED_DATA")?.let(Base64::decode) } if (data != null) { try { @@ -50,10 +51,10 @@ class PushNotificationService : FirebaseMessagingService() { val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) JobQueue.shared.add(job) } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message due to error: $e.") + Log.d(TAG, "Failed to unwrap data for message due to error: $e.") } } else { - Log.d("Loki", "Failed to decode data for message.") + Log.d(TAG, "Failed to decode data for message.") val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) .setColor(resources.getColor(network.loki.messenger.R.color.textsecure_primary)) @@ -66,8 +67,8 @@ class PushNotificationService : FirebaseMessagingService() { } override fun onDeletedMessages() { - Log.d("Loki", "Called onDeletedMessages.") + Log.d(TAG, "Called onDeletedMessages.") super.onDeletedMessages() - PushNotificationAPI.register() + LegacyGroupsPushManager.register() } -} \ No newline at end of file +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 7bb2707a80..3ab92210d2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -3,14 +3,13 @@ package org.session.libsession.messaging.jobs import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI.server +import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager +import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager.server import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.OnionRequestAPI @@ -42,7 +41,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { OnionRequestAPI.sendOnionRequest( request, server, - PushNotificationAPI.serverPublicKey, + LegacyGroupsPushManager.serverPublicKey, Version.V2 ) success { response -> when (response.info["code"]) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index e8cb79188e..926554d3e2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -8,7 +8,7 @@ import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.sending_receiving.MessageSender.Error -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address @@ -83,7 +83,7 @@ fun MessageSender.create(name: String, members: Collection): Promise = all( - register(token, TextSecurePreferences.getLocalNumber(context)), - subscribeGroups() - ) - - fun register(token: String?, publicKey: String?): Promise = + token: String? = TextSecurePreferences.getFCMToken(context), + publicKey: String? = TextSecurePreferences.getLocalNumber(context), + legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() + ): Promise = retryIfNeeded(maxRetryCount) { - val parameters = mapOf("token" to token!!, "pubKey" to publicKey!!) - val url = "$legacyServer/register" + Log.d(TAG, "register() called") + + val parameters = mapOf( + "token" to token!!, + "pubKey" to publicKey!!, + "legacyGroupPublicKeys" to legacyGroupPublicKeys.takeIf { it.isNotEmpty() }!! + ) + val url = "$legacyServer/register_legacy_groups_only" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> when (response.info["code"]) { null, 0 -> throw Exception("error: ${response.info["message"]}.") - else -> TextSecurePreferences.setLegacyFCMToken(context, token) + else -> Log.d(TAG, "register() success!!") } - } fail { exception -> - Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") } + } fail { exception -> + Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") } /** * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager. */ fun unregister(): Promise<*, Exception> { - val token = TextSecurePreferences.getLegacyFCMToken(context) ?: emptyPromise() + Log.d(TAG, "unregister() called") + + val token = TextSecurePreferences.getFCMToken(context) ?: emptyPromise() return retryIfNeeded(maxRetryCount) { val parameters = mapOf( "token" to token ) @@ -75,13 +74,14 @@ object PushNotificationAPI { legacyServerPublicKey, Version.V2 ) success { + android.util.Log.d(TAG, "unregister() success!!") + when (it.info["code"]) { null, 0 -> throw Exception("error: ${it.info["message"]}.") else -> Log.d(TAG, "unregisterV1 success token: $token") } - TextSecurePreferences.clearLegacyFCMToken(context) } fail { - exception -> Log.d(TAG, "Couldn't disable FCM with token: $token due to error: ${exception}.") + Log.d(TAG, "Couldn't disable FCM with token: $token due to error: $it.") } } } @@ -91,30 +91,20 @@ object PushNotificationAPI { fun subscribeGroup( closedGroupPublicKey: String, publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = performGroupOperation(ClosedGroupOperation.Subscribe, closedGroupPublicKey, publicKey) - - private fun subscribeGroups( - closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), - publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = closedGroupPublicKeys.map { performGroupOperation(ClosedGroupOperation.Subscribe, it, publicKey) }.let(::all) + ) = performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey) fun unsubscribeGroup( closedGroupPublicKey: String, publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = performGroupOperation(ClosedGroupOperation.Unsubscribe, closedGroupPublicKey, publicKey) - - private fun unsubscribeGroups( - closedGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys(), - publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = closedGroupPublicKeys.map { performGroupOperation(ClosedGroupOperation.Unsubscribe, it, publicKey) }.let(::all) + ) = performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey) private fun performGroupOperation( - operation: ClosedGroupOperation, + operation: String, closedGroupPublicKey: String, publicKey: String ): Promise<*, Exception> { val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) - val url = "$legacyServer/${operation.rawValue}" + val url = "$legacyServer/$operation" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() @@ -127,10 +117,10 @@ object PushNotificationAPI { ) success { when (it.info["code"]) { null, 0 -> throw Exception("${it.info["message"]}") - else -> Log.d(TAG, "performGroupOperation success: ${operation.rawValue}") + else -> Log.d(TAG, "performGroupOperation success: $operation") } } fail { exception -> - Log.d(TAG, "performGroupOperation fail: ${operation.rawValue}: $closedGroupPublicKey due to error: ${exception}.") + Log.d(TAG, "performGroupOperation fail: $operation: $closedGroupPublicKey due to error: ${exception}.") } } } diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 97fe9d3b49..946c73565c 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -250,7 +250,6 @@ interface TextSecurePreferences { const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning" const val GIF_GRID_LAYOUT = "pref_gif_grid_layout" const val IS_USING_FCM = "pref_is_using_fcm" - const val FCM_TOKEN_LEGACY = "pref_fcm_token" const val FCM_TOKEN = "pref_fcm_token_2" const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2" const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time" @@ -313,21 +312,6 @@ interface TextSecurePreferences { setBooleanPreference(context, IS_USING_FCM, value) } - @JvmStatic - fun getLegacyFCMToken(context: Context): String? { - return getStringPreference(context, FCM_TOKEN_LEGACY, "") - } - - @JvmStatic - fun setLegacyFCMToken(context: Context, token: String?) { - setStringPreference(context, FCM_TOKEN_LEGACY, token) - } - - @JvmStatic - fun clearLegacyFCMToken(context: Context) { - removePreference(context, FCM_TOKEN_LEGACY) - } - @JvmStatic fun getFCMToken(context: Context): String? { return getStringPreference(context, FCM_TOKEN, "") From 42cfce0c3e073855b515a616e00a6605b3f6c149 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 21 Jun 2023 10:01:35 +0930 Subject: [PATCH 016/112] Refactor v1 and v2 --- .../notifications/FirebasePushManager.kt | 127 ++------------ .../securesms/notifications/PushManagerV2.kt | 155 ++++++++++++++++++ .../notifications/PushNotificationService.kt | 4 +- .../messaging/jobs/NotifyPNServerJob.kt | 6 +- .../MessageSenderClosedGroupHandler.kt | 4 +- .../ReceivedMessageHandler.kt | 6 +- ...yGroupsPushManager.kt => PushManagerV1.kt} | 60 ++++--- .../session/libsignal/utilities/Retrying.kt | 1 - 8 files changed, 215 insertions(+), 148 deletions(-) create mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt rename libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/{LegacyGroupsPushManager.kt => PushManagerV1.kt} (74%) diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 65314d76d4..d6b738d8c4 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -18,7 +18,7 @@ import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody -import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager +import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.messaging.sending_receiving.notifications.Response import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest @@ -43,52 +43,18 @@ import org.thoughtcrime.securesms.crypto.KeyPairUtilities private const val TAG = "FirebasePushManager" -class FirebasePushManager (private val context: Context): PushManager { +class FirebasePushManager( + private val context: Context + ): PushManager { + + private val pushManagerV2 = PushManagerV2(context) companion object { - private const val maxRetryCount = 4 + const val maxRetryCount = 4 } private val tokenManager = FcmTokenManager(context, ExpiryManager(context)) private var firebaseInstanceIdJob: Job? = null - private val sodium = LazySodiumAndroid(SodiumAndroid()) - - private fun getOrCreateNotificationKey(): Key { - if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { - // generate the key and store it - val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) - IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) - } - return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) - } - - fun decrypt(encPayload: ByteArray): ByteArray? { - Log.d(TAG, "decrypt() called") - - val encKey = getOrCreateNotificationKey() - val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() - val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() - val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) - ?: error("Failed to decrypt push notification") - val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() - val bencoded = Bencode.Decoder(decrypted) - val expectedList = (bencoded.decode() as? BencodeList)?.values - ?: error("Failed to decode bencoded list from payload") - - val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") - val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) - - val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null - // null content is valid only if we got a "data_too_long" flag - if (content == null) - check(metadata.data_too_long) { "missing message data, but no too-long flag" } - else - check(metadata.data_len == content.size) { "wrong message data size" } - - Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") - - return content - } @Synchronized override fun refresh(force: Boolean) { @@ -143,12 +109,12 @@ class FirebasePushManager (private val context: Context): PushManager { publicKey: String, userEd25519Key: KeyPair, namespaces: List = listOf(Namespace.DEFAULT) - ): Promise<*, Exception> = LegacyGroupsPushManager.register(token, publicKey) and getSubscription( + ): Promise<*, Exception> = PushManagerV1.register(token, publicKey) and pushManagerV2.register( token, publicKey, userEd25519Key, namespaces ) fail { Log.e(TAG, "Couldn't register for FCM due to error: $it.", it) } success { - Log.d(TAG, "register() success!!") + Log.d(TAG, "register() success... saving token!!") tokenManager.fcmToken = token } @@ -156,82 +122,13 @@ class FirebasePushManager (private val context: Context): PushManager { token: String, userPublicKey: String, userEdKey: KeyPair - ): Promise<*, Exception> = LegacyGroupsPushManager.unregister() and getUnsubscription( + ): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister( token, userPublicKey, userEdKey ) fail { - Log.e(TAG, "Couldn't unregister for FCM due to error: ${it}.", it) + Log.e(TAG, "Couldn't unregister for FCM due to error: $it.", it) } success { tokenManager.fcmToken = null } - private fun getSubscription( - token: String, - publicKey: String, - userEd25519Key: KeyPair, - namespaces: List - ): Promise { - val pnKey = getOrCreateNotificationKey() - - val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s - // if we want to support passing namespace list, here is the place to do it - val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray() - val signature = ByteArray(Sign.BYTES) - sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes) - val requestParameters = SubscriptionRequest( - pubkey = publicKey, - session_ed25519 = userEd25519Key.publicKey.asHexString, - namespaces = listOf(Namespace.DEFAULT), - data = true, // only permit data subscription for now (?) - service = "firebase", - sig_ts = timestamp, - signature = Base64.encodeBytes(signature), - service_info = mapOf("token" to token), - enc_key = pnKey.asHexString, - ).let(Json::encodeToString) - - return retryResponseBody("subscribe", requestParameters) - } - - private fun getUnsubscription( - token: String, - userPublicKey: String, - userEdKey: KeyPair - ): Promise { - val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s - // if we want to support passing namespace list, here is the place to do it - val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() - val signature = ByteArray(Sign.BYTES) - sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) - - val requestParameters = UnsubscriptionRequest( - pubkey = userPublicKey, - session_ed25519 = userEdKey.publicKey.asHexString, - service = "firebase", - sig_ts = timestamp, - signature = Base64.encodeBytes(signature), - service_info = mapOf("token" to token), - ).let(Json::encodeToString) - - return retryResponseBody("unsubscribe", requestParameters) - } - - private inline fun retryResponseBody(path: String, requestParameters: String): Promise = - retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) } - - private inline fun getResponseBody(path: String, requestParameters: String): Promise { - val url = "${LegacyGroupsPushManager.server}/$path" - val body = RequestBody.create(MediaType.get("application/json"), requestParameters) - val request = Request.Builder().url(url).post(body).build() - - return OnionRequestAPI.sendOnionRequest( - request, - LegacyGroupsPushManager.server, - LegacyGroupsPushManager.serverPublicKey, - Version.V4 - ).map { response -> - response.body!!.inputStream() - .let { Json.decodeFromStream(it) } - .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } - } - } + fun decrypt(decode: ByteArray) = pushManagerV2.decrypt(decode) } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt new file mode 100644 index 0000000000..b63d6be5f0 --- /dev/null +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt @@ -0,0 +1,155 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.interfaces.AEAD +import com.goterl.lazysodium.interfaces.Sign +import com.goterl.lazysodium.utils.Key +import com.goterl.lazysodium.utils.KeyPair +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.map +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata +import org.session.libsession.messaging.sending_receiving.notifications.Response +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest +import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.Version +import org.session.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeList +import org.session.libsession.utilities.bencode.BencodeString +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Namespace +import org.session.libsignal.utilities.retryIfNeeded +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil + +private const val TAG = "PushManagerV2" + +class PushManagerV2(private val context: Context) { + private val sodium = LazySodiumAndroid(SodiumAndroid()) + + fun register( + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List + ): Promise { + val pnKey = getOrCreateNotificationKey() + + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes) + val requestParameters = SubscriptionRequest( + pubkey = publicKey, + session_ed25519 = userEd25519Key.publicKey.asHexString, + namespaces = listOf(Namespace.DEFAULT), + data = true, // only permit data subscription for now (?) + service = "firebase", + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + enc_key = pnKey.asHexString, + ).let(Json::encodeToString) + + return retryResponseBody("subscribe", requestParameters) success { + Log.d(TAG, "register() success!!") + } + } + + fun unregister( + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise { + val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s + // if we want to support passing namespace list, here is the place to do it + val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray() + val signature = ByteArray(Sign.BYTES) + sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes) + + val requestParameters = UnsubscriptionRequest( + pubkey = userPublicKey, + session_ed25519 = userEdKey.publicKey.asHexString, + service = "firebase", + sig_ts = timestamp, + signature = Base64.encodeBytes(signature), + service_info = mapOf("token" to token), + ).let(Json::encodeToString) + + return retryResponseBody("unsubscribe", requestParameters) success { + Log.d(TAG, "unregister() success!!") + } + } + + private inline fun retryResponseBody(path: String, requestParameters: String): Promise = + retryIfNeeded(FirebasePushManager.maxRetryCount) { getResponseBody(path, requestParameters) } + + private inline fun getResponseBody(path: String, requestParameters: String): Promise { + val url = "${PushManagerV1.server}/$path" + val body = RequestBody.create(MediaType.get("application/json"), requestParameters) + val request = Request.Builder().url(url).post(body).build() + + return OnionRequestAPI.sendOnionRequest( + request, + PushManagerV1.server, + PushManagerV1.serverPublicKey, + Version.V4 + ).map { response -> + response.body!!.inputStream() + .let { Json.decodeFromStream(it) } + .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } + } + } + + private fun getOrCreateNotificationKey(): Key { + if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { + // generate the key and store it + val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) + IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) + } + return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) + } + + fun decrypt(encPayload: ByteArray): ByteArray? { + Log.d(TAG, "decrypt() called") + + val encKey = getOrCreateNotificationKey() + val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) + ?: error("Failed to decrypt push notification") + val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() + val bencoded = Bencode.Decoder(decrypted) + val expectedList = (bencoded.decode() as? BencodeList)?.values + ?: error("Failed to decode bencoded list from payload") + + val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") + val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) + + val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null + // null content is valid only if we got a "data_too_long" flag + if (content == null) + check(metadata.data_too_long) { "missing message data, but no too-long flag" } + else + check(metadata.data_len == content.size) { "wrong message data size" } + + Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") + + return content + } +} \ No newline at end of file diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index d09fd79913..0a73d0e428 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -8,7 +8,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager +import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 @@ -69,6 +69,6 @@ class PushNotificationService : FirebaseMessagingService() { override fun onDeletedMessages() { Log.d(TAG, "Called onDeletedMessages.") super.onDeletedMessages() - LegacyGroupsPushManager.register() + PushManagerV1.register() } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 3ab92210d2..7ade248fa6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -8,8 +8,8 @@ import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE -import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager -import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager.server +import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1.server import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.OnionRequestAPI @@ -41,7 +41,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { OnionRequestAPI.sendOnionRequest( request, server, - LegacyGroupsPushManager.serverPublicKey, + PushManagerV1.serverPublicKey, Version.V2 ) success { response -> when (response.info["code"]) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index 926554d3e2..ca4aa609d8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -8,7 +8,7 @@ import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.sending_receiving.MessageSender.Error -import org.session.libsession.messaging.sending_receiving.notifications.LegacyGroupsPushManager +import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address @@ -83,7 +83,7 @@ fun MessageSender.create(name: String, members: Collection): Promise = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() - ): Promise = + ): Promise<*, Exception> = retryIfNeeded(maxRetryCount) { - Log.d(TAG, "register() called") - - val parameters = mapOf( - "token" to token!!, - "pubKey" to publicKey!!, - "legacyGroupPublicKeys" to legacyGroupPublicKeys.takeIf { it.isNotEmpty() }!! - ) - val url = "$legacyServer/register_legacy_groups_only" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body).build() - - OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> - when (response.info["code"]) { - null, 0 -> throw Exception("error: ${response.info["message"]}.") - else -> Log.d(TAG, "register() success!!") - } - } + doRegister(token, publicKey, legacyGroupPublicKeys) } fail { exception -> Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") + } success { + Log.d(TAG, "register success!!") } + private fun doRegister(token: String?, publicKey: String?, legacyGroupPublicKeys: Collection): Promise<*, Exception> { + Log.d(TAG, "doRegister() called $token, $publicKey, $legacyGroupPublicKeys") + + token ?: return emptyPromise() + publicKey ?: return emptyPromise() + legacyGroupPublicKeys.takeIf { it.isNotEmpty() } ?: return emptyPromise() + + Log.d(TAG, "actually registering...") + + val parameters = mapOf( + "token" to token, + "pubKey" to publicKey, + "legacyGroupPublicKeys" to legacyGroupPublicKeys + ) + + val url = "$legacyServer/register_legacy_groups_only" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body).build() + + return OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> + when (response.info["code"]) { + null, 0 -> throw Exception("error: ${response.info["message"]}.") + else -> Log.d(TAG, "register() success!!") + } + } success { + Log.d(TAG, "onion request success") + } fail { + Log.d(TAG, "onion fail: $it") + } + } + /** * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager. */ @@ -74,7 +90,7 @@ object LegacyGroupsPushManager { legacyServerPublicKey, Version.V2 ) success { - android.util.Log.d(TAG, "unregister() success!!") + Log.d(TAG, "unregister() success!!") when (it.info["code"]) { null, 0 -> throw Exception("error: ${it.info["message"]}.") diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt index 28ed93b5a5..9f1b1a3e82 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt @@ -28,4 +28,3 @@ fun > retryIfNeeded(maxRetryCount: Int, retryInterv retryIfNeeded() return deferred.promise } - From e3f60eb5f2b263ec284788073d75e9f950ee366d Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 21 Jun 2023 10:32:29 +0930 Subject: [PATCH 017/112] Fix individual group subs --- .../notifications/FirebasePushManager.kt | 7 +++-- .../notifications/PushNotificationService.kt | 3 +- .../ReceivedMessageHandler.kt | 2 +- .../notifications/PushManagerV1.kt | 28 ++++++------------- .../libsignal/utilities/PromiseUtilities.kt | 10 ++++++- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index d6b738d8c4..25fed8373b 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -73,7 +73,7 @@ class FirebasePushManager( } private fun refresh(token: String, force: Boolean): Promise<*, Exception> { - Log.d(TAG, "refresh() called with: token = $token, force = $force") + Log.d(TAG, "refresh() called") val userPublicKey = getLocalNumber(context) ?: return emptyPromise() val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() @@ -109,7 +109,10 @@ class FirebasePushManager( publicKey: String, userEd25519Key: KeyPair, namespaces: List = listOf(Namespace.DEFAULT) - ): Promise<*, Exception> = PushManagerV1.register(token, publicKey) and pushManagerV2.register( + ): Promise<*, Exception> = PushManagerV1.register( + token = token, + publicKey = publicKey + ) and pushManagerV2.register( token, publicKey, userEd25519Key, namespaces ) fail { Log.e(TAG, "Couldn't register for FCM due to error: $it.", it) diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 0a73d0e428..4c6ce9f4ea 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -24,7 +24,6 @@ class PushNotificationService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) - Log.d(TAG, "New FCM token: $token.") TextSecurePreferences.getLocalNumber(this) ?: return if (TextSecurePreferences.getFCMToken(this) != token) { pushManager.refresh(true) @@ -69,6 +68,6 @@ class PushNotificationService : FirebaseMessagingService() { override fun onDeletedMessages() { Log.d(TAG, "Called onDeletedMessages.") super.onDeletedMessages() - PushManagerV1.register() + pushManager.refresh(true) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 2f8fdd3629..dec50f0206 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -772,7 +772,7 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou storage.setActive(groupID, false) storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) // Notify the PN server - PushManagerV1.register(publicKey = userPublicKey) + PushManagerV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey) // Stop polling ClosedGroupPollerV2.shared.stopPolling(groupPublicKey) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index 3e2bb3a844..6e4c61fee8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -14,6 +14,7 @@ import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.emptyPromise import org.session.libsignal.utilities.retryIfNeeded +import org.session.libsignal.utilities.sideEffect @SuppressLint("StaticFieldLeak") object PushManagerV1 { @@ -27,12 +28,14 @@ object PushManagerV1 { private const val maxRetryCount = 4 fun register( + isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), token: String? = TextSecurePreferences.getFCMToken(context), publicKey: String? = TextSecurePreferences.getLocalNumber(context), legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() ): Promise<*, Exception> = retryIfNeeded(maxRetryCount) { - doRegister(token, publicKey, legacyGroupPublicKeys) + if (isUsingFCM) doRegister(token, publicKey, legacyGroupPublicKeys) + else emptyPromise() } fail { exception -> Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") } success { @@ -40,13 +43,11 @@ object PushManagerV1 { } private fun doRegister(token: String?, publicKey: String?, legacyGroupPublicKeys: Collection): Promise<*, Exception> { - Log.d(TAG, "doRegister() called $token, $publicKey, $legacyGroupPublicKeys") + Log.d(TAG, "doRegister() called") token ?: return emptyPromise() publicKey ?: return emptyPromise() - legacyGroupPublicKeys.takeIf { it.isNotEmpty() } ?: return emptyPromise() - - Log.d(TAG, "actually registering...") + legacyGroupPublicKeys.takeIf { it.isNotEmpty() } ?: return unregister() val parameters = mapOf( "token" to token, @@ -63,10 +64,6 @@ object PushManagerV1 { null, 0 -> throw Exception("error: ${response.info["message"]}.") else -> Log.d(TAG, "register() success!!") } - } success { - Log.d(TAG, "onion request success") - } fail { - Log.d(TAG, "onion fail: $it") } } @@ -90,14 +87,10 @@ object PushManagerV1 { legacyServerPublicKey, Version.V2 ) success { - Log.d(TAG, "unregister() success!!") - when (it.info["code"]) { null, 0 -> throw Exception("error: ${it.info["message"]}.") - else -> Log.d(TAG, "unregisterV1 success token: $token") + else -> Log.d(TAG, "unregisterV1 success") } - } fail { - Log.d(TAG, "Couldn't disable FCM with token: $token due to error: $it.") } } } @@ -130,13 +123,10 @@ object PushManagerV1 { legacyServer, legacyServerPublicKey, Version.V2 - ) success { + ) sideEffect { when (it.info["code"]) { - null, 0 -> throw Exception("${it.info["message"]}") - else -> Log.d(TAG, "performGroupOperation success: $operation") + 0, null -> throw Exception("${it.info["message"]}") } - } fail { exception -> - Log.d(TAG, "performGroupOperation fail: $operation: $closedGroupPublicKey due to error: ${exception}.") } } } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt index 8095b44597..fdf8f107b9 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/PromiseUtilities.kt @@ -3,6 +3,7 @@ package org.session.libsignal.utilities import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.task import java.util.concurrent.TimeoutException @@ -58,4 +59,11 @@ fun Promise.timeout(millis: Long): Promise { if (!deferred.promise.isDone()) { deferred.reject(it) } } return deferred.promise -} \ No newline at end of file +} + +infix fun Promise.sideEffect( + callback: (value: V) -> Unit +) = map { + callback(it) + it +} From dc7602a1d3449aece0f8df4fa9a105847d7f34b8 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 21 Jun 2023 12:37:10 +0930 Subject: [PATCH 018/112] Check fcm is enabled before modifying group sub --- .../notifications/PushManagerV1.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index 6e4c61fee8..09827ef8fa 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -33,13 +33,14 @@ object PushManagerV1 { publicKey: String? = TextSecurePreferences.getLocalNumber(context), legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() ): Promise<*, Exception> = - retryIfNeeded(maxRetryCount) { - if (isUsingFCM) doRegister(token, publicKey, legacyGroupPublicKeys) - else emptyPromise() + if (!isUsingFCM) { + emptyPromise() + } else retryIfNeeded(maxRetryCount) { + doRegister(token, publicKey, legacyGroupPublicKeys) } fail { exception -> Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") } success { - Log.d(TAG, "register success!!") + Log.d(TAG, "register success") } private fun doRegister(token: String?, publicKey: String?, legacyGroupPublicKeys: Collection): Promise<*, Exception> { @@ -62,7 +63,7 @@ object PushManagerV1 { return OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> when (response.info["code"]) { null, 0 -> throw Exception("error: ${response.info["message"]}.") - else -> Log.d(TAG, "register() success!!") + else -> Log.d(TAG, "doRegister success") } } } @@ -99,13 +100,19 @@ object PushManagerV1 { fun subscribeGroup( closedGroupPublicKey: String, + isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey) + ) = if (isUsingFCM) { + performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey) + } else emptyPromise() fun unsubscribeGroup( closedGroupPublicKey: String, + isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey) + ) = if (isUsingFCM) { + performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey) + } else emptyPromise() private fun performGroupOperation( operation: String, From 8be088ad56dbf4283e5e1ef3d47b02229e5aa85d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 23 Jun 2023 10:29:49 +0930 Subject: [PATCH 019/112] Reinstate v1 PNServerJob --- .../notifications/FirebasePushManager.kt | 8 +-- .../securesms/notifications/PushManagerV2.kt | 14 +++-- .../messaging/jobs/NotifyPNServerJob.kt | 20 +++--- .../notifications/PushManagerV1.kt | 61 ++++++++----------- .../sending_receiving/notifications/Server.kt | 6 ++ .../libsession/snode/OnionRequestAPI.kt | 5 +- 6 files changed, 59 insertions(+), 55 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 25fed8373b..89712c2e85 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -66,7 +66,7 @@ class FirebasePushManager( firebaseInstanceIdJob = getFcmInstanceId { task -> when { - task.isSuccessful -> try { task.result?.token?.let { refresh(it, force).get() } } catch(e: Exception) { Log.d(TAG, "refresh() failed", e) } + task.isSuccessful -> try { task.result?.token?.let { refresh(it, force).get() } } catch(e: Exception) { Log.e(TAG, "refresh() failed", e) } else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception) } } @@ -115,9 +115,9 @@ class FirebasePushManager( ) and pushManagerV2.register( token, publicKey, userEd25519Key, namespaces ) fail { - Log.e(TAG, "Couldn't register for FCM due to error: $it.", it) + Log.e(TAG, "registerBoth failed", it) } success { - Log.d(TAG, "register() success... saving token!!") + Log.d(TAG, "registerBoth success... saving token!!") tokenManager.fcmToken = token } @@ -128,7 +128,7 @@ class FirebasePushManager( ): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister( token, userPublicKey, userEdKey ) fail { - Log.e(TAG, "Couldn't unregister for FCM due to error: $it.", it) + Log.e(TAG, "unregisterBoth failed", it) } success { tokenManager.fcmToken = null } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt index b63d6be5f0..66c7590e5f 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt @@ -19,6 +19,7 @@ import okhttp3.RequestBody import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.messaging.sending_receiving.notifications.Response +import org.session.libsession.messaging.sending_receiving.notifications.Server import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse @@ -67,7 +68,7 @@ class PushManagerV2(private val context: Context) { ).let(Json::encodeToString) return retryResponseBody("subscribe", requestParameters) success { - Log.d(TAG, "register() success!!") + Log.d(TAG, "registerV2 success") } } @@ -92,7 +93,7 @@ class PushManagerV2(private val context: Context) { ).let(Json::encodeToString) return retryResponseBody("unsubscribe", requestParameters) success { - Log.d(TAG, "unregister() success!!") + Log.d(TAG, "unregisterV2 success") } } @@ -100,14 +101,15 @@ class PushManagerV2(private val context: Context) { retryIfNeeded(FirebasePushManager.maxRetryCount) { getResponseBody(path, requestParameters) } private inline fun getResponseBody(path: String, requestParameters: String): Promise { - val url = "${PushManagerV1.server}/$path" + val server = Server.LATEST + val url = "${server.url}/$path" val body = RequestBody.create(MediaType.get("application/json"), requestParameters) val request = Request.Builder().url(url).post(body).build() return OnionRequestAPI.sendOnionRequest( request, - PushManagerV1.server, - PushManagerV1.serverPublicKey, + server.url, + server.publicKey, Version.V4 ).map { response -> response.body!!.inputStream() @@ -152,4 +154,4 @@ class PushManagerV2(private val context: Context) { return content } -} \ No newline at end of file +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 7ade248fa6..6a0d049ce3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -8,8 +8,7 @@ import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1.server +import org.session.libsession.messaging.sending_receiving.notifications.Server import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.OnionRequestAPI @@ -33,26 +32,27 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { } override fun execute(dispatcherName: String) { + val server = Server.LEGACY val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) - val url = "${server}/notify" + val url = "${server.url}/notify" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() retryIfNeeded(4) { OnionRequestAPI.sendOnionRequest( request, - server, - PushManagerV1.serverPublicKey, + server.url, + server.publicKey, Version.V2 ) success { response -> - when (response.info["code"]) { - null, 0 -> Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"]}.") + when (response.code) { + null, 0 -> Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: ${response.message}.") } } fail { exception -> - Log.d("Loki", "Couldn't notify PN server due to error: $exception.") + Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: $exception.") } - }.success { + } success { handleSuccess(dispatcherName) - }. fail { + } fail { handleFailure(dispatcherName, it) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index 09827ef8fa..e112cd2682 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -2,12 +2,12 @@ package org.session.libsession.messaging.sending_receiving.notifications import android.annotation.SuppressLint import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.OnionResponse import org.session.libsession.snode.Version import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.JsonUtil @@ -21,12 +21,10 @@ object PushManagerV1 { private const val TAG = "PushManagerV1" val context = MessagingModuleConfiguration.shared.context - const val server = "https://push.getsession.org" - const val serverPublicKey: String = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" - private const val legacyServer = "https://dev.apns.getsession.org" - private const val legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" private const val maxRetryCount = 4 + private val server = Server.LEGACY + fun register( isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), token: String? = TextSecurePreferences.getFCMToken(context), @@ -38,17 +36,14 @@ object PushManagerV1 { } else retryIfNeeded(maxRetryCount) { doRegister(token, publicKey, legacyGroupPublicKeys) } fail { exception -> - Log.d(TAG, "Couldn't register for FCM due to error: ${exception}.") - } success { - Log.d(TAG, "register success") + Log.d(TAG, "Couldn't register for FCM due to error: $exception.") } private fun doRegister(token: String?, publicKey: String?, legacyGroupPublicKeys: Collection): Promise<*, Exception> { - Log.d(TAG, "doRegister() called") + Log.d(TAG, "registerV1 requested") token ?: return emptyPromise() publicKey ?: return emptyPromise() - legacyGroupPublicKeys.takeIf { it.isNotEmpty() } ?: return unregister() val parameters = mapOf( "token" to token, @@ -56,15 +51,16 @@ object PushManagerV1 { "legacyGroupPublicKeys" to legacyGroupPublicKeys ) - val url = "$legacyServer/register_legacy_groups_only" + val url = "${server.url}/register_legacy_groups_only" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() - return OnionRequestAPI.sendOnionRequest(request, legacyServer, legacyServerPublicKey, Version.V2).map { response -> - when (response.info["code"]) { - null, 0 -> throw Exception("error: ${response.info["message"]}.") - else -> Log.d(TAG, "doRegister success") + return sendOnionRequest(request) sideEffect { response -> + when (response.code) { + null, 0 -> throw Exception("error: ${response.message}.") } + } success { + Log.d(TAG, "registerV1 success") } } @@ -72,24 +68,19 @@ object PushManagerV1 { * Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager. */ fun unregister(): Promise<*, Exception> { - Log.d(TAG, "unregister() called") + Log.d(TAG, "unregisterV1 requested") val token = TextSecurePreferences.getFCMToken(context) ?: emptyPromise() return retryIfNeeded(maxRetryCount) { val parameters = mapOf( "token" to token ) - val url = "$legacyServer/unregister" + val url = "${server.url}/unregister" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() - OnionRequestAPI.sendOnionRequest( - request, - legacyServer, - legacyServerPublicKey, - Version.V2 - ) success { - when (it.info["code"]) { - null, 0 -> throw Exception("error: ${it.info["message"]}.") + sendOnionRequest(request) success { + when (it.code) { + null, 0 -> throw Exception("error: ${it.message}.") else -> Log.d(TAG, "unregisterV1 success") } } @@ -120,21 +111,23 @@ object PushManagerV1 { publicKey: String ): Promise<*, Exception> { val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) - val url = "$legacyServer/$operation" + val url = "${server.url}/$operation" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() return retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest( - request, - legacyServer, - legacyServerPublicKey, - Version.V2 - ) sideEffect { - when (it.info["code"]) { - 0, null -> throw Exception("${it.info["message"]}") + sendOnionRequest(request) sideEffect { + when (it.code) { + 0, null -> throw Exception(it.message) } } } } + + private fun sendOnionRequest(request: Request): Promise = OnionRequestAPI.sendOnionRequest( + request, + server.url, + server.publicKey, + Version.V2 + ) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt new file mode 100644 index 0000000000..6fefd694e0 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt @@ -0,0 +1,6 @@ +package org.session.libsession.messaging.sending_receiving.notifications + +enum class Server(val url: String, val publicKey: String) { + LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"), + LEGACY("https://dev.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049") +} diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 087c8e29d3..0ed3a1afe7 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -684,4 +684,7 @@ enum class Version(val value: String) { data class OnionResponse( val info: Map<*, *>, val body: ByteArray? = null -) +) { + val code: Int? get() = info["code"] as? Int + val message: String? get() = info["message"] as? String +} From ab8b2c42b9ceb0ddfbc0948686d1154b658d31cb Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 28 Jun 2023 10:34:48 +0930 Subject: [PATCH 020/112] Add jetpack compose --- app/build.gradle | 12 +++ .../securesms/calls/WebRtcCallActivity.kt | 13 +-- .../v2/menus/ConversationMenuHelper.kt | 2 +- .../securesms/dms/NewMessageFragment.kt | 2 +- .../securesms/groups/JoinCommunityFragment.kt | 2 +- .../home/search/GlobalSearchAdapterUtils.kt | 1 + .../keyboard/emoji/KeyboardPageSearchView.kt | 2 +- .../securesms/util/ViewUtilities.kt | 2 +- build.gradle | 3 +- gradle.properties | 4 +- .../messaging/jobs/BatchMessageReceiveJob.kt | 3 +- .../messaging/jobs/MessageReceiveJob.kt | 1 - .../messaging/mentions/MentionsManager.kt | 5 +- .../pollers/OpenGroupPoller.kt | 1 + .../utilities/UpdateMessageBuilder.kt | 95 ++++++++----------- 15 files changed, 72 insertions(+), 76 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8a42419732..6170763a60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,6 +22,7 @@ apply plugin: 'com.google.gms.google-services' apply plugin: 'kotlinx-serialization' apply plugin: 'dagger.hilt.android.plugin' + configurations.all { exclude module: "commons-logging" } @@ -158,6 +159,10 @@ dependencies { testImplementation 'org.robolectric:robolectric:4.4' testImplementation 'org.robolectric:shadows-multidex:4.4' + + implementation 'androidx.compose.ui:ui:1.4.3' + implementation 'androidx.compose.material:material:1.4.3' + implementation 'androidx.compose.ui:ui-tooling:1.4.3' } def canonicalVersionCode = 338 @@ -203,6 +208,13 @@ android { } } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.4.7' + } + defaultConfig { versionCode canonicalVersionCode * postFixSize versionName canonicalVersionName diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt index 7e732d1aa7..b87eac12c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt @@ -249,17 +249,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { viewModel.callState.collect { state -> Log.d("Loki", "Consuming view model state $state") when (state) { - CALL_RINGING -> { - if (wantsToAnswer) { - answerCall() - wantsToAnswer = false - } - } - CALL_OUTGOING -> { - } - CALL_CONNECTED -> { + CALL_RINGING -> if (wantsToAnswer) { + answerCall() wantsToAnswer = false } + CALL_CONNECTED -> wantsToAnswer = false + else -> {} } updateControls(state) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index ce29efa3a0..fee9b094b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -68,7 +68,7 @@ object ConversationMenuHelper { if (thread.expireMessages > 0) { inflater.inflate(R.menu.menu_conversation_expiration_on, menu) val item = menu.findItem(R.id.menu_expiring_messages) - val actionView = item.actionView + val actionView = item.actionView!! val iconView = actionView.findViewById(R.id.menu_badge_icon) val badgeView = actionView.findViewById(R.id.expiration_badge) @ColorInt val color = context.getColorFromAttr(android.R.attr.textColorPrimary) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt index 8b880d2189..74e2cac4c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dms/NewMessageFragment.kt @@ -98,7 +98,7 @@ class NewMessageFragment : Fragment() { private fun hideLoader() { binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) binding.loader.visibility = View.GONE } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt index d37b17ef9f..068c01c325 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt @@ -55,7 +55,7 @@ class JoinCommunityFragment : Fragment() { fun hideLoader() { binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) binding.loader.visibility = View.GONE } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt index 2c64ded866..47e9c75942 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -76,6 +76,7 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) { } binding.searchResultSubtitle.text = getHighlight(query, membersString) } + else -> {} } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt index 07da14b090..a85ea525ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt @@ -154,7 +154,7 @@ class KeyboardPageSearchView @JvmOverloads constructor( .setDuration(REVEAL_DURATION) .alpha(0f) .setListener(object : AnimationCompleteListener() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { visibility = INVISIBLE } }) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt index 8345473490..dfd4ffe419 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt @@ -58,7 +58,7 @@ fun View.fadeIn(duration: Long = 150) { fun View.fadeOut(duration: Long = 150) { animate().setDuration(duration).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) visibility = View.GONE } diff --git a/build.gradle b/build.gradle index 7e7e14f00f..39701076cf 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "com.google.gms:google-services:$googleServicesVersion" classpath files('libs/gradle-witness.jar') + classpath "com.squareup:javapoet:1.13.0" } } @@ -52,6 +53,6 @@ allprojects { project.ext { androidMinimumSdkVersion = 23 androidTargetSdkVersion = 31 - androidCompileSdkVersion = 32 + androidCompileSdkVersion = 33 } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index fa51fdbca0..7af942564a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,11 +4,11 @@ org.gradle.jvmargs=-Xmx8g gradlePluginVersion=7.3.1 googleServicesVersion=4.3.12 -kotlinVersion=1.6.21 +kotlinVersion=1.8.21 coroutinesVersion=1.6.4 kotlinxJsonVersion=1.3.3 lifecycleVersion=2.5.1 -daggerVersion=2.40.1 +daggerVersion=2.46.1 glideVersion=4.11.0 kovenantVersion=3.3.0 curve25519Version=0.6.0 diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index fa07a7d9c0..62f27c19c4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -235,10 +235,9 @@ class BatchMessageReceiveJob( val openGroupID = data.getStringOrDefault(OPEN_GROUP_ID_KEY, null) val parameters = (0 until numMessages).map { index -> - val data = contents[index] val serverHash = serverHashes[index].let { if (it.isEmpty()) null else it } val serverId = openGroupMessageServerIDs[index].let { if (it == -1L) null else it } - MessageReceiveParameters(data, serverHash, serverId) + MessageReceiveParameters(contents[index], serverHash, serverId) } return BatchMessageReceiveJob(parameters, openGroupID) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index 2ba33b5632..121e04dcf7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt @@ -32,7 +32,6 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val fun executeAsync(dispatcherName: String): Promise { val deferred = deferred() try { - val isRetry: Boolean = failureCount != 0 val serverPublicKey = openGroupID?.let { MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString(".")) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt b/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt index e9eae0ba5a..fd16061e67 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt @@ -2,6 +2,7 @@ package org.session.libsession.messaging.mentions import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact +import java.util.* object MentionsManager { var userPublicKeyCache = mutableMapOf>() // Thread ID to set of user hex encoded public keys @@ -32,9 +33,9 @@ object MentionsManager { candidates.sortedBy { it.displayName } if (query.length >= 2) { // Filter out any non-matching candidates - candidates = candidates.filter { it.displayName.toLowerCase().contains(query.toLowerCase()) } + candidates = candidates.filter { it.displayName.lowercase(Locale.getDefault()).contains(query.lowercase(Locale.getDefault())) } // Sort based on where in the candidate the query occurs - candidates.sortedBy { it.displayName.toLowerCase().indexOf(query.toLowerCase()) } + candidates.sortedBy { it.displayName.lowercase(Locale.getDefault()).indexOf(query.lowercase(Locale.getDefault())) } } // Return return candidates diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 387381c9cc..a75a70433b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -169,6 +169,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S is Endpoint.Outbox, is Endpoint.OutboxSince -> { handleDirectMessages(server, true, response.body as List) } + else -> {} } if (secondToLastJob == null && !isCaughtUp) { isCaughtUp = true diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt index 35328b9742..6cf18ba5c5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -4,52 +4,51 @@ import android.content.Context import org.session.libsession.R import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.calls.CallMessageType +import org.session.libsession.messaging.calls.CallMessageType.CALL_FIRST_MISSED +import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING +import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED +import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.truncateIdForDisplay object UpdateMessageBuilder { + val storage = MessagingModuleConfiguration.shared.storage + + fun getSenderName(senderId: String) = storage.getContactWithSessionID(senderId) + ?.displayName(Contact.ContactContext.REGULAR) + ?: truncateIdForDisplay(senderId) fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, senderId: String? = null, isOutgoing: Boolean = false): String { - var message = "" - val updateData = updateMessageData.kind ?: return message - if (!isOutgoing && senderId == null) return message - val storage = MessagingModuleConfiguration.shared.storage - val senderName: String = if (!isOutgoing) { - storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId) - } else { context.getString(R.string.MessageRecord_you) } + val updateData = updateMessageData.kind + if (updateData == null || !isOutgoing && senderId == null) return "" + val senderName: String = if (isOutgoing) context.getString(R.string.MessageRecord_you) + else getSenderName(senderId!!) - when (updateData) { - is UpdateMessageData.Kind.GroupCreation -> { - message = if (isOutgoing) { - context.getString(R.string.MessageRecord_you_created_a_new_group) - } else { - context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName) - } + return when (updateData) { + is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) { + context.getString(R.string.MessageRecord_you_created_a_new_group) + } else { + context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName) } - is UpdateMessageData.Kind.GroupNameChange -> { - message = if (isOutgoing) { - context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name) - } else { - context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name) - } + is UpdateMessageData.Kind.GroupNameChange -> if (isOutgoing) { + context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name) + } else { + context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name) } is UpdateMessageData.Kind.GroupMemberAdded -> { - val members = updateData.updatedMembers.joinToString(", ") { - storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it - } - message = if (isOutgoing) { + val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName) + if (isOutgoing) { context.getString(R.string.MessageRecord_you_added_s_to_the_group, members) } else { context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members) } } is UpdateMessageData.Kind.GroupMemberRemoved -> { - val storage = MessagingModuleConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey()!! // 1st case: you are part of the removed members - message = if (userPublicKey in updateData.updatedMembers) { + return if (userPublicKey in updateData.updatedMembers) { if (isOutgoing) { context.getString(R.string.MessageRecord_left_group) } else { @@ -57,9 +56,7 @@ object UpdateMessageBuilder { } } else { // 2nd case: you are not part of the removed members - val members = updateData.updatedMembers.joinToString(", ") { - storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it - } + val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName) if (isOutgoing) { context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members) } else { @@ -67,22 +64,19 @@ object UpdateMessageBuilder { } } } - is UpdateMessageData.Kind.GroupMemberLeft -> { - message = if (isOutgoing) { - context.getString(R.string.MessageRecord_left_group) - } else { - context.getString(R.string.ConversationItem_group_action_left, senderName) - } + is UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) { + context.getString(R.string.MessageRecord_left_group) + } else { + context.getString(R.string.ConversationItem_group_action_left, senderName) } + else -> return "" } - return message } fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String { if (!isOutgoing && senderId == null) return "" - val storage = MessagingModuleConfiguration.shared.storage - val senderName: String? = if (!isOutgoing) { - storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId) + val senderName: String = if (!isOutgoing) { + getSenderName(senderId!!) } else { context.getString(R.string.MessageRecord_you) } return if (duration <= 0) { if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages) @@ -95,8 +89,7 @@ object UpdateMessageBuilder { } fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String { - val storage = MessagingModuleConfiguration.shared.storage - val senderName = storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId) + val senderName = getSenderName(senderId!!) return when (kind) { DataExtractionNotificationInfoMessage.Kind.SCREENSHOT -> context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName) @@ -105,18 +98,12 @@ object UpdateMessageBuilder { } } - fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String { - val storage = MessagingModuleConfiguration.shared.storage - val senderName = storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender - return when (type) { - CallMessageType.CALL_MISSED -> - context.getString(R.string.MessageRecord_missed_call_from, senderName) - CallMessageType.CALL_INCOMING -> - context.getString(R.string.MessageRecord_s_called_you, senderName) - CallMessageType.CALL_OUTGOING -> - context.getString(R.string.MessageRecord_called_s, senderName) - CallMessageType.CALL_FIRST_MISSED -> - context.getString(R.string.MessageRecord_missed_call_from, senderName) + fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String = + when (type) { + CALL_INCOMING -> R.string.MessageRecord_s_called_you + CALL_OUTGOING -> R.string.MessageRecord_called_s + CALL_MISSED, CALL_FIRST_MISSED -> R.string.MessageRecord_missed_call_from + }.let { + context.getString(it, storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender) } - } } From fc108b34dbbd4cd31ceea8e44f3391d7a47c5e8e Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Jun 2023 11:37:55 +0930 Subject: [PATCH 021/112] Create Message Details screen --- app/build.gradle | 6 +- .../conversation/v2/MessageDetailActivity.kt | 221 +++++++++++++----- 2 files changed, 166 insertions(+), 61 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6170763a60..8194ef4b7b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,7 @@ configurations.all { } dependencies { + implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "com.google.android.material:material:$materialVersion" @@ -161,8 +162,11 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' implementation 'androidx.compose.ui:ui:1.4.3' - implementation 'androidx.compose.material:material:1.4.3' implementation 'androidx.compose.ui:ui-tooling:1.4.3' + implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.30.1" + + implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02' + implementation 'androidx.compose.material:material:1.5.0-alpha02' } def canonicalVersionCode = 338 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 0938b21dd9..2c1698ae8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,32 +2,49 @@ package org.thoughtcrime.securesms.conversation.v2 import android.os.Bundle import android.view.View -import androidx.core.view.isVisible +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityMessageDetailBinding -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.utilities.SessionId -import org.session.libsession.messaging.utilities.SodiumUtilities -import org.session.libsession.snode.SnodeAPI -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.ExpirationUtil -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.DateUtils -import java.text.SimpleDateFormat import java.util.* import javax.inject.Inject @AndroidEntryPoint class MessageDetailActivity: PassphraseRequiredActionBarActivity() { - private lateinit var binding: ActivityMessageDetailBinding var messageRecord: MessageRecord? = null @Inject @@ -42,58 +59,142 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) - binding = ActivityMessageDetailBinding.inflate(layoutInflater) - setContentView(binding.root) - title = resources.getString(R.string.conversation_context__menu_message_details) + val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) - // We only show this screen for messages fail to send, - // so the author of the messages must be the current user. - val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) - messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run { - finish() - return - } - val threadId = messageRecord!!.threadId - val openGroup = storage.getOpenGroup(threadId) - val blindedKey = openGroup?.let { group -> - val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return@let null - val blindingEnabled = storage.getServerCapabilities(group.server).contains(OpenGroupApi.Capability.BLIND.name.lowercase()) - if (blindingEnabled) { - SodiumUtilities.blindedKeyPair(group.publicKey, userEdKeyPair)?.publicKey?.asBytes - ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString - } else null - } - updateContent() - binding.resendButton.setOnClickListener { - ResendMessageUtilities.resend(this, messageRecord!!, blindedKey) - finish() + + title = resources.getString(R.string.conversation_context__menu_message_details) + + setContentView(createComposeView()) + } + + private fun createComposeView(): ComposeView = ComposeView(this).apply { + id = View.generateViewId() + setContent { + MessageDetails() } } - fun updateContent() { - val dateLocale = Locale.getDefault() - val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale) - binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent)) + data class TitledText(val title: String, val value: String) - val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) - if (errorMessage != null) { - binding.errorMessage.text = errorMessage - binding.resendContainer.isVisible = true - binding.errorContainer.isVisible = true - } else { - binding.errorContainer.isVisible = false - binding.resendContainer.isVisible = false + @OptIn(ExperimentalLayoutApi::class) + @Preview + @Composable + fun MessageDetails() { + val fileDetails = listOf( + TitledText("File Id:", "1237896548514214124235985214"), + TitledText("File Type:", ".PNG"), + TitledText("File Size:", "6mb"), + TitledText("Resolution:", "550x550"), + TitledText("Duration:", "N/A"), + ) + + val sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022 ") + val received = TitledText("Received:", "6:12 AM Tue, 09/08/2022 ") + val user = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") + + AppCompatTheme { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp)) { + CardWithPadding { + FlowRow( + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = 2 + ) { + fileDetails.forEach { + titledText(it, Modifier.weight(1f)) + } + } + } + CardWithPadding { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + titledText(sent) + titledText(received) + titledView("From:") { + Row { + Box(modifier = Modifier.width(60.dp).height(60.dp)) + Column { + titledText(user) + } + } + } + } + } + Card { + Column { + ItemButton("Reply", R.drawable.ic_reply) + Divider() + ItemButton("Resend", R.drawable.ic_reply) + Divider() + ItemButton("Delete", R.drawable.ic_delete_24, color = Color.Red) + } + } + } } + } - if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) { - binding.expiresContainer.visibility = View.GONE - } else { - binding.expiresContainer.visibility = View.VISIBLE - val elapsed = SnodeAPI.nowWithOffset - messageRecord!!.expireStarted - val remaining = messageRecord!!.expiresIn - elapsed + @Composable + fun Divider() { + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 1.dp, color = Color(0xff414141)) + } - val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1)) - binding.expiresIn.text = duration + @Composable + fun ItemButton(text: String, @DrawableRes icon: Int, color: Color = Color.White) { + TextButton( + modifier = Modifier + .fillMaxWidth() + .height(60.dp), + onClick = {}, + shape = RectangleShape, + ) { + Box(modifier = Modifier.width(80.dp).fillMaxHeight()) { + Icon( + painter = painterResource(id = icon), + contentDescription = "", + tint = color, + modifier = Modifier.align(Alignment.Center) + ) + } + Text(text, color = color, modifier = Modifier.fillMaxWidth()) } } + + @Composable + fun Card(content: @Composable () -> Unit) { + CardWithPadding(0.dp) { content() } + } + + @Composable + fun CardWithPadding(padding: Dp = 24.dp, content: @Composable () -> Unit) { + Card( + shape = RoundedCornerShape(16.dp), + elevation = 0.dp, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 32.dp), + backgroundColor = Color(0xff1b1b1b), + contentColor = Color.White + ) { Box(Modifier.padding(padding)) { content() } } + } + + @Composable + fun titledText(titledText: TitledText, modifier: Modifier = Modifier) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Title(titledText.title) + Text(titledText.value) + } + } + @Composable + fun titledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Title(title) + content() + } + } + + @Composable + fun Title(text: String) { + Text(text, fontWeight = FontWeight.Bold) + } } \ No newline at end of file From 0c2682fe47354e5940352233b5f2e79d55a42d17 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Jun 2023 17:11:11 +0930 Subject: [PATCH 022/112] Improve compose theming --- .../conversation/v2/MessageDetailActivity.kt | 80 +++++-------------- .../org/thoughtcrime/securesms/ui/Colors.kt | 13 +++ .../thoughtcrime/securesms/ui/Components.kt | 67 ++++++++++++++++ .../org/thoughtcrime/securesms/ui/Themes.kt | 42 ++++++++++ app/src/main/res/values/themes.xml | 6 ++ 5 files changed, 146 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 2c1698ae8d..04eeb90056 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,47 +2,40 @@ package org.thoughtcrime.securesms.conversation.v2 import android.os.Bundle import android.view.View -import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Card import androidx.compose.material.Divider -import androidx.compose.material.Icon import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.Cell +import org.thoughtcrime.securesms.ui.CellWithPadding +import org.thoughtcrime.securesms.ui.ItemButton +import org.thoughtcrime.securesms.ui.LocalExtraColors +import org.thoughtcrime.securesms.ui.destructiveButtonColors import java.util.* import javax.inject.Inject + @AndroidEntryPoint class MessageDetailActivity: PassphraseRequiredActionBarActivity() { var messageRecord: MessageRecord? = null @@ -92,11 +85,11 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val received = TitledText("Received:", "6:12 AM Tue, 09/08/2022 ") val user = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") - AppCompatTheme { + AppTheme { Column( modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp)) { - CardWithPadding { + CellWithPadding { FlowRow( verticalArrangement = Arrangement.spacedBy(16.dp), maxItemsInEachRow = 2 @@ -106,13 +99,15 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } } } - CardWithPadding { + CellWithPadding { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { titledText(sent) titledText(received) titledView("From:") { Row { - Box(modifier = Modifier.width(60.dp).height(60.dp)) + Box(modifier = Modifier + .width(60.dp) + .height(60.dp)) Column { titledText(user) } @@ -120,13 +115,13 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } } } - Card { + Cell { Column { ItemButton("Reply", R.drawable.ic_reply) Divider() ItemButton("Resend", R.drawable.ic_reply) Divider() - ItemButton("Delete", R.drawable.ic_delete_24, color = Color.Red) + ItemButton("Delete", R.drawable.ic_delete_24 , colors = destructiveButtonColors()) } } } @@ -135,47 +130,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { @Composable fun Divider() { - Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 1.dp, color = Color(0xff414141)) - } - - @Composable - fun ItemButton(text: String, @DrawableRes icon: Int, color: Color = Color.White) { - TextButton( - modifier = Modifier - .fillMaxWidth() - .height(60.dp), - onClick = {}, - shape = RectangleShape, - ) { - Box(modifier = Modifier.width(80.dp).fillMaxHeight()) { - Icon( - painter = painterResource(id = icon), - contentDescription = "", - tint = color, - modifier = Modifier.align(Alignment.Center) - ) - } - Text(text, color = color, modifier = Modifier.fillMaxWidth()) - } - } - - @Composable - fun Card(content: @Composable () -> Unit) { - CardWithPadding(0.dp) { content() } - } - - @Composable - fun CardWithPadding(padding: Dp = 24.dp, content: @Composable () -> Unit) { - Card( - shape = RoundedCornerShape(16.dp), - elevation = 0.dp, - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .padding(horizontal = 32.dp), - backgroundColor = Color(0xff1b1b1b), - contentColor = Color.White - ) { Box(Modifier.padding(padding)) { content() } } + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 1.dp, color = LocalExtraColors.current.divider) } @Composable @@ -185,6 +140,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { Text(titledText.value) } } + @Composable fun titledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { @@ -197,4 +153,4 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { fun Title(text: String) { Text(text, fontWeight = FontWeight.Bold) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt new file mode 100644 index 0000000000..0ea7e07141 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.ui + +import androidx.compose.material.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +val colorDestructive = Color(0xffFF453A) + +@Composable +fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent) + +@Composable +fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt new file mode 100644 index 0000000000..b5d78e99d2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonColors +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun ItemButton( + text: String, + @DrawableRes icon: Int, + colors: ButtonColors = transparentButtonColors(), +) { + TextButton( + modifier = Modifier + .fillMaxWidth() + .height(60.dp), + colors = colors, + onClick = {}, + shape = RectangleShape, + ) { + Box(modifier = Modifier + .width(80.dp) + .fillMaxHeight()) { + Icon( + painter = painterResource(id = icon), + contentDescription = "", + modifier = Modifier.align(Alignment.Center) + ) + } + Text(text, modifier = Modifier.fillMaxWidth()) + } +} + +@Composable +fun Cell(content: @Composable () -> Unit) { + CellWithPadding(0.dp) { content() } +} + +@Composable +fun CellWithPadding(padding: Dp = 24.dp, content: @Composable () -> Unit) { + androidx.compose.material.Card( + shape = RoundedCornerShape(16.dp), + elevation = 0.dp, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = 32.dp), + backgroundColor = LocalExtraColors.current.settingsBackground + ) { Box(Modifier.padding(padding)) { content() } } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt new file mode 100644 index 0000000000..a19082c088 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.ui + +import android.content.Context +import androidx.annotation.AttrRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import com.google.android.material.color.MaterialColors +import network.loki.messenger.R + +val LocalExtraColors = staticCompositionLocalOf { error("No Custom Attribute value provided") } + +data class ExtraColors( + val cell: Color, + val divider: Color, + val settingsBackground: Color, +) + +fun Context.getColorFromTheme(@AttrRes attr: Int): Color = + MaterialColors.getColor(this, attr, 0).let(::Color) + +@Composable +fun AppTheme( + content: @Composable () -> Unit +) { + val extraColors = LocalContext.current.run { + ExtraColors( + cell = getColorFromTheme(R.attr.colorCellBackground), + divider = getColorFromTheme(R.attr.dividerColor), + settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground) + ) + } + + CompositionLocalProvider(LocalExtraColors provides extraColors) { + AppCompatTheme { + content() + } + } +} diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index edf8108d60..bc85638501 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -342,6 +342,8 @@ ?colorAccent @color/classic_dark_3 + false + #1B1B1B #E5E5E8 @@ -424,6 +426,7 @@ ?colorPrimary true true + true true ?colorPrimary @@ -507,6 +510,8 @@ ?colorAccent @color/ocean_dark_4 + false + @color/ocean_dark_3 @color/ocean_dark_7 @@ -594,6 +599,7 @@ ?colorPrimary true true + true true ?colorPrimary From d5b3d9bcf9b3f2a408079d7db67625b05ba8a5ef Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Jun 2023 19:14:47 +0930 Subject: [PATCH 023/112] Improve theming --- .../conversation/v2/MessageDetailActivity.kt | 13 ++++++++----- .../org/thoughtcrime/securesms/ui/Components.kt | 10 +++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 04eeb90056..34d53acf78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -14,10 +14,13 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider +import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -81,8 +84,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { TitledText("Duration:", "N/A"), ) - val sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022 ") - val received = TitledText("Received:", "6:12 AM Tue, 09/08/2022 ") + val sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022") + val received = TitledText("Received:", "6:12 AM Tue, 09/08/2022") val user = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") AppTheme { @@ -109,7 +112,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { .width(60.dp) .height(60.dp)) Column { - titledText(user) + titledText(user, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) } } } @@ -134,10 +137,10 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } @Composable - fun titledText(titledText: TitledText, modifier: Modifier = Modifier) { + fun titledText(titledText: TitledText, modifier: Modifier = Modifier, valueStyle: TextStyle = LocalTextStyle.current) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(titledText.title) - Text(titledText.value) + Text(titledText.value, style = valueStyle) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index b5d78e99d2..046ba4635a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -10,7 +10,9 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonColors +import androidx.compose.material.Card import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable @@ -55,13 +57,15 @@ fun Cell(content: @Composable () -> Unit) { @Composable fun CellWithPadding(padding: Dp = 24.dp, content: @Composable () -> Unit) { - androidx.compose.material.Card( + Card( shape = RoundedCornerShape(16.dp), elevation = 0.dp, modifier = Modifier .wrapContentHeight() .fillMaxWidth() .padding(horizontal = 32.dp), - backgroundColor = LocalExtraColors.current.settingsBackground + backgroundColor = LocalExtraColors.current.settingsBackground, + // probably wrong + contentColor = MaterialTheme.colors.onSurface ) { Box(Modifier.padding(padding)) { content() } } -} \ No newline at end of file +} From ac476f4382996ef9ec716c0cffdd5fad9e9d32a2 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Jun 2023 22:11:15 +0930 Subject: [PATCH 024/112] Add icons and respect nullability --- app/build.gradle | 2 + .../conversation/v2/MessageDetailActivity.kt | 139 +++++++++++------- .../drawable/ic_message_details__refresh.xml | 9 ++ .../drawable/ic_message_details__reply.xml | 9 ++ .../drawable/ic_message_details__trash.xml | 9 ++ 5 files changed, 116 insertions(+), 52 deletions(-) create mode 100644 app/src/main/res/drawable/ic_message_details__refresh.xml create mode 100644 app/src/main/res/drawable/ic_message_details__reply.xml create mode 100644 app/src/main/res/drawable/ic_message_details__trash.xml diff --git a/app/build.gradle b/app/build.gradle index 8194ef4b7b..c3ae7de49a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,6 +164,8 @@ dependencies { implementation 'androidx.compose.ui:ui:1.4.3' implementation 'androidx.compose.ui:ui-tooling:1.4.3' implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.30.1" + implementation "androidx.compose.runtime:runtime-livedata:1.4.3" + implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02' implementation 'androidx.compose.material:material:1.5.0-alpha02' diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 34d53acf78..7421412e5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.conversation.v2 import android.os.Bundle -import android.view.View import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,12 +10,15 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.text.TextStyle @@ -24,6 +26,9 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity @@ -60,71 +65,101 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { title = resources.getString(R.string.conversation_context__menu_message_details) - setContentView(createComposeView()) + setContentView(ComposeView(this).apply { + setContent { + MessageDetailsScreen() + } + }) } - private fun createComposeView(): ComposeView = ComposeView(this).apply { - id = View.generateViewId() - setContent { - MessageDetails() - } + class MessageDetailsViewModel: ViewModel() { + private val _details = MutableLiveData(MessageDetails()) + val details: LiveData = _details + } + + @Composable + private fun MessageDetailsScreen(viewModel: MessageDetailsViewModel = MessageDetailsViewModel()) { + val details by viewModel.details.observeAsState(MessageDetails()) + MessageDetails(details) } data class TitledText(val title: String, val value: String) - @OptIn(ExperimentalLayoutApi::class) + data class MessageDetails( + val fileDetails: List? = null, + val sent: TitledText? = null, + val received: TitledText? = null, + val user: TitledText? = null, + ) + @Preview @Composable - fun MessageDetails() { - val fileDetails = listOf( - TitledText("File Id:", "1237896548514214124235985214"), - TitledText("File Type:", ".PNG"), - TitledText("File Size:", "6mb"), - TitledText("Resolution:", "550x550"), - TitledText("Duration:", "N/A"), + fun PreviewMessageDetails() { + MessageDetails( + fileDetails = listOf( + TitledText("File Id:", "1237896548514214124235985214"), + TitledText("File Type:", ".PNG"), + TitledText("File Size:", "6mb"), + TitledText("Resolution:", "550x550"), + TitledText("Duration:", "N/A"), + ), + sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), + received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), + user = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") ) + } - val sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022") - val received = TitledText("Received:", "6:12 AM Tue, 09/08/2022") - val user = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") - - AppTheme { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp)) { - CellWithPadding { - FlowRow( - verticalArrangement = Arrangement.spacedBy(16.dp), - maxItemsInEachRow = 2 - ) { - fileDetails.forEach { - titledText(it, Modifier.weight(1f)) - } - } - } - CellWithPadding { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - titledText(sent) - titledText(received) - titledView("From:") { - Row { - Box(modifier = Modifier - .width(60.dp) - .height(60.dp)) - Column { - titledText(user, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) + @OptIn(ExperimentalLayoutApi::class) + @Composable + fun MessageDetails(messageDetails: MessageDetails) { + messageDetails.apply { + AppTheme { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + fileDetails?.takeIf { it.isNotEmpty() }?.let { + CellWithPadding { + FlowRow( + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = 2 + ) { + it.forEach { + titledText(it, Modifier.weight(1f)) } } } } - } - Cell { - Column { - ItemButton("Reply", R.drawable.ic_reply) - Divider() - ItemButton("Resend", R.drawable.ic_reply) - Divider() - ItemButton("Delete", R.drawable.ic_delete_24 , colors = destructiveButtonColors()) + if (sent != null || received != null || user != null) CellWithPadding { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + sent?.let { titledText(it) } + received?.let { titledText(it) } + user?.let { + titledView("From:") { + Row { + Box(modifier = Modifier + .width(60.dp) + .height(60.dp)) + Column { + titledText(it, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) + } + } + } + } + } + } + Cell { + Column { + ItemButton("Reply", R.drawable.ic_message_details__reply) + Divider() + ItemButton("Resend", R.drawable.ic_message_details__refresh) + Divider() + ItemButton( + "Delete", + R.drawable.ic_message_details__trash, + colors = destructiveButtonColors() + ) + } } } } diff --git a/app/src/main/res/drawable/ic_message_details__refresh.xml b/app/src/main/res/drawable/ic_message_details__refresh.xml new file mode 100644 index 0000000000..2aabe6fbe3 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_details__refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_details__reply.xml b/app/src/main/res/drawable/ic_message_details__reply.xml new file mode 100644 index 0000000000..c9e1591a53 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_details__reply.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_details__trash.xml b/app/src/main/res/drawable/ic_message_details__trash.xml new file mode 100644 index 0000000000..85d4216958 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_details__trash.xml @@ -0,0 +1,9 @@ + + + From 676c29ca60b105d9481f985e25e570c3ccd9175c Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 30 Jun 2023 09:48:48 +0930 Subject: [PATCH 025/112] Add click listeners --- .../conversation/v2/MessageDetailActivity.kt | 81 +++++++++++++++---- .../thoughtcrime/securesms/ui/Components.kt | 3 +- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 7421412e5f..292b1fa013 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.accounts.AccountManager +import android.content.Intent import android.os.Bundle import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -10,7 +12,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider @@ -31,9 +32,12 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellWithPadding @@ -46,22 +50,53 @@ import javax.inject.Inject @AndroidEntryPoint class MessageDetailActivity: PassphraseRequiredActionBarActivity() { + + private var timestamp: Long = 0L + var messageRecord: MessageRecord? = null @Inject lateinit var storage: Storage - // region Settings companion object { // Extras const val MESSAGE_TIMESTAMP = "message_timestamp" + + const val ON_REPLY = 1 + const val ON_RESEND = 2 + const val ON_DELETE = 3 + } + + val viewModel = MessageDetailsViewModel() + + class MessageDetailsViewModel: ViewModel() { + + fun setMessageRecord(value: MessageRecord?) { + _details.value = value?.run { + MessageDetails( + sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, + received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, + user = null + ) + } ?: MessageDetails() + } + + private var _details = MutableLiveData(MessageDetails()) + val details: LiveData = _details } - // endregion override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) - val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) + timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) + + val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) + messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run { + finish() + return + } + + viewModel.setMessageRecord(messageRecord) title = resources.getString(R.string.conversation_context__menu_message_details) @@ -72,15 +107,25 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { }) } - class MessageDetailsViewModel: ViewModel() { - private val _details = MutableLiveData(MessageDetails()) - val details: LiveData = _details + @Composable + private fun MessageDetailsScreen() { + val details by viewModel.details.observeAsState(MessageDetails()) + MessageDetails( + details, + onReply = { setResultAndFinish(ON_REPLY) }, + onResend = { setResultAndFinish(ON_RESEND) }, + onDelete = { setResultAndFinish(ON_DELETE) } + ) } - @Composable - private fun MessageDetailsScreen(viewModel: MessageDetailsViewModel = MessageDetailsViewModel()) { - val details by viewModel.details.observeAsState(MessageDetails()) - MessageDetails(details) + private fun setResultAndFinish(code: Int) { + setResult(code) + + Bundle().apply { putLong(MESSAGE_TIMESTAMP, timestamp) } + .let(Intent()::putExtras) + .let { setResult(RESULT_OK, it) } + + finish() } data class TitledText(val title: String, val value: String) @@ -111,7 +156,12 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { @OptIn(ExperimentalLayoutApi::class) @Composable - fun MessageDetails(messageDetails: MessageDetails) { + fun MessageDetails( + messageDetails: MessageDetails, + onReply: () -> Unit = {}, + onResend: () -> Unit = {}, + onDelete: () -> Unit = {}, + ) { messageDetails.apply { AppTheme { Column( @@ -150,14 +200,15 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } Cell { Column { - ItemButton("Reply", R.drawable.ic_message_details__reply) + ItemButton("Reply", R.drawable.ic_message_details__reply, onClick = onReply) Divider() - ItemButton("Resend", R.drawable.ic_message_details__refresh) + ItemButton("Resend", R.drawable.ic_message_details__refresh, onClick = onResend) Divider() ItemButton( "Delete", R.drawable.ic_message_details__trash, - colors = destructiveButtonColors() + colors = destructiveButtonColors(), + onClick = onDelete ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 046ba4635a..2421c39b87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -28,13 +28,14 @@ fun ItemButton( text: String, @DrawableRes icon: Int, colors: ButtonColors = transparentButtonColors(), + onClick: () -> Unit ) { TextButton( modifier = Modifier .fillMaxWidth() .height(60.dp), colors = colors, - onClick = {}, + onClick = onClick, shape = RectangleShape, ) { Box(modifier = Modifier From 6d596226b3b216b70dd4e221344e7cc0652008c4 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 30 Jun 2023 10:33:56 +0930 Subject: [PATCH 026/112] Add user info --- .../securesms/conversation/v2/ConversationActivityV2.kt | 7 ++++--- .../conversation/v2/ConversationReactionOverlay.java | 4 +--- .../securesms/conversation/v2/MessageDetailActivity.kt | 8 ++------ .../v2/menus/ConversationActionModeCallback.kt | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 7844ca4c22..4e55d65100 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -1773,9 +1773,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun showMessageDetail(messages: Set) { - val intent = Intent(this, MessageDetailActivity::class.java) - intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp) - push(intent) + Intent(this, MessageDetailActivity::class.java) + .apply { putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp) } + .let(::push) + endActionMode() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java index 20462bef34..eee8b5ecd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java @@ -695,9 +695,7 @@ public final class ConversationReactionOverlay extends FrameLayout { items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL))); } // Message detail - if (message.isFailed()) { - items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO))); - } + items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO))); // Resend if (message.isFailed()) { items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 292b1fa013..df2e2c6e9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.conversation.v2 -import android.accounts.AccountManager import android.content.Intent import android.os.Bundle import androidx.compose.foundation.layout.Arrangement @@ -32,8 +31,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord @@ -76,7 +73,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { MessageDetails( sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, - user = null + user = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } } ) } ?: MessageDetails() } @@ -90,8 +87,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) - val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) - messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run { + messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run { finish() return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt index f86920f90f..3746aa52e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt @@ -67,7 +67,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p menu.findItem(R.id.menu_context_copy_public_key).isVisible = (thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey) // Message detail - menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing) + menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1 // Resend menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed) // Resync From 876e12c4119170ea5bcfee5de007debe43c254da Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 30 Jun 2023 11:01:57 +0930 Subject: [PATCH 027/112] Refactor ProfilePictureView --- .../components/ProfilePictureView.kt | 5 ++++- .../securesms/contacts/UserView.kt | 6 +++--- .../conversation/start/ContactListAdapter.kt | 6 +++--- .../conversation/v2/ConversationActivityV2.kt | 8 ++++---- .../conversation/v2/MessageDetailActivity.kt | 5 ++++- .../v2/components/MentionCandidateView.kt | 10 +++++----- .../mentions/MentionCandidateView.kt | 10 +++++----- .../v2/messages/VisibleMessageView.kt | 20 +++++++++---------- .../securesms/home/ConversationView.kt | 6 +++--- .../securesms/home/HomeActivity.kt | 16 +++++++-------- .../securesms/home/UserDetailsBottomSheet.kt | 8 ++++---- .../home/search/GlobalSearchAdapter.kt | 6 +++--- .../home/search/GlobalSearchAdapterUtils.kt | 14 ++++++------- .../messagerequests/MessageRequestView.kt | 6 +++--- .../preferences/BlockedContactsAdapter.kt | 4 ++-- .../securesms/preferences/SettingsActivity.kt | 8 ++++---- .../activity_conversation_v2_action_bar.xml | 2 +- app/src/main/res/layout/activity_home.xml | 2 +- app/src/main/res/layout/activity_settings.xml | 2 +- .../res/layout/blocked_contact_layout.xml | 2 +- .../main/res/layout/dialog_change_avatar.xml | 2 +- .../res/layout/fragment_call_bottom_sheet.xml | 2 +- .../fragment_user_details_bottom_sheet.xml | 2 +- ...m_sheet_dialog_fragment_recipient_item.xml | 2 +- app/src/main/res/layout/view_contact.xml | 2 +- app/src/main/res/layout/view_conversation.xml | 2 +- .../res/layout/view_global_search_result.xml | 2 +- .../res/layout/view_mention_candidate.xml | 2 +- .../res/layout/view_mention_candidate_v2.xml | 2 +- .../main/res/layout/view_message_request.xml | 2 +- .../main/res/layout/view_profile_picture.xml | 8 ++------ app/src/main/res/layout/view_user.xml | 2 +- .../main/res/layout/view_visible_message.xml | 3 +-- 33 files changed, 90 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index a827a7d260..f078001af2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import android.view.View import android.widget.ImageView import android.widget.RelativeLayout @@ -9,6 +10,7 @@ import androidx.annotation.DimenRes import com.bumptech.glide.load.engine.DiskCacheStrategy import network.loki.messenger.R import network.loki.messenger.databinding.ViewProfilePictureBinding +import network.loki.messenger.databinding.ViewUserBinding import org.session.libsession.avatars.ContactColors import org.session.libsession.avatars.PlaceholderAvatarPhoto import org.session.libsession.avatars.ProfileContactPhoto @@ -23,7 +25,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests class ProfilePictureView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : RelativeLayout(context, attrs) { - private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) } + private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this) lateinit var glide: GlideRequests var publicKey: String? = null var displayName: String? = null @@ -37,6 +39,7 @@ class ProfilePictureView @JvmOverloads constructor( private val unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification) .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) + // endregion // region Updating diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt index e88cf1d08b..18e43b434c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt @@ -54,8 +54,8 @@ class UserView : LinearLayout { val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user) MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this val address = user.address.serialize() - binding.profilePictureView.root.glide = glide - binding.profilePictureView.root.update(user) + binding.profilePictureView.glide = glide + binding.profilePictureView.update(user) binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24) binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address) when (actionIndicator) { @@ -87,7 +87,7 @@ class UserView : LinearLayout { } fun unbind() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() } // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt index 99e7c90615..830b5302b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt @@ -32,14 +32,14 @@ class ContactListAdapter( class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) { - binding.profilePictureView.root.glide = glide - binding.profilePictureView.root.update(contact.recipient) + binding.profilePictureView.glide = glide + binding.profilePictureView.update(contact.recipient) binding.nameTextView.text = contact.displayName binding.root.setOnClickListener { listener(contact.recipient) } } fun unbind() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 4e55d65100..2514620e8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -467,10 +467,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe R.dimen.small_profile_picture_size } val size = resources.getDimension(sizeID).roundToInt() - binding.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size) - binding.toolbarContent.profilePictureView.root.glide = glide + binding.toolbarContent.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size) + binding.toolbarContent.profilePictureView.glide = glide MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this) - val profilePictureView = binding.toolbarContent.profilePictureView.root + val profilePictureView = binding.toolbarContent.profilePictureView viewModel.recipient?.let(profilePictureView::update) } @@ -658,7 +658,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe updateSendAfterApprovalText() showOrHideInputIfNeeded() - binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient) + binding?.toolbarContent?.profilePictureView?.update(threadRecipient) binding?.toolbarContent?.conversationTitleView?.text = when { threadRecipient.isLocalNumber -> getString(R.string.note_to_self) else -> threadRecipient.toShortString() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index df2e2c6e9b..c1246f57f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -185,7 +186,9 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { Row { Box(modifier = Modifier .width(60.dp) - .height(60.dp)) + .height(60.dp)) { + + } Column { titledText(it, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt index 834b77eccb..50bf53b76d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt @@ -28,11 +28,11 @@ class MentionCandidateView : LinearLayout { private fun update() = with(binding) { mentionCandidateNameTextView.text = mentionCandidate.displayName - profilePictureView.root.publicKey = mentionCandidate.publicKey - profilePictureView.root.displayName = mentionCandidate.displayName - profilePictureView.root.additionalPublicKey = null - profilePictureView.root.glide = glide!! - profilePictureView.root.update() + profilePictureView.publicKey = mentionCandidate.publicKey + profilePictureView.displayName = mentionCandidate.displayName + profilePictureView.additionalPublicKey = null + profilePictureView.glide = glide!! + profilePictureView.update() if (openGroupServer != null && openGroupRoom != null) { val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey) moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt index a21ba1b502..ab002d5e60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt @@ -28,11 +28,11 @@ class MentionCandidateView : RelativeLayout { private fun update() = with(binding) { mentionCandidateNameTextView.text = candidate.displayName - profilePictureView.root.publicKey = candidate.publicKey - profilePictureView.root.displayName = candidate.displayName - profilePictureView.root.additionalPublicKey = null - profilePictureView.root.glide = glide!! - profilePictureView.root.update() + profilePictureView.publicKey = candidate.publicKey + profilePictureView.displayName = candidate.displayName + profilePictureView.additionalPublicKey = null + profilePictureView.glide = glide!! + profilePictureView.update() if (openGroupServer != null && openGroupRoom != null) { val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey) moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 319140731a..f9366df179 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -136,7 +136,7 @@ class VisibleMessageView : LinearLayout { // Show profile picture and sender name if this is a group thread AND // the message is incoming binding.moderatorIconImageView.isVisible = false - binding.profilePictureView.root.visibility = when { + binding.profilePictureView.visibility = when { thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE thread.isGroupRecipient -> View.INVISIBLE else -> View.GONE @@ -145,22 +145,22 @@ class VisibleMessageView : LinearLayout { val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing) else ViewUtil.dpToPx(context,2) - if (binding.profilePictureView.root.visibility == View.GONE) { + if (binding.profilePictureView.visibility == View.GONE) { val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams expirationParams.bottomMargin = bottomMargin binding.messageInnerContainer.layoutParams = expirationParams } else { - val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams + val avatarLayoutParams = binding.profilePictureView.layoutParams as MarginLayoutParams avatarLayoutParams.bottomMargin = bottomMargin - binding.profilePictureView.root.layoutParams = avatarLayoutParams + binding.profilePictureView.layoutParams = avatarLayoutParams } if (isGroupThread && !message.isOutgoing) { if (isEndOfMessageCluster) { - binding.profilePictureView.root.publicKey = senderSessionID - binding.profilePictureView.root.glide = glide - binding.profilePictureView.root.update(message.individualRecipient) - binding.profilePictureView.root.setOnClickListener { + binding.profilePictureView.publicKey = senderSessionID + binding.profilePictureView.glide = glide + binding.profilePictureView.update(message.individualRecipient) + binding.profilePictureView.setOnClickListener { if (thread.isOpenGroupRecipient) { val openGroup = lokiThreadDb.getOpenGroupChat(threadID) if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) { @@ -386,7 +386,7 @@ class VisibleMessageView : LinearLayout { val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val iconSize = toPx(24, context.resources) val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing - val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2) + val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.marginBottom - (iconSize / 2) val right = left + iconSize val bottom = top + iconSize swipeToReplyIconRect.left = left @@ -406,7 +406,7 @@ class VisibleMessageView : LinearLayout { } fun recycle() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() binding.messageContentView.root.recycle() } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index c6a6e1f7f5..7574eee9ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -58,7 +58,7 @@ class ConversationView : LinearLayout { } else { ContextCompat.getDrawable(context, R.drawable.conversation_view_background) } - binding.profilePictureView.root.glide = glide + binding.profilePictureView.glide = glide val unreadCount = thread.unreadCount if (thread.recipient.isBlocked) { binding.accentView.setBackgroundResource(R.color.destructive) @@ -117,11 +117,11 @@ class ConversationView : LinearLayout { thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check) else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check) } - binding.profilePictureView.root.update(thread.recipient) + binding.profilePictureView.update(thread.recipient) } fun recycle() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() } private fun getUserDisplayName(recipient: Recipient): String? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 0215040d37..09d3a428e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -151,8 +151,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), // Set up Glide glide = GlideApp.with(this) // Set up toolbar buttons - binding.profileButton.root.glide = glide - binding.profileButton.root.setOnClickListener { openSettings() } + binding.profileButton.glide = glide + binding.profileButton.setOnClickListener { openSettings() } binding.searchViewContainer.setOnClickListener { binding.globalSearchInputLayout.requestFocus() } @@ -317,8 +317,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared IdentityKeyUtil.checkUpdate(this) - binding.profileButton.root.recycle() // clear cached image before update tje profilePictureView - binding.profileButton.root.update() + binding.profileButton.recycle() // clear cached image before update tje profilePictureView + binding.profileButton.update() if (textSecurePreferences.getHasViewedSeed()) { binding.seedReminderView.isVisible = false } @@ -388,10 +388,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } private fun updateProfileButton() { - binding.profileButton.root.publicKey = publicKey - binding.profileButton.root.displayName = textSecurePreferences.getProfileName() - binding.profileButton.root.recycle() - binding.profileButton.root.update() + binding.profileButton.publicKey = publicKey + binding.profileButton.displayName = textSecurePreferences.getProfileName() + binding.profileButton.recycle() + binding.profileButton.update() } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index bc9a9beced..fc3627a096 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -55,10 +55,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false) val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() with(binding) { - profilePictureView.root.publicKey = publicKey - profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet) - profilePictureView.root.isLarge = true - profilePictureView.root.update(recipient) + profilePictureView.publicKey = publicKey + profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet) + profilePictureView.isLarge = true + profilePictureView.update(recipient) nameTextViewContainer.visibility = View.VISIBLE nameTextViewContainer.setOnClickListener { nameTextViewContainer.visibility = View.INVISIBLE diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt index fab8bca998..cab31304b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt @@ -83,14 +83,14 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi override fun onViewRecycled(holder: RecyclerView.ViewHolder) { if (holder is ContentView) { - holder.binding.searchResultProfilePicture.root.recycle() + holder.binding.searchResultProfilePicture.recycle() } } class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) { val binding = ViewGlobalSearchResultBinding.bind(view).apply { - searchResultProfilePicture.root.glide = GlideApp.with(root) + searchResultProfilePicture.glide = GlideApp.with(root) } fun bindPayload(newQuery: String, model: Model) { @@ -98,7 +98,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi } fun bind(query: String, model: Model) { - binding.searchResultProfilePicture.root.recycle() + binding.searchResultProfilePicture.recycle() when (model) { is Model.GroupConversation -> bindModel(query, model) is Model.Contact -> bindModel(query, model) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt index 47e9c75942..0aa62da02e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -85,12 +85,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? { } fun ContentView.bindModel(query: String?, model: GroupConversation) { - binding.searchResultProfilePicture.root.isVisible = true + binding.searchResultProfilePicture.isVisible = true binding.searchResultSavedMessages.isVisible = false binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup binding.searchResultTimestamp.isVisible = false val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false) - binding.searchResultProfilePicture.root.update(threadRecipient) + binding.searchResultProfilePicture.update(threadRecipient) val nameString = model.groupRecord.title binding.searchResultTitle.text = getHighlight(query, nameString) @@ -106,14 +106,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) { } fun ContentView.bindModel(query: String?, model: ContactModel) { - binding.searchResultProfilePicture.root.isVisible = true + binding.searchResultProfilePicture.isVisible = true binding.searchResultSavedMessages.isVisible = false binding.searchResultSubtitle.isVisible = false binding.searchResultTimestamp.isVisible = false binding.searchResultSubtitle.text = null val recipient = Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false) - binding.searchResultProfilePicture.root.update(recipient) + binding.searchResultProfilePicture.update(recipient) val nameString = model.contact.getSearchName() binding.searchResultTitle.text = getHighlight(query, nameString) } @@ -122,12 +122,12 @@ fun ContentView.bindModel(model: SavedMessages) { binding.searchResultSubtitle.isVisible = false binding.searchResultTimestamp.isVisible = false binding.searchResultTitle.setText(R.string.note_to_self) - binding.searchResultProfilePicture.root.isVisible = false + binding.searchResultProfilePicture.isVisible = false binding.searchResultSavedMessages.isVisible = true } fun ContentView.bindModel(query: String?, model: Message) { - binding.searchResultProfilePicture.root.isVisible = true + binding.searchResultProfilePicture.isVisible = true binding.searchResultSavedMessages.isVisible = false binding.searchResultTimestamp.isVisible = true // val hasUnreads = model.unread > 0 @@ -136,7 +136,7 @@ fun ContentView.bindModel(query: String?, model: Message) { // binding.unreadCountTextView.text = model.unread.toString() // } binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs) - binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient) + binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient) val textSpannable = SpannableStringBuilder() if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { // group chat, bind diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt index 9a8d061297..5f1afe960d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt @@ -34,7 +34,7 @@ class MessageRequestView : LinearLayout { // region Updating fun bind(thread: ThreadRecord, glide: GlideRequests) { this.thread = thread - binding.profilePictureView.root.glide = glide + binding.profilePictureView.glide = glide val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() binding.displayNameTextView.text = senderDisplayName @@ -44,12 +44,12 @@ class MessageRequestView : LinearLayout { binding.snippetTextView.text = snippet post { - binding.profilePictureView.root.update(thread.recipient) + binding.profilePictureView.update(thread.recipient) } } fun recycle() { - binding.profilePictureView.root.recycle() + binding.profilePictureView.recycle() } private fun getUserDisplayName(recipient: Recipient): String? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index a75d53c4f1..66f98a03c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -38,7 +38,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) - holder.binding.profilePictureView.root.recycle() + holder.binding.profilePictureView.recycle() } class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { @@ -48,7 +48,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) { binding.recipientName.text = selectable.item.name - with (binding.profilePictureView.root) { + with (binding.profilePictureView) { glide = this@ViewHolder.glide update(selectable.item) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 5a03cebc37..ece54d7e01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -75,8 +75,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { val displayName = getDisplayName() glide = GlideApp.with(this) with(binding) { - setupProfilePictureView(profilePictureView.root) - profilePictureView.root.setOnClickListener { showEditProfilePictureUI() } + setupProfilePictureView(profilePictureView) + profilePictureView.setOnClickListener { showEditProfilePictureUI() } ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } btnGroupNameDisplay.text = displayName publicKeyTextView.text = hexEncodedPublicKey @@ -231,8 +231,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { binding.btnGroupNameDisplay.text = displayName } if (isUpdatingProfilePicture) { - binding.profilePictureView.root.recycle() // Clear the cached image before updating - binding.profilePictureView.root.update() + binding.profilePictureView.recycle() // Clear the cached image before updating + binding.profilePictureView.update() } binding.loader.isVisible = false } diff --git a/app/src/main/res/layout/activity_conversation_v2_action_bar.xml b/app/src/main/res/layout/activity_conversation_v2_action_bar.xml index fe726f7cf7..7322bb7f00 100644 --- a/app/src/main/res/layout/activity_conversation_v2_action_bar.xml +++ b/app/src/main/res/layout/activity_conversation_v2_action_bar.xml @@ -8,7 +8,7 @@ android:orientation="horizontal" android:gravity="center_vertical"> - diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index a18661f890..795ef49ca1 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -27,7 +27,7 @@ android:layout_marginLeft="20dp" android:layout_marginRight="20dp"> - - - - - - - - diff --git a/app/src/main/res/layout/view_conversation.xml b/app/src/main/res/layout/view_conversation.xml index d66a1722bc..12a7a8ac8c 100644 --- a/app/src/main/res/layout/view_conversation.xml +++ b/app/src/main/res/layout/view_conversation.xml @@ -14,7 +14,7 @@ android:layout_height="match_parent" android:background="?colorAccent" /> - - - - - - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/view_user.xml b/app/src/main/res/layout/view_user.xml index a9330ae646..177b3ff6c9 100644 --- a/app/src/main/res/layout/view_user.xml +++ b/app/src/main/res/layout/view_user.xml @@ -15,7 +15,7 @@ android:gravity="center_vertical" android:paddingHorizontal="@dimen/medium_spacing"> - - Date: Fri, 30 Jun 2023 11:07:27 +0930 Subject: [PATCH 028/112] Add ProfilePictureView to MessageDetailActivity --- .../conversation/v2/MessageDetailActivity.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index c1246f57f7..2f4dc4415a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -19,6 +19,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.text.TextStyle @@ -33,6 +34,7 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent @@ -184,10 +186,17 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { user?.let { titledView("From:") { Row { - Box(modifier = Modifier - .width(60.dp) - .height(60.dp)) { - + Box( + modifier = Modifier.align(Alignment.CenterVertically) + .width(60.dp) + .height(60.dp) + ) { + AndroidView( + factory = { ProfilePictureView(it) }, + modifier = Modifier.align(Alignment.Center) + .width(46.dp) + .height(46.dp) + ) } Column { titledText(it, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) From f68c01b2ee6ad4b07962e66d8de38b1bd64303b0 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 30 Jun 2023 11:35:22 +0930 Subject: [PATCH 029/112] Populate profile pic --- .../securesms/components/ProfilePictureView.kt | 4 ++-- .../thoughtcrime/securesms/contacts/UserView.kt | 1 - .../conversation/start/ContactListAdapter.kt | 1 - .../conversation/v2/ConversationActivityV2.kt | 1 - .../conversation/v2/MessageDetailActivity.kt | 15 +++++++++------ .../v2/components/MentionCandidateView.kt | 1 - .../v2/input_bar/mentions/MentionCandidateView.kt | 1 - .../v2/messages/VisibleMessageView.kt | 1 - .../securesms/home/ConversationView.kt | 1 - .../thoughtcrime/securesms/home/HomeActivity.kt | 1 - .../securesms/home/UserDetailsBottomSheet.kt | 1 - .../securesms/home/search/GlobalSearchAdapter.kt | 4 +--- .../messagerequests/MessageRequestView.kt | 1 - .../preferences/BlockedContactsAdapter.kt | 1 - .../securesms/preferences/SettingsActivity.kt | 1 - .../reactions/ReactionRecipientsAdapter.java | 1 - 16 files changed, 12 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index f078001af2..37a3e764e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -20,13 +20,14 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests class ProfilePictureView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : RelativeLayout(context, attrs) { private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this) - lateinit var glide: GlideRequests + private val glide: GlideRequests = GlideApp.with(this) var publicKey: String? = null var displayName: String? = null var additionalPublicKey: String? = null @@ -76,7 +77,6 @@ class ProfilePictureView @JvmOverloads constructor( } fun update() { - if (!this::glide.isInitialized) return val publicKey = publicKey ?: return val additionalPublicKey = additionalPublicKey if (additionalPublicKey != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt index 18e43b434c..399dc9b31b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt @@ -54,7 +54,6 @@ class UserView : LinearLayout { val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user) MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this val address = user.address.serialize() - binding.profilePictureView.glide = glide binding.profilePictureView.update(user) binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24) binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt index 830b5302b7..68e2f975c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/ContactListAdapter.kt @@ -32,7 +32,6 @@ class ContactListAdapter( class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) { - binding.profilePictureView.glide = glide binding.profilePictureView.update(contact.recipient) binding.nameTextView.text = contact.displayName binding.root.setOnClickListener { listener(contact.recipient) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 2514620e8a..be65fa32ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -468,7 +468,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } val size = resources.getDimension(sizeID).roundToInt() binding.toolbarContent.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size) - binding.toolbarContent.profilePictureView.glide = glide MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this) val profilePictureView = binding.toolbarContent.profilePictureView viewModel.recipient?.let(profilePictureView::update) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 2f4dc4415a..820dfd915a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -33,6 +33,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R +import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.database.Storage @@ -76,7 +77,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { MessageDetails( sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, - user = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } } + senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, + sender = individualRecipient ) } ?: MessageDetails() } @@ -133,7 +135,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val fileDetails: List? = null, val sent: TitledText? = null, val received: TitledText? = null, - val user: TitledText? = null, + val senderInfo: TitledText? = null, + val sender: Recipient? = null ) @Preview @@ -149,7 +152,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { ), sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), - user = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") + senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") ) } @@ -179,11 +182,11 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } } } - if (sent != null || received != null || user != null) CellWithPadding { + if (sent != null || received != null || senderInfo != null) CellWithPadding { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { titledText(it) } received?.let { titledText(it) } - user?.let { + senderInfo?.let { titledView("From:") { Row { Box( @@ -192,7 +195,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { .height(60.dp) ) { AndroidView( - factory = { ProfilePictureView(it) }, + factory = { ProfilePictureView(it).apply { sender?.let(::update) } }, modifier = Modifier.align(Alignment.Center) .width(46.dp) .height(46.dp) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt index 50bf53b76d..d544263915 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt @@ -31,7 +31,6 @@ class MentionCandidateView : LinearLayout { profilePictureView.publicKey = mentionCandidate.publicKey profilePictureView.displayName = mentionCandidate.displayName profilePictureView.additionalPublicKey = null - profilePictureView.glide = glide!! profilePictureView.update() if (openGroupServer != null && openGroupRoom != null) { val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt index ab002d5e60..2d8f745967 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt @@ -31,7 +31,6 @@ class MentionCandidateView : RelativeLayout { profilePictureView.publicKey = candidate.publicKey profilePictureView.displayName = candidate.displayName profilePictureView.additionalPublicKey = null - profilePictureView.glide = glide!! profilePictureView.update() if (openGroupServer != null && openGroupRoom != null) { val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index f9366df179..40a087ea64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -158,7 +158,6 @@ class VisibleMessageView : LinearLayout { if (isGroupThread && !message.isOutgoing) { if (isEndOfMessageCluster) { binding.profilePictureView.publicKey = senderSessionID - binding.profilePictureView.glide = glide binding.profilePictureView.update(message.individualRecipient) binding.profilePictureView.setOnClickListener { if (thread.isOpenGroupRecipient) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 7574eee9ff..0deab4030e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -58,7 +58,6 @@ class ConversationView : LinearLayout { } else { ContextCompat.getDrawable(context, R.drawable.conversation_view_background) } - binding.profilePictureView.glide = glide val unreadCount = thread.unreadCount if (thread.recipient.isBlocked) { binding.accentView.setBackgroundResource(R.color.destructive) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 09d3a428e8..df70d2f80b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -151,7 +151,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), // Set up Glide glide = GlideApp.with(this) // Set up toolbar buttons - binding.profileButton.glide = glide binding.profileButton.setOnClickListener { openSettings() } binding.searchViewContainer.setOnClickListener { binding.globalSearchInputLayout.requestFocus() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index fc3627a096..a95535e35c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -56,7 +56,6 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() { val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() with(binding) { profilePictureView.publicKey = publicKey - profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet) profilePictureView.isLarge = true profilePictureView.update(recipient) nameTextViewContainer.visibility = View.VISIBLE diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt index cab31304b3..7cf953be24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt @@ -89,9 +89,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) { - val binding = ViewGlobalSearchResultBinding.bind(view).apply { - searchResultProfilePicture.glide = GlideApp.with(root) - } + val binding = ViewGlobalSearchResultBinding.bind(view) fun bindPayload(newQuery: String, model: Model) { bindQuery(newQuery, model) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt index 5f1afe960d..af3d269c6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt @@ -34,7 +34,6 @@ class MessageRequestView : LinearLayout { // region Updating fun bind(thread: ThreadRecord, glide: GlideRequests) { this.thread = thread - binding.profilePictureView.glide = glide val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() binding.displayNameTextView.text = senderDisplayName diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index 66f98a03c6..e0b92bdbea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -49,7 +49,6 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) { binding.recipientName.text = selectable.item.name with (binding.profilePictureView) { - glide = this@ViewHolder.glide update(selectable.item) } binding.root.setOnClickListener { toggle(selectable) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index ece54d7e01..e72c3e063b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -101,7 +101,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey) private fun setupProfilePictureView(view: ProfilePictureView) { - view.glide = glide view.apply { publicKey = hexEncodedPublicKey displayName = getDisplayName() diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java index f1cbea16c3..1c05e68bdf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java @@ -144,7 +144,6 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter Date: Fri, 30 Jun 2023 12:32:56 +0930 Subject: [PATCH 030/112] Fix LiveDataTestUtil --- .../java/org/thoughtcrime/securesms/LiveDataTestUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt b/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt index 03155a910c..a8cff6341c 100644 --- a/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt +++ b/app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt @@ -22,7 +22,7 @@ fun LiveData.getOrAwaitValue( var data: T? = null val latch = CountDownLatch(1) val observer = object : Observer { - override fun onChanged(o: T?) { + override fun onChanged(o: T) { data = o latch.countDown() this@getOrAwaitValue.removeObserver(this) From 351b259449c0483b994e144cfc6ba674088b4b57 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 30 Jun 2023 13:25:16 +0930 Subject: [PATCH 031/112] Add error --- .../conversation/v2/MessageDetailActivity.kt | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 820dfd915a..d885ade005 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellWithPadding import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.LocalExtraColors +import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors import java.util.* import javax.inject.Inject @@ -72,11 +73,12 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { class MessageDetailsViewModel: ViewModel() { - fun setMessageRecord(value: MessageRecord?) { + fun setMessageRecord(value: MessageRecord?, error: String?) { _details.value = value?.run { MessageDetails( sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, + error = error?.let { TitledText("Error:", it) }, senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, sender = individualRecipient ) @@ -97,7 +99,9 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { return } - viewModel.setMessageRecord(messageRecord) + val error = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) + + viewModel.setMessageRecord(messageRecord, error) title = resources.getString(R.string.conversation_context__menu_message_details) @@ -135,6 +139,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val fileDetails: List? = null, val sent: TitledText? = null, val received: TitledText? = null, + val error: TitledText? = null, val senderInfo: TitledText? = null, val sender: Recipient? = null ) @@ -143,16 +148,19 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { @Composable fun PreviewMessageDetails() { MessageDetails( - fileDetails = listOf( - TitledText("File Id:", "1237896548514214124235985214"), - TitledText("File Type:", ".PNG"), - TitledText("File Size:", "6mb"), - TitledText("Resolution:", "550x550"), - TitledText("Duration:", "N/A"), - ), - sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), - received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), - senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") + MessageDetails( + fileDetails = listOf( + TitledText("File Id:", "1237896548514214124235985214"), + TitledText("File Type:", ".PNG"), + TitledText("File Size:", "6mb"), + TitledText("Resolution:", "550x550"), + TitledText("Duration:", "N/A"), + ), + sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), + received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), + error = TitledText("Error:", "Message failed to send"), + senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") + ) ) } @@ -186,20 +194,19 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { titledText(it) } received?.let { titledText(it) } + error?.let { titledText(it, valueStyle = LocalTextStyle.current.copy(color = colorDestructive)) } senderInfo?.let { titledView("From:") { Row { - Box( - modifier = Modifier.align(Alignment.CenterVertically) - .width(60.dp) - .height(60.dp) - ) { - AndroidView( - factory = { ProfilePictureView(it).apply { sender?.let(::update) } }, - modifier = Modifier.align(Alignment.Center) - .width(46.dp) - .height(46.dp) - ) + sender?.let { + Box(modifier = Modifier.width(60.dp).align(Alignment.CenterVertically)) { + AndroidView( + factory = { ProfilePictureView(it).apply { update(sender) } }, + modifier = Modifier + .width(46.dp) + .height(46.dp) + ) + } } Column { titledText(it, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) @@ -213,14 +220,11 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { Column { ItemButton("Reply", R.drawable.ic_message_details__reply, onClick = onReply) Divider() - ItemButton("Resend", R.drawable.ic_message_details__refresh, onClick = onResend) - Divider() - ItemButton( - "Delete", - R.drawable.ic_message_details__trash, - colors = destructiveButtonColors(), - onClick = onDelete - ) + if (error != null) { + ItemButton("Resend", R.drawable.ic_message_details__refresh, onClick = onResend) + Divider() + } + ItemButton("Delete", R.drawable.ic_message_details__trash, colors = destructiveButtonColors(), onClick = onDelete) } } } From d44dbe089fda60319726c9ca9e23cdacb1066cf0 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 30 Jun 2023 22:49:23 +0930 Subject: [PATCH 032/112] Wire up buttons --- .../conversation/v2/ConversationActivityV2.kt | 23 +++++++++++++++++-- .../conversation/v2/MessageDetailActivity.kt | 21 +++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index be65fa32ef..fcc0803729 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -18,6 +18,8 @@ import android.view.* import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.DimenRes import androidx.appcompat.app.AlertDialog @@ -78,6 +80,10 @@ import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.sele import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener +import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP +import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY +import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND +import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog @@ -1771,10 +1777,23 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe endActionMode() } + private val handleMessageDetail = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + val message = result.data?.extras?.getLong(MESSAGE_TIMESTAMP) + ?.let(mmsSmsDb::getMessageForTimestamp) + + val set = setOfNotNull(message) + + when (result.resultCode) { + ON_REPLY -> reply(set) + ON_RESEND -> resendMessage(set) + ON_DELETE -> deleteMessages(set) + } + } + override fun showMessageDetail(messages: Set) { Intent(this, MessageDetailActivity::class.java) - .apply { putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp) } - .let(::push) + .apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) } + .let { handleMessageDetail.launch(it) } endActionMode() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index d885ade005..68c9c9e7a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent import android.os.Bundle +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider @@ -38,6 +40,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Cell @@ -74,6 +77,10 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { class MessageDetailsViewModel: ViewModel() { fun setMessageRecord(value: MessageRecord?, error: String?) { + val mmsRecord = value as? MmsMessageRecord + + val slides = mmsRecord?.slideDeck?.thumbnailSlides + _details.value = value?.run { MessageDetails( sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, @@ -124,11 +131,9 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } private fun setResultAndFinish(code: Int) { - setResult(code) - Bundle().apply { putLong(MESSAGE_TIMESTAMP, timestamp) } .let(Intent()::putExtras) - .let { setResult(RESULT_OK, it) } + .let { setResult(code, it) } finish() } @@ -164,7 +169,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { ) } - @OptIn(ExperimentalLayoutApi::class) + @OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) @Composable fun MessageDetails( messageDetails: MessageDetails, @@ -178,6 +183,10 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + HorizontalPager(pageCount = 1) { + + } + fileDetails?.takeIf { it.isNotEmpty() }?.let { CellWithPadding { FlowRow( @@ -199,7 +208,9 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { titledView("From:") { Row { sender?.let { - Box(modifier = Modifier.width(60.dp).align(Alignment.CenterVertically)) { + Box(modifier = Modifier + .width(60.dp) + .align(Alignment.CenterVertically)) { AndroidView( factory = { ProfilePictureView(it).apply { update(sender) } }, modifier = Modifier From 4decce9dde479d4f113c0823e828a63f03d8096c Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 1 Jul 2023 17:38:58 +0930 Subject: [PATCH 033/112] Add images --- app/build.gradle | 1 + .../conversation/v2/MessageDetailActivity.kt | 104 ++++++++++++++---- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c3ae7de49a..d8795b218b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,6 +161,7 @@ dependencies { testImplementation 'org.robolectric:robolectric:4.4' testImplementation 'org.robolectric:shadows-multidex:4.4' + implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1' implementation 'androidx.compose.ui:ui:1.4.3' implementation 'androidx.compose.ui:ui-tooling:1.4.3' implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.30.1" diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 68c9c9e7a4..bf32c2c61f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent import android.os.Bundle +import android.view.View import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -23,6 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -33,15 +36,21 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.components.ProfilePictureView +import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellWithPadding @@ -50,6 +59,7 @@ import org.thoughtcrime.securesms.ui.LocalExtraColors import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors import java.util.* +import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -63,6 +73,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { @Inject lateinit var storage: Storage + companion object { // Extras const val MESSAGE_TIMESTAMP = "message_timestamp" @@ -75,21 +86,47 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val viewModel = MessageDetailsViewModel() class MessageDetailsViewModel: ViewModel() { + @Inject + lateinit var attachmentDb: AttachmentDatabase fun setMessageRecord(value: MessageRecord?, error: String?) { val mmsRecord = value as? MmsMessageRecord - val slides = mmsRecord?.slideDeck?.thumbnailSlides + val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() _details.value = value?.run { MessageDetails( + attachments = slides.map { slide -> + val duration = slide.takeIf { it.hasAudio() } + ?.let { it.asAttachment() as? DatabaseAttachment } + ?.let { attachment -> + attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> + audioExtras.durationMs.takeIf { it > 0 }?.let { + String.format("%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(it), + TimeUnit.MILLISECONDS.toSeconds(it) % 60) + } + } + } + + val details = slide.run { + listOfNotNull( + fileName.orNull()?.let { TitledText("File Id:", it) }, + TitledText("File Type:", asAttachment().contentType), + TitledText("File Size:", Util.getPrettyFileSize(fileSize)), + if (slide.hasImage()) { TitledText("Resolution:", slide.asAttachment().run { "${width}x$height" } ) } else null, + duration?.let { TitledText("Duration:", it) }, + ) + } + Attachment(slide, details) + }, sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, error = error?.let { TitledText("Error:", it) }, senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, sender = individualRecipient ) - } ?: MessageDetails() + } } private var _details = MutableLiveData(MessageDetails()) @@ -141,7 +178,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { data class TitledText(val title: String, val value: String) data class MessageDetails( - val fileDetails: List? = null, + val attachments: List = emptyList(), +// val fileDetails: List? = null, val sent: TitledText? = null, val received: TitledText? = null, val error: TitledText? = null, @@ -149,27 +187,36 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val sender: Recipient? = null ) + data class Attachment( + val slide: Slide, + val fileDetails: List + ) + @Preview @Composable fun PreviewMessageDetails() { MessageDetails( MessageDetails( - fileDetails = listOf( - TitledText("File Id:", "1237896548514214124235985214"), - TitledText("File Type:", ".PNG"), - TitledText("File Size:", "6mb"), - TitledText("Resolution:", "550x550"), - TitledText("Duration:", "N/A"), - ), + attachments = listOf(), +// fileDetails = listOf( +// TitledText("File Id:", "1237896548514214124235985214"), +// TitledText("File Type:", ".PNG"), +// TitledText("File Size:", "6mb"), +// TitledText("Resolution:", "550x550"), +// TitledText("Duration:", "N/A"), +// ), sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), error = TitledText("Error:", "Message failed to send"), - senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg") + senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg"), ) ) } - @OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) + @OptIn( + ExperimentalLayoutApi::class, + ExperimentalFoundationApi::class, + ExperimentalGlideComposeApi::class) @Composable fun MessageDetails( messageDetails: MessageDetails, @@ -183,18 +230,27 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - HorizontalPager(pageCount = 1) { - - } - - fileDetails?.takeIf { it.isNotEmpty() }?.let { - CellWithPadding { - FlowRow( - verticalArrangement = Arrangement.spacedBy(16.dp), - maxItemsInEachRow = 2 - ) { - it.forEach { - titledText(it, Modifier.weight(1f)) + HorizontalPager(pageCount = attachments.size) {i -> + val attachment = attachments[i] + attachment.apply { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Cell { + if (slide.hasImage()) GlideImage( + contentScale = ContentScale.FillHeight, + modifier = Modifier.fillMaxWidth(), + model = attachment.slide.uri, + contentDescription = attachment.slide.fileName.orNull() ?: "image" + ) + } + fileDetails.takeIf { it.isNotEmpty() }?.let { + CellWithPadding { + FlowRow( + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = 2 + ) { + it.forEach { titledText(it, Modifier.weight(1f)) } + } + } } } } From 1303979cdfdb96b047be1bef74cc3da153a08cd3 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 10:35:50 +0930 Subject: [PATCH 034/112] Add image attachments --- .../conversation/v2/MessageDetailActivity.kt | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index bf32c2c61f..0fd5e6eb90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -10,11 +10,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider @@ -230,31 +232,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - HorizontalPager(pageCount = attachments.size) {i -> - val attachment = attachments[i] - attachment.apply { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - Cell { - if (slide.hasImage()) GlideImage( - contentScale = ContentScale.FillHeight, - modifier = Modifier.fillMaxWidth(), - model = attachment.slide.uri, - contentDescription = attachment.slide.fileName.orNull() ?: "image" - ) - } - fileDetails.takeIf { it.isNotEmpty() }?.let { - CellWithPadding { - FlowRow( - verticalArrangement = Arrangement.spacedBy(16.dp), - maxItemsInEachRow = 2 - ) { - it.forEach { titledText(it, Modifier.weight(1f)) } - } - } - } - } - } - } + Attachments(attachments) if (sent != null || received != null || senderInfo != null) CellWithPadding { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { titledText(it) } @@ -299,6 +277,50 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } } + @Composable + fun Attachments(attachments: List) { + val slide = attachments.firstOrNull()?.slide ?: return + when { + slide.hasImage() -> ImageAttachments(attachments) + } + } + + @OptIn( + ExperimentalFoundationApi::class, + ExperimentalGlideComposeApi::class, + ExperimentalLayoutApi::class, + ) + @Composable + fun ImageAttachments(attachments: List) { + val pagerState = rememberPagerState() + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Cell { + val imageAttachments = attachments.filter { it.slide.hasImage() } + HorizontalPager(state = pagerState, pageCount = imageAttachments.size) { i -> + imageAttachments[i].slide.apply { + GlideImage( + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(1f), + model = uri, + contentDescription = fileName.orNull() ?: "image" + ) + } + } + } + attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let { + CellWithPadding { + FlowRow( + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = 2 + ) { + it.forEach { titledText(it, Modifier.weight(1f)) } + } + } + } + } + } + @Composable fun Divider() { Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 1.dp, color = LocalExtraColors.current.divider) From 1902d4755c09e223692d843d1fbf2de01b8274c0 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 13:00:11 +0930 Subject: [PATCH 035/112] Add pager indicator --- app/build.gradle | 4 +- .../conversation/v2/MessageDetailActivity.kt | 54 +++++++++---------- .../thoughtcrime/securesms/ui/Components.kt | 30 +++++++++-- 3 files changed, 56 insertions(+), 32 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d8795b218b..4bb54eb20e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,10 +164,10 @@ dependencies { implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1' implementation 'androidx.compose.ui:ui:1.4.3' implementation 'androidx.compose.ui:ui-tooling:1.4.3' - implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.30.1" + implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.31.5-beta" + implementation "com.google.accompanist:accompanist-pager-indicators:0.31.5-beta" implementation "androidx.compose.runtime:runtime-livedata:1.4.3" - implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02' implementation 'androidx.compose.material:material:1.5.0-alpha02' } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 0fd5e6eb90..1c59c82016 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent import android.os.Bundle -import android.view.View import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -19,6 +17,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card import androidx.compose.material.Divider import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text @@ -55,9 +54,11 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Cell -import org.thoughtcrime.securesms.ui.CellWithPadding +import org.thoughtcrime.securesms.ui.CellNoMargin +import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.LocalExtraColors +import org.thoughtcrime.securesms.ui.SessionHorizontalPagerIndicator import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors import java.util.* @@ -181,7 +182,6 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { data class MessageDetails( val attachments: List = emptyList(), -// val fileDetails: List? = null, val sent: TitledText? = null, val received: TitledText? = null, val error: TitledText? = null, @@ -200,13 +200,6 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { MessageDetails( MessageDetails( attachments = listOf(), -// fileDetails = listOf( -// TitledText("File Id:", "1237896548514214124235985214"), -// TitledText("File Type:", ".PNG"), -// TitledText("File Size:", "6mb"), -// TitledText("Resolution:", "550x550"), -// TitledText("Duration:", "N/A"), -// ), sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), error = TitledText("Error:", "Message failed to send"), @@ -215,10 +208,6 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { ) } - @OptIn( - ExperimentalLayoutApi::class, - ExperimentalFoundationApi::class, - ExperimentalGlideComposeApi::class) @Composable fun MessageDetails( messageDetails: MessageDetails, @@ -233,7 +222,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { Attachments(attachments) - if (sent != null || received != null || senderInfo != null) CellWithPadding { + if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { titledText(it) } received?.let { titledText(it) } @@ -292,24 +281,35 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { ) @Composable fun ImageAttachments(attachments: List) { - val pagerState = rememberPagerState() + val imageAttachments = attachments.filter { it.slide.hasImage() } + val pagerState = rememberPagerState { + imageAttachments.size + } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - Cell { - val imageAttachments = attachments.filter { it.slide.hasImage() } - HorizontalPager(state = pagerState, pageCount = imageAttachments.size) { i -> - imageAttachments[i].slide.apply { - GlideImage( - contentScale = ContentScale.Crop, - modifier = Modifier.aspectRatio(1f), - model = uri, - contentDescription = fileName.orNull() ?: "image" + CellNoMargin { + Box { + HorizontalPager(state = pagerState) { i -> + imageAttachments[i].slide.apply { + GlideImage( + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(1f), + model = uri, + contentDescription = fileName.orNull() ?: "image" + ) + } + } + if (imageAttachments.size >= 2) { + SessionHorizontalPagerIndicator( + modifier = Modifier.align(Alignment.BottomCenter), + pagerState = pagerState, + pageCount = imageAttachments.size, ) } } } attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let { - CellWithPadding { + CellWithPaddingAndMargin { FlowRow( verticalArrangement = Arrangement.spacedBy(16.dp), maxItemsInEachRow = 2 diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 2421c39b87..9f8be2514d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonColors import androidx.compose.material.Card @@ -18,10 +20,12 @@ import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.google.accompanist.pager.HorizontalPagerIndicator @Composable fun ItemButton( @@ -53,20 +57,40 @@ fun ItemButton( @Composable fun Cell(content: @Composable () -> Unit) { - CellWithPadding(0.dp) { content() } + CellWithPaddingAndMargin(0.dp) { content() } +} +@Composable +fun CellNoMargin(content: @Composable () -> Unit) { + CellWithPaddingAndMargin(0.dp, 0.dp) { content() } } @Composable -fun CellWithPadding(padding: Dp = 24.dp, content: @Composable () -> Unit) { +fun CellWithPaddingAndMargin( + padding: Dp = 24.dp, + margin: Dp = 32.dp, + content: @Composable () -> Unit +) { Card( shape = RoundedCornerShape(16.dp), elevation = 0.dp, modifier = Modifier .wrapContentHeight() .fillMaxWidth() - .padding(horizontal = 32.dp), + .padding(horizontal = margin), backgroundColor = LocalExtraColors.current.settingsBackground, // probably wrong contentColor = MaterialTheme.colors.onSurface ) { Box(Modifier.padding(padding)) { content() } } } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SessionHorizontalPagerIndicator(modifier: Modifier, pagerState: PagerState, pageCount: Int) { + Card(shape = RoundedCornerShape(50.dp), + backgroundColor = Color.Black.copy(alpha = 0.4f), + modifier = Modifier.padding(8.dp).then(modifier)) { + Box(modifier = Modifier.padding(8.dp)) { + HorizontalPagerIndicator(pagerState = pagerState, pageCount = pageCount) + } + } +} From db4ff940844bbea19c759ba44d7c19feaa89c330 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 17:19:33 +0930 Subject: [PATCH 036/112] Add prev and next buttons to carousel --- .../conversation/v2/MessageDetailActivity.kt | 229 +++++++++++++----- .../thoughtcrime/securesms/ui/Components.kt | 4 +- app/src/main/res/drawable/ic_next.xml | 8 + app/src/main/res/drawable/ic_prev.xml | 8 + 4 files changed, 186 insertions(+), 63 deletions(-) create mode 100644 app/src/main/res/drawable/ic_next.xml create mode 100644 app/src/main/res/drawable/ic_prev.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 1c59c82016..1c9997854f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent import android.os.Bundle +import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -9,25 +10,30 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Card import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -40,6 +46,7 @@ import androidx.lifecycle.ViewModel import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util @@ -67,7 +74,7 @@ import javax.inject.Inject @AndroidEntryPoint -class MessageDetailActivity: PassphraseRequiredActionBarActivity() { +class MessageDetailActivity : PassphraseRequiredActionBarActivity() { private var timestamp: Long = 0L @@ -76,7 +83,6 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { @Inject lateinit var storage: Storage - companion object { // Extras const val MESSAGE_TIMESTAMP = "message_timestamp" @@ -88,7 +94,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val viewModel = MessageDetailsViewModel() - class MessageDetailsViewModel: ViewModel() { + class MessageDetailsViewModel : ViewModel() { @Inject lateinit var attachmentDb: AttachmentDatabase @@ -103,30 +109,45 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val duration = slide.takeIf { it.hasAudio() } ?.let { it.asAttachment() as? DatabaseAttachment } ?.let { attachment -> - attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> - audioExtras.durationMs.takeIf { it > 0 }?.let { - String.format("%01d:%02d", - TimeUnit.MILLISECONDS.toMinutes(it), - TimeUnit.MILLISECONDS.toSeconds(it) % 60) + attachmentDb.getAttachmentAudioExtras(attachment.attachmentId) + ?.let { audioExtras -> + audioExtras.durationMs.takeIf { it > 0 }?.let { + String.format( + "%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(it), + TimeUnit.MILLISECONDS.toSeconds(it) % 60 + ) + } } - } } - val details = slide.run { - listOfNotNull( - fileName.orNull()?.let { TitledText("File Id:", it) }, - TitledText("File Type:", asAttachment().contentType), - TitledText("File Size:", Util.getPrettyFileSize(fileSize)), - if (slide.hasImage()) { TitledText("Resolution:", slide.asAttachment().run { "${width}x$height" } ) } else null, - duration?.let { TitledText("Duration:", it) }, - ) - } - Attachment(slide, details) + val details = slide.run { + listOfNotNull( + fileName.orNull()?.let { TitledText("File Id:", it) }, + TitledText("File Type:", asAttachment().contentType), + TitledText("File Size:", Util.getPrettyFileSize(fileSize)), + if (slide.hasImage()) { + TitledText( + "Resolution:", + slide.asAttachment().run { "${width}x$height" }) + } else null, + duration?.let { TitledText("Duration:", it) }, + ) + } + Attachment(slide, details) }, sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, - received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, + received = dateReceived.let(::Date).toString() + .let { TitledText("Received:", it) }, error = error?.let { TitledText("Error:", it) }, - senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, + senderInfo = individualRecipient.run { + name?.let { + TitledText( + it, + address.serialize() + ) + } + }, sender = individualRecipient ) } @@ -141,12 +162,14 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) - messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run { - finish() - return - } + messageRecord = + DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run { + finish() + return + } - val error = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) + val error = DatabaseComponent.get(this).lokiMessageDatabase() + .getErrorMessage(messageRecord!!.getId()) viewModel.setMessageRecord(messageRecord, error) @@ -226,16 +249,29 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { titledText(it) } received?.let { titledText(it) } - error?.let { titledText(it, valueStyle = LocalTextStyle.current.copy(color = colorDestructive)) } + error?.let { + titledText( + it, + valueStyle = LocalTextStyle.current.copy(color = colorDestructive) + ) + } senderInfo?.let { titledView("From:") { Row { sender?.let { - Box(modifier = Modifier - .width(60.dp) - .align(Alignment.CenterVertically)) { + Box( + modifier = Modifier + .width(60.dp) + .align(Alignment.CenterVertically) + ) { AndroidView( - factory = { ProfilePictureView(it).apply { update(sender) } }, + factory = { + ProfilePictureView(it).apply { + update( + sender + ) + } + }, modifier = Modifier .width(46.dp) .height(46.dp) @@ -243,22 +279,38 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { } } Column { - titledText(it, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) + titledText( + it, + valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) + ) } } - } + } } } } Cell { Column { - ItemButton("Reply", R.drawable.ic_message_details__reply, onClick = onReply) + ItemButton( + "Reply", + R.drawable.ic_message_details__reply, + onClick = onReply + ) Divider() if (error != null) { - ItemButton("Resend", R.drawable.ic_message_details__refresh, onClick = onResend) + ItemButton( + "Resend", + R.drawable.ic_message_details__refresh, + onClick = onResend + ) Divider() } - ItemButton("Delete", R.drawable.ic_message_details__trash, colors = destructiveButtonColors(), onClick = onDelete) + ItemButton( + "Delete", + R.drawable.ic_message_details__trash, + colors = destructiveButtonColors(), + onClick = onDelete + ) } } } @@ -277,26 +329,27 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { @OptIn( ExperimentalFoundationApi::class, ExperimentalGlideComposeApi::class, - ExperimentalLayoutApi::class, ) @Composable fun ImageAttachments(attachments: List) { val imageAttachments = attachments.filter { it.slide.hasImage() } - val pagerState = rememberPagerState { - imageAttachments.size - } + val pagerState = rememberPagerState { imageAttachments.size } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - CellNoMargin { - Box { - HorizontalPager(state = pagerState) { i -> - imageAttachments[i].slide.apply { - GlideImage( - contentScale = ContentScale.Crop, - modifier = Modifier.aspectRatio(1f), - model = uri, - contentDescription = fileName.orNull() ?: "image" - ) + Row { + if (imageAttachments.size >= 2) PrevButton(pagerState, modifier = Modifier.align(Alignment.CenterVertically)) + else Spacer(modifier = Modifier.width(32.dp)) + Box(modifier = Modifier.weight(1f)) { + CellNoMargin { + HorizontalPager(state = pagerState) { i -> + imageAttachments[i].slide.apply { + GlideImage( + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(1f), + model = uri, + contentDescription = fileName.orNull() ?: "image" + ) + } } } if (imageAttachments.size >= 2) { @@ -307,15 +360,61 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { ) } } + if (imageAttachments.size >= 2) NextButton(pagerState, modifier = Modifier.align(Alignment.CenterVertically)) + else Spacer(modifier = Modifier.width(32.dp)) } - attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let { - CellWithPaddingAndMargin { - FlowRow( - verticalArrangement = Arrangement.spacedBy(16.dp), - maxItemsInEachRow = 2 - ) { - it.forEach { titledText(it, Modifier.weight(1f)) } - } + + FileDetails(attachments, pagerState) + } + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun PrevButton(pagerState: PagerState, modifier: Modifier = Modifier) { + CarouselButton(pagerState, modifier = modifier, enabled = pagerState.canScrollBackward, id = R.drawable.ic_prev, delta = -1) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun NextButton(pagerState: PagerState, modifier: Modifier = Modifier) { + CarouselButton(pagerState, modifier = modifier, enabled = pagerState.canScrollForward, id = R.drawable.ic_next, delta = 1) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun CarouselButton( + pagerState: PagerState, + modifier: Modifier = Modifier, + enabled: Boolean, + @DrawableRes id: Int, + delta: Int + ) { + val animationScope = rememberCoroutineScope() + pagerState.apply { + IconButton( + modifier = Modifier + .width(40.dp) + .then(modifier), + enabled = enabled, + onClick = { animationScope.launch { animateScrollToPage(currentPage + delta) } }) { + Icon( + painter = painterResource(id = id), + contentDescription = "", + ) + } + } + } + + @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) + @Composable + fun FileDetails(attachments: List, pagerState: PagerState) { + attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let { + CellWithPaddingAndMargin { + FlowRow( + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = 2 + ) { + it.forEach { titledText(it, Modifier.weight(1f)) } } } } @@ -323,11 +422,19 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { @Composable fun Divider() { - Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 1.dp, color = LocalExtraColors.current.divider) + Divider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 1.dp, + color = LocalExtraColors.current.divider + ) } @Composable - fun titledText(titledText: TitledText, modifier: Modifier = Modifier, valueStyle: TextStyle = LocalTextStyle.current) { + fun titledText( + titledText: TitledText, + modifier: Modifier = Modifier, + valueStyle: TextStyle = LocalTextStyle.current + ) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(titledText.title) Text(titledText.value, style = valueStyle) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 9f8be2514d..49d419e056 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -57,11 +57,11 @@ fun ItemButton( @Composable fun Cell(content: @Composable () -> Unit) { - CellWithPaddingAndMargin(0.dp) { content() } + CellWithPaddingAndMargin(padding = 0.dp) { content() } } @Composable fun CellNoMargin(content: @Composable () -> Unit) { - CellWithPaddingAndMargin(0.dp, 0.dp) { content() } + CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() } } @Composable diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml new file mode 100644 index 0000000000..1e72d86cb6 --- /dev/null +++ b/app/src/main/res/drawable/ic_next.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_prev.xml b/app/src/main/res/drawable/ic_prev.xml new file mode 100644 index 0000000000..f720261670 --- /dev/null +++ b/app/src/main/res/drawable/ic_prev.xml @@ -0,0 +1,8 @@ + + + + + + From 1d29b5465f2b14c1dfb82e2e865f040aab640f1b Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 17:41:30 +0930 Subject: [PATCH 037/112] Refactor prev and next buttons --- .../conversation/v2/MessageDetailActivity.kt | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 1c9997854f..9836ef4861 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -337,8 +337,10 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row { - if (imageAttachments.size >= 2) PrevButton(pagerState, modifier = Modifier.align(Alignment.CenterVertically)) - else Spacer(modifier = Modifier.width(32.dp)) + CarouselButtonOrSpace( + Direction.PREVIOUS, + pagerState, + modifier = Modifier.align(Alignment.CenterVertically)) Box(modifier = Modifier.weight(1f)) { CellNoMargin { HorizontalPager(state = pagerState) { i -> @@ -360,24 +362,39 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ) } } - if (imageAttachments.size >= 2) NextButton(pagerState, modifier = Modifier.align(Alignment.CenterVertically)) - else Spacer(modifier = Modifier.width(32.dp)) + CarouselButtonOrSpace( + Direction.NEXT, + pagerState, + modifier = Modifier.align(Alignment.CenterVertically) + ) } - FileDetails(attachments, pagerState) } } @OptIn(ExperimentalFoundationApi::class) - @Composable - fun PrevButton(pagerState: PagerState, modifier: Modifier = Modifier) { - CarouselButton(pagerState, modifier = modifier, enabled = pagerState.canScrollBackward, id = R.drawable.ic_prev, delta = -1) + enum class Direction constructor( + val enabled: (PagerState) -> Boolean, + @DrawableRes val id: Int, + val delta: Int + ) { + PREVIOUS( + PagerState::canScrollBackward, + R.drawable.ic_prev, + -1 + ), + NEXT( + PagerState::canScrollForward, + R.drawable.ic_next, + 1 + ) } @OptIn(ExperimentalFoundationApi::class) @Composable - fun NextButton(pagerState: PagerState, modifier: Modifier = Modifier) { - CarouselButton(pagerState, modifier = modifier, enabled = pagerState.canScrollForward, id = R.drawable.ic_next, delta = 1) + fun CarouselButtonOrSpace(direction: Direction, pagerState: PagerState, modifier: Modifier = Modifier) { + if (pagerState.pageCount >= 2) CarouselButton(pagerState, modifier = modifier, direction = direction) + else Spacer(modifier = Modifier.width(32.dp)) } @OptIn(ExperimentalFoundationApi::class) @@ -385,9 +402,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { fun CarouselButton( pagerState: PagerState, modifier: Modifier = Modifier, - enabled: Boolean, - @DrawableRes id: Int, - delta: Int + direction: Direction ) { val animationScope = rememberCoroutineScope() pagerState.apply { @@ -395,10 +410,10 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { modifier = Modifier .width(40.dp) .then(modifier), - enabled = enabled, - onClick = { animationScope.launch { animateScrollToPage(currentPage + delta) } }) { + enabled = direction.enabled(pagerState), + onClick = { animationScope.launch { animateScrollToPage(currentPage + direction.delta) } }) { Icon( - painter = painterResource(id = id), + painter = painterResource(id = direction.id), contentDescription = "", ) } From 0ec93e4b36e0ba684692fe57dcad9eed9248ae9d Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 18:30:15 +0930 Subject: [PATCH 038/112] Refactor CarouselButtons --- .../conversation/v2/MessageDetailActivity.kt | 64 ++----------------- .../thoughtcrime/securesms/ui/Components.kt | 47 +++++++++++++- 2 files changed, 52 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 9836ef4861..a38ae5d505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent import android.os.Bundle -import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -10,7 +9,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -21,19 +19,15 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -46,7 +40,6 @@ import androidx.lifecycle.ViewModel import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util @@ -60,6 +53,8 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.CarouselNextButton +import org.thoughtcrime.securesms.ui.CarouselPrevButton import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellNoMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin @@ -337,10 +332,10 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row { - CarouselButtonOrSpace( - Direction.PREVIOUS, + CarouselPrevButton( pagerState, - modifier = Modifier.align(Alignment.CenterVertically)) + modifier = Modifier.align(Alignment.CenterVertically) + ) Box(modifier = Modifier.weight(1f)) { CellNoMargin { HorizontalPager(state = pagerState) { i -> @@ -362,8 +357,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ) } } - CarouselButtonOrSpace( - Direction.NEXT, + CarouselNextButton( pagerState, modifier = Modifier.align(Alignment.CenterVertically) ) @@ -372,53 +366,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } - @OptIn(ExperimentalFoundationApi::class) - enum class Direction constructor( - val enabled: (PagerState) -> Boolean, - @DrawableRes val id: Int, - val delta: Int - ) { - PREVIOUS( - PagerState::canScrollBackward, - R.drawable.ic_prev, - -1 - ), - NEXT( - PagerState::canScrollForward, - R.drawable.ic_next, - 1 - ) - } - @OptIn(ExperimentalFoundationApi::class) - @Composable - fun CarouselButtonOrSpace(direction: Direction, pagerState: PagerState, modifier: Modifier = Modifier) { - if (pagerState.pageCount >= 2) CarouselButton(pagerState, modifier = modifier, direction = direction) - else Spacer(modifier = Modifier.width(32.dp)) - } - - @OptIn(ExperimentalFoundationApi::class) - @Composable - fun CarouselButton( - pagerState: PagerState, - modifier: Modifier = Modifier, - direction: Direction - ) { - val animationScope = rememberCoroutineScope() - pagerState.apply { - IconButton( - modifier = Modifier - .width(40.dp) - .then(modifier), - enabled = direction.enabled(pagerState), - onClick = { animationScope.launch { animateScrollToPage(currentPage + direction.delta) } }) { - Icon( - painter = painterResource(id = direction.id), - contentDescription = "", - ) - } - } - } @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 49d419e056..1819b9a318 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -14,10 +15,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonColors import androidx.compose.material.Card import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -26,6 +29,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.google.accompanist.pager.HorizontalPagerIndicator +import kotlinx.coroutines.launch +import network.loki.messenger.R @Composable fun ItemButton( @@ -88,9 +93,49 @@ fun CellWithPaddingAndMargin( fun SessionHorizontalPagerIndicator(modifier: Modifier, pagerState: PagerState, pageCount: Int) { Card(shape = RoundedCornerShape(50.dp), backgroundColor = Color.Black.copy(alpha = 0.4f), - modifier = Modifier.padding(8.dp).then(modifier)) { + modifier = Modifier + .padding(8.dp) + .then(modifier)) { Box(modifier = Modifier.padding(8.dp)) { HorizontalPagerIndicator(pagerState = pagerState, pageCount = pageCount) } } } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CarouselPrevButton(pagerState: PagerState, modifier: Modifier = Modifier) { + CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1, modifier) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CarouselNextButton(pagerState: PagerState, modifier: Modifier = Modifier) { + CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1, modifier) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CarouselButton( + pagerState: PagerState, + enabled: Boolean, + @DrawableRes id: Int, + delta: Int, + modifier: Modifier = Modifier +) { + if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp)) + else { + val animationScope = rememberCoroutineScope() + IconButton( + modifier = Modifier + .width(40.dp) + .then(modifier), + enabled = enabled, + onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) { + Icon( + painter = painterResource(id = id), + contentDescription = "", + ) + } + } +} From 70e63a23bc1af88112181554238c125003d0ecf5 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 20:12:23 +0930 Subject: [PATCH 039/112] Fix indicator colors --- .../org/thoughtcrime/securesms/ui/Colors.kt | 19 ++++++++++++++++++- .../thoughtcrime/securesms/ui/Components.kt | 6 +++++- app/src/main/res/drawable/ic_expand.xml | 5 +++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_expand.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index 0ea7e07141..dedff26cd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -6,8 +6,25 @@ import androidx.compose.ui.graphics.Color val colorDestructive = Color(0xffFF453A) +val classicDark0 = Color(0xff111111) + +val classicDark1 = Color(0xff1B1B1B) +val classicDark2 = Color(0xff2D2D2D) +val classicDark3 = Color(0xff414141) +val classicDark4 = Color(0xff767676) +val classicDark5 = Color(0xffA1A2A1) +val classicDark6 = Color(0xffFFFFFF) + +val classicLight0 = Color(0xff000000) +val classicLight1 = Color(0xff6D6D6D) +val classicLight2 = Color(0xffA1A2A1) +val classicLight3 = Color(0xffDFDFDF) +val classicLight4 = Color(0xffF0F0F0) +val classicLight5 = Color(0xffF9F9F9) +val classicLight6 = Color(0xffFFFFFF) + @Composable fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent) @Composable -fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive) \ No newline at end of file +fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 1819b9a318..a8848adbd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -97,7 +97,11 @@ fun SessionHorizontalPagerIndicator(modifier: Modifier, pagerState: PagerState, .padding(8.dp) .then(modifier)) { Box(modifier = Modifier.padding(8.dp)) { - HorizontalPagerIndicator(pagerState = pagerState, pageCount = pageCount) + HorizontalPagerIndicator( + pagerState = pagerState, + pageCount = pageCount, + activeColor = Color.White, + inactiveColor = classicDark5) } } } diff --git a/app/src/main/res/drawable/ic_expand.xml b/app/src/main/res/drawable/ic_expand.xml new file mode 100644 index 0000000000..3b2b816a45 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand.xml @@ -0,0 +1,5 @@ + + + From 0fcd9972900f5a6fedb09ea4639c7753fde0b9b7 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 20:40:16 +0930 Subject: [PATCH 040/112] Fix divider color --- app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index a19082c088..67c44c6368 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -19,8 +19,8 @@ data class ExtraColors( val settingsBackground: Color, ) -fun Context.getColorFromTheme(@AttrRes attr: Int): Color = - MaterialColors.getColor(this, attr, 0).let(::Color) +fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = + MaterialColors.getColor(this, attr, defaultValue).let(::Color) @Composable fun AppTheme( @@ -29,7 +29,7 @@ fun AppTheme( val extraColors = LocalContext.current.run { ExtraColors( cell = getColorFromTheme(R.attr.colorCellBackground), - divider = getColorFromTheme(R.attr.dividerColor), + divider = getColorFromTheme(R.attr.dividerHorizontal), settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground) ) } From d719660030e5d8ea881e3565c38ac33ae6874b15 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 3 Jul 2023 21:25:59 +0930 Subject: [PATCH 041/112] Add expand button --- .../conversation/v2/MessageDetailActivity.kt | 90 ++++++++++++------- .../thoughtcrime/securesms/ui/Components.kt | 8 ++ 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index a38ae5d505..247505659e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -17,17 +17,24 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -59,7 +66,6 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellNoMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.ItemButton -import org.thoughtcrime.securesms.ui.LocalExtraColors import org.thoughtcrime.securesms.ui.SessionHorizontalPagerIndicator import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors @@ -284,35 +290,50 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } } - Cell { - Column { - ItemButton( - "Reply", - R.drawable.ic_message_details__reply, - onClick = onReply - ) - Divider() - if (error != null) { - ItemButton( - "Resend", - R.drawable.ic_message_details__refresh, - onClick = onResend - ) - Divider() - } - ItemButton( - "Delete", - R.drawable.ic_message_details__trash, - colors = destructiveButtonColors(), - onClick = onDelete - ) - } - } + Buttons( + messageDetails.error != null, + onReply, + onResend, + onDelete, + ) } } } } + @Composable + fun Buttons( + hasError: Boolean, + onReply: () -> Unit = {}, + onResend: () -> Unit = {}, + onDelete: () -> Unit = {}, + ) { + Cell { + Column { + ItemButton( + "Reply", + R.drawable.ic_message_details__reply, + onClick = onReply + ) + Divider() + if (hasError) { + ItemButton( + "Resend", + R.drawable.ic_message_details__refresh, + onClick = onResend + ) + Divider() + } + ItemButton( + "Delete", + R.drawable.ic_message_details__trash, + colors = destructiveButtonColors(), + onClick = onDelete + ) + } + } + } + @Composable fun Attachments(attachments: List) { val slide = attachments.firstOrNull()?.slide ?: return @@ -356,6 +377,16 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { pageCount = imageAttachments.size, ) } + Surface( + shape = CircleShape, + color = Color.Black.copy(alpha = 0.4f), + modifier = Modifier.align(Alignment.BottomEnd).padding(8.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_expand), + contentDescription = "" + ) + } } CarouselNextButton( pagerState, @@ -383,15 +414,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } - @Composable - fun Divider() { - Divider( - modifier = Modifier.padding(horizontal = 16.dp), - thickness = 1.dp, - color = LocalExtraColors.current.divider - ) - } - @Composable fun titledText( titledText: TitledText, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index a8848adbd1..7cba0fb94b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -143,3 +143,11 @@ fun CarouselButton( } } } + +@Composable +fun Divider() { + androidx.compose.material.Divider( + modifier = Modifier.padding(horizontal = 16.dp), + color = LocalExtraColors.current.divider + ) +} From 6890f5c44884b03b636b22bf50e41d614726a7e8 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 09:31:14 +0930 Subject: [PATCH 042/112] Add image click listener --- .../conversation/v2/MessageDetailActivity.kt | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 247505659e..3d9e1fa458 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent import android.os.Bundle import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -51,12 +52,14 @@ import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord + import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme @@ -69,6 +72,7 @@ import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.SessionHorizontalPagerIndicator import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors +import org.thoughtcrime.securesms.util.ActivityDispatcher import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -106,6 +110,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { _details.value = value?.run { MessageDetails( + mmsRecord = mmsRecord, attachments = slides.map { slide -> val duration = slide.takeIf { it.hasAudio() } ?.let { it.asAttachment() as? DatabaseAttachment } @@ -190,7 +195,11 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { details, onReply = { setResultAndFinish(ON_REPLY) }, onResend = { setResultAndFinish(ON_RESEND) }, - onDelete = { setResultAndFinish(ON_DELETE) } + onDelete = { setResultAndFinish(ON_DELETE) }, + onClickImage = { slide -> + MediaPreviewActivity.getPreviewIntent(this, slide, details.mmsRecord, details.sender) + .let(::startActivity) + } ) } @@ -206,6 +215,8 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { data class MessageDetails( val attachments: List = emptyList(), + val mmsRecord: MmsMessageRecord? = null, + val sent: TitledText? = null, val received: TitledText? = null, val error: TitledText? = null, @@ -238,6 +249,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { onReply: () -> Unit = {}, onResend: () -> Unit = {}, onDelete: () -> Unit = {}, + onClickImage: (Slide) -> Unit = {}, ) { messageDetails.apply { AppTheme { @@ -245,7 +257,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Attachments(attachments) + Attachments(attachments) { onClickImage(it) } if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { titledText(it) } @@ -335,10 +347,10 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } @Composable - fun Attachments(attachments: List) { + fun Attachments(attachments: List, onClick: (Slide) -> Unit) { val slide = attachments.firstOrNull()?.slide ?: return when { - slide.hasImage() -> ImageAttachments(attachments) + slide.hasImage() -> ImageAttachments(attachments) { onClick(it) } } } @@ -347,7 +359,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ExperimentalGlideComposeApi::class, ) @Composable - fun ImageAttachments(attachments: List) { + fun ImageAttachments(attachments: List, onClick: (Slide) -> Unit) { val imageAttachments = attachments.filter { it.slide.hasImage() } val pagerState = rememberPagerState { imageAttachments.size } @@ -360,10 +372,13 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { Box(modifier = Modifier.weight(1f)) { CellNoMargin { HorizontalPager(state = pagerState) { i -> - imageAttachments[i].slide.apply { + val slide = imageAttachments[i].slide + slide.apply { GlideImage( contentScale = ContentScale.Crop, - modifier = Modifier.aspectRatio(1f), + modifier = Modifier + .aspectRatio(1f) + .clickable { onClick(slide) }, model = uri, contentDescription = fileName.orNull() ?: "image" ) From 26aed783e84d625f790ca1d0347bd5c52e4ef519 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 11:34:20 +0930 Subject: [PATCH 043/112] Fix Divider padding --- .../securesms/conversation/v2/MessageDetailActivity.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 3d9e1fa458..c6d22e423f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -20,10 +20,7 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.Divider import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.Surface import androidx.compose.material.Text @@ -59,7 +56,6 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord - import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme @@ -68,11 +64,11 @@ import org.thoughtcrime.securesms.ui.CarouselPrevButton import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellNoMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin +import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.SessionHorizontalPagerIndicator import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors -import org.thoughtcrime.securesms.util.ActivityDispatcher import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject From f82ed7718d6fd3e6ed73a83c412e0807f573da97 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 13:26:26 +0930 Subject: [PATCH 044/112] Cleanup CarouselButton --- .../conversation/v2/MessageDetailActivity.kt | 10 ++-------- .../org/thoughtcrime/securesms/ui/Components.kt | 16 ++++++++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index c6d22e423f..0fddfd7ee4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -361,10 +361,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row { - CarouselPrevButton( - pagerState, - modifier = Modifier.align(Alignment.CenterVertically) - ) + CarouselPrevButton(pagerState) Box(modifier = Modifier.weight(1f)) { CellNoMargin { HorizontalPager(state = pagerState) { i -> @@ -399,10 +396,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ) } } - CarouselNextButton( - pagerState, - modifier = Modifier.align(Alignment.CenterVertically) - ) + CarouselNextButton(pagerState) } FileDetails(attachments, pagerState) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 7cba0fb94b..c7720f2396 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -108,24 +109,23 @@ fun SessionHorizontalPagerIndicator(modifier: Modifier, pagerState: PagerState, @OptIn(ExperimentalFoundationApi::class) @Composable -fun CarouselPrevButton(pagerState: PagerState, modifier: Modifier = Modifier) { - CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1, modifier) +fun RowScope.CarouselPrevButton(pagerState: PagerState) { + CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1) } @OptIn(ExperimentalFoundationApi::class) @Composable -fun CarouselNextButton(pagerState: PagerState, modifier: Modifier = Modifier) { - CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1, modifier) +fun RowScope.CarouselNextButton(pagerState: PagerState) { + CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1) } @OptIn(ExperimentalFoundationApi::class) @Composable -fun CarouselButton( +fun RowScope.CarouselButton( pagerState: PagerState, enabled: Boolean, @DrawableRes id: Int, - delta: Int, - modifier: Modifier = Modifier + delta: Int ) { if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp)) else { @@ -133,7 +133,7 @@ fun CarouselButton( IconButton( modifier = Modifier .width(40.dp) - .then(modifier), + .align(Alignment.CenterVertically), enabled = enabled, onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) { Icon( From ee0141f82d74e2e6d62f4df3ef69ba599e6f202b Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 14:09:40 +0930 Subject: [PATCH 045/112] Refactor to Components --- .../conversation/v2/MessageDetailActivity.kt | 76 ++++++++++--------- .../org/thoughtcrime/securesms/ui/Colors.kt | 2 + .../thoughtcrime/securesms/ui/Components.kt | 9 ++- 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 0fddfd7ee4..bf051a3b74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -65,8 +65,9 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellNoMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.Divider +import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.ItemButton -import org.thoughtcrime.securesms.ui.SessionHorizontalPagerIndicator +import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors import java.util.* @@ -350,10 +351,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } - @OptIn( - ExperimentalFoundationApi::class, - ExperimentalGlideComposeApi::class, - ) + @OptIn(ExperimentalFoundationApi::class,) @Composable fun ImageAttachments(attachments: List, onClick: (Slide) -> Unit) { val imageAttachments = attachments.filter { it.slide.hasImage() } @@ -363,38 +361,9 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { Row { CarouselPrevButton(pagerState) Box(modifier = Modifier.weight(1f)) { - CellNoMargin { - HorizontalPager(state = pagerState) { i -> - val slide = imageAttachments[i].slide - slide.apply { - GlideImage( - contentScale = ContentScale.Crop, - modifier = Modifier - .aspectRatio(1f) - .clickable { onClick(slide) }, - model = uri, - contentDescription = fileName.orNull() ?: "image" - ) - } - } - } - if (imageAttachments.size >= 2) { - SessionHorizontalPagerIndicator( - modifier = Modifier.align(Alignment.BottomCenter), - pagerState = pagerState, - pageCount = imageAttachments.size, - ) - } - Surface( - shape = CircleShape, - color = Color.Black.copy(alpha = 0.4f), - modifier = Modifier.align(Alignment.BottomEnd).padding(8.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_expand), - contentDescription = "" - ) - } + CellPager(pagerState, imageAttachments) { onClick(it) } + HorizontalPagerIndicator(pagerState) + ExpandButton(modifier = Modifier.align(Alignment.BottomEnd).padding(8.dp)) } CarouselNextButton(pagerState) } @@ -402,7 +371,40 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } + @OptIn( + ExperimentalFoundationApi::class, + ExperimentalGlideComposeApi::class + ) + @Composable + private fun CellPager(pagerState: PagerState, imageAttachments: List, onClick: (Slide) -> Unit) { + CellNoMargin { + HorizontalPager(state = pagerState) { i -> + val slide = imageAttachments[i].slide + GlideImage( + contentScale = ContentScale.Crop, + modifier = Modifier + .aspectRatio(1f) + .clickable { onClick(slide) }, + model = slide.uri, + contentDescription = slide.fileName.orNull() ?: "image" + ) + } + } + } + @Composable + fun ExpandButton(modifier: Modifier) { + Surface( + shape = CircleShape, + color = blackAlpha40, + modifier = modifier + ) { + Icon( + painter = painterResource(id = R.drawable.ic_expand), + contentDescription = "" + ) + } + } @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index dedff26cd5..cace0c6af7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -23,6 +23,8 @@ val classicLight4 = Color(0xffF0F0F0) val classicLight5 = Color(0xffF9F9F9) val classicLight6 = Color(0xffFFFFFF) +val blackAlpha40 = Color.Black.copy(alpha = 0.4f) + @Composable fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index c7720f2396..c70b557ee3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -91,16 +92,16 @@ fun CellWithPaddingAndMargin( @OptIn(ExperimentalFoundationApi::class) @Composable -fun SessionHorizontalPagerIndicator(modifier: Modifier, pagerState: PagerState, pageCount: Int) { +fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) { Card(shape = RoundedCornerShape(50.dp), backgroundColor = Color.Black.copy(alpha = 0.4f), modifier = Modifier - .padding(8.dp) - .then(modifier)) { + .align(Alignment.BottomCenter) + .padding(8.dp)) { Box(modifier = Modifier.padding(8.dp)) { HorizontalPagerIndicator( pagerState = pagerState, - pageCount = pageCount, + pageCount = pagerState.pageCount, activeColor = Color.White, inactiveColor = classicDark5) } From 6209ae68a8bfed34e49caac7d29bf232abe31994 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 14:46:56 +0930 Subject: [PATCH 046/112] Cleanup --- .../conversation/v2/MessageDetailActivity.kt | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index bf051a3b74..8d59dea734 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -29,7 +29,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource @@ -80,8 +79,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { private var timestamp: Long = 0L - var messageRecord: MessageRecord? = null - @Inject lateinit var storage: Storage @@ -165,24 +162,22 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) - messageRecord = + val messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run { finish() return } val error = DatabaseComponent.get(this).lokiMessageDatabase() - .getErrorMessage(messageRecord!!.getId()) + .getErrorMessage(messageRecord.getId()) viewModel.setMessageRecord(messageRecord, error) title = resources.getString(R.string.conversation_context__menu_message_details) - setContentView(ComposeView(this).apply { - setContent { - MessageDetailsScreen() - } - }) + ComposeView(this) + .apply { setContent { MessageDetailsScreen() } } + .let(::setContentView) } @Composable @@ -255,52 +250,9 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { verticalArrangement = Arrangement.spacedBy(16.dp) ) { Attachments(attachments) { onClickImage(it) } - if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - sent?.let { titledText(it) } - received?.let { titledText(it) } - error?.let { - titledText( - it, - valueStyle = LocalTextStyle.current.copy(color = colorDestructive) - ) - } - senderInfo?.let { - titledView("From:") { - Row { - sender?.let { - Box( - modifier = Modifier - .width(60.dp) - .align(Alignment.CenterVertically) - ) { - AndroidView( - factory = { - ProfilePictureView(it).apply { - update( - sender - ) - } - }, - modifier = Modifier - .width(46.dp) - .height(46.dp) - ) - } - } - Column { - titledText( - it, - valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) - ) - } - } - } - } - } - } + MetaDataCell(messageDetails) Buttons( - messageDetails.error != null, + error != null, onReply, onResend, onDelete, @@ -310,6 +262,50 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } + @Composable + fun MetaDataCell( + messageDetails: MessageDetails, + ) { + if (messageDetails.sent != null || messageDetails.received != null || messageDetails.senderInfo != null) CellWithPaddingAndMargin { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + messageDetails.sent?.let { TitledText(it) } + messageDetails.received?.let { TitledText(it) } + messageDetails.error?.let { + TitledText( + it, + valueStyle = LocalTextStyle.current.copy(color = colorDestructive) + ) + } + messageDetails.senderInfo?.let { + TitledView("From:") { + Row { + messageDetails.sender?.let { sender -> + Box( + modifier = Modifier + .width(60.dp) + .align(Alignment.CenterVertically) + ) { + AndroidView( + factory = { + ProfilePictureView(it).apply { update(sender) } + }, + modifier = Modifier.width(46.dp).height(46.dp) + ) + } + } + Column { + TitledText( + it, + valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) + ) + } + } + } + } + } + } + } + @Composable fun Buttons( hasError: Boolean, @@ -347,13 +343,13 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { fun Attachments(attachments: List, onClick: (Slide) -> Unit) { val slide = attachments.firstOrNull()?.slide ?: return when { - slide.hasImage() -> ImageAttachments(attachments) { onClick(it) } + slide.hasImage() -> Carousel(attachments, onClick) } } @OptIn(ExperimentalFoundationApi::class,) @Composable - fun ImageAttachments(attachments: List, onClick: (Slide) -> Unit) { + fun Carousel(attachments: List, onClick: (Slide) -> Unit) { val imageAttachments = attachments.filter { it.slide.hasImage() } val pagerState = rememberPagerState { imageAttachments.size } @@ -361,7 +357,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { Row { CarouselPrevButton(pagerState) Box(modifier = Modifier.weight(1f)) { - CellPager(pagerState, imageAttachments) { onClick(it) } + CellPager(pagerState, imageAttachments, onClick) HorizontalPagerIndicator(pagerState) ExpandButton(modifier = Modifier.align(Alignment.BottomEnd).padding(8.dp)) } @@ -415,14 +411,14 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { verticalArrangement = Arrangement.spacedBy(16.dp), maxItemsInEachRow = 2 ) { - it.forEach { titledText(it, Modifier.weight(1f)) } + it.forEach { TitledText(it, Modifier.weight(1f)) } } } } } @Composable - fun titledText( + fun TitledText( titledText: TitledText, modifier: Modifier = Modifier, valueStyle: TextStyle = LocalTextStyle.current @@ -434,7 +430,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } @Composable - fun titledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { + fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(title) content() From 7cd2bd0e0d8c1ca4cf98e82d2f40239fd1b45d35 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 15:11:40 +0930 Subject: [PATCH 047/112] Cleanup --- .../components/ProfilePictureView.kt | 4 + .../conversation/v2/MessageDetailActivity.kt | 76 +++++++++++-------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 37a3e764e8..4ee4777c62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -43,6 +43,10 @@ class ProfilePictureView @JvmOverloads constructor( // endregion + constructor(context: Context, sender: Recipient): this(context) { + update(sender) + } + // region Updating fun update(recipient: Recipient) { fun getUserDisplayName(publicKey: String): String { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 8d59dea734..b8abb7152a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -266,38 +267,17 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { fun MetaDataCell( messageDetails: MessageDetails, ) { - if (messageDetails.sent != null || messageDetails.received != null || messageDetails.senderInfo != null) CellWithPaddingAndMargin { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - messageDetails.sent?.let { TitledText(it) } - messageDetails.received?.let { TitledText(it) } - messageDetails.error?.let { - TitledText( - it, - valueStyle = LocalTextStyle.current.copy(color = colorDestructive) - ) - } - messageDetails.senderInfo?.let { - TitledView("From:") { - Row { - messageDetails.sender?.let { sender -> - Box( - modifier = Modifier - .width(60.dp) - .align(Alignment.CenterVertically) - ) { - AndroidView( - factory = { - ProfilePictureView(it).apply { update(sender) } - }, - modifier = Modifier.width(46.dp).height(46.dp) - ) - } - } - Column { - TitledText( - it, - valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) - ) + messageDetails.apply { + if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + sent?.let { TitledText(it) } + received?.let { TitledText(it) } + error?.let { TitledErrorText(it) } + senderInfo?.let { + TitledView("From:") { + Row { + sender?.let { Avatar(it) } + TitledMonospaceText(it) } } } @@ -306,6 +286,22 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } + @Composable + fun RowScope.Avatar(sender: Recipient) { + Box( + modifier = Modifier + .width(60.dp) + .align(Alignment.CenterVertically) + ) { + AndroidView( + factory = { + ProfilePictureView(it).apply { update(sender) } + }, + modifier = Modifier.width(46.dp).height(46.dp) + ) + } + } + @Composable fun Buttons( hasError: Boolean, @@ -417,6 +413,22 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } + @Composable + fun TitledErrorText(titledText: TitledText, modifier: Modifier = Modifier) { + TitledText( + titledText, + modifier = modifier, + valueStyle = LocalTextStyle.current.copy(color = colorDestructive)) + } + + @Composable + fun TitledMonospaceText(titledText: TitledText, modifier: Modifier = Modifier) { + TitledText( + titledText, + modifier = modifier, + valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) + } + @Composable fun TitledText( titledText: TitledText, From d6b14402175bb3307f51874628c2b89dc7783b58 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 15:27:00 +0930 Subject: [PATCH 048/112] Refactor MessageDetailsViewModel --- .../conversation/v2/MessageDetailActivity.kt | 92 ------------------- .../v2/MessageDetailsViewModel.kt | 87 ++++++++++++++++++ 2 files changed, 87 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index b8abb7152a..7a0c24e15f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -39,23 +39,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment -import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.components.ProfilePictureView -import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme @@ -70,8 +62,6 @@ import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors -import java.util.* -import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -94,70 +84,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { val viewModel = MessageDetailsViewModel() - class MessageDetailsViewModel : ViewModel() { - @Inject - lateinit var attachmentDb: AttachmentDatabase - - fun setMessageRecord(value: MessageRecord?, error: String?) { - val mmsRecord = value as? MmsMessageRecord - - val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() - - _details.value = value?.run { - MessageDetails( - mmsRecord = mmsRecord, - attachments = slides.map { slide -> - val duration = slide.takeIf { it.hasAudio() } - ?.let { it.asAttachment() as? DatabaseAttachment } - ?.let { attachment -> - attachmentDb.getAttachmentAudioExtras(attachment.attachmentId) - ?.let { audioExtras -> - audioExtras.durationMs.takeIf { it > 0 }?.let { - String.format( - "%01d:%02d", - TimeUnit.MILLISECONDS.toMinutes(it), - TimeUnit.MILLISECONDS.toSeconds(it) % 60 - ) - } - } - } - - val details = slide.run { - listOfNotNull( - fileName.orNull()?.let { TitledText("File Id:", it) }, - TitledText("File Type:", asAttachment().contentType), - TitledText("File Size:", Util.getPrettyFileSize(fileSize)), - if (slide.hasImage()) { - TitledText( - "Resolution:", - slide.asAttachment().run { "${width}x$height" }) - } else null, - duration?.let { TitledText("Duration:", it) }, - ) - } - Attachment(slide, details) - }, - sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, - received = dateReceived.let(::Date).toString() - .let { TitledText("Received:", it) }, - error = error?.let { TitledText("Error:", it) }, - senderInfo = individualRecipient.run { - name?.let { - TitledText( - it, - address.serialize() - ) - } - }, - sender = individualRecipient - ) - } - } - - private var _details = MutableLiveData(MessageDetails()) - val details: LiveData = _details - } - override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) @@ -204,24 +130,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { finish() } - data class TitledText(val title: String, val value: String) - - data class MessageDetails( - val attachments: List = emptyList(), - val mmsRecord: MmsMessageRecord? = null, - - val sent: TitledText? = null, - val received: TitledText? = null, - val error: TitledText? = null, - val senderInfo: TitledText? = null, - val sender: Recipient? = null - ) - - data class Attachment( - val slide: Slide, - val fileDetails: List - ) - @Preview @Composable fun PreviewMessageDetails() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt new file mode 100644 index 0000000000..80693a167c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.utilities.Util +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.mms.Slide +import java.util.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +data class TitledText(val title: String, val value: String) + +data class MessageDetails( + val attachments: List = emptyList(), + val mmsRecord: MmsMessageRecord? = null, + val sent: TitledText? = null, + val received: TitledText? = null, + val error: TitledText? = null, + val senderInfo: TitledText? = null, + val sender: Recipient? = null +) + +data class Attachment( + val slide: Slide, + val fileDetails: List +) + +class MessageDetailsViewModel : ViewModel() { + @Inject + lateinit var attachmentDb: AttachmentDatabase + + fun setMessageRecord(value: MessageRecord?, error: String?) { + val mmsRecord = value as? MmsMessageRecord + + val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() + + _details.value = value?.run { + MessageDetails( + mmsRecord = mmsRecord, + attachments = slides.map { Attachment(it, it.details) }, + sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, + received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, + error = error?.let { TitledText("Error:", it) }, + senderInfo = individualRecipient.run { + name?.let { TitledText(it, address.serialize()) } + }, + sender = individualRecipient + ) + } + } + + private var _details = MutableLiveData(MessageDetails()) + val details: LiveData = _details + + private val Slide.details: List + get() = listOfNotNull( + fileName.orNull()?.let { TitledText("File Id:", it) }, + TitledText("File Type:", asAttachment().contentType), + TitledText("File Size:", Util.getPrettyFileSize(fileSize)), + takeIf { it.hasImage() } + .run { asAttachment().run { "${width}x$height" } } + .let { TitledText("Resolution:", it) }, + attachmentDb.duration(this)?.let { TitledText("Duration:", it) }, + ) + + private fun AttachmentDatabase.duration(slide: Slide): String? = + slide.takeIf { it.hasAudio() } + ?.run { asAttachment() as? DatabaseAttachment } + ?.run { + getAttachmentAudioExtras(attachmentId) + ?.let { audioExtras -> + audioExtras.durationMs.takeIf { it > 0 }?.let { + String.format( + "%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(it), + TimeUnit.MILLISECONDS.toSeconds(it) % 60 + ) + } + } + } +} \ No newline at end of file From 8d66d948ca34d052c5bea08d09aafa783911f075 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 4 Jul 2023 22:10:48 +0930 Subject: [PATCH 049/112] Polish --- app/build.gradle | 15 ++- .../conversation/v2/MessageDetailActivity.kt | 97 +++++++++++++------ .../v2/MessageDetailsViewModel.kt | 9 +- .../v2/messages/VisibleMessageContentView.kt | 11 ++- .../v2/messages/VisibleMessageView.kt | 14 +-- .../thoughtcrime/securesms/ui/Components.kt | 6 +- build.gradle | 5 + 7 files changed, 112 insertions(+), 45 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4bb54eb20e..64f6419180 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,4 @@ + buildscript { repositories { google() @@ -13,6 +14,11 @@ buildscript { } } +plugins { + id 'kotlin-kapt' + id 'com.google.dagger.hilt.android' +} + apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'witness' @@ -29,6 +35,9 @@ configurations.all { dependencies { + implementation("com.google.dagger:hilt-android:2.46.1") + kapt("com.google.dagger:hilt-android-compiler:2.44") + implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "com.google.android.material:material:$materialVersion" @@ -41,7 +50,6 @@ dependencies { implementation 'androidx.exifinterface:exifinterface:1.3.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" @@ -328,3 +336,8 @@ def autoResConfig() { .collect { matcher -> matcher.group(1) } .sort() } + +// Allow references to generated code +kapt { + correctErrorTypes = true +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 7a0c24e15f..c7838047b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent import android.os.Bundle +import android.view.LayoutInflater +import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -39,16 +41,24 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import network.loki.messenger.R +import network.loki.messenger.databinding.ViewVisibleMessageBinding +import network.loki.messenger.databinding.ViewVisibleMessageContentBinding +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.CarouselNextButton @@ -64,7 +74,6 @@ import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors import javax.inject.Inject - @AndroidEntryPoint class MessageDetailActivity : PassphraseRequiredActionBarActivity() { @@ -73,6 +82,8 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { @Inject lateinit var storage: Storage + private val viewModel: MessageDetailsViewModel by viewModels() + companion object { // Extras const val MESSAGE_TIMESTAMP = "message_timestamp" @@ -82,8 +93,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { const val ON_DELETE = 3 } - val viewModel = MessageDetailsViewModel() - override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) @@ -116,7 +125,12 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { onResend = { setResultAndFinish(ON_RESEND) }, onDelete = { setResultAndFinish(ON_DELETE) }, onClickImage = { slide -> - MediaPreviewActivity.getPreviewIntent(this, slide, details.mmsRecord, details.sender) + MediaPreviewActivity.getPreviewIntent( + this, + slide, + details.mmsRecord, + details.sender + ) .let(::startActivity) } ) @@ -144,6 +158,12 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ) } + private fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) { + lifecycleScope.launch(Dispatchers.IO) { + JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) + } + } + @Composable fun MessageDetails( messageDetails: MessageDetails, @@ -152,27 +172,39 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { onDelete: () -> Unit = {}, onClickImage: (Slide) -> Unit = {}, ) { - messageDetails.apply { - AppTheme { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Attachments(attachments) { onClickImage(it) } - MetaDataCell(messageDetails) - Buttons( - error != null, - onReply, - onResend, - onDelete, + AppTheme { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + messageDetails.mmsRecord?.let { message -> + AndroidView( + modifier = Modifier.padding(32.dp), + factory = { + ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { + bind( + message, + thread = message.individualRecipient, + onAttachmentNeedsDownload = ::onAttachmentNeedsDownload + ) + } + } ) } + Attachments(messageDetails.attachments) { onClickImage(it) } + MetadataCell(messageDetails) + Buttons( + messageDetails.error != null, + onReply, + onResend, + onDelete, + ) } } } @Composable - fun MetaDataCell( + fun MetadataCell( messageDetails: MessageDetails, ) { messageDetails.apply { @@ -205,7 +237,9 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { factory = { ProfilePictureView(it).apply { update(sender) } }, - modifier = Modifier.width(46.dp).height(46.dp) + modifier = Modifier + .width(46.dp) + .height(46.dp) ) } } @@ -245,13 +279,12 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { @Composable fun Attachments(attachments: List, onClick: (Slide) -> Unit) { - val slide = attachments.firstOrNull()?.slide ?: return - when { - slide.hasImage() -> Carousel(attachments, onClick) + when(attachments.firstOrNull()?.slide) { + is ImageSlide -> Carousel(attachments, onClick) } } - @OptIn(ExperimentalFoundationApi::class,) + @OptIn(ExperimentalFoundationApi::class) @Composable fun Carousel(attachments: List, onClick: (Slide) -> Unit) { val imageAttachments = attachments.filter { it.slide.hasImage() } @@ -263,7 +296,11 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { Box(modifier = Modifier.weight(1f)) { CellPager(pagerState, imageAttachments, onClick) HorizontalPagerIndicator(pagerState) - ExpandButton(modifier = Modifier.align(Alignment.BottomEnd).padding(8.dp)) + ExpandButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(8.dp) + ) } CarouselNextButton(pagerState) } @@ -276,7 +313,11 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ExperimentalGlideComposeApi::class ) @Composable - private fun CellPager(pagerState: PagerState, imageAttachments: List, onClick: (Slide) -> Unit) { + private fun CellPager( + pagerState: PagerState, + imageAttachments: List, + onClick: (Slide) -> Unit + ) { CellNoMargin { HorizontalPager(state = pagerState) { i -> val slide = imageAttachments[i].slide @@ -326,7 +367,8 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { TitledText( titledText, modifier = modifier, - valueStyle = LocalTextStyle.current.copy(color = colorDestructive)) + valueStyle = LocalTextStyle.current.copy(color = colorDestructive) + ) } @Composable @@ -334,7 +376,8 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { TitledText( titledText, modifier = modifier, - valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) + valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) + ) } @Composable diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 80693a167c..b92a399349 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.conversation.v2 import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.HiltViewModel import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient @@ -31,9 +33,10 @@ data class Attachment( val fileDetails: List ) -class MessageDetailsViewModel : ViewModel() { - @Inject - lateinit var attachmentDb: AttachmentDatabase +@HiltViewModel +class MessageDetailsViewModel @Inject constructor( + private val attachmentDb: AttachmentDatabase +): ViewModel() { fun setMessageRecord(value: MessageRecord?, error: String?) { val mmsRecord = value as? MmsMessageRecord diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 75a3c58752..c2133c7145 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getInt import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.SmsMessageRecord +import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.getAccentColor @@ -60,12 +61,12 @@ class VisibleMessageContentView : ConstraintLayout { // region Updating fun bind( message: MessageRecord, - isStartOfMessageCluster: Boolean, - isEndOfMessageCluster: Boolean, - glide: GlideRequests, + isStartOfMessageCluster: Boolean = true, + isEndOfMessageCluster: Boolean = true, + glide: GlideRequests = GlideApp.with(this), thread: Recipient, - searchQuery: String?, - contactIsTrusted: Boolean, + searchQuery: String? = null, + contactIsTrusted: Boolean = true, onAttachmentNeedsDownload: (Long, Long) -> Unit ) { // Background diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 40a087ea64..79056ed9c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.home.UserDetailsBottomSheet +import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.disableClipping @@ -70,7 +71,6 @@ class VisibleMessageView : LinearLayout { @Inject lateinit var mmsDb: MmsDatabase private val binding by lazy { ViewVisibleMessageBinding.bind(this) } - private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate() private val swipeToReplyIconRect = Rect() private var dx = 0.0f @@ -119,13 +119,13 @@ class VisibleMessageView : LinearLayout { // region Updating fun bind( message: MessageRecord, - previous: MessageRecord?, - next: MessageRecord?, - glide: GlideRequests, - searchQuery: String?, - contact: Contact?, + previous: MessageRecord? = null, + next: MessageRecord? = null, + glide: GlideRequests = GlideApp.with(this), + searchQuery: String? = null, + contact: Contact? = null, senderSessionID: String, - delegate: VisibleMessageViewDelegate?, + delegate: VisibleMessageViewDelegate? = null, onAttachmentNeedsDownload: (Long, Long) -> Unit ) { val threadID = message.threadId diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index c70b557ee3..34c9f0b8bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -93,11 +93,13 @@ fun CellWithPaddingAndMargin( @OptIn(ExperimentalFoundationApi::class) @Composable fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) { - Card(shape = RoundedCornerShape(50.dp), + if (pagerState.pageCount >= 2) Card( + shape = RoundedCornerShape(50.dp), backgroundColor = Color.Black.copy(alpha = 0.4f), modifier = Modifier .align(Alignment.BottomCenter) - .padding(8.dp)) { + .padding(8.dp) + ) { Box(modifier = Modifier.padding(8.dp)) { HorizontalPagerIndicator( pagerState = pagerState, diff --git a/build.gradle b/build.gradle index 39701076cf..e7bc8cae84 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,4 @@ + buildscript { repositories { google() @@ -12,6 +13,10 @@ buildscript { } } +plugins{ + id("com.google.dagger.hilt.android") version "2.44" apply false +} + allprojects { repositories { google() From efb5b27191ffcb2114e61913b09a67f7cba3787c Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 6 Jul 2023 12:03:33 +0930 Subject: [PATCH 050/112] Fix divider color --- app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 67c44c6368..88916e03ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -29,7 +29,7 @@ fun AppTheme( val extraColors = LocalContext.current.run { ExtraColors( cell = getColorFromTheme(R.attr.colorCellBackground), - divider = getColorFromTheme(R.attr.dividerHorizontal), + divider = getColorFromTheme(R.attr.dividerHorizontal).copy(alpha = 0.15f), settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground) ) } From 68684bb8397bfc74dee3d83930f36ae4f671c9e7 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 6 Jul 2023 13:09:00 +0930 Subject: [PATCH 051/112] Refactor VisibleMessageContentView#onContentClick --- .../securesms/conversation/v2/MessageDetailActivity.kt | 1 - .../conversation/v2/messages/VisibleMessageContentView.kt | 7 ++++++- .../conversation/v2/messages/VisibleMessageView.kt | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index c7838047b2..96fcb596f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -48,7 +48,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import network.loki.messenger.R -import network.loki.messenger.databinding.ViewVisibleMessageBinding import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index c2133c7145..b081d548dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -47,7 +47,6 @@ import kotlin.math.roundToInt class VisibleMessageContentView : ConstraintLayout { private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) } - var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() var onContentDoubleTap: (() -> Unit)? = null var delegate: VisibleMessageViewDelegate? = null var indexInAdapter: Int = -1 @@ -242,6 +241,12 @@ class VisibleMessageContentView : ConstraintLayout { binding.contentParent.layoutParams = layoutParams } + private val onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() + + fun onContentClick(event: MotionEvent) { + onContentClick.forEach { clickHandler -> clickHandler.invoke(event) } + } + private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean = listOf(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 79056ed9c2..55916dc806 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context import android.content.Intent -import android.content.res.Resources import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.ColorDrawable @@ -502,7 +501,7 @@ class VisibleMessageView : LinearLayout { } fun onContentClick(event: MotionEvent) { - binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) } + binding.messageContentView.root.onContentClick(event) } private fun onPress(event: MotionEvent) { From 8be1e8e87e419036c9a2b2c71263cda52f268c41 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 6 Jul 2023 15:20:45 +0930 Subject: [PATCH 052/112] Wire up voiceMessageView --- .../conversation/v2/MessageDetailActivity.kt | 32 ++++++------------- .../thoughtcrime/securesms/ui/Components.kt | 21 ++++++++++++ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 96fcb596f9..3dc001f00d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent.ACTION_UP import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable @@ -12,11 +14,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState @@ -51,15 +50,14 @@ import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.Avatar import org.thoughtcrime.securesms.ui.CarouselNextButton import org.thoughtcrime.securesms.ui.CarouselPrevButton import org.thoughtcrime.securesms.ui.Cell @@ -163,6 +161,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } + @SuppressLint("ClickableViewAccessibility") @Composable fun MessageDetails( messageDetails: MessageDetails, @@ -186,6 +185,11 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { thread = message.individualRecipient, onAttachmentNeedsDownload = ::onAttachmentNeedsDownload ) + + setOnTouchListener { _, event -> + if (event.actionMasked == ACTION_UP) onContentClick(event) + true + } } } ) @@ -225,24 +229,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } } - @Composable - fun RowScope.Avatar(sender: Recipient) { - Box( - modifier = Modifier - .width(60.dp) - .align(Alignment.CenterVertically) - ) { - AndroidView( - factory = { - ProfilePictureView(it).apply { update(sender) } - }, - modifier = Modifier - .width(46.dp) - .height(46.dp) - ) - } - } - @Composable fun Buttons( hasError: Boolean, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 34c9f0b8bb..b7684a22f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -30,9 +30,12 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import com.google.accompanist.pager.HorizontalPagerIndicator import kotlinx.coroutines.launch import network.loki.messenger.R +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.components.ProfilePictureView @Composable fun ItemButton( @@ -154,3 +157,21 @@ fun Divider() { color = LocalExtraColors.current.divider ) } + +@Composable +fun RowScope.Avatar(recipient: Recipient) { + Box( + modifier = Modifier + .width(60.dp) + .align(Alignment.CenterVertically) + ) { + AndroidView( + factory = { + ProfilePictureView(it).apply { update(recipient) } + }, + modifier = Modifier + .width(46.dp) + .height(46.dp) + ) + } +} From c417b37236b3dbcfd6d98c2a96cf0e3782214479 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 6 Jul 2023 15:23:58 +0930 Subject: [PATCH 053/112] Hide thumbnails --- .../securesms/conversation/v2/MessageDetailActivity.kt | 3 ++- .../conversation/v2/messages/VisibleMessageContentView.kt | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 3dc001f00d..acba309c5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -183,7 +183,8 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { bind( message, thread = message.individualRecipient, - onAttachmentNeedsDownload = ::onAttachmentNeedsDownload + onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, + suppressThumbnails = true ) setOnTouchListener { _, event -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index b081d548dd..d1989a207a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -66,7 +66,8 @@ class VisibleMessageContentView : ConstraintLayout { thread: Recipient, searchQuery: String? = null, contactIsTrusted: Boolean = true, - onAttachmentNeedsDownload: (Long, Long) -> Unit + onAttachmentNeedsDownload: (Long, Long) -> Unit, + suppressThumbnails: Boolean = false ) { // Background val background = getBackground(message.isOutgoing) @@ -188,7 +189,7 @@ class VisibleMessageContentView : ConstraintLayout { onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } } } - message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty() -> { + message is MmsMessageRecord && !suppressThumbnails && message.slideDeck.asAttachments().isNotEmpty() -> { /* * Images / Video attachment */ From 1d1977ca7a2b608e619b0a12c0857e2dc1434fac Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 6 Jul 2023 15:32:51 +0930 Subject: [PATCH 054/112] Make expand button clickable --- .../securesms/conversation/v2/MessageDetailActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index acba309c5b..41b0499750 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -284,6 +284,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { HorizontalPagerIndicator(pagerState) ExpandButton( modifier = Modifier + .clickable { onClick(imageAttachments[pagerState.currentPage].slide) } .align(Alignment.BottomEnd) .padding(8.dp) ) From fc87ae18f558090a16886ea4fdf1a1eb98b9848e Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 6 Jul 2023 15:48:31 +0930 Subject: [PATCH 055/112] Fix message padding --- .../securesms/conversation/v2/MessageDetailActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 41b0499750..07230eadee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -7,6 +7,7 @@ import android.view.LayoutInflater import android.view.MotionEvent.ACTION_UP import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +32,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource @@ -177,7 +179,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ) { messageDetails.mmsRecord?.let { message -> AndroidView( - modifier = Modifier.padding(32.dp), + modifier = Modifier.padding(horizontal = 32.dp), factory = { ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { bind( From e5b19d4ea4e6e1c52076ac8f46c3259c21626a9f Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 6 Jul 2023 19:05:09 +0930 Subject: [PATCH 056/112] Fix incorrect recipient used as threadRecipient to launch MediaPreview --- .../securesms/MediaPreviewActivity.java | 1 - .../conversation/v2/MessageDetailActivity.kt | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 6544c2ab89..a1648e0763 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -531,7 +531,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im @Override public void onLoadFinished(@NonNull Loader> loader, @Nullable Pair data) { if (data != null) { - @SuppressWarnings("ConstantConditions") CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent); mediaPager.setAdapter(adapter); adapter.setActive(true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 07230eadee..d8f8a9d7f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -7,7 +7,6 @@ import android.view.LayoutInflater import android.view.MotionEvent.ACTION_UP import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -32,7 +31,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource @@ -52,6 +50,8 @@ import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage @@ -124,13 +124,19 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { onResend = { setResultAndFinish(ON_RESEND) }, onDelete = { setResultAndFinish(ON_DELETE) }, onClickImage = { slide -> - MediaPreviewActivity.getPreviewIntent( + // only open to downloaded images + if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { + // Restart download here (on IO thread) + (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> + onAttachmentNeedsDownload(attachment.attachmentId.rowId, details.mmsRecord!!.getId()) + } + } + if (!slide.isInProgress) MediaPreviewActivity.getPreviewIntent( this, slide, details.mmsRecord, - details.sender - ) - .let(::startActivity) + DatabaseComponent.get(this).threadDatabase().getRecipientForThreadId(details.mmsRecord!!.threadId), + ).let(::startActivity) } ) } @@ -184,7 +190,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { bind( message, - thread = message.individualRecipient, + thread = DatabaseComponent.get(this@MessageDetailActivity).threadDatabase().getRecipientForThreadId(message.threadId)!!, onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, suppressThumbnails = true ) @@ -286,10 +292,9 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { HorizontalPagerIndicator(pagerState) ExpandButton( modifier = Modifier - .clickable { onClick(imageAttachments[pagerState.currentPage].slide) } .align(Alignment.BottomEnd) .padding(8.dp) - ) + ) { onClick(imageAttachments[pagerState.currentPage].slide) } } CarouselNextButton(pagerState) } @@ -323,7 +328,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } @Composable - fun ExpandButton(modifier: Modifier) { + fun ExpandButton(modifier: Modifier, onClick: () -> Unit) { Surface( shape = CircleShape, color = blackAlpha40, @@ -331,7 +336,8 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ) { Icon( painter = painterResource(id = R.drawable.ic_expand), - contentDescription = "" + contentDescription = "", + modifier = Modifier.clickable { onClick() } ) } } From d8b85768d2240b95755a26267ced1ed978d1a503 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 7 Jul 2023 10:46:43 +0930 Subject: [PATCH 057/112] Fix sms messages --- .../securesms/conversation/v2/MessageDetailActivity.kt | 2 +- .../securesms/conversation/v2/MessageDetailsViewModel.kt | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index d8f8a9d7f9..f95f257199 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -183,7 +183,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - messageDetails.mmsRecord?.let { message -> + messageDetails.record?.let { message -> AndroidView( modifier = Modifier.padding(horizontal = 32.dp), factory = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index b92a399349..01864aeb17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -20,6 +20,7 @@ data class TitledText(val title: String, val value: String) data class MessageDetails( val attachments: List = emptyList(), + val record: MessageRecord? = null, val mmsRecord: MmsMessageRecord? = null, val sent: TitledText? = null, val received: TitledText? = null, @@ -38,13 +39,14 @@ class MessageDetailsViewModel @Inject constructor( private val attachmentDb: AttachmentDatabase ): ViewModel() { - fun setMessageRecord(value: MessageRecord?, error: String?) { - val mmsRecord = value as? MmsMessageRecord + fun setMessageRecord(record: MessageRecord?, error: String?) { + val mmsRecord = record as? MmsMessageRecord val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() - _details.value = value?.run { + _details.value = record?.run { MessageDetails( + record = record, mmsRecord = mmsRecord, attachments = slides.map { Attachment(it, it.details) }, sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, From bbc9cdfeeb5751ec8dbedce3aed71bacc0a46b42 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 9 Jul 2023 23:31:43 +0930 Subject: [PATCH 058/112] Cleanup ViewModel --- .../v2/MessageDetailsViewModel.kt | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 01864aeb17..5ff1e05176 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2 import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util @@ -11,6 +10,7 @@ import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide import java.util.* import java.util.concurrent.TimeUnit @@ -32,12 +32,14 @@ data class MessageDetails( data class Attachment( val slide: Slide, val fileDetails: List -) +) { + fun hasImage() = slide is ImageSlide +} @HiltViewModel class MessageDetailsViewModel @Inject constructor( private val attachmentDb: AttachmentDatabase -): ViewModel() { +) : ViewModel() { fun setMessageRecord(record: MessageRecord?, error: String?) { val mmsRecord = record as? MmsMessageRecord @@ -77,16 +79,13 @@ class MessageDetailsViewModel @Inject constructor( private fun AttachmentDatabase.duration(slide: Slide): String? = slide.takeIf { it.hasAudio() } ?.run { asAttachment() as? DatabaseAttachment } - ?.run { - getAttachmentAudioExtras(attachmentId) - ?.let { audioExtras -> - audioExtras.durationMs.takeIf { it > 0 }?.let { - String.format( - "%01d:%02d", - TimeUnit.MILLISECONDS.toMinutes(it), - TimeUnit.MILLISECONDS.toSeconds(it) % 60 - ) - } - } + ?.run { getAttachmentAudioExtras(attachmentId)?.durationMs } + ?.takeIf { it > 0 } + ?.let { + String.format( + "%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(it), + TimeUnit.MILLISECONDS.toSeconds(it) % 60 + ) } -} \ No newline at end of file +} From 0824713ac5220c1e807b8a34daea5778bf099ad6 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jul 2023 00:12:42 +0930 Subject: [PATCH 059/112] Fix theming and polish --- .../conversation/v2/MessageDetailActivity.kt | 511 ++++++++++-------- .../org/thoughtcrime/securesms/ui/Colors.kt | 61 ++- .../thoughtcrime/securesms/ui/Components.kt | 51 +- .../org/thoughtcrime/securesms/ui/Themes.kt | 53 +- app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/themes.xml | 2 - 6 files changed, 408 insertions(+), 274 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index f95f257199..46eeb4ecc0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -13,9 +13,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState @@ -31,9 +34,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -55,8 +60,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Avatar @@ -68,6 +73,7 @@ import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.ItemButton +import org.thoughtcrime.securesms.ui.Theme import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors @@ -118,27 +124,32 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { @Composable private fun MessageDetailsScreen() { val details by viewModel.details.observeAsState(MessageDetails()) - MessageDetails( - details, - onReply = { setResultAndFinish(ON_REPLY) }, - onResend = { setResultAndFinish(ON_RESEND) }, - onDelete = { setResultAndFinish(ON_DELETE) }, - onClickImage = { slide -> - // only open to downloaded images - if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { - // Restart download here (on IO thread) - (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - onAttachmentNeedsDownload(attachment.attachmentId.rowId, details.mmsRecord!!.getId()) + val threadDb = DatabaseComponent.get(this@MessageDetailActivity).threadDatabase() + AppTheme { + MessageDetails( + threadDb = threadDb, + messageDetails = details, + onReply = { setResultAndFinish(ON_REPLY) }, + onResend = { setResultAndFinish(ON_RESEND) }, + onDelete = { setResultAndFinish(ON_DELETE) }, + onClickImage = { slide -> + // only open to downloaded images + if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { + // Restart download here (on IO thread) + (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> + onAttachmentNeedsDownload(attachment.attachmentId.rowId, details.mmsRecord!!.getId()) + } } - } - if (!slide.isInProgress) MediaPreviewActivity.getPreviewIntent( - this, - slide, - details.mmsRecord, - DatabaseComponent.get(this).threadDatabase().getRecipientForThreadId(details.mmsRecord!!.threadId), - ).let(::startActivity) - } - ) + if (!slide.isInProgress) MediaPreviewActivity.getPreviewIntent( + this, + slide, + details.mmsRecord, + threadDb.getRecipientForThreadId(details.mmsRecord!!.threadId), + ).let(::startActivity) + }, + onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, + ) + } } private fun setResultAndFinish(code: Int) { @@ -149,254 +160,276 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { finish() } - @Preview - @Composable - fun PreviewMessageDetails() { - MessageDetails( - MessageDetails( - attachments = listOf(), - sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), - received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), - error = TitledText("Error:", "Message failed to send"), - senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg"), - ) - ) - } - private fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) { lifecycleScope.launch(Dispatchers.IO) { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) } } - @SuppressLint("ClickableViewAccessibility") - @Composable - fun MessageDetails( - messageDetails: MessageDetails, - onReply: () -> Unit = {}, - onResend: () -> Unit = {}, - onDelete: () -> Unit = {}, - onClickImage: (Slide) -> Unit = {}, - ) { - AppTheme { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - messageDetails.record?.let { message -> - AndroidView( - modifier = Modifier.padding(horizontal = 32.dp), - factory = { - ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { - bind( - message, - thread = DatabaseComponent.get(this@MessageDetailActivity).threadDatabase().getRecipientForThreadId(message.threadId)!!, - onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, - suppressThumbnails = true - ) +} - setOnTouchListener { _, event -> - if (event.actionMasked == ACTION_UP) onContentClick(event) - true - } - } - } - ) - } - Attachments(messageDetails.attachments) { onClickImage(it) } - MetadataCell(messageDetails) - Buttons( - messageDetails.error != null, - onReply, - onResend, - onDelete, - ) - } - } +@Composable +fun PreviewMessageDetails() { + AppTheme { + MessageDetails( + messageDetails = MessageDetails( + attachments = listOf(), + sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), + received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), + error = TitledText("Error:", "Message failed to send"), + senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"), + ) + ) } +} - @Composable - fun MetadataCell( - messageDetails: MessageDetails, +@SuppressLint("ClickableViewAccessibility") +@Composable +fun MessageDetails( + threadDb: ThreadDatabase? = null, + messageDetails: MessageDetails, + onReply: () -> Unit = {}, + onResend: (() -> Unit)? = null, + onDelete: () -> Unit = {}, + onClickImage: (Slide) -> Unit = {}, + onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> } +) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - messageDetails.apply { - if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - sent?.let { TitledText(it) } - received?.let { TitledText(it) } - error?.let { TitledErrorText(it) } - senderInfo?.let { - TitledView("From:") { - Row { - sender?.let { Avatar(it) } - TitledMonospaceText(it) - } + messageDetails.record?.let { message -> + AndroidView( + modifier = Modifier.padding(horizontal = 32.dp), + factory = { + ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { + bind( + message, + thread = threadDb?.getRecipientForThreadId(message.threadId)!!, + onAttachmentNeedsDownload = onAttachmentNeedsDownload, + suppressThumbnails = true + ) + + setOnTouchListener { _, event -> + if (event.actionMasked == ACTION_UP) onContentClick(event) + true + } + } + } + ) + } + Carousel(messageDetails.attachments) { onClickImage(it) } + MetadataCell(messageDetails) + Buttons( + onReply, + onResend, + onDelete, + ) + } +} + +@Composable +fun MetadataCell( + messageDetails: MessageDetails, +) { + messageDetails.apply { + if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + sent?.let { TitledText(it) } + received?.let { TitledText(it) } + error?.let { TitledErrorText(it) } + senderInfo?.let { + TitledView(stringResource(id = R.string.message_details_header__from)) { + Row { + sender?.let { Avatar(it) } + TitledMonospaceText(it) } } } } } } +} - @Composable - fun Buttons( - hasError: Boolean, - onReply: () -> Unit = {}, - onResend: () -> Unit = {}, - onDelete: () -> Unit = {}, - ) { - Cell { - Column { +@Composable +fun Buttons( + onReply: () -> Unit = {}, + onResend: (() -> Unit)? = null, + onDelete: () -> Unit = {}, +) { + Cell { + Column { + ItemButton( + stringResource(id = R.string.reply), + R.drawable.ic_message_details__reply, + onClick = onReply + ) + Divider() + onResend?.let { ItemButton( - "Reply", - R.drawable.ic_message_details__reply, - onClick = onReply + stringResource(id = R.string.resend), + R.drawable.ic_message_details__refresh, + onClick = it ) Divider() - if (hasError) { - ItemButton( - "Resend", - R.drawable.ic_message_details__refresh, - onClick = onResend - ) - Divider() - } - ItemButton( - "Delete", - R.drawable.ic_message_details__trash, - colors = destructiveButtonColors(), - onClick = onDelete - ) } - } - } - - @Composable - fun Attachments(attachments: List, onClick: (Slide) -> Unit) { - when(attachments.firstOrNull()?.slide) { - is ImageSlide -> Carousel(attachments, onClick) - } - } - - @OptIn(ExperimentalFoundationApi::class) - @Composable - fun Carousel(attachments: List, onClick: (Slide) -> Unit) { - val imageAttachments = attachments.filter { it.slide.hasImage() } - val pagerState = rememberPagerState { imageAttachments.size } - - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - Row { - CarouselPrevButton(pagerState) - Box(modifier = Modifier.weight(1f)) { - CellPager(pagerState, imageAttachments, onClick) - HorizontalPagerIndicator(pagerState) - ExpandButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(8.dp) - ) { onClick(imageAttachments[pagerState.currentPage].slide) } - } - CarouselNextButton(pagerState) - } - FileDetails(attachments, pagerState) - } - } - - @OptIn( - ExperimentalFoundationApi::class, - ExperimentalGlideComposeApi::class - ) - @Composable - private fun CellPager( - pagerState: PagerState, - imageAttachments: List, - onClick: (Slide) -> Unit - ) { - CellNoMargin { - HorizontalPager(state = pagerState) { i -> - val slide = imageAttachments[i].slide - GlideImage( - contentScale = ContentScale.Crop, - modifier = Modifier - .aspectRatio(1f) - .clickable { onClick(slide) }, - model = slide.uri, - contentDescription = slide.fileName.orNull() ?: "image" - ) - } - } - } - - @Composable - fun ExpandButton(modifier: Modifier, onClick: () -> Unit) { - Surface( - shape = CircleShape, - color = blackAlpha40, - modifier = modifier - ) { - Icon( - painter = painterResource(id = R.drawable.ic_expand), - contentDescription = "", - modifier = Modifier.clickable { onClick() } + ItemButton( + stringResource(id = R.string.delete), + R.drawable.ic_message_details__trash, + colors = destructiveButtonColors(), + onClick = onDelete ) } } +} - @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) - @Composable - fun FileDetails(attachments: List, pagerState: PagerState) { - attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let { - CellWithPaddingAndMargin { - FlowRow( - verticalArrangement = Arrangement.spacedBy(16.dp), - maxItemsInEachRow = 2 - ) { - it.forEach { TitledText(it, Modifier.weight(1f)) } - } +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun Carousel(attachments: List, onClick: (Slide) -> Unit) { + val imageAttachments = attachments.filter { it.hasImage() }.takeIf { it.isNotEmpty() } ?: return + val pagerState = rememberPagerState { imageAttachments.size } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Row { + CarouselPrevButton(pagerState) + Box(modifier = Modifier.weight(1f)) { + CellCarousel(pagerState, imageAttachments, onClick) + HorizontalPagerIndicator(pagerState) + ExpandButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(8.dp) + ) { onClick(imageAttachments[pagerState.currentPage].slide) } + } + CarouselNextButton(pagerState) + } + FileDetails(attachments, pagerState) + } +} + +@OptIn( + ExperimentalFoundationApi::class, + ExperimentalGlideComposeApi::class +) +@Composable +private fun CellCarousel( + pagerState: PagerState, + imageAttachments: List, + onClick: (Slide) -> Unit +) { + CellNoMargin { + HorizontalPager(state = pagerState) { i -> + val slide = imageAttachments[i].slide + GlideImage( + contentScale = ContentScale.Crop, + modifier = Modifier + .aspectRatio(1f) + .clickable { onClick(slide) }, + model = slide.uri, + contentDescription = slide.fileName.orNull() ?: stringResource(id = R.string.image) + ) + } + } +} + +@Composable +fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) { + Surface( + shape = CircleShape, + color = blackAlpha40, + modifier = modifier, + contentColor = Color.White, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_expand), + contentDescription = "", + modifier = Modifier.clickable { onClick() }, + ) + } +} + +@Preview +@Composable +fun PreviewFileDetails() { + Theme(R.style.Ocean_Dark) { + FileDetails( + fileDetails = listOf( + TitledText("File Id:", "Screen Shot 2023-07-06 at 11.35.50 am.png"), + TitledText("File Type:", "image/png"), + TitledText("File Size:", "195.6kB"), + TitledText("Resolution:", "342x312"), + ) + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FileDetails(attachments: List, pagerState: PagerState) { + FileDetails(attachments[pagerState.currentPage].fileDetails) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun FileDetails(fileDetails: List) { + if (fileDetails.isEmpty()) return + + CellWithPaddingAndMargin { + FlowRow(verticalArrangement = Arrangement.spacedBy(16.dp)) { + fileDetails.forEach { + TitledText( + it, + modifier = Modifier + .widthIn(min = 100.dp) // set minimum width + .width(IntrinsicSize.Max) // make the text as wide as necessary + .weight(1f) // space evenly + ) } } } +} - @Composable - fun TitledErrorText(titledText: TitledText, modifier: Modifier = Modifier) { - TitledText( - titledText, - modifier = modifier, - valueStyle = LocalTextStyle.current.copy(color = colorDestructive) - ) - } +@Composable +fun TitledErrorText(titledText: TitledText, modifier: Modifier = Modifier) { + TitledText( + titledText, + modifier = modifier, + valueStyle = LocalTextStyle.current.copy(color = colorDestructive) + ) +} - @Composable - fun TitledMonospaceText(titledText: TitledText, modifier: Modifier = Modifier) { - TitledText( - titledText, - modifier = modifier, - valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) - ) - } +@Composable +fun TitledMonospaceText(titledText: TitledText, modifier: Modifier = Modifier) { + TitledText( + titledText, + modifier = modifier, + valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) + ) +} - @Composable - fun TitledText( - titledText: TitledText, - modifier: Modifier = Modifier, - valueStyle: TextStyle = LocalTextStyle.current - ) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { - Title(titledText.title) - Text(titledText.value, style = valueStyle) - } - } - - @Composable - fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { - Title(title) - content() - } - } - - @Composable - fun Title(text: String) { - Text(text, fontWeight = FontWeight.Bold) +@Composable +fun TitledText( + titledText: TitledText, + modifier: Modifier = Modifier, + valueStyle: TextStyle = LocalTextStyle.current +) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Title(titledText.title) + Text(titledText.value, style = valueStyle) } } + +@Composable +fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Title(title) + content() + } +} + +@Composable +fun Title(text: String, modifier: Modifier = Modifier) { + Text(text, modifier = modifier, fontWeight = FontWeight.Bold) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index cace0c6af7..d63b46d8dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -1,27 +1,60 @@ package org.thoughtcrime.securesms.ui import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color val colorDestructive = Color(0xffFF453A) -val classicDark0 = Color(0xff111111) +const val classicDark0 = 0xff111111 +const val classicDark1 = 0xff1B1B1B +const val classicDark2 = 0xff2D2D2D +const val classicDark3 = 0xff414141 +const val classicDark4 = 0xff767676 +const val classicDark5 = 0xffA1A2A1 +const val classicDark6 = 0xffFFFFFF -val classicDark1 = Color(0xff1B1B1B) -val classicDark2 = Color(0xff2D2D2D) -val classicDark3 = Color(0xff414141) -val classicDark4 = Color(0xff767676) -val classicDark5 = Color(0xffA1A2A1) -val classicDark6 = Color(0xffFFFFFF) -val classicLight0 = Color(0xff000000) -val classicLight1 = Color(0xff6D6D6D) -val classicLight2 = Color(0xffA1A2A1) -val classicLight3 = Color(0xffDFDFDF) -val classicLight4 = Color(0xffF0F0F0) -val classicLight5 = Color(0xffF9F9F9) -val classicLight6 = Color(0xffFFFFFF) + +const val classicLight0 = 0xff000000 +const val classicLight1 = 0xff6D6D6D +const val classicLight2 = 0xffA1A2A1 +const val classicLight3 = 0xffDFDFDF +const val classicLight4 = 0xffF0F0F0 +const val classicLight5 = 0xffF9F9F9 +const val classicLight6 = 0xffFFFFFF + +const val oceanDark0 = 0xff000000 +const val oceanDark1 = 0xff1A1C28 +const val oceanDark2 = 0xff252735 +const val oceanDark3 = 0xff2B2D40 +const val oceanDark4 = 0xff3D4A5D +const val oceanDark5 = 0xffA6A9CE +const val oceanDark6 = 0xff5CAACC +const val oceanDark7 = 0xffFFFFFF + +const val oceanLight0 = 0xff000000 +const val oceanLight1 = 0xff19345D +const val oceanLight2 = 0xff6A6E90 +const val oceanLight3 = 0xff5CAACC +const val oceanLight4 = 0xffB3EDF2 +const val oceanLight5 = 0xffE7F3F4 +const val oceanLight6 = 0xffECFAFB +const val oceanLight7 = 0xffFCFFFF + +val ocean_accent = Color(0xff57C9FA) + +val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7) +val oceanDarks = arrayOf(oceanDark0, oceanDark1, oceanDark2, oceanDark3, oceanDark4, oceanDark5, oceanDark6, oceanDark7) +val classicLights = arrayOf(classicLight0, classicLight1, classicLight2, classicLight3, classicLight4, classicLight5, classicLight6) +val classicDarks = arrayOf(classicDark0, classicDark1, classicDark2, classicDark3, classicDark4, classicDark5, classicDark6) + +val oceanLightColors = oceanLights.map(::Color) +val oceanDarkColors = oceanDarks.map(::Color) +val classicLightColors = classicLights.map(::Color) +val classicDarkColors = classicDarks.map(::Color) val blackAlpha40 = Color.Black.copy(alpha = 0.4f) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index b7684a22f9..8589f3e7e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,32 +1,50 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes +import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.FlowRowScope +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonColors import androidx.compose.material.Card +import androidx.compose.material.Colors import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -36,6 +54,13 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.ProfilePictureView +import kotlin.math.roundToInt + +private val Colors.cellColors: Colors + @Composable + get() = MaterialTheme.colors.copy( + surface = LocalExtraColors.current.settingsBackground, + ) @Composable fun ItemButton( @@ -80,17 +105,18 @@ fun CellWithPaddingAndMargin( margin: Dp = 32.dp, content: @Composable () -> Unit ) { - Card( - shape = RoundedCornerShape(16.dp), - elevation = 0.dp, - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .padding(horizontal = margin), - backgroundColor = LocalExtraColors.current.settingsBackground, - // probably wrong - contentColor = MaterialTheme.colors.onSurface - ) { Box(Modifier.padding(padding)) { content() } } + MaterialTheme(colors = MaterialTheme.colors.cellColors) { + Card( + shape = RoundedCornerShape(16.dp), + elevation = 0.dp, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = margin), + ) { + Box(Modifier.padding(padding)) { content() } + } + } } @OptIn(ExperimentalFoundationApi::class) @@ -108,7 +134,7 @@ fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) { pagerState = pagerState, pageCount = pagerState.pageCount, activeColor = Color.White, - inactiveColor = classicDark5) + inactiveColor = classicDarkColors[5]) } } } @@ -154,7 +180,6 @@ fun RowScope.CarouselButton( fun Divider() { androidx.compose.material.Divider( modifier = Modifier.padding(horizontal = 16.dp), - color = LocalExtraColors.current.divider ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 88916e03ee..59402b6811 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -2,25 +2,36 @@ package org.thoughtcrime.securesms.ui import android.content.Context import androidx.annotation.AttrRes +import androidx.annotation.StyleRes +import androidx.appcompat.view.ContextThemeWrapper +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import com.google.android.material.color.MaterialColors import network.loki.messenger.R +import org.thoughtcrime.securesms.conversation.v2.PreviewMessageDetails val LocalExtraColors = staticCompositionLocalOf { error("No Custom Attribute value provided") } data class ExtraColors( - val cell: Color, - val divider: Color, val settingsBackground: Color, ) -fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = +fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = try { MaterialColors.getColor(this, attr, defaultValue).let(::Color) +} catch (e: Exception) { + colorDestructive +} @Composable fun AppTheme( @@ -28,9 +39,7 @@ fun AppTheme( ) { val extraColors = LocalContext.current.run { ExtraColors( - cell = getColorFromTheme(R.attr.colorCellBackground), - divider = getColorFromTheme(R.attr.dividerHorizontal).copy(alpha = 0.15f), - settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground) + settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground), ) } @@ -40,3 +49,35 @@ fun AppTheme( } } } + +@Preview +@Composable +fun PreviewMessageDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + Theme(themeResId) { + Box(modifier = Modifier.background(color = MaterialTheme.colors.background)) { + PreviewMessageDetails() + } + } +} + +@Composable +fun Theme(@StyleRes themeResId: Int, content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalContext provides ContextThemeWrapper(LocalContext.current, themeResId) + ) { + AppTheme { + content() + } + } +} + +class ThemeResPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + R.style.Classic_Dark, + R.style.Classic_Light, + R.style.Ocean_Dark, + R.style.Ocean_Light, + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d7948c4f2d..1c718932fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,9 +4,12 @@ Yes No Delete + Resend + Reply Ban Please wait… Save + Image Note to Self Version %s @@ -516,6 +519,7 @@ To: From: With: + Create passphrase Select contacts diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bc85638501..ad03b3e38c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -490,7 +490,6 @@ ?android:textColorPrimary @color/ocean_dark_5 ?colorPrimary - @color/default_background_start @color/navigation_bar ?colorPrimary ?colorPrimaryDark @@ -575,7 +574,6 @@ @color/ocean_light_6 @color/ocean_light_navigation_bar ?colorPrimary - @color/default_background_start @color/ocean_light_7 @color/ocean_light_6 @color/ocean_light_5 From b26c98af6894e0010f0bd42d59aab1b7b656f3e3 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jul 2023 00:35:13 +0930 Subject: [PATCH 060/112] Hide Resend if no error --- .../securesms/conversation/v2/MessageDetailActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 46eeb4ecc0..95c1a874a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -130,7 +130,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { threadDb = threadDb, messageDetails = details, onReply = { setResultAndFinish(ON_REPLY) }, - onResend = { setResultAndFinish(ON_RESEND) }, + onResend = details.error?.let { { setResultAndFinish(ON_RESEND) } }, onDelete = { setResultAndFinish(ON_DELETE) }, onClickImage = { slide -> // only open to downloaded images From 821327569e8069e46b27ed1d92d1391bd234a637 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jul 2023 01:05:09 +0930 Subject: [PATCH 061/112] Cleanup Themes --- .../conversation/v2/MessageDetailActivity.kt | 20 +++++++++-- .../org/thoughtcrime/securesms/ui/Themes.kt | 35 +++++++++---------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 95c1a874a6..ba94353b4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.lifecycleScope @@ -73,7 +74,8 @@ import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.ItemButton -import org.thoughtcrime.securesms.ui.Theme +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors @@ -352,8 +354,10 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) { @Preview @Composable -fun PreviewFileDetails() { - Theme(R.style.Ocean_Dark) { +fun PreviewFileDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { FileDetails( fileDetails = listOf( TitledText("File Id:", "Screen Shot 2023-07-06 at 11.35.50 am.png"), @@ -365,6 +369,16 @@ fun PreviewFileDetails() { } } +@Preview +@Composable +fun PreviewMessageDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + PreviewMessageDetails() + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun FileDetails(attachments: List, pagerState: PagerState) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 59402b6811..2216c404e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -23,16 +23,14 @@ import org.thoughtcrime.securesms.conversation.v2.PreviewMessageDetails val LocalExtraColors = staticCompositionLocalOf { error("No Custom Attribute value provided") } + data class ExtraColors( val settingsBackground: Color, ) -fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = try { - MaterialColors.getColor(this, attr, defaultValue).let(::Color) -} catch (e: Exception) { - colorDestructive -} - +/** + * Converts current Theme to Compose Theme. + */ @Composable fun AppTheme( content: @Composable () -> Unit @@ -50,25 +48,24 @@ fun AppTheme( } } -@Preview -@Composable -fun PreviewMessageDetails( - @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int -) { - Theme(themeResId) { - Box(modifier = Modifier.background(color = MaterialTheme.colors.background)) { - PreviewMessageDetails() - } - } -} +fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = + MaterialColors.getColor(this, attr, defaultValue).let(::Color) +/** + * Set the theme and a background for Compose Previews. + */ @Composable -fun Theme(@StyleRes themeResId: Int, content: @Composable () -> Unit) { +fun PreviewTheme( + themeResId: Int, + content: @Composable () -> Unit +) { CompositionLocalProvider( LocalContext provides ContextThemeWrapper(LocalContext.current, themeResId) ) { AppTheme { - content() + Box(modifier = Modifier.background(color = MaterialTheme.colors.background)) { + content() + } } } } From a1e8ad2c37cf5f60c42440b377facd7dbe200b00 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jul 2023 13:01:46 +0930 Subject: [PATCH 062/112] Cleanup ViewModel --- .../conversation/v2/MessageDetailActivity.kt | 112 ++++++++---------- .../v2/MessageDetailsViewModel.kt | 50 +++++--- .../org/thoughtcrime/securesms/ui/Colors.kt | 2 - .../thoughtcrime/securesms/ui/Components.kt | 31 +++-- app/src/main/res/values/strings.xml | 1 + 5 files changed, 101 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index ba94353b4e..33690da4a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -10,13 +10,16 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager @@ -61,9 +64,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.Avatar import org.thoughtcrime.securesms.ui.CarouselNextButton @@ -103,21 +103,15 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) - timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) - - val messageRecord = - DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run { - finish() - return - } - - val error = DatabaseComponent.get(this).lokiMessageDatabase() - .getErrorMessage(messageRecord.getId()) - - viewModel.setMessageRecord(messageRecord, error) - title = resources.getString(R.string.conversation_context__menu_message_details) + intent.getLongExtra(MESSAGE_TIMESTAMP, -1L).let(viewModel::setMessageTimestamp) + + if (viewModel.details.value == null) { + finish() + return + } + ComposeView(this) .apply { setContent { MessageDetailsScreen() } } .let(::setContentView) @@ -125,28 +119,27 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { @Composable private fun MessageDetailsScreen() { - val details by viewModel.details.observeAsState(MessageDetails()) - val threadDb = DatabaseComponent.get(this@MessageDetailActivity).threadDatabase() + val state by viewModel.details.observeAsState(MessageDetailsState()) AppTheme { MessageDetails( - threadDb = threadDb, - messageDetails = details, + state = state, onReply = { setResultAndFinish(ON_REPLY) }, - onResend = details.error?.let { { setResultAndFinish(ON_RESEND) } }, + onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } }, onDelete = { setResultAndFinish(ON_DELETE) }, - onClickImage = { slide -> + onClickImage = { i -> + val slide = state.attachments[i].slide // only open to downloaded images if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { // Restart download here (on IO thread) (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - onAttachmentNeedsDownload(attachment.attachmentId.rowId, details.mmsRecord!!.getId()) + onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord!!.getId()) } } if (!slide.isInProgress) MediaPreviewActivity.getPreviewIntent( this, slide, - details.mmsRecord, - threadDb.getRecipientForThreadId(details.mmsRecord!!.threadId), + state.mmsRecord, + state.thread, ).let(::startActivity) }, onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, @@ -174,7 +167,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { fun PreviewMessageDetails() { AppTheme { MessageDetails( - messageDetails = MessageDetails( + state = MessageDetailsState( attachments = listOf(), sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), @@ -188,12 +181,11 @@ fun PreviewMessageDetails() { @SuppressLint("ClickableViewAccessibility") @Composable fun MessageDetails( - threadDb: ThreadDatabase? = null, - messageDetails: MessageDetails, + state: MessageDetailsState, onReply: () -> Unit = {}, onResend: (() -> Unit)? = null, onDelete: () -> Unit = {}, - onClickImage: (Slide) -> Unit = {}, + onClickImage: (Int) -> Unit = {}, onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> } ) { Column( @@ -202,14 +194,14 @@ fun MessageDetails( .padding(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - messageDetails.record?.let { message -> + state.record?.let { message -> AndroidView( modifier = Modifier.padding(horizontal = 32.dp), factory = { ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { bind( message, - thread = threadDb?.getRecipientForThreadId(message.threadId)!!, + thread = state.thread!!, onAttachmentNeedsDownload = onAttachmentNeedsDownload, suppressThumbnails = true ) @@ -222,8 +214,9 @@ fun MessageDetails( } ) } - Carousel(messageDetails.attachments) { onClickImage(it) } - MetadataCell(messageDetails) + Carousel(state.imageAttachments) { onClickImage(it) } + state.nonImageAttachment?.fileDetails?.let { FileDetails(it) } + MetadataCell(state) Buttons( onReply, onResend, @@ -234,9 +227,9 @@ fun MessageDetails( @Composable fun MetadataCell( - messageDetails: MessageDetails, + state: MessageDetailsState, ) { - messageDetails.apply { + state.apply { if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { sent?.let { TitledText(it) } @@ -289,25 +282,26 @@ fun Buttons( @OptIn(ExperimentalFoundationApi::class) @Composable -fun Carousel(attachments: List, onClick: (Slide) -> Unit) { - val imageAttachments = attachments.filter { it.hasImage() }.takeIf { it.isNotEmpty() } ?: return - val pagerState = rememberPagerState { imageAttachments.size } +fun Carousel(attachments: List, onClick: (Int) -> Unit) { + if (attachments.isEmpty()) return + + val pagerState = rememberPagerState { attachments.size } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Row { CarouselPrevButton(pagerState) Box(modifier = Modifier.weight(1f)) { - CellCarousel(pagerState, imageAttachments, onClick) + CellCarousel(pagerState, attachments, onClick) HorizontalPagerIndicator(pagerState) ExpandButton( modifier = Modifier .align(Alignment.BottomEnd) .padding(8.dp) - ) { onClick(imageAttachments[pagerState.currentPage].slide) } + ) { onClick(pagerState.currentPage) } } CarouselNextButton(pagerState) } - FileDetails(attachments, pagerState) + attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) } } } @@ -318,19 +312,18 @@ fun Carousel(attachments: List, onClick: (Slide) -> Unit) { @Composable private fun CellCarousel( pagerState: PagerState, - imageAttachments: List, - onClick: (Slide) -> Unit + attachments: List, + onClick: (Int) -> Unit ) { CellNoMargin { HorizontalPager(state = pagerState) { i -> - val slide = imageAttachments[i].slide GlideImage( contentScale = ContentScale.Crop, modifier = Modifier .aspectRatio(1f) - .clickable { onClick(slide) }, - model = slide.uri, - contentDescription = slide.fileName.orNull() ?: stringResource(id = R.string.image) + .clickable { onClick(i) }, + model = attachments[i].uri, + contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image) ) } } @@ -346,7 +339,7 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) { ) { Icon( painter = painterResource(id = R.drawable.ic_expand), - contentDescription = "", + contentDescription = stringResource(id = R.string.expand), modifier = Modifier.clickable { onClick() }, ) } @@ -379,12 +372,6 @@ fun PreviewMessageDetails( } } -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun FileDetails(attachments: List, pagerState: PagerState) { - FileDetails(attachments[pagerState.currentPage].fileDetails) -} - @OptIn(ExperimentalLayoutApi::class) @Composable fun FileDetails(fileDetails: List) { @@ -393,13 +380,14 @@ fun FileDetails(fileDetails: List) { CellWithPaddingAndMargin { FlowRow(verticalArrangement = Arrangement.spacedBy(16.dp)) { fileDetails.forEach { - TitledText( - it, - modifier = Modifier - .widthIn(min = 100.dp) // set minimum width - .width(IntrinsicSize.Max) // make the text as wide as necessary - .weight(1f) // space evenly - ) + BoxWithConstraints { + TitledText( + it, + modifier = Modifier + .widthIn(min = maxWidth.div(2)) + .width(IntrinsicSize.Max) + ) + } } } } @@ -431,7 +419,7 @@ fun TitledText( ) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(titledText.title) - Text(titledText.value, style = valueStyle) + Text(titledText.value, style = valueStyle, modifier = Modifier.fillMaxWidth()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 5ff1e05176..6bf4223b4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -3,11 +3,19 @@ package org.thoughtcrime.securesms.conversation.v2 import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.ImageSlide @@ -18,7 +26,7 @@ import javax.inject.Inject data class TitledText(val title: String, val value: String) -data class MessageDetails( +data class MessageDetailsState( val attachments: List = emptyList(), val record: MessageRecord? = null, val mmsRecord: MmsMessageRecord? = null, @@ -26,44 +34,57 @@ data class MessageDetails( val received: TitledText? = null, val error: TitledText? = null, val senderInfo: TitledText? = null, - val sender: Recipient? = null -) + val sender: Recipient? = null, + val thread: Recipient? = null, +) { + val imageAttachments = attachments.filter { it.hasImage() } + val nonImageAttachment: Attachment? = attachments.firstOrNull { !it.hasImage() } +} data class Attachment( val slide: Slide, val fileDetails: List ) { + val fileName: String? get() = slide.fileName.orNull() + val uri get() = slide.uri + fun hasImage() = slide is ImageSlide } @HiltViewModel class MessageDetailsViewModel @Inject constructor( - private val attachmentDb: AttachmentDatabase + private val attachmentDb: AttachmentDatabase, + private val lokiMessageDatabase: LokiMessageDatabase, + private val mmsSmsDatabase: MmsSmsDatabase, + private val threadDb: ThreadDatabase, ) : ViewModel() { - fun setMessageRecord(record: MessageRecord?, error: String?) { + fun setMessageTimestamp(timestamp: Long) { + mmsSmsDatabase.getMessageForTimestamp(timestamp).let(::setMessageRecord) + } + + fun setMessageRecord(record: MessageRecord?) { val mmsRecord = record as? MmsMessageRecord val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() _details.value = record?.run { - MessageDetails( + MessageDetailsState( + attachments = slides.map { Attachment(it, it.details) }, record = record, mmsRecord = mmsRecord, - attachments = slides.map { Attachment(it, it.details) }, sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, - error = error?.let { TitledText("Error:", it) }, - senderInfo = individualRecipient.run { - name?.let { TitledText(it, address.serialize()) } - }, - sender = individualRecipient + error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText("Error:", it) }, + senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, + sender = individualRecipient, + thread = threadDb.getRecipientForThreadId(threadId)!!, ) } } - private var _details = MutableLiveData(MessageDetails()) - val details: LiveData = _details + private var _details = MutableLiveData(MessageDetailsState()) + val details: LiveData = _details private val Slide.details: List get() = listOfNotNull( @@ -88,4 +109,5 @@ class MessageDetailsViewModel @Inject constructor( TimeUnit.MILLISECONDS.toSeconds(it) % 60 ) } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index d63b46d8dd..55bc1be62e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -16,8 +16,6 @@ const val classicDark4 = 0xff767676 const val classicDark5 = 0xffA1A2A1 const val classicDark6 = 0xffFFFFFF - - const val classicLight0 = 0xff000000 const val classicLight1 = 0xff6D6D6D const val classicLight2 = 0xffA1A2A1 diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 8589f3e7e0..0b712467fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -56,12 +56,6 @@ import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.ProfilePictureView import kotlin.math.roundToInt -private val Colors.cellColors: Colors - @Composable - get() = MaterialTheme.colors.copy( - surface = LocalExtraColors.current.settingsBackground, - ) - @Composable fun ItemButton( text: String, @@ -105,20 +99,23 @@ fun CellWithPaddingAndMargin( margin: Dp = 32.dp, content: @Composable () -> Unit ) { - MaterialTheme(colors = MaterialTheme.colors.cellColors) { - Card( - shape = RoundedCornerShape(16.dp), - elevation = 0.dp, - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .padding(horizontal = margin), - ) { - Box(Modifier.padding(padding)) { content() } - } + Card( + backgroundColor = MaterialTheme.colors.cellColor, + shape = RoundedCornerShape(16.dp), + elevation = 0.dp, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(horizontal = margin), + ) { + Box(Modifier.padding(padding)) { content() } } } +private val Colors.cellColor: Color + @Composable + get() = LocalExtraColors.current.settingsBackground + @OptIn(ExperimentalFoundationApi::class) @Composable fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c718932fd..ff5303e6c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Image Note to Self Version %s + Expand Create session ID From 09b321530d8ed0b7954b87d81b305d7ed4fd0a03 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jul 2023 16:09:38 +0930 Subject: [PATCH 063/112] Add strings --- .../conversation/v2/MessageDetailActivity.kt | 100 +++++++----------- .../v2/MessageDetailsViewModel.kt | 94 ++++++++-------- .../thoughtcrime/securesms/ui/Components.kt | 5 +- .../org/thoughtcrime/securesms/ui/Data.kt | 34 ++++++ .../org/thoughtcrime/securesms/ui/Themes.kt | 4 - app/src/main/res/values/strings.xml | 5 + 6 files changed, 128 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 33690da4a6..33ef148245 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager @@ -72,10 +71,12 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CellNoMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.Divider +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.TitledText import org.thoughtcrime.securesms.ui.blackAlpha40 import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.destructiveButtonColors @@ -160,22 +161,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) } } - -} - -@Composable -fun PreviewMessageDetails() { - AppTheme { - MessageDetails( - state = MessageDetailsState( - attachments = listOf(), - sent = TitledText("Sent:", "6:12 AM Tue, 09/08/2022"), - received = TitledText("Received:", "6:12 AM Tue, 09/08/2022"), - error = TitledText("Error:", "Message failed to send"), - senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"), - ) - ) - } } @SuppressLint("ClickableViewAccessibility") @@ -215,9 +200,9 @@ fun MessageDetails( ) } Carousel(state.imageAttachments) { onClickImage(it) } - state.nonImageAttachment?.fileDetails?.let { FileDetails(it) } - MetadataCell(state) - Buttons( + state.nonImageAttachmentFileDetails?.let { FileDetails(it) } + CellMetadata(state) + CellButtons( onReply, onResend, onDelete, @@ -226,17 +211,18 @@ fun MessageDetails( } @Composable -fun MetadataCell( +fun CellMetadata( state: MessageDetailsState, ) { state.apply { - if (sent != null || received != null || senderInfo != null) CellWithPaddingAndMargin { + if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return + CellWithPaddingAndMargin { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - sent?.let { TitledText(it) } - received?.let { TitledText(it) } - error?.let { TitledErrorText(it) } + TitledText(sent) + TitledText(received) + TitledErrorText(error) senderInfo?.let { - TitledView(stringResource(id = R.string.message_details_header__from)) { + TitledView(state.fromTitle) { Row { sender?.let { Avatar(it) } TitledMonospaceText(it) @@ -249,7 +235,7 @@ fun MetadataCell( } @Composable -fun Buttons( +fun CellButtons( onReply: () -> Unit = {}, onResend: (() -> Unit)? = null, onDelete: () -> Unit = {}, @@ -257,21 +243,21 @@ fun Buttons( Cell { Column { ItemButton( - stringResource(id = R.string.reply), + stringResource(R.string.reply), R.drawable.ic_message_details__reply, onClick = onReply ) Divider() onResend?.let { ItemButton( - stringResource(id = R.string.resend), + stringResource(R.string.resend), R.drawable.ic_message_details__refresh, onClick = it ) Divider() } ItemButton( - stringResource(id = R.string.delete), + stringResource(R.string.delete), R.drawable.ic_message_details__trash, colors = destructiveButtonColors(), onClick = onDelete @@ -345,22 +331,6 @@ fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) { } } -@Preview -@Composable -fun PreviewFileDetails( - @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int -) { - PreviewTheme(themeResId) { - FileDetails( - fileDetails = listOf( - TitledText("File Id:", "Screen Shot 2023-07-06 at 11.35.50 am.png"), - TitledText("File Type:", "image/png"), - TitledText("File Size:", "195.6kB"), - TitledText("Resolution:", "342x312"), - ) - ) - } -} @Preview @Composable @@ -368,7 +338,20 @@ fun PreviewMessageDetails( @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int ) { PreviewTheme(themeResId) { - PreviewMessageDetails() + MessageDetails( + state = MessageDetailsState( + nonImageAttachmentFileDetails = listOf( + TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"), + TitledText(R.string.message_details_header__file_type, "image/png"), + TitledText(R.string.message_details_header__file_size, "195.6kB"), + TitledText(R.string.message_details_header__resolution, "342x312"), + ), + sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"), + received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"), + error = TitledText(R.string.message_details_header__error, "Message failed to send"), + senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"), + ) + ) } } @@ -394,37 +377,36 @@ fun FileDetails(fileDetails: List) { } @Composable -fun TitledErrorText(titledText: TitledText, modifier: Modifier = Modifier) { +fun TitledErrorText(titledText: TitledText?) { TitledText( titledText, - modifier = modifier, valueStyle = LocalTextStyle.current.copy(color = colorDestructive) ) } @Composable -fun TitledMonospaceText(titledText: TitledText, modifier: Modifier = Modifier) { +fun TitledMonospaceText(titledText: TitledText?) { TitledText( titledText, - modifier = modifier, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) ) } @Composable fun TitledText( - titledText: TitledText, + titledText: TitledText?, modifier: Modifier = Modifier, - valueStyle: TextStyle = LocalTextStyle.current + valueStyle: TextStyle = LocalTextStyle.current, ) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { - Title(titledText.title) - Text(titledText.value, style = valueStyle, modifier = Modifier.fillMaxWidth()) + titledText?.apply { + TitledView(title, modifier) { + Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth()) + } } } @Composable -fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composable () -> Unit) { +fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Title(title) content() @@ -432,6 +414,6 @@ fun TitledView(title: String, modifier: Modifier = Modifier, content: @Composabl } @Composable -fun Title(text: String, modifier: Modifier = Modifier) { - Text(text, modifier = modifier, fontWeight = FontWeight.Bold) +fun Title(title: GetString) { + Text(title.string(), fontWeight = FontWeight.Bold) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 6bf4223b4b..890c82c0a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -3,12 +3,8 @@ package org.thoughtcrime.securesms.conversation.v2 import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.session.libsession.messaging.jobs.AttachmentDownloadJob -import org.session.libsession.messaging.jobs.JobQueue +import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient @@ -20,37 +16,12 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide -import java.util.* +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.TitledText +import java.util.Date import java.util.concurrent.TimeUnit import javax.inject.Inject -data class TitledText(val title: String, val value: String) - -data class MessageDetailsState( - val attachments: List = emptyList(), - val record: MessageRecord? = null, - val mmsRecord: MmsMessageRecord? = null, - val sent: TitledText? = null, - val received: TitledText? = null, - val error: TitledText? = null, - val senderInfo: TitledText? = null, - val sender: Recipient? = null, - val thread: Recipient? = null, -) { - val imageAttachments = attachments.filter { it.hasImage() } - val nonImageAttachment: Attachment? = attachments.firstOrNull { !it.hasImage() } -} - -data class Attachment( - val slide: Slide, - val fileDetails: List -) { - val fileName: String? get() = slide.fileName.orNull() - val uri get() = slide.uri - - fun hasImage() = slide is ImageSlide -} - @HiltViewModel class MessageDetailsViewModel @Inject constructor( private val attachmentDb: AttachmentDatabase, @@ -60,22 +31,18 @@ class MessageDetailsViewModel @Inject constructor( ) : ViewModel() { fun setMessageTimestamp(timestamp: Long) { - mmsSmsDatabase.getMessageForTimestamp(timestamp).let(::setMessageRecord) - } - - fun setMessageRecord(record: MessageRecord?) { + val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return val mmsRecord = record as? MmsMessageRecord - val slides: List = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() + _details.value = record.run { + val slides = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() - _details.value = record?.run { MessageDetailsState( attachments = slides.map { Attachment(it, it.details) }, record = record, - mmsRecord = mmsRecord, - sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, - received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, - error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText("Error:", it) }, + sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) }, + received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) }, + error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) }, senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, sender = individualRecipient, thread = threadDb.getRecipientForThreadId(threadId)!!, @@ -88,13 +55,14 @@ class MessageDetailsViewModel @Inject constructor( private val Slide.details: List get() = listOfNotNull( - fileName.orNull()?.let { TitledText("File Id:", it) }, - TitledText("File Type:", asAttachment().contentType), - TitledText("File Size:", Util.getPrettyFileSize(fileSize)), - takeIf { it.hasImage() } - .run { asAttachment().run { "${width}x$height" } } - .let { TitledText("Resolution:", it) }, - attachmentDb.duration(this)?.let { TitledText("Duration:", it) }, + fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) }, + TitledText(R.string.message_details_header__file_type, asAttachment().contentType), + TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)), + takeIf { it is ImageSlide } + ?.let(Slide::asAttachment) + ?.run { "${width}x$height" } + ?.let { TitledText(R.string.message_details_header__resolution, it) }, + attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) }, ) private fun AttachmentDatabase.duration(slide: Slide): String? = @@ -111,3 +79,29 @@ class MessageDetailsViewModel @Inject constructor( } } + +data class MessageDetailsState( + val attachments: List = emptyList(), + val imageAttachments: List = attachments.filter { it.hasImage() }, + val nonImageAttachmentFileDetails: List? = attachments.firstOrNull { !it.hasImage() }?.fileDetails, + val record: MessageRecord? = null, + val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord, + val sent: TitledText? = null, + val received: TitledText? = null, + val error: TitledText? = null, + val senderInfo: TitledText? = null, + val sender: Recipient? = null, + val thread: Recipient? = null, +) { + val fromTitle = GetString(R.string.message_details_header__from) +} + +data class Attachment( + val slide: Slide, + val fileDetails: List +) { + val fileName: String? get() = slide.fileName.orNull() + val uri get() = slide.uri + + fun hasImage() = slide is ImageSlide +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 0b712467fb..883e3a2098 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.FlingBehavior @@ -46,6 +47,7 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -61,6 +63,7 @@ fun ItemButton( text: String, @DrawableRes icon: Int, colors: ButtonColors = transparentButtonColors(), + contentDescription: String = text, onClick: () -> Unit ) { TextButton( @@ -76,7 +79,7 @@ fun ItemButton( .fillMaxHeight()) { Icon( painter = painterResource(id = icon), - contentDescription = "", + contentDescription = contentDescription, modifier = Modifier.align(Alignment.Center) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt new file mode 100644 index 0000000000..44ff4a42d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.ui + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource + +/** + * Compatibility class to allow ViewModels to use strings and string resources interchangeably. + */ +sealed class GetString { + @Composable + abstract fun string(): String + data class FromString(val string: String): GetString() { + @Composable + override fun string(): String = string + } + data class FromResId(@StringRes val resId: Int): GetString() { + @Composable + override fun string(): String = stringResource(resId) + + } +} + +fun GetString(@StringRes resId: Int) = GetString.FromResId(resId) +fun GetString(string: String) = GetString.FromString(string) + + +/** + * Represents some text with an associated title. + */ +data class TitledText(val title: GetString, val text: String) { + constructor(title: String, text: String): this(GetString(title), text) + constructor(@StringRes title: Int, text: String): this(GetString(title), text) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 2216c404e2..64bbd21d8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.ui import android.content.Context import androidx.annotation.AttrRes -import androidx.annotation.StyleRes import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -13,13 +12,10 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import com.google.android.material.color.MaterialColors import network.loki.messenger.R -import org.thoughtcrime.securesms.conversation.v2.PreviewMessageDetails val LocalExtraColors = staticCompositionLocalOf { error("No Custom Attribute value provided") } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff5303e6c3..5afcb40b7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -520,6 +520,11 @@ To: From: With: + File Id: + File Type: + File Size: + Resolution: + Duration: Create passphrase From fb68aaede63f6b43ed11770b7007a9834b7a3139 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 11 Jul 2023 01:29:44 +0930 Subject: [PATCH 064/112] Cleanup Components imports --- .../thoughtcrime/securesms/ui/Components.kt | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 883e3a2098..1724bde8a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,32 +1,17 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.FlingBehavior -import androidx.compose.foundation.gestures.ScrollableDefaults -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.FlowRowScope -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyGridScope -import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonColors @@ -38,16 +23,12 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -56,7 +37,6 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.ProfilePictureView -import kotlin.math.roundToInt @Composable fun ItemButton( From 172f85ae4f5264bbccf6510c2266dd016fec818b Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 11 Jul 2023 01:30:03 +0930 Subject: [PATCH 065/112] Fix some edge cases in fileDetails --- .../securesms/conversation/v2/MessageDetailActivity.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 33ef148245..5397c30736 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -360,14 +361,18 @@ fun PreviewMessageDetails( fun FileDetails(fileDetails: List) { if (fileDetails.isEmpty()) return - CellWithPaddingAndMargin { - FlowRow(verticalArrangement = Arrangement.spacedBy(16.dp)) { + CellWithPaddingAndMargin(padding = 0.dp) { + FlowRow( + modifier = Modifier.padding(vertical = 24.dp, horizontal = 12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { fileDetails.forEach { BoxWithConstraints { TitledText( it, modifier = Modifier .widthIn(min = maxWidth.div(2)) + .padding(horizontal = 12.dp) .width(IntrinsicSize.Max) ) } From 1845b60dace0f389bdfef5704ce06672847eb9a5 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 11 Jul 2023 12:28:04 +0930 Subject: [PATCH 066/112] Refactor slide out of MessageDetailsActivity --- .../securesms/MediaPreviewActivity.java | 4 ++ .../securesms/MediaPreviewArgs.kt | 11 ++++ .../conversation/v2/MessageDetailActivity.kt | 42 +++--------- .../v2/MessageDetailsViewModel.kt | 66 ++++++++++++++----- 4 files changed, 74 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index a1648e0763..e9ee120e26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -146,6 +146,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } }; + public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) { + return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread()); + } + public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) { Intent previewIntent = null; if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt new file mode 100644 index 0000000000..00e2c3d6d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewArgs.kt @@ -0,0 +1,11 @@ +package org.thoughtcrime.securesms + +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.mms.Slide + +data class MediaPreviewArgs( + val slide: Slide, + val mmsRecord: MmsMessageRecord?, + val thread: Recipient?, +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 5397c30736..8f23101953 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -49,18 +48,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.lifecycleScope +import androidx.core.content.ContextCompat import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding -import org.session.libsession.messaging.jobs.AttachmentDownloadJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage @@ -109,7 +102,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { intent.getLongExtra(MESSAGE_TIMESTAMP, -1L).let(viewModel::setMessageTimestamp) - if (viewModel.details.value == null) { + if (viewModel.state.value == null) { finish() return } @@ -117,34 +110,23 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { ComposeView(this) .apply { setContent { MessageDetailsScreen() } } .let(::setContentView) + + viewModel.event.observe(this) { + startActivity(MediaPreviewActivity.getPreviewIntent(this, it)) + } } @Composable private fun MessageDetailsScreen() { - val state by viewModel.details.observeAsState(MessageDetailsState()) + val state by viewModel.state.observeAsState(MessageDetailsState()) AppTheme { MessageDetails( state = state, onReply = { setResultAndFinish(ON_REPLY) }, onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } }, onDelete = { setResultAndFinish(ON_DELETE) }, - onClickImage = { i -> - val slide = state.attachments[i].slide - // only open to downloaded images - if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { - // Restart download here (on IO thread) - (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord!!.getId()) - } - } - if (!slide.isInProgress) MediaPreviewActivity.getPreviewIntent( - this, - slide, - state.mmsRecord, - state.thread, - ).let(::startActivity) - }, - onAttachmentNeedsDownload = ::onAttachmentNeedsDownload, + onClickImage = { viewModel.onClickImage(it) }, + onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload, ) } } @@ -156,12 +138,6 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { finish() } - - private fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) { - lifecycleScope.launch(Dispatchers.IO) { - JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) - } - } } @SuppressLint("ClickableViewAccessibility") diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 890c82c0a9..ad4bf24d43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -1,13 +1,21 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import network.loki.messenger.R +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.MediaPreviewArgs import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase @@ -30,15 +38,21 @@ class MessageDetailsViewModel @Inject constructor( private val threadDb: ThreadDatabase, ) : ViewModel() { + private var _state = MutableLiveData(MessageDetailsState()) + val state: LiveData = _state + + private var _event = MutableLiveData() + val event: LiveData = _event + fun setMessageTimestamp(timestamp: Long) { val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return val mmsRecord = record as? MmsMessageRecord - _details.value = record.run { - val slides = mmsRecord?.slideDeck?.thumbnailSlides?.toList() ?: emptyList() + _state.value = record.run { + val slides = mmsRecord?.slideDeck?.slides ?: emptyList() MessageDetailsState( - attachments = slides.map { Attachment(it, it.details) }, + attachments = slides.map(::Attachment), record = record, sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) }, received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) }, @@ -50,9 +64,6 @@ class MessageDetailsViewModel @Inject constructor( } } - private var _details = MutableLiveData(MessageDetailsState()) - val details: LiveData = _details - private val Slide.details: List get() = listOfNotNull( fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) }, @@ -78,12 +89,38 @@ class MessageDetailsViewModel @Inject constructor( ) } + fun Attachment(slide: Slide): Attachment = + Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide) + + fun onClickImage(index: Int) { + val state = state.value ?: return + val mmsRecord = state.mmsRecord ?: return + val slide = mmsRecord.slideDeck.slides[index] ?: return + // only open to downloaded images + if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { + // Restart download here (on IO thread) + (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> + onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord.getId()) + } + } + + if (slide.isInProgress) return + + _event.value = MediaPreviewArgs(slide, state.mmsRecord, state.thread) + } + + + fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) { + viewModelScope.launch(Dispatchers.IO) { + JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) + } + } } data class MessageDetailsState( val attachments: List = emptyList(), - val imageAttachments: List = attachments.filter { it.hasImage() }, - val nonImageAttachmentFileDetails: List? = attachments.firstOrNull { !it.hasImage() }?.fileDetails, + val imageAttachments: List = attachments.filter { it.hasImage }, + val nonImageAttachmentFileDetails: List? = attachments.firstOrNull { !it.hasImage }?.fileDetails, val record: MessageRecord? = null, val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord, val sent: TitledText? = null, @@ -97,11 +134,8 @@ data class MessageDetailsState( } data class Attachment( - val slide: Slide, - val fileDetails: List -) { - val fileName: String? get() = slide.fileName.orNull() - val uri get() = slide.uri - - fun hasImage() = slide is ImageSlide -} + val fileDetails: List, + val fileName: String?, + val uri: Uri?, + val hasImage: Boolean +) From d83532b6af85c28523bcd8592b667d35b8c8fbee Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 11 Jul 2023 13:28:44 +0930 Subject: [PATCH 067/112] Use Channel and Flow to avoid duplicate events --- .../conversation/v2/MessageDetailActivity.kt | 25 +++++++------ .../v2/MessageDetailsViewModel.kt | 36 +++++++++++++------ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 8f23101953..0ce83f9afa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -32,8 +32,8 @@ import androidx.compose.material.LocalTextStyle import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -48,13 +48,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding -import org.thoughtcrime.securesms.MediaPreviewActivity +import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.ui.AppTheme @@ -102,23 +103,25 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { intent.getLongExtra(MESSAGE_TIMESTAMP, -1L).let(viewModel::setMessageTimestamp) - if (viewModel.state.value == null) { - finish() - return - } - ComposeView(this) .apply { setContent { MessageDetailsScreen() } } .let(::setContentView) - viewModel.event.observe(this) { - startActivity(MediaPreviewActivity.getPreviewIntent(this, it)) + lifecycleScope.launch { + viewModel.eventFlow.collect { + when (it) { + Event.Finish -> finish() + is Event.StartMediaPreview -> startActivity( + getPreviewIntent(this@MessageDetailActivity, it.args) + ) + } + } } } @Composable private fun MessageDetailsScreen() { - val state by viewModel.state.observeAsState(MessageDetailsState()) + val state by viewModel.stateFlow.collectAsState() AppTheme { MessageDetails( state = state, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index ad4bf24d43..7f97645b69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -1,12 +1,14 @@ package org.thoughtcrime.securesms.conversation.v2 import android.net.Uri -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.messaging.jobs.AttachmentDownloadJob @@ -38,17 +40,23 @@ class MessageDetailsViewModel @Inject constructor( private val threadDb: ThreadDatabase, ) : ViewModel() { - private var _state = MutableLiveData(MessageDetailsState()) - val state: LiveData = _state + private val state = MutableStateFlow(MessageDetailsState()) + val stateFlow = state.asStateFlow() - private var _event = MutableLiveData() - val event: LiveData = _event + private val event = Channel() + val eventFlow = event.receiveAsFlow() fun setMessageTimestamp(timestamp: Long) { - val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return + val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) + + if (record == null) { + viewModelScope.launch { event.send(Event.Finish) } + return + } + val mmsRecord = record as? MmsMessageRecord - _state.value = record.run { + state.value = record.run { val slides = mmsRecord?.slideDeck?.slides ?: emptyList() MessageDetailsState( @@ -106,10 +114,13 @@ class MessageDetailsViewModel @Inject constructor( if (slide.isInProgress) return - _event.value = MediaPreviewArgs(slide, state.mmsRecord, state.thread) + viewModelScope.launch { + MediaPreviewArgs(slide, state.mmsRecord, state.thread) + .let(Event::StartMediaPreview) + .let { event.send(it) } + } } - fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) { viewModelScope.launch(Dispatchers.IO) { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) @@ -139,3 +150,8 @@ data class Attachment( val uri: Uri?, val hasImage: Boolean ) + +sealed class Event { + object Finish: Event() + data class StartMediaPreview(val args: MediaPreviewArgs): Event() +} \ No newline at end of file From 452db6dfa371ecf7f92e594e164a31664b018c18 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 18 Jul 2023 13:36:08 +0930 Subject: [PATCH 068/112] Make reply safer --- .../securesms/conversation/v2/ConversationActivityV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 5e78d9e1cc..233d43eaeb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -1982,7 +1982,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun reply(messages: Set) { val recipient = viewModel.recipient ?: return - binding?.inputBar?.draftQuote(recipient, messages.first(), glide) + messages.firstOrNull()?.let { binding?.inputBar?.draftQuote(recipient, it, glide) } endActionMode() } From fc8a92998cc5d4f19b9f9c8c9e4fb974350643f7 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 19 Jul 2023 15:04:30 +0930 Subject: [PATCH 069/112] Fix message clipping --- .../v2/messages/VisibleMessageView.kt | 16 +++++--- .../main/res/layout/view_visible_message.xml | 41 +++++++++++-------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 00c5c08a4d..90cb58cdc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -9,9 +9,11 @@ import android.graphics.drawable.ColorDrawable import android.os.Handler import android.os.Looper import android.util.AttributeSet +import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View +import android.widget.FrameLayout import android.widget.LinearLayout import androidx.annotation.ColorInt import androidx.annotation.DrawableRes @@ -114,6 +116,7 @@ class VisibleMessageView : LinearLayout { binding.root.disableClipping() binding.mainContainer.disableClipping() binding.messageInnerContainer.disableClipping() + binding.messageInnerLayout.disableClipping() binding.messageContentView.root.disableClipping() } // endregion @@ -342,11 +345,14 @@ class VisibleMessageView : LinearLayout { private fun updateExpirationTimer(message: MessageRecord) { val container = binding.messageInnerContainer - val content = binding.messageContentView.root - val expiration = binding.expirationTimerView - container.removeAllViewsInLayout() - container.addView(if (message.isOutgoing) expiration else content) - container.addView(if (message.isOutgoing) content else expiration) + val layout = binding.messageInnerLayout + + if (message.isOutgoing) binding.messageContentView.root.bringToFront() + else binding.expirationTimerView.bringToFront() + + layout.layoutParams = layout.layoutParams.let { it as FrameLayout.LayoutParams } + .apply { gravity = if (message.isOutgoing) Gravity.END else Gravity.START } + val containerParams = container.layoutParams as ConstraintLayout.LayoutParams containerParams.horizontalBias = if (message.isOutgoing) 1f else 0f container.layoutParams = containerParams diff --git a/app/src/main/res/layout/view_visible_message.xml b/app/src/main/res/layout/view_visible_message.xml index 11155ccc20..37b65c8fcb 100644 --- a/app/src/main/res/layout/view_visible_message.xml +++ b/app/src/main/res/layout/view_visible_message.xml @@ -107,35 +107,40 @@ app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/full_names" /> - - + android:orientation="horizontal"> - + - + + + + Date: Fri, 21 Jul 2023 11:50:42 +0930 Subject: [PATCH 070/112] Fix buttons --- .../conversation/v2/MessageDetailActivity.kt | 6 +-- .../v2/MessageDetailsViewModel.kt | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 0ce83f9afa..61732827f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -80,8 +80,6 @@ import javax.inject.Inject @AndroidEntryPoint class MessageDetailActivity : PassphraseRequiredActionBarActivity() { - private var timestamp: Long = 0L - @Inject lateinit var storage: Storage @@ -101,7 +99,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { title = resources.getString(R.string.conversation_context__menu_message_details) - intent.getLongExtra(MESSAGE_TIMESTAMP, -1L).let(viewModel::setMessageTimestamp) + viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) ComposeView(this) .apply { setContent { MessageDetailsScreen() } } @@ -135,7 +133,7 @@ class MessageDetailActivity : PassphraseRequiredActionBarActivity() { } private fun setResultAndFinish(code: Int) { - Bundle().apply { putLong(MESSAGE_TIMESTAMP, timestamp) } + Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) } .let(Intent()::putExtras) .let { setResult(code, it) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 7f97645b69..a73fe41139 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -46,32 +46,34 @@ class MessageDetailsViewModel @Inject constructor( private val event = Channel() val eventFlow = event.receiveAsFlow() - fun setMessageTimestamp(timestamp: Long) { - val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) + var timestamp: Long = 0L + set(value) { + field = value + val record = mmsSmsDatabase.getMessageForTimestamp(timestamp) - if (record == null) { - viewModelScope.launch { event.send(Event.Finish) } - return + if (record == null) { + viewModelScope.launch { event.send(Event.Finish) } + return + } + + val mmsRecord = record as? MmsMessageRecord + + state.value = record.run { + val slides = mmsRecord?.slideDeck?.slides ?: emptyList() + + MessageDetailsState( + attachments = slides.map(::Attachment), + record = record, + sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) }, + received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) }, + error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) }, + senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, + sender = individualRecipient, + thread = threadDb.getRecipientForThreadId(threadId)!!, + ) + } } - val mmsRecord = record as? MmsMessageRecord - - state.value = record.run { - val slides = mmsRecord?.slideDeck?.slides ?: emptyList() - - MessageDetailsState( - attachments = slides.map(::Attachment), - record = record, - sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) }, - received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) }, - error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) }, - senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, - sender = individualRecipient, - thread = threadDb.getRecipientForThreadId(threadId)!!, - ) - } - } - private val Slide.details: List get() = listOfNotNull( fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) }, From 58cda9ba4a6c4a6bc54181ddef5fa0b28c35c56f Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 25 Jul 2023 10:32:41 +0930 Subject: [PATCH 071/112] Fix website --- .../org/thoughtcrime/securesms/notifications/FcmTokenManager.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/{main/java => play/kotlin}/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt (100%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt similarity index 100% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt rename to app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt From 01e9d15872481d446fc62c13fdb9c7ecb67f67d4 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 25 Jul 2023 11:38:06 +0930 Subject: [PATCH 072/112] Add Huawei flavor --- app/build.gradle | 8 ++++++ .../notifications/HuaweiPushManager.kt | 9 +++++++ .../notifications/HuaweiPushModule.kt | 27 +++++++++++++++++++ build.gradle | 3 +++ libsession/build.gradle | 1 + 5 files changed, 48 insertions(+) create mode 100644 app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt create mode 100644 app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt diff --git a/app/build.gradle b/app/build.gradle index 9e0bd76e97..9192d48c56 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,6 +136,13 @@ android { buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" } + huawei { + dimension "distribution" + ext.websiteUpdateUrl = "null" + buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" + buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" + } + website { dimension "distribution" ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases" @@ -203,6 +210,7 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } + huaweiImplementation 'com.huawei.hms:push:6.7.0.300' implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1' implementation 'org.conscrypt:conscrypt-android:2.0.0' diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt new file mode 100644 index 0000000000..72a22ecfa4 --- /dev/null +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context + +class HuaweiPushManager(val context: Context): PushManager { + override fun refresh(force: Boolean) { + + } +} diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt new file mode 100644 index 0000000000..17c40ea826 --- /dev/null +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object HuaweiPushModule { + @Provides + @Singleton + fun provideFirebasePushManager( + @ApplicationContext context: Context, + ) = HuaweiPushManager(context) +} + +@Module +@InstallIn(SingletonComponent::class) +abstract class FirebaseBindingModule { + @Binds + abstract fun bindPushManager(firebasePushManager: HuaweiPushManager): PushManager +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 70da7183d8..8e344c5e14 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ buildscript { repositories { google() mavenCentral() + maven {url 'https://developer.huawei.com/repo/'} } dependencies { classpath "com.android.tools.build:gradle:$gradlePluginVersion" @@ -12,6 +13,7 @@ buildscript { classpath files('libs/gradle-witness.jar') classpath "com.squareup:javapoet:1.13.0" classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" + classpath 'com.huawei.agconnect:agcp:1.6.0.300' } } @@ -55,6 +57,7 @@ allprojects { } jcenter() maven { url "https://jitpack.io" } + maven { url 'https://developer.huawei.com/repo/' } } project.ext { diff --git a/libsession/build.gradle b/libsession/build.gradle index 3cfb66cd32..5dc67f525f 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.library' id 'kotlin-android' id 'kotlinx-serialization' + id 'com.huawei.agconnect' } android { From 34990b13d3e08f689d9cfeed2cd62ea83a73c9ac Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 25 Jul 2023 14:49:41 +0930 Subject: [PATCH 073/112] ... --- app/build.gradle | 3 + app/src/huawei/AndroidManifest.xml | 16 +++ .../notifications/HuaweiPushModule.kt | 6 +- .../HuaweiPushNotificationService.kt | 59 ++++++++++ .../securesms/notifications/PushHandler.kt | 107 ++++++++++++++++++ .../securesms/notifications/ExpiryManager.kt | 0 .../notifications/FcmTokenManager.kt | 0 .../notifications/FirebasePushManager.kt | 5 +- .../securesms/notifications/PushManagerV2.kt | 41 +------ .../notifications/PushNotificationService.kt | 34 +----- .../notifications/PushManagerV1.kt | 13 ++- 11 files changed, 202 insertions(+), 82 deletions(-) create mode 100644 app/src/huawei/AndroidManifest.xml create mode 100644 app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt rename app/src/{play => main}/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt (100%) rename app/src/{play => main}/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt (100%) diff --git a/app/build.gradle b/app/build.gradle index 9192d48c56..db9c42d43a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,6 +133,7 @@ android { apply plugin: 'com.google.gms.google-services' ext.websiteUpdateUrl = "null" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" + buildConfigField "String", "DEVICE", "\"android\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" } @@ -140,6 +141,7 @@ android { dimension "distribution" ext.websiteUpdateUrl = "null" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" + buildConfigField "String", "DEVICE", "\"huawei\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" } @@ -147,6 +149,7 @@ android { dimension "distribution" ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" + buildConfigField "String", "DEVICE", "\"android\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" } } diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml new file mode 100644 index 0000000000..4745c454b2 --- /dev/null +++ b/app/src/huawei/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt index 17c40ea826..e10ed77abf 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt @@ -21,7 +21,7 @@ object HuaweiPushModule { @Module @InstallIn(SingletonComponent::class) -abstract class FirebaseBindingModule { +abstract class HuaweiBindingModule { @Binds - abstract fun bindPushManager(firebasePushManager: HuaweiPushManager): PushManager -} \ No newline at end of file + abstract fun bindPushManager(pushManager: HuaweiPushManager): PushManager +} diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt new file mode 100644 index 0000000000..dd475c8344 --- /dev/null +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -0,0 +1,59 @@ +package org.thoughtcrime.securesms.notifications + +import android.os.Bundle +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.huawei.hms.push.HmsMessageService +import com.huawei.hms.push.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint +import org.session.libsession.messaging.jobs.BatchMessageReceiveJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import javax.inject.Inject + +@AndroidEntryPoint +class HuaweiPushNotificationService: HmsMessageService() { + + @Inject token + + override fun onNewToken(token: String?, bundle: Bundle?) { + Log.d("Loki", "New HCM token: $token.") + + if (!token.isNullOrEmpty()) { + val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return + PushManager.register(token, userPublicKey, this, false) + } + } + + override fun onMessageReceived(message: RemoteMessage?) { + Log.d("Loki", "Received a push notification.") + val base64EncodedData = message?.data + val data = base64EncodedData?.let { Base64.decode(it) } + if (data != null) { + try { + val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() + val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) + JobQueue.shared.add(job) + } catch (e: Exception) { + Log.d("Loki", "Failed to unwrap data for message due to error: $e.") + } + } else { + Log.d("Loki", "Failed to decode data for message.") + val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) + .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) + .setColor(this.getResources().getColor(network.loki.messenger.R.color.textsecure_primary)) + .setContentTitle("Session") + .setContentText("You've got a new message.") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + with(NotificationManagerCompat.from(this)) { + notify(11111, builder.build()) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt new file mode 100644 index 0000000000..be4832104d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.interfaces.AEAD +import com.goterl.lazysodium.utils.Key +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.session.libsession.messaging.jobs.BatchMessageReceiveJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsession.utilities.bencode.Bencode +import org.session.libsession.utilities.bencode.BencodeList +import org.session.libsession.utilities.bencode.BencodeString +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import javax.inject.Inject + +private const val TAG = "PushHandler" + +class PushHandler @Inject constructor(@ApplicationContext val context: Context) { + private val sodium = LazySodiumAndroid(SodiumAndroid()) + + fun onPush(dataMap: Map) { + val data: ByteArray? = if (dataMap.containsKey("spns")) { + // this is a v2 push notification + try { + decrypt(Base64.decode(dataMap["enc_payload"])) + } catch(e: Exception) { + Log.e(TAG, "Invalid push notification: ${e.message}") + return + } + } else { + // old v1 push notification; we still need this for receiving legacy closed group notifications + dataMap.get("ENCRYPTED_DATA")?.let(Base64::decode) + } + data?.let { onPush(data) } ?: onPush() + + } + + fun onPush() { + Log.d(TAG, "Failed to decode data for message.") + val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER) + .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) + .setColor(context.getColor(network.loki.messenger.R.color.textsecure_primary)) + .setContentTitle("Session") + .setContentText("You've got a new message.") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + NotificationManagerCompat.from(context).notify(11111, builder.build()) + } + + fun onPush(data: ByteArray) { + try { + val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() + val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) + JobQueue.shared.add(job) + } catch (e: Exception) { + Log.d(TAG, "Failed to unwrap data for message due to error: $e.") + } + } + + fun decrypt(encPayload: ByteArray): ByteArray? { + Log.d(TAG, "decrypt() called") + + val encKey = getOrCreateNotificationKey() + val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() + val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) + ?: error("Failed to decrypt push notification") + val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() + val bencoded = Bencode.Decoder(decrypted) + val expectedList = (bencoded.decode() as? BencodeList)?.values + ?: error("Failed to decode bencoded list from payload") + + val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") + val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) + + val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null + // null content is valid only if we got a "data_too_long" flag + if (content == null) + check(metadata.data_too_long) { "missing message data, but no too-long flag" } + else + check(metadata.data_len == content.size) { "wrong message data size" } + + Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") + + return content + } + + fun getOrCreateNotificationKey(): Key { + if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { + // generate the key and store it + val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) + IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) + } + return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) + } +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt similarity index 100% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt similarity index 100% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 89712c2e85..df15efe80c 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -40,6 +40,7 @@ import org.session.libsignal.utilities.emptyPromise import org.session.libsignal.utilities.retryIfNeeded import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import javax.inject.Inject private const val TAG = "FirebasePushManager" @@ -47,7 +48,7 @@ class FirebasePushManager( private val context: Context ): PushManager { - private val pushManagerV2 = PushManagerV2(context) + @Inject lateinit var pushManagerV2: PushManagerV2 companion object { const val maxRetryCount = 4 @@ -132,6 +133,4 @@ class FirebasePushManager( } success { tokenManager.fcmToken = null } - - fun decrypt(decode: ByteArray) = pushManagerV2.decrypt(decode) } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt index 66c7590e5f..4fe885758f 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt @@ -39,7 +39,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil private const val TAG = "PushManagerV2" -class PushManagerV2(private val context: Context) { +class PushManagerV2(private val pushHandler: PushHandler) { private val sodium = LazySodiumAndroid(SodiumAndroid()) fun register( @@ -48,7 +48,7 @@ class PushManagerV2(private val context: Context) { userEd25519Key: KeyPair, namespaces: List ): Promise { - val pnKey = getOrCreateNotificationKey() + val pnKey = pushHandler.getOrCreateNotificationKey() val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s // if we want to support passing namespace list, here is the place to do it @@ -117,41 +117,4 @@ class PushManagerV2(private val context: Context) { .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } } } - - private fun getOrCreateNotificationKey(): Key { - if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) { - // generate the key and store it - val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) - IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) - } - return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) - } - - fun decrypt(encPayload: ByteArray): ByteArray? { - Log.d(TAG, "decrypt() called") - - val encKey = getOrCreateNotificationKey() - val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() - val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray() - val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce) - ?: error("Failed to decrypt push notification") - val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray() - val bencoded = Bencode.Decoder(decrypted) - val expectedList = (bencoded.decode() as? BencodeList)?.values - ?: error("Failed to decode bencoded list from payload") - - val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") - val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) - - val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null - // null content is valid only if we got a "data_too_long" flag - if (content == null) - check(metadata.data_too_long) { "missing message data, but no too-long flag" } - else - check(metadata.data_len == content.size) { "wrong message data size" } - - Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") - - return content - } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index 4c6ce9f4ea..e546053fce 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -8,7 +8,6 @@ import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 @@ -21,6 +20,7 @@ private const val TAG = "PushNotificationService" class PushNotificationService : FirebaseMessagingService() { @Inject lateinit var pushManager: FirebasePushManager + @Inject lateinit var pushHandler: PushHandler override fun onNewToken(token: String) { super.onNewToken(token) @@ -32,37 +32,7 @@ class PushNotificationService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Log.d(TAG, "Received a push notification.") - val data: ByteArray? = if (message.data.containsKey("spns")) { - // this is a v2 push notification - try { - pushManager.decrypt(Base64.decode(message.data["enc_payload"])) - } catch(e: Exception) { - Log.e(TAG, "Invalid push notification: ${e.message}") - return - } - } else { - // old v1 push notification; we still need this for receiving legacy closed group notifications - message.data?.get("ENCRYPTED_DATA")?.let(Base64::decode) - } - if (data != null) { - try { - val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() - val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) - JobQueue.shared.add(job) - } catch (e: Exception) { - Log.d(TAG, "Failed to unwrap data for message due to error: $e.") - } - } else { - Log.d(TAG, "Failed to decode data for message.") - val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) - .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) - .setColor(resources.getColor(network.loki.messenger.R.color.textsecure_primary)) - .setContentTitle("Session") - .setContentText("You've got a new message.") - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - NotificationManagerCompat.from(this).notify(11111, builder.build()) - } + pushHandler.onPush(message.data) } override fun onDeletedMessages() { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index e112cd2682..1533002d6c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -52,10 +52,13 @@ object PushManagerV1 { ) val url = "${server.url}/register_legacy_groups_only" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val body = RequestBody.create( + MediaType.get("application/json"), + JsonUtil.toJson(parameters) + ) val request = Request.Builder().url(url).post(body).build() - return sendOnionRequest(request) sideEffect { response -> + return sendOnionRequest(request) sideEffect { response -> when (response.code) { null, 0 -> throw Exception("error: ${response.message}.") } @@ -73,7 +76,7 @@ object PushManagerV1 { val token = TextSecurePreferences.getFCMToken(context) ?: emptyPromise() return retryIfNeeded(maxRetryCount) { - val parameters = mapOf( "token" to token ) + val parameters = mapOf("token" to token) val url = "${server.url}/unregister" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() @@ -110,7 +113,7 @@ object PushManagerV1 { closedGroupPublicKey: String, publicKey: String ): Promise<*, Exception> { - val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) + val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey) val url = "${server.url}/$operation" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() @@ -118,7 +121,7 @@ object PushManagerV1 { return retryIfNeeded(maxRetryCount) { sendOnionRequest(request) sideEffect { when (it.code) { - 0, null -> throw Exception(it.message) + 0, null -> throw Exception(it.message) } } } From 55216875ac14b7aee08e6561f0a0027136e9f781 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 26 Jul 2023 10:48:20 +0930 Subject: [PATCH 074/112] Connect Huawei push notifications --- app/build.gradle | 6 +- .../notifications/HuaweiPushManager.kt | 22 +++- .../HuaweiPushNotificationService.kt | 39 ++---- .../securesms/ApplicationContext.java | 6 +- .../thoughtcrime/securesms/DeviceModule.kt | 16 +++ .../securesms/groups/CreateGroupFragment.kt | 7 +- .../securesms/notifications/ExpiryManager.kt | 0 .../notifications/FcmTokenManager.kt | 0 .../securesms/notifications/PushHandler.kt | 70 +++++++---- .../notifications/FirebasePushManager.kt | 117 ++---------------- .../notifications/FirebasePushModule.kt | 17 +-- .../notifications/GenericPushManager.kt | 87 +++++++++++++ .../securesms/notifications/PushManagerV2.kt | 26 ++-- .../notifications/PushNotificationService.kt | 2 +- .../messaging/MessagingModuleConfiguration.kt | 2 + .../sending_receiving/MessageSender.kt | 5 +- .../MessageSenderClosedGroupHandler.kt | 9 +- .../ReceivedMessageHandler.kt | 2 +- .../notifications/PushManagerV1.kt | 7 +- .../session/libsession/utilities/Device.kt | 6 + 20 files changed, 231 insertions(+), 215 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt rename app/src/main/{kotlin => java}/org/thoughtcrime/securesms/notifications/ExpiryManager.kt (100%) rename app/src/main/{kotlin => java}/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt (100%) create mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt create mode 100644 libsession/src/main/java/org/session/libsession/utilities/Device.kt diff --git a/app/build.gradle b/app/build.gradle index db9c42d43a..91ee23fbb4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,7 +133,7 @@ android { apply plugin: 'com.google.gms.google-services' ext.websiteUpdateUrl = "null" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" - buildConfigField "String", "DEVICE", "\"android\"" + buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" } @@ -141,7 +141,7 @@ android { dimension "distribution" ext.websiteUpdateUrl = "null" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" - buildConfigField "String", "DEVICE", "\"huawei\"" + buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" } @@ -149,7 +149,7 @@ android { dimension "distribution" ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" - buildConfigField "String", "DEVICE", "\"android\"" + buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID" buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" } } diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt index 72a22ecfa4..f17b35f292 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt @@ -1,9 +1,29 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import com.huawei.hms.aaid.HmsInstanceId +import kotlinx.coroutines.Job class HuaweiPushManager(val context: Context): PushManager { - override fun refresh(force: Boolean) { + private var huaweiPushInstanceIdJob: Job? = null + @Synchronized + override fun refresh(force: Boolean) { + val huaweiPushInstanceIdJob = huaweiPushInstanceIdJob + + huaweiPushInstanceIdJob?.apply { + if (force) cancel() else if (isActive) return + } + + val hmsInstanceId = HmsInstanceId.getInstance(context) + + val task = hmsInstanceId.aaid + +// HuaweiPushNotificationService().start() +// +// huaweiPushInstanceIdJob = HmsInstanceId.getInstance(this) { hmsInstanceId -> +// RegisterHuaweiPushService(hmsInstanceId, this, force).start() +// Unit.INSTANCE +// } } } diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt index dd475c8344..e6941a563d 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -18,42 +18,19 @@ import javax.inject.Inject @AndroidEntryPoint class HuaweiPushNotificationService: HmsMessageService() { - @Inject token + @Inject lateinit var pushManager: PushManager + @Inject lateinit var pushHandler: PushHandler override fun onNewToken(token: String?, bundle: Bundle?) { Log.d("Loki", "New HCM token: $token.") - - if (!token.isNullOrEmpty()) { - val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return - PushManager.register(token, userPublicKey, this, false) - } + pushManager.refresh(true) } override fun onMessageReceived(message: RemoteMessage?) { - Log.d("Loki", "Received a push notification.") - val base64EncodedData = message?.data - val data = base64EncodedData?.let { Base64.decode(it) } - if (data != null) { - try { - val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() - val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) - JobQueue.shared.add(job) - } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message due to error: $e.") - } - } else { - Log.d("Loki", "Failed to decode data for message.") - val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER) - .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) - .setColor(this.getResources().getColor(network.loki.messenger.R.color.textsecure_primary)) - .setContentTitle("Session") - .setContentText("You've got a new message.") - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - with(NotificationManagerCompat.from(this)) { - notify(11111, builder.build()) - } - } + pushHandler.onPush(message?.dataOfMap) } -} \ No newline at end of file + override fun onDeletedMessages() { + pushManager.refresh(true) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 0ba8a77879..2fe1b1e1c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -41,6 +41,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.snode.SnodeModule; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.ConfigFactoryUpdateListener; +import org.session.libsession.utilities.Device; import org.session.libsession.utilities.ProfilePictureUtilities; import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.TextSecurePreferences; @@ -142,6 +143,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO @Inject LokiAPIDatabase lokiAPIDatabase; @Inject public Storage storage; + @Inject Device device; @Inject MessageDataProvider messageDataProvider; @Inject TextSecurePreferences textSecurePreferences; @Inject PushManager pushManager; @@ -207,8 +209,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO DatabaseModule.init(this); MessagingModuleConfiguration.configure(this); super.onCreate(); - messagingModuleConfiguration = new MessagingModuleConfiguration(this, + messagingModuleConfiguration = new MessagingModuleConfiguration( + this, storage, + device, messageDataProvider, ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this), configFactory diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt b/app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt new file mode 100644 index 0000000000..bdfa9b6088 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceModule.kt @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import network.loki.messenger.BuildConfig +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DeviceModule { + @Provides + @Singleton + fun provides() = BuildConfig.DEVICE +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt index ead979b773..ecd40938a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt @@ -21,6 +21,7 @@ import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Device import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.contacts.SelectContactsAdapter @@ -31,10 +32,14 @@ import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeOut +import javax.inject.Inject @AndroidEntryPoint class CreateGroupFragment : Fragment() { + @Inject + lateinit var device: Device + private lateinit var binding: FragmentCreateGroupBinding private val viewModel: CreateGroupViewModel by viewModels() @@ -86,7 +91,7 @@ class CreateGroupFragment : Fragment() { val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!! isLoading = true binding.loaderContainer.fadeIn() - MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> + MessageSender.createClosedGroup(device, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> binding.loaderContainer.fadeOut() isLoading = false val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(requireContext(), Address.fromSerialized(groupID), false)) diff --git a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt similarity index 100% rename from app/src/main/kotlin/org/thoughtcrime/securesms/notifications/ExpiryManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt diff --git a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt similarity index 100% rename from app/src/main/kotlin/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt index be4832104d..bba89d2e96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt @@ -29,24 +29,26 @@ private const val TAG = "PushHandler" class PushHandler @Inject constructor(@ApplicationContext val context: Context) { private val sodium = LazySodiumAndroid(SodiumAndroid()) - fun onPush(dataMap: Map) { - val data: ByteArray? = if (dataMap.containsKey("spns")) { - // this is a v2 push notification - try { - decrypt(Base64.decode(dataMap["enc_payload"])) - } catch(e: Exception) { - Log.e(TAG, "Invalid push notification: ${e.message}") - return - } - } else { - // old v1 push notification; we still need this for receiving legacy closed group notifications - dataMap.get("ENCRYPTED_DATA")?.let(Base64::decode) - } - data?.let { onPush(data) } ?: onPush() - + fun onPush(dataMap: Map?) { + onPush(dataMap?.asByteArray()) } - fun onPush() { + private fun onPush(data: ByteArray?) { + if (data == null) { + onPush() + return + } + + try { + val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() + val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) + JobQueue.shared.add(job) + } catch (e: Exception) { + Log.d(TAG, "Failed to unwrap data for message due to error: $e.") + } + } + + private fun onPush() { Log.d(TAG, "Failed to decode data for message.") val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER) .setSmallIcon(network.loki.messenger.R.drawable.ic_notification) @@ -58,15 +60,20 @@ class PushHandler @Inject constructor(@ApplicationContext val context: Context) NotificationManagerCompat.from(context).notify(11111, builder.build()) } - fun onPush(data: ByteArray) { - try { - val envelopeAsData = MessageWrapper.unwrap(data).toByteArray() - val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null) - JobQueue.shared.add(job) - } catch (e: Exception) { - Log.d(TAG, "Failed to unwrap data for message due to error: $e.") + private fun Map.asByteArray() = + when { + // this is a v2 push notification + containsKey("spns") -> { + try { + decrypt(Base64.decode(this["enc_payload"])) + } catch (e: Exception) { + Log.e(TAG, "Invalid push notification: ${e.message}") + null + } + } + // old v1 push notification; we still need this for receiving legacy closed group notifications + else -> this["ENCRYPTED_DATA"]?.let(Base64::decode) } - } fun decrypt(encPayload: ByteArray): ByteArray? { Log.d(TAG, "decrypt() called") @@ -84,14 +91,18 @@ class PushHandler @Inject constructor(@ApplicationContext val context: Context) val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson)) - val content: ByteArray? = if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null + val content: ByteArray? = + if (expectedList.size >= 2) (expectedList[1] as? BencodeString)?.value else null // null content is valid only if we got a "data_too_long" flag if (content == null) check(metadata.data_too_long) { "missing message data, but no too-long flag" } else check(metadata.data_len == content.size) { "wrong message data size" } - Log.d(TAG, "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B") + Log.d( + TAG, + "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B" + ) return content } @@ -102,6 +113,11 @@ class PushHandler @Inject constructor(@ApplicationContext val context: Context) val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF) IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) } - return Key.fromHexString(IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY)) + return Key.fromHexString( + IdentityKeyUtil.retrieve( + context, + IdentityKeyUtil.NOTIFICATION_KEY + ) + ) } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index df15efe80c..3df54fc119 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -1,60 +1,22 @@ package org.thoughtcrime.securesms.notifications import android.content.Context -import com.goterl.lazysodium.LazySodiumAndroid -import com.goterl.lazysodium.SodiumAndroid -import com.goterl.lazysodium.interfaces.AEAD -import com.goterl.lazysodium.interfaces.Sign -import com.goterl.lazysodium.utils.Key -import com.goterl.lazysodium.utils.KeyPair +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.combine.and -import nl.komponents.kovenant.functional.map -import okhttp3.MediaType -import okhttp3.Request -import okhttp3.RequestBody -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata -import org.session.libsession.messaging.sending_receiving.notifications.Response -import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest -import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse -import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse -import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest -import org.session.libsession.messaging.utilities.SodiumUtilities -import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.Version -import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber -import org.session.libsession.utilities.bencode.Bencode -import org.session.libsession.utilities.bencode.BencodeList -import org.session.libsession.utilities.bencode.BencodeString -import org.session.libsignal.utilities.Base64 +import org.session.libsession.utilities.Device import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace -import org.session.libsignal.utilities.emptyPromise -import org.session.libsignal.utilities.retryIfNeeded -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil -import org.thoughtcrime.securesms.crypto.KeyPairUtilities import javax.inject.Inject +import javax.inject.Singleton private const val TAG = "FirebasePushManager" -class FirebasePushManager( - private val context: Context - ): PushManager { +@Singleton +class FirebasePushManager @Inject constructor( + @ApplicationContext private val context: Context +): PushManager { - @Inject lateinit var pushManagerV2: PushManagerV2 + @Inject lateinit var genericPushManager: GenericPushManager - companion object { - const val maxRetryCount = 4 - } - - private val tokenManager = FcmTokenManager(context, ExpiryManager(context)) private var firebaseInstanceIdJob: Job? = null @Synchronized @@ -67,70 +29,9 @@ class FirebasePushManager( firebaseInstanceIdJob = getFcmInstanceId { task -> when { - task.isSuccessful -> try { task.result?.token?.let { refresh(it, force).get() } } catch(e: Exception) { Log.e(TAG, "refresh() failed", e) } + task.isSuccessful -> try { task.result?.token?.let { genericPushManager.refresh(it, force).get() } } catch(e: Exception) { Log.e(TAG, "refresh() failed", e) } else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception) } } } - - private fun refresh(token: String, force: Boolean): Promise<*, Exception> { - Log.d(TAG, "refresh() called") - - val userPublicKey = getLocalNumber(context) ?: return emptyPromise() - val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() - - return when { - tokenManager.isUsingFCM -> register(force, token, userPublicKey, userEdKey) - tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey) - else -> emptyPromise() - } - } - - /** - * Register for push notifications if: - * force is true - * there is no FCM Token - * FCM Token has expired - */ - private fun register( - force: Boolean, - token: String, - publicKey: String, - userEd25519Key: KeyPair, - namespaces: List = listOf(Namespace.DEFAULT) - ): Promise<*, Exception> = if (force || tokenManager.isInvalid()) { - register(token, publicKey, userEd25519Key, namespaces) - } else emptyPromise() - - /** - * Register for push notifications. - */ - private fun register( - token: String, - publicKey: String, - userEd25519Key: KeyPair, - namespaces: List = listOf(Namespace.DEFAULT) - ): Promise<*, Exception> = PushManagerV1.register( - token = token, - publicKey = publicKey - ) and pushManagerV2.register( - token, publicKey, userEd25519Key, namespaces - ) fail { - Log.e(TAG, "registerBoth failed", it) - } success { - Log.d(TAG, "registerBoth success... saving token!!") - tokenManager.fcmToken = token - } - - private fun unregister( - token: String, - userPublicKey: String, - userEdKey: KeyPair - ): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister( - token, userPublicKey, userEdKey - ) fail { - Log.e(TAG, "unregisterBoth failed", it) - } success { - tokenManager.fcmToken = null - } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt index 3f33016a02..925ad9d4d7 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt @@ -1,28 +1,13 @@ package org.thoughtcrime.securesms.notifications -import android.content.Context import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import org.session.libsession.utilities.TextSecurePreferences -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object FirebasePushModule { - @Provides - @Singleton - fun provideFirebasePushManager( - @ApplicationContext context: Context, - ) = FirebasePushManager(context) -} @Module @InstallIn(SingletonComponent::class) abstract class FirebaseBindingModule { @Binds abstract fun bindPushManager(firebasePushManager: FirebasePushManager): PushManager -} \ No newline at end of file +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt new file mode 100644 index 0000000000..692826225d --- /dev/null +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import com.goterl.lazysodium.utils.KeyPair +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.combine.and +import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.utilities.Device +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Namespace +import org.session.libsignal.utilities.emptyPromise +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import javax.inject.Inject +import javax.inject.Singleton + +private const val TAG = "GenericPushManager" + +@Singleton +class GenericPushManager @Inject constructor( + private val context: Context, + private val device: Device, + private val tokenManager: FcmTokenManager, + private val pushManagerV2: PushManagerV2, +) { + fun refresh(token: String, force: Boolean): Promise<*, Exception> { + Log.d(TAG, "refresh() called") + + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return emptyPromise() + val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() + + return when { + tokenManager.isUsingFCM -> register(force, token, userPublicKey, userEdKey) + tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey) + else -> emptyPromise() + } + } + + /** + * Register for push notifications if: + * force is true + * there is no FCM Token + * FCM Token has expired + */ + private fun register( + force: Boolean, + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List = listOf(Namespace.DEFAULT) + ): Promise<*, Exception> = if (force || tokenManager.isInvalid()) { + register(token, publicKey, userEd25519Key, namespaces) + } else emptyPromise() + + /** + * Register for push notifications. + */ + private fun register( + token: String, + publicKey: String, + userEd25519Key: KeyPair, + namespaces: List = listOf(Namespace.DEFAULT) + ): Promise<*, Exception> = PushManagerV1.register( + token = token, + device = device, + publicKey = publicKey + ) and pushManagerV2.register( + token, publicKey, userEd25519Key, namespaces + ) fail { + Log.e(TAG, "registerBoth failed", it) + } success { + Log.d(TAG, "registerBoth success... saving token!!") + tokenManager.fcmToken = token + } + + private fun unregister( + token: String, + userPublicKey: String, + userEdKey: KeyPair + ): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister( + token, userPublicKey, userEdKey + ) fail { + Log.e(TAG, "unregisterBoth failed", it) + } success { + tokenManager.fcmToken = null + } +} \ No newline at end of file diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt index 4fe885758f..9f9a132aba 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt @@ -1,13 +1,9 @@ package org.thoughtcrime.securesms.notifications -import android.content.Context import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid -import com.goterl.lazysodium.interfaces.AEAD import com.goterl.lazysodium.interfaces.Sign -import com.goterl.lazysodium.utils.Key import com.goterl.lazysodium.utils.KeyPair -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -16,30 +12,22 @@ import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 -import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata -import org.session.libsession.messaging.sending_receiving.notifications.Response -import org.session.libsession.messaging.sending_receiving.notifications.Server -import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest -import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse -import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse -import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest -import org.session.libsession.messaging.utilities.SodiumUtilities +import org.session.libsession.messaging.sending_receiving.notifications.* import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.Version -import org.session.libsession.utilities.bencode.Bencode -import org.session.libsession.utilities.bencode.BencodeList -import org.session.libsession.utilities.bencode.BencodeString import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.retryIfNeeded -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import javax.inject.Inject +import javax.inject.Singleton private const val TAG = "PushManagerV2" +private const val maxRetryCount = 4 -class PushManagerV2(private val pushHandler: PushHandler) { +@Singleton +class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) { private val sodium = LazySodiumAndroid(SodiumAndroid()) fun register( @@ -98,7 +86,7 @@ class PushManagerV2(private val pushHandler: PushHandler) { } private inline fun retryResponseBody(path: String, requestParameters: String): Promise = - retryIfNeeded(FirebasePushManager.maxRetryCount) { getResponseBody(path, requestParameters) } + retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) } private inline fun getResponseBody(path: String, requestParameters: String): Promise { val server = Server.LATEST diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt index e546053fce..bc641f82cd 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt @@ -19,7 +19,7 @@ private const val TAG = "PushNotificationService" @AndroidEntryPoint class PushNotificationService : FirebaseMessagingService() { - @Inject lateinit var pushManager: FirebasePushManager + @Inject lateinit var pushManager: PushManager @Inject lateinit var pushHandler: PushHandler override fun onNewToken(token: String) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt index 0437196772..3d48325bf7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt @@ -5,10 +5,12 @@ import com.goterl.lazysodium.utils.KeyPair import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.Device class MessagingModuleConfiguration( val context: Context, val storage: StorageProtocol, + val device: Device, val messageDataProvider: MessageDataProvider, val getUserED25519KeyPair: () -> KeyPair?, val configFactory: ConfigFactoryProtocol diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 804b2f15be..be7075dc54 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -29,6 +29,7 @@ import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Device import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.SSKEnvironment import org.session.libsignal.crypto.PushTransportDetails @@ -454,8 +455,8 @@ object MessageSender { } // Closed groups - fun createClosedGroup(name: String, members: Collection): Promise { - return create(name, members) + fun createClosedGroup(device: Device, name: String, members: Collection): Promise { + return create(device, name, members) } fun explicitNameChange(groupPublicKey: String, newName: String) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index 3b4b4b583c..9a068f68c3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -13,6 +13,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPol import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.session.libsession.utilities.Device import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient @@ -33,7 +34,11 @@ const val groupSizeLimit = 100 val pendingKeyPairs = ConcurrentHashMap>() -fun MessageSender.create(name: String, members: Collection): Promise { +fun MessageSender.create( + device: Device, + name: String, + members: Collection +): Promise { val deferred = deferred() ThreadUtils.queue { // Prepare @@ -89,7 +94,7 @@ fun MessageSender.create(name: String, members: Collection): Promise = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() ): Promise<*, Exception> = if (!isUsingFCM) { emptyPromise() } else retryIfNeeded(maxRetryCount) { - doRegister(token, publicKey, legacyGroupPublicKeys) + doRegister(token, publicKey, device, legacyGroupPublicKeys) } fail { exception -> Log.d(TAG, "Couldn't register for FCM due to error: $exception.") } - private fun doRegister(token: String?, publicKey: String?, legacyGroupPublicKeys: Collection): Promise<*, Exception> { + private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection): Promise<*, Exception> { Log.d(TAG, "registerV1 requested") token ?: return emptyPromise() @@ -48,6 +50,7 @@ object PushManagerV1 { val parameters = mapOf( "token" to token, "pubKey" to publicKey, + "device" to device, "legacyGroupPublicKeys" to legacyGroupPublicKeys ) diff --git a/libsession/src/main/java/org/session/libsession/utilities/Device.kt b/libsession/src/main/java/org/session/libsession/utilities/Device.kt new file mode 100644 index 0000000000..3f8d0d6d5a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/Device.kt @@ -0,0 +1,6 @@ +package org.session.libsession.utilities + +enum class Device(val value: String) { + ANDROID("android"), + HUAWEI("huawei"); +} From d3ea4e2e30de981ca2131242c1192a638324f9de Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 26 Jul 2023 14:36:06 +0930 Subject: [PATCH 075/112] Fix --- .../notifications/HuaweiPushManager.kt | 28 +++++++++----- .../notifications/HuaweiPushModule.kt | 11 ------ .../HuaweiPushNotificationService.kt | 11 ------ .../securesms/home/HomeActivity.kt | 5 ++- .../securesms/notifications/ExpiryManager.kt | 3 ++ .../notifications/FcmTokenManager.kt | 11 ++++-- .../securesms/onboarding/PNModeActivity.kt | 7 +++- .../NotificationSettingsActivity.kt | 2 + .../NotificationsPreferenceFragment.java | 10 ++++- .../notifications/GenericPushManager.kt | 37 ++++++++++++------- .../securesms/notifications/PushManagerV2.kt | 7 +++- app/src/play/AndroidManifest.xml | 2 +- .../notifications/FirebasePushManager.kt | 12 +++--- ....kt => FirebasePushNotificationService.kt} | 13 +------ .../MessageSenderClosedGroupHandler.kt | 2 +- .../ReceivedMessageHandler.kt | 2 +- .../notifications/PushManagerV1.kt | 17 +++++---- .../session/libsession/utilities/Device.kt | 4 +- 18 files changed, 99 insertions(+), 85 deletions(-) rename app/src/{play => main}/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt (76%) rename app/src/{play => main}/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt (96%) rename app/src/play/kotlin/org/thoughtcrime/securesms/notifications/{PushNotificationService.kt => FirebasePushNotificationService.kt} (64%) diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt index f17b35f292..6e495357f3 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt @@ -1,10 +1,22 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import com.huawei.hmf.tasks.Tasks import com.huawei.hms.aaid.HmsInstanceId +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton -class HuaweiPushManager(val context: Context): PushManager { +@Singleton +class HuaweiPushManager @Inject constructor( + @ApplicationContext private val context: Context, + private val genericPushManager: GenericPushManager +): PushManager { private var huaweiPushInstanceIdJob: Job? = null @Synchronized @@ -17,13 +29,11 @@ class HuaweiPushManager(val context: Context): PushManager { val hmsInstanceId = HmsInstanceId.getInstance(context) - val task = hmsInstanceId.aaid - -// HuaweiPushNotificationService().start() -// -// huaweiPushInstanceIdJob = HmsInstanceId.getInstance(this) { hmsInstanceId -> -// RegisterHuaweiPushService(hmsInstanceId, this, force).start() -// Unit.INSTANCE -// } + MainScope().launch(Dispatchers.IO) { + val task = hmsInstanceId.aaid + Tasks.await(task) + if (!isActive) return@launch // don't 'complete' task if we were canceled + task.result?.id?.let { genericPushManager.refresh(it, force) } + } } } diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt index e10ed77abf..c1be786afc 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt @@ -8,17 +8,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object HuaweiPushModule { - @Provides - @Singleton - fun provideFirebasePushManager( - @ApplicationContext context: Context, - ) = HuaweiPushManager(context) -} - @Module @InstallIn(SingletonComponent::class) abstract class HuaweiBindingModule { diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt index e6941a563d..507787123c 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -1,17 +1,9 @@ package org.thoughtcrime.securesms.notifications import android.os.Bundle -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage import dagger.hilt.android.AndroidEntryPoint -import org.session.libsession.messaging.jobs.BatchMessageReceiveJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.utilities.MessageWrapper -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import javax.inject.Inject @@ -20,16 +12,13 @@ class HuaweiPushNotificationService: HmsMessageService() { @Inject lateinit var pushManager: PushManager @Inject lateinit var pushHandler: PushHandler - override fun onNewToken(token: String?, bundle: Bundle?) { Log.d("Loki", "New HCM token: $token.") pushManager.refresh(true) } - override fun onMessageReceived(message: RemoteMessage?) { pushHandler.onPush(message?.dataOfMap) } - override fun onDeletedMessages() { pushManager.refresh(true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 6570c0ca18..c64d6ef071 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -67,6 +67,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.notifications.PushManager import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.permissions.Permissions @@ -106,6 +107,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), @Inject lateinit var groupDatabase: GroupDatabase @Inject lateinit var textSecurePreferences: TextSecurePreferences @Inject lateinit var configFactory: ConfigFactory + @Inject lateinit var pushManager: PushManager private val globalSearchViewModel by viewModels() private val homeViewModel by viewModels() @@ -230,8 +232,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), (applicationContext as ApplicationContext).startPollingIfNeeded() // update things based on TextSecurePrefs (profile info etc) // Set up remaining components if needed - val application = ApplicationContext.getInstance(this@HomeActivity) - application.registerForPnIfNeeded(false) + pushManager.refresh(false) if (textSecurePreferences.getLocalNumber() != null) { OpenGroupManager.startPolling() JobQueue.shared.resumePendingJobs() diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt index 36c986f0ea..45318d43ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt @@ -1,7 +1,10 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import org.session.libsession.utilities.TextSecurePreferences +import javax.inject.Inject +import javax.inject.Singleton class ExpiryManager( private val context: Context, diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt index e1182a480b..295c8296fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt @@ -1,12 +1,17 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import org.session.libsession.utilities.TextSecurePreferences +import javax.inject.Inject +import javax.inject.Singleton -class FcmTokenManager( - private val context: Context, - private val expiryManager: ExpiryManager +@Singleton +class FcmTokenManager @Inject constructor( + @ApplicationContext private val context: Context, ) { + private val expiryManager = ExpiryManager(context) + val isUsingFCM get() = TextSecurePreferences.isUsingFCM(context) var fcmToken diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index 8210937830..c8f036f15f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -19,6 +19,7 @@ import org.session.libsession.utilities.ThemeUtil import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.home.HomeActivity +import org.thoughtcrime.securesms.notifications.PushManager import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.PNModeView @@ -27,8 +28,12 @@ import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getColorWithID import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.thoughtcrime.securesms.util.show +import javax.inject.Inject class PNModeActivity : BaseActionBarActivity() { + + @Inject lateinit var pushManager: PushManager + private lateinit var binding: ActivityPnModeBinding private var selectedOptionView: PNModeView? = null @@ -161,7 +166,7 @@ class PNModeActivity : BaseActionBarActivity() { TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView)) val application = ApplicationContext.getInstance(this) application.startPollingIfNeeded() - application.registerForPnIfNeeded(true) + pushManager.refresh(true) val intent = Intent(this, HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.putExtra(HomeActivity.FROM_ONBOARDING, true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt index af039a4fdb..b18859ea07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationSettingsActivity.kt @@ -1,10 +1,12 @@ package org.thoughtcrime.securesms.preferences import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment +@AndroidEntryPoint class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index 8afc0ff41b..228392ad76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -23,14 +23,22 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.notifications.PushManager; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import network.loki.messenger.R; +@AndroidEntryPoint public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment { @SuppressWarnings("unused") private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName(); + @Inject + PushManager pushManager; + @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); @@ -41,7 +49,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme this.findPreference(fcmKey) .setOnPreferenceChangeListener((preference, newValue) -> { TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue); - ApplicationContext.getInstance(getContext()).registerForPnIfNeeded(true); + pushManager.refresh(true); return true; }); diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt similarity index 76% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt index 692826225d..ce4210fcfd 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt +++ b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.notifications import android.content.Context import com.goterl.lazysodium.utils.KeyPair +import dagger.hilt.android.qualifiers.ApplicationContext import nl.komponents.kovenant.Promise import nl.komponents.kovenant.combine.and import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 @@ -18,7 +19,7 @@ private const val TAG = "GenericPushManager" @Singleton class GenericPushManager @Inject constructor( - private val context: Context, + @ApplicationContext private val context: Context, private val device: Device, private val tokenManager: FcmTokenManager, private val pushManagerV2: PushManagerV2, @@ -60,17 +61,25 @@ class GenericPushManager @Inject constructor( publicKey: String, userEd25519Key: KeyPair, namespaces: List = listOf(Namespace.DEFAULT) - ): Promise<*, Exception> = PushManagerV1.register( - token = token, - device = device, - publicKey = publicKey - ) and pushManagerV2.register( - token, publicKey, userEd25519Key, namespaces - ) fail { - Log.e(TAG, "registerBoth failed", it) - } success { - Log.d(TAG, "registerBoth success... saving token!!") - tokenManager.fcmToken = token + ): Promise<*, Exception> { + val v1 = PushManagerV1.register( + device = device, + token = token, + publicKey = publicKey + ) fail { + Log.e(TAG, "register v1 failed", it) + } + + val v2 = pushManagerV2.register( + Device.ANDROID, token, publicKey, userEd25519Key, namespaces + ) fail { + Log.e(TAG, "register v2 failed", it) + } + + return v1 and v2 success { + Log.d(TAG, "registerBoth success... saving token!!") + tokenManager.fcmToken = token + } } private fun unregister( @@ -78,10 +87,10 @@ class GenericPushManager @Inject constructor( userPublicKey: String, userEdKey: KeyPair ): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister( - token, userPublicKey, userEdKey + Device.ANDROID, token, userPublicKey, userEdKey ) fail { Log.e(TAG, "unregisterBoth failed", it) } success { tokenManager.fcmToken = null } -} \ No newline at end of file +} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt similarity index 96% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt index 9f9a132aba..93e6fc6819 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt +++ b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt @@ -16,6 +16,7 @@ import org.session.libsession.messaging.sending_receiving.notifications.* import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.Version +import org.session.libsession.utilities.Device import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace @@ -31,6 +32,7 @@ class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) { private val sodium = LazySodiumAndroid(SodiumAndroid()) fun register( + device: Device, token: String, publicKey: String, userEd25519Key: KeyPair, @@ -48,7 +50,7 @@ class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) { session_ed25519 = userEd25519Key.publicKey.asHexString, namespaces = listOf(Namespace.DEFAULT), data = true, // only permit data subscription for now (?) - service = "firebase", + service = device.service, sig_ts = timestamp, signature = Base64.encodeBytes(signature), service_info = mapOf("token" to token), @@ -61,6 +63,7 @@ class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) { } fun unregister( + device: Device, token: String, userPublicKey: String, userEdKey: KeyPair @@ -74,7 +77,7 @@ class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) { val requestParameters = UnsubscriptionRequest( pubkey = userPublicKey, session_ed25519 = userEdKey.publicKey.asHexString, - service = "firebase", + service = device.service, sig_ts = timestamp, signature = Base64.encodeBytes(signature), service_info = mapOf("token" to token), diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml index f5b54fd47e..8b975b0634 100644 --- a/app/src/play/AndroidManifest.xml +++ b/app/src/play/AndroidManifest.xml @@ -4,7 +4,7 @@ diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 3df54fc119..074ef379f8 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -1,9 +1,6 @@ package org.thoughtcrime.securesms.notifications -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job -import org.session.libsession.utilities.Device import org.session.libsignal.utilities.Log import javax.inject.Inject import javax.inject.Singleton @@ -12,11 +9,9 @@ private const val TAG = "FirebasePushManager" @Singleton class FirebasePushManager @Inject constructor( - @ApplicationContext private val context: Context + private val genericPushManager: GenericPushManager ): PushManager { - @Inject lateinit var genericPushManager: GenericPushManager - private var firebaseInstanceIdJob: Job? = null @Synchronized @@ -24,7 +19,10 @@ class FirebasePushManager @Inject constructor( Log.d(TAG, "refresh() called with: force = $force") firebaseInstanceIdJob?.apply { - if (force) cancel() else if (isActive) return + when { + force -> cancel() + isActive -> return + } } firebaseInstanceIdJob = getFcmInstanceId { task -> diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushNotificationService.kt similarity index 64% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt rename to app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushNotificationService.kt index bc641f82cd..b8640296fa 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/PushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushNotificationService.kt @@ -1,23 +1,15 @@ package org.thoughtcrime.securesms.notifications -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import dagger.hilt.android.AndroidEntryPoint -import org.session.libsession.messaging.jobs.BatchMessageReceiveJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveParameters -import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import javax.inject.Inject -private const val TAG = "PushNotificationService" - +private const val TAG = "FirebasePushNotificationService" @AndroidEntryPoint -class PushNotificationService : FirebaseMessagingService() { +class FirebasePushNotificationService : FirebaseMessagingService() { @Inject lateinit var pushManager: PushManager @Inject lateinit var pushHandler: PushHandler @@ -37,7 +29,6 @@ class PushNotificationService : FirebaseMessagingService() { override fun onDeletedMessages() { Log.d(TAG, "Called onDeletedMessages.") - super.onDeletedMessages() pushManager.refresh(true) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index 9a068f68c3..022d32f170 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -94,7 +94,7 @@ fun MessageSender.create( // Add the group to the config now that it was successfully created storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair) // Notify the PN server - PushManagerV1.register(publicKey = userPublicKey, device = device) + PushManagerV1.register(device = device, publicKey = userPublicKey) // Start polling ClosedGroupPollerV2.shared.startPolling(groupPublicKey) // Fulfill the promise diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 91197649c7..7c5092e5ea 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -556,7 +556,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli // Set expiration timer storage.setExpirationTimer(groupID, expireTimer) // Notify the PN server - PushManagerV1.register(publicKey = userPublicKey, device = MessagingModuleConfiguration.shared.device) + PushManagerV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey) // Notify the user if (userPublicKey == sender && !groupExists) { val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index 32e5f6b2dc..b8894162d5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -27,18 +27,19 @@ object PushManagerV1 { private val server = Server.LEGACY fun register( + device: Device, isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), token: String? = TextSecurePreferences.getFCMToken(context), publicKey: String? = TextSecurePreferences.getLocalNumber(context), - device: Device, legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() ): Promise<*, Exception> = - if (!isUsingFCM) { - emptyPromise() - } else retryIfNeeded(maxRetryCount) { - doRegister(token, publicKey, device, legacyGroupPublicKeys) - } fail { exception -> - Log.d(TAG, "Couldn't register for FCM due to error: $exception.") + when { + isUsingFCM -> retryIfNeeded(maxRetryCount) { + doRegister(token, publicKey, device, legacyGroupPublicKeys) + } fail { exception -> + Log.d(TAG, "Couldn't register for FCM due to error: $exception.") + } + else -> emptyPromise() } private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection): Promise<*, Exception> { @@ -50,7 +51,7 @@ object PushManagerV1 { val parameters = mapOf( "token" to token, "pubKey" to publicKey, - "device" to device, + "device" to device.value, "legacyGroupPublicKeys" to legacyGroupPublicKeys ) diff --git a/libsession/src/main/java/org/session/libsession/utilities/Device.kt b/libsession/src/main/java/org/session/libsession/utilities/Device.kt index 3f8d0d6d5a..c00e0a69dd 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Device.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Device.kt @@ -1,6 +1,6 @@ package org.session.libsession.utilities -enum class Device(val value: String) { - ANDROID("android"), +enum class Device(val value: String, val service: String = value) { + ANDROID("android", "firebase"), HUAWEI("huawei"); } From 4738c9b4f909c3ddcd76d7eae7cc01f8238b1b33 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 26 Jul 2023 14:48:15 +0930 Subject: [PATCH 076/112] Fix v2 --- .../securesms/notifications/GenericPushManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt index ce4210fcfd..471affcd24 100644 --- a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt +++ b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt @@ -71,7 +71,7 @@ class GenericPushManager @Inject constructor( } val v2 = pushManagerV2.register( - Device.ANDROID, token, publicKey, userEd25519Key, namespaces + device, token, publicKey, userEd25519Key, namespaces ) fail { Log.e(TAG, "register v2 failed", it) } @@ -87,7 +87,7 @@ class GenericPushManager @Inject constructor( userPublicKey: String, userEdKey: KeyPair ): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister( - Device.ANDROID, token, userPublicKey, userEdKey + device, token, userPublicKey, userEdKey ) fail { Log.e(TAG, "unregisterBoth failed", it) } success { From 7be1f092f90ce8be4069ccc6625d5aa457a35be5 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 26 Jul 2023 22:09:24 +0930 Subject: [PATCH 077/112] More Huawei stuff --- app/agconnect-services.json | 92 +++++++++++++++++++ app/src/huawei/AndroidManifest.xml | 4 +- .../HuaweiPushNotificationService.kt | 3 +- 3 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 app/agconnect-services.json diff --git a/app/agconnect-services.json b/app/agconnect-services.json new file mode 100644 index 0000000000..99e95057e4 --- /dev/null +++ b/app/agconnect-services.json @@ -0,0 +1,92 @@ +{ + "agcgw":{ + "backurl":"connect-dre.hispace.hicloud.com", + "url":"connect-dre.dbankcloud.cn", + "websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com", + "websocketurl":"connect-ws-dre.hispace.dbankcloud.cn" + }, + "agcgw_all":{ + "CN":"connect-drcn.dbankcloud.cn", + "CN_back":"connect-drcn.hispace.hicloud.com", + "DE":"connect-dre.dbankcloud.cn", + "DE_back":"connect-dre.hispace.hicloud.com", + "RU":"connect-drru.hispace.dbankcloud.ru", + "RU_back":"connect-drru.hispace.dbankcloud.cn", + "SG":"connect-dra.dbankcloud.cn", + "SG_back":"connect-dra.hispace.hicloud.com" + }, + "websocketgw_all":{ + "CN":"connect-ws-drcn.hispace.dbankcloud.cn", + "CN_back":"connect-ws-drcn.hispace.dbankcloud.com", + "DE":"connect-ws-dre.hispace.dbankcloud.cn", + "DE_back":"connect-ws-dre.hispace.dbankcloud.com", + "RU":"connect-ws-drru.hispace.dbankcloud.ru", + "RU_back":"connect-ws-drru.hispace.dbankcloud.cn", + "SG":"connect-ws-dra.hispace.dbankcloud.cn", + "SG_back":"connect-ws-dra.hispace.dbankcloud.com" + }, + "client":{ + "cp_id":"30061000024605000", + "product_id":"99536292102650699", + "client_id":"993355872258247936", + "client_secret":"0A83DC166FBF0696B884D38830E0B90AE8BBFF2D9CA8C26C9D9DA26816C2A9E3", + "project_id":"99536292102650699", + "app_id":"107146885", + "api_key":"DAEDAPld5VAZXpjfD+g0PwlXfya5ykPMYTMRYkySUjGfJg055ijAFNZBUtcykDPWZiaUcc4Mo9oJqmooL6HT8n4toO5COlWZjkqGXA==", + "package_name":"network.loki.messenger" + }, + "oauth_client":{ + "client_id":"107146885", + "client_type":1 + }, + "app_info":{ + "app_id":"107146885", + "package_name":"network.loki.messenger" + }, + "service":{ + "analytics":{ + "collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com", + "collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", + "collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", + "resource_id":"p1", + "channel_id":"" + }, + "search":{ + "url":"https://search-dre.cloud.huawei.com" + }, + "cloudstorage":{ + "storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia", + "storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru", + "storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru", + "storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu", + "storage_url_de":"https://ops-dre.agcstorage.link", + "storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn", + "storage_url_sg":"https://ops-dra.agcstorage.link", + "storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn", + "storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn" + }, + "ml":{ + "mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" + } + }, + "region":"DE", + "configuration_version":"3.0", + "appInfos":[ + { + "package_name":"network.loki.messenger", + "client":{ + "app_id":"107146885" + }, + "app_info":{ + "package_name":"network.loki.messenger", + "app_id":"107146885" + }, + "oauth_client":{ + "client_type":1, + "client_id":"107146885" + } + } + ] +} diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml index 4745c454b2..57279978c1 100644 --- a/app/src/huawei/AndroidManifest.xml +++ b/app/src/huawei/AndroidManifest.xml @@ -8,9 +8,9 @@ android:enabled="true" android:exported="false"> - + - \ No newline at end of file + diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt index 507787123c..640caf3cf4 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -17,7 +17,8 @@ class HuaweiPushNotificationService: HmsMessageService() { pushManager.refresh(true) } override fun onMessageReceived(message: RemoteMessage?) { - pushHandler.onPush(message?.dataOfMap) + TODO("Huawei works!") +// pushHandler.onPush(message?.dataOfMap) } override fun onDeletedMessages() { pushManager.refresh(true) From d1e22ca36934129c2bfa366e3bd4d5c6de51e1a6 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 27 Jul 2023 13:53:33 +0930 Subject: [PATCH 078/112] Fix --- .../securesms/notifications/HuaweiPushNotificationService.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt index 640caf3cf4..507787123c 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -17,8 +17,7 @@ class HuaweiPushNotificationService: HmsMessageService() { pushManager.refresh(true) } override fun onMessageReceived(message: RemoteMessage?) { - TODO("Huawei works!") -// pushHandler.onPush(message?.dataOfMap) + pushHandler.onPush(message?.dataOfMap) } override fun onDeletedMessages() { pushManager.refresh(true) From 7ee9b14247c7c277a5ecaca7df9d20b2ad57bb9e Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Aug 2023 10:41:50 +0930 Subject: [PATCH 079/112] Fix PNModeActivity DI --- .../org/thoughtcrime/securesms/onboarding/PNModeActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index c8f036f15f..edaf646aaa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -12,6 +12,7 @@ import android.view.View import android.widget.Toast import androidx.annotation.ColorInt import androidx.annotation.DrawableRes +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ActivityPnModeBinding import org.session.libsession.utilities.TextSecurePreferences @@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.thoughtcrime.securesms.util.show import javax.inject.Inject +@AndroidEntryPoint class PNModeActivity : BaseActionBarActivity() { @Inject lateinit var pushManager: PushManager From 41d24ef2c36f78b858b790c22367ea010c5b2399 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Aug 2023 14:55:31 +0930 Subject: [PATCH 080/112] ... --- .../notifications/HuaweiPushManager.kt | 15 +++++++--- .../HuaweiPushNotificationService.kt | 29 ++++++++++++++++++- app/src/main/AndroidManifest.xml | 10 +++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt index 6e495357f3..87a78ef035 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.notifications import android.content.Context +import android.util.Log import com.huawei.hmf.tasks.Tasks import com.huawei.hms.aaid.HmsInstanceId import dagger.hilt.android.qualifiers.ApplicationContext @@ -9,9 +10,12 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.session.libsession.utilities.TextSecurePreferences import javax.inject.Inject import javax.inject.Singleton +private val TAG = HuaweiPushManager::class.java.name + @Singleton class HuaweiPushManager @Inject constructor( @ApplicationContext private val context: Context, @@ -27,13 +31,16 @@ class HuaweiPushManager @Inject constructor( if (force) cancel() else if (isActive) return } + val appId = "107146885" + val tokenScope = "HCM" val hmsInstanceId = HmsInstanceId.getInstance(context) MainScope().launch(Dispatchers.IO) { - val task = hmsInstanceId.aaid - Tasks.await(task) - if (!isActive) return@launch // don't 'complete' task if we were canceled - task.result?.id?.let { genericPushManager.refresh(it, force) } + val token = hmsInstanceId.getToken(appId, tokenScope) + + Log.d(TAG, "refresh() with huawei token: $token") + + genericPushManager.refresh(token, force) } } } diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt index 507787123c..7dae82dd30 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -5,18 +5,45 @@ import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage import dagger.hilt.android.AndroidEntryPoint import org.session.libsignal.utilities.Log +import java.lang.Exception import javax.inject.Inject @AndroidEntryPoint class HuaweiPushNotificationService: HmsMessageService() { + init { + Log.d("pnh", "init Huawei Service") + } + @Inject lateinit var pushManager: PushManager @Inject lateinit var pushHandler: PushHandler + + override fun onCreate() { + Log.d("pnh", "onCreate Huawei Service") + super.onCreate() + } + + override fun onMessageDelivered(p0: String?, p1: Exception?) { + Log.d("pnh", "onMessageDelivered") + super.onMessageDelivered(p0, p1) + } + + override fun onMessageSent(p0: String?) { + Log.d("pnh", "onMessageSent") + super.onMessageSent(p0) + } + + override fun onNewToken(p0: String?) { + Log.d("pnh", "onNewToken") + super.onNewToken(p0) + } + override fun onNewToken(token: String?, bundle: Bundle?) { - Log.d("Loki", "New HCM token: $token.") + Log.d("pnh", "New HCM token: $token.") pushManager.refresh(true) } override fun onMessageReceived(message: RemoteMessage?) { + Log.d("pnh", "onMessageReceived: $message.") pushHandler.onPush(message?.dataOfMap) } override fun onDeletedMessages() { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc63ab6a87..d6bed361e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,6 +78,16 @@ android:theme="@style/Theme.Session.DayNight" tools:replace="android:allowBackup"> + + + + + + Date: Thu, 3 Aug 2023 14:14:55 +0930 Subject: [PATCH 081/112] ... --- app/agconnect-services.json | 186 +++++++++--------- .../notifications/HuaweiPushManager.kt | 11 +- .../HuaweiPushNotificationService.kt | 5 + app/src/main/AndroidManifest.xml | 2 +- .../notifications/GenericPushManager.kt | 10 +- .../notifications/PushManagerV1.kt | 25 ++- 6 files changed, 131 insertions(+), 108 deletions(-) diff --git a/app/agconnect-services.json b/app/agconnect-services.json index 99e95057e4..0c81d0477a 100644 --- a/app/agconnect-services.json +++ b/app/agconnect-services.json @@ -1,92 +1,96 @@ { - "agcgw":{ - "backurl":"connect-dre.hispace.hicloud.com", - "url":"connect-dre.dbankcloud.cn", - "websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com", - "websocketurl":"connect-ws-dre.hispace.dbankcloud.cn" - }, - "agcgw_all":{ - "CN":"connect-drcn.dbankcloud.cn", - "CN_back":"connect-drcn.hispace.hicloud.com", - "DE":"connect-dre.dbankcloud.cn", - "DE_back":"connect-dre.hispace.hicloud.com", - "RU":"connect-drru.hispace.dbankcloud.ru", - "RU_back":"connect-drru.hispace.dbankcloud.cn", - "SG":"connect-dra.dbankcloud.cn", - "SG_back":"connect-dra.hispace.hicloud.com" - }, - "websocketgw_all":{ - "CN":"connect-ws-drcn.hispace.dbankcloud.cn", - "CN_back":"connect-ws-drcn.hispace.dbankcloud.com", - "DE":"connect-ws-dre.hispace.dbankcloud.cn", - "DE_back":"connect-ws-dre.hispace.dbankcloud.com", - "RU":"connect-ws-drru.hispace.dbankcloud.ru", - "RU_back":"connect-ws-drru.hispace.dbankcloud.cn", - "SG":"connect-ws-dra.hispace.dbankcloud.cn", - "SG_back":"connect-ws-dra.hispace.dbankcloud.com" - }, - "client":{ - "cp_id":"30061000024605000", - "product_id":"99536292102650699", - "client_id":"993355872258247936", - "client_secret":"0A83DC166FBF0696B884D38830E0B90AE8BBFF2D9CA8C26C9D9DA26816C2A9E3", - "project_id":"99536292102650699", - "app_id":"107146885", - "api_key":"DAEDAPld5VAZXpjfD+g0PwlXfya5ykPMYTMRYkySUjGfJg055ijAFNZBUtcykDPWZiaUcc4Mo9oJqmooL6HT8n4toO5COlWZjkqGXA==", - "package_name":"network.loki.messenger" - }, - "oauth_client":{ - "client_id":"107146885", - "client_type":1 - }, - "app_info":{ - "app_id":"107146885", - "package_name":"network.loki.messenger" - }, - "service":{ - "analytics":{ - "collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com", - "collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", - "collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", - "resource_id":"p1", - "channel_id":"" - }, - "search":{ - "url":"https://search-dre.cloud.huawei.com" - }, - "cloudstorage":{ - "storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia", - "storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru", - "storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru", - "storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu", - "storage_url_de":"https://ops-dre.agcstorage.link", - "storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn", - "storage_url_sg":"https://ops-dra.agcstorage.link", - "storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn", - "storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn" - }, - "ml":{ - "mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" - } - }, - "region":"DE", - "configuration_version":"3.0", - "appInfos":[ - { - "package_name":"network.loki.messenger", - "client":{ - "app_id":"107146885" - }, - "app_info":{ - "package_name":"network.loki.messenger", - "app_id":"107146885" - }, - "oauth_client":{ - "client_type":1, - "client_id":"107146885" - } - } - ] -} + "agcgw":{ + "backurl":"connect-dre.hispace.hicloud.com", + "url":"connect-dre.dbankcloud.cn", + "websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com", + "websocketurl":"connect-ws-dre.hispace.dbankcloud.cn" + }, + "agcgw_all":{ + "CN":"connect-drcn.dbankcloud.cn", + "CN_back":"connect-drcn.hispace.hicloud.com", + "DE":"connect-dre.dbankcloud.cn", + "DE_back":"connect-dre.hispace.hicloud.com", + "RU":"connect-drru.hispace.dbankcloud.ru", + "RU_back":"connect-drru.hispace.dbankcloud.cn", + "SG":"connect-dra.dbankcloud.cn", + "SG_back":"connect-dra.hispace.hicloud.com" + }, + "websocketgw_all":{ + "CN":"connect-ws-drcn.hispace.dbankcloud.cn", + "CN_back":"connect-ws-drcn.hispace.dbankcloud.com", + "DE":"connect-ws-dre.hispace.dbankcloud.cn", + "DE_back":"connect-ws-dre.hispace.dbankcloud.com", + "RU":"connect-ws-drru.hispace.dbankcloud.ru", + "RU_back":"connect-ws-drru.hispace.dbankcloud.cn", + "SG":"connect-ws-dra.hispace.dbankcloud.cn", + "SG_back":"connect-ws-dra.hispace.dbankcloud.com" + }, + "client":{ + "cp_id":"890061000023000573", + "product_id":"99536292102532562", + "client_id":"954244311350791232", + "client_secret":"555999202D718B6744DAD2E923B386DC17F3F4E29F5105CE0D061EED328DADEE", + "project_id":"99536292102532562", + "app_id":"107205081", + "api_key":"DAEDABeddLEqUy0QRwa1THLwRA0OqrSuyci/HjNvVSmsdWsXRM2U2hRaCyqfvGYH1IFOKrauArssz/WPMLRHCYxliWf+DTj9bDwlWA==", + "package_name":"network.loki.messenger" + }, + "oauth_client":{ + "client_id":"107205081", + "client_type":1 + }, + "app_info":{ + "app_id":"107205081", + "package_name":"network.loki.messenger" + }, + "service":{ + "analytics":{ + "collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com", + "collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", + "collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", + "collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", + "resource_id":"p1", + "channel_id":"" + }, + "edukit":{ + "edu_url":"edukit.edu.cloud.huawei.com.cn", + "dh_url":"edukit.edu.cloud.huawei.com.cn" + }, + "search":{ + "url":"https://search-dre.cloud.huawei.com" + }, + "cloudstorage":{ + "storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia", + "storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru", + "storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru", + "storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu", + "storage_url_de":"https://ops-dre.agcstorage.link", + "storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn", + "storage_url_sg":"https://ops-dra.agcstorage.link", + "storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn", + "storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn" + }, + "ml":{ + "mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" + } + }, + "region":"DE", + "configuration_version":"3.0", + "appInfos":[ + { + "package_name":"network.loki.messenger", + "client":{ + "app_id":"107205081" + }, + "app_info":{ + "package_name":"network.loki.messenger", + "app_id":"107205081" + }, + "oauth_client":{ + "client_type":1, + "client_id":"107205081" + } + } + ] +} \ No newline at end of file diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt index 87a78ef035..26da350372 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt @@ -31,16 +31,17 @@ class HuaweiPushManager @Inject constructor( if (force) cancel() else if (isActive) return } - val appId = "107146885" + val appId = "107205081" val tokenScope = "HCM" val hmsInstanceId = HmsInstanceId.getInstance(context) + genericPushManager.refresh(TextSecurePreferences.getFCMToken(context), force) + MainScope().launch(Dispatchers.IO) { val token = hmsInstanceId.getToken(appId, tokenScope) - - Log.d(TAG, "refresh() with huawei token: $token") - - genericPushManager.refresh(token, force) + Log.d(TAG, "refresh() hmsInstanceId => huawei token: $token") +// +//// genericPushManager.refresh(token, force) } } } diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt index 7dae82dd30..16ac969d2c 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -4,6 +4,7 @@ import android.os.Bundle import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage import dagger.hilt.android.AndroidEntryPoint +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log import java.lang.Exception import javax.inject.Inject @@ -40,6 +41,10 @@ class HuaweiPushNotificationService: HmsMessageService() { override fun onNewToken(token: String?, bundle: Bundle?) { Log.d("pnh", "New HCM token: $token.") + + if (token == TextSecurePreferences.getFCMToken(this)) return + + TextSecurePreferences.setFCMToken(this, token) pushManager.refresh(true) } override fun onMessageReceived(message: RemoteMessage?) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d6bed361e5..ed51a7ea8d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -80,7 +80,7 @@ + android:value="appid=107205081"> { - Log.d(TAG, "refresh() called") + fun refresh(token: String?, force: Boolean): Promise<*, Exception> { + Log.d(TAG, "refresh($token, $force) called") + token ?: return emptyPromise() val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return emptyPromise() val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() @@ -62,6 +63,11 @@ class GenericPushManager @Inject constructor( userEd25519Key: KeyPair, namespaces: List = listOf(Namespace.DEFAULT) ): Promise<*, Exception> { + android.util.Log.d( + TAG, + "register() called with: token = $token, publicKey = $publicKey, userEd25519Key = $userEd25519Key, namespaces = $namespaces" + ) + val v1 = PushManagerV1.register( device = device, token = token, diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index b8894162d5..3e82bda4a3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -32,18 +32,25 @@ object PushManagerV1 { token: String? = TextSecurePreferences.getFCMToken(context), publicKey: String? = TextSecurePreferences.getLocalNumber(context), legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() - ): Promise<*, Exception> = - when { - isUsingFCM -> retryIfNeeded(maxRetryCount) { - doRegister(token, publicKey, device, legacyGroupPublicKeys) - } fail { exception -> - Log.d(TAG, "Couldn't register for FCM due to error: $exception.") - } - else -> emptyPromise() + ): Promise<*, Exception> = when { + isUsingFCM -> retryIfNeeded(maxRetryCount) { + android.util.Log.d( + TAG, + "register() called with: device = $device, isUsingFCM = $isUsingFCM, token = $token, publicKey = $publicKey, legacyGroupPublicKeys = $legacyGroupPublicKeys" + ) + doRegister(token, publicKey, device, legacyGroupPublicKeys) + } fail { exception -> + Log.d(TAG, "Couldn't register for FCM due to error: $exception... $device $token $publicKey $legacyGroupPublicKeys") } + else -> emptyPromise() + } + private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection): Promise<*, Exception> { - Log.d(TAG, "registerV1 requested") + android.util.Log.d( + TAG, + "doRegister() called with: token = $token, publicKey = $publicKey, device = $device, legacyGroupPublicKeys = $legacyGroupPublicKeys" + ) token ?: return emptyPromise() publicKey ?: return emptyPromise() From a27f81db30a58961ef4d3fd629b5f007e87f59db Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 4 Aug 2023 13:36:09 +0930 Subject: [PATCH 082/112] Fix Huawei push notifications --- .../notifications/HuaweiPushManager.kt | 7 ++- .../HuaweiPushNotificationService.kt | 41 ++++++++++------ .../securesms/ApplicationContext.java | 4 +- .../notifications/FcmTokenManager.kt | 2 +- .../securesms/notifications/PushHandler.kt | 2 +- .../securesms/onboarding/PNModeActivity.kt | 2 +- .../NotificationsPreferenceFragment.java | 4 +- .../notifications/FirebasePushManager.kt | 4 +- .../notifications/PushManagerV1.kt | 8 ++-- .../utilities/TextSecurePreferences.kt | 48 +++++++++---------- 10 files changed, 70 insertions(+), 52 deletions(-) diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt index 26da350372..c6f0161a43 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt @@ -25,6 +25,8 @@ class HuaweiPushManager @Inject constructor( @Synchronized override fun refresh(force: Boolean) { + Log.d(TAG, "refresh() called with: force = $force") + val huaweiPushInstanceIdJob = huaweiPushInstanceIdJob huaweiPushInstanceIdJob?.apply { @@ -35,9 +37,12 @@ class HuaweiPushManager @Inject constructor( val tokenScope = "HCM" val hmsInstanceId = HmsInstanceId.getInstance(context) - genericPushManager.refresh(TextSecurePreferences.getFCMToken(context), force) + Log.d(TAG, "hmsInstanceId: $hmsInstanceId") + +// genericPushManager.refresh(TextSecurePreferences.getFCMToken(context), force) MainScope().launch(Dispatchers.IO) { + Log.d(TAG, "hmInstanceId getting token...") val token = hmsInstanceId.getToken(appId, tokenScope) Log.d(TAG, "refresh() hmsInstanceId => huawei token: $token") // diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt index 16ac969d2c..a0ce0283d4 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt @@ -5,52 +5,63 @@ import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import java.lang.Exception import javax.inject.Inject +private val TAG = HuaweiPushNotificationService::class.java.simpleName + @AndroidEntryPoint class HuaweiPushNotificationService: HmsMessageService() { init { - Log.d("pnh", "init Huawei Service") + Log.d(TAG, "init Huawei Service") } @Inject lateinit var pushManager: PushManager + @Inject lateinit var genericPushManager: GenericPushManager @Inject lateinit var pushHandler: PushHandler override fun onCreate() { - Log.d("pnh", "onCreate Huawei Service") + Log.d(TAG, "onCreate Huawei Service") super.onCreate() } - override fun onMessageDelivered(p0: String?, p1: Exception?) { - Log.d("pnh", "onMessageDelivered") - super.onMessageDelivered(p0, p1) + override fun onMessageReceived(message: RemoteMessage?) { + Log.d(TAG, "onMessageReceived: $message.") + pushHandler.onPush(message?.data?.let(Base64::decode)) } override fun onMessageSent(p0: String?) { - Log.d("pnh", "onMessageSent") + Log.d(TAG, "onMessageSent() called with: p0 = $p0") super.onMessageSent(p0) } + override fun onSendError(p0: String?, p1: Exception?) { + Log.d(TAG, "onSendError() called with: p0 = $p0, p1 = $p1") + super.onSendError(p0, p1) + } + + override fun onMessageDelivered(p0: String?, p1: Exception?) { + Log.d(TAG, "onMessageDelivered") + super.onMessageDelivered(p0, p1) + } + + override fun onNewToken(p0: String?) { - Log.d("pnh", "onNewToken") + Log.d(TAG, "onNewToken") super.onNewToken(p0) } override fun onNewToken(token: String?, bundle: Bundle?) { - Log.d("pnh", "New HCM token: $token.") - - if (token == TextSecurePreferences.getFCMToken(this)) return + Log.d(TAG, "New HCM token: $token.") TextSecurePreferences.setFCMToken(this, token) - pushManager.refresh(true) - } - override fun onMessageReceived(message: RemoteMessage?) { - Log.d("pnh", "onMessageReceived: $message.") - pushHandler.onPush(message?.dataOfMap) + + genericPushManager.refresh(token, true) } + override fun onDeletedMessages() { pushManager.refresh(true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 2fe1b1e1c7..f3cf59255f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -506,10 +506,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO firebaseInstanceIdJob.cancel(null); } String displayName = TextSecurePreferences.getProfileName(this); - boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this); + boolean isUsingFCM = TextSecurePreferences.isPushEnabled(this); TextSecurePreferences.clearAll(this); if (isMigratingToV2KeyPair) { - TextSecurePreferences.setIsUsingFCM(this, isUsingFCM); + TextSecurePreferences.setPushEnabled(this, isUsingFCM); TextSecurePreferences.setProfileName(this, displayName); } getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt index 295c8296fc..cfccb7f721 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt @@ -12,7 +12,7 @@ class FcmTokenManager @Inject constructor( ) { private val expiryManager = ExpiryManager(context) - val isUsingFCM get() = TextSecurePreferences.isUsingFCM(context) + val isUsingFCM get() = TextSecurePreferences.isPushEnabled(context) var fcmToken get() = TextSecurePreferences.getFCMToken(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt index bba89d2e96..087d1ee982 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt @@ -33,7 +33,7 @@ class PushHandler @Inject constructor(@ApplicationContext val context: Context) onPush(dataMap?.asByteArray()) } - private fun onPush(data: ByteArray?) { + fun onPush(data: ByteArray?) { if (data == null) { onPush() return diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index edaf646aaa..c6de3cd676 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -165,7 +165,7 @@ class PNModeActivity : BaseActionBarActivity() { return } - TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView)) + TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView)) val application = ApplicationContext.getInstance(this) application.startPollingIfNeeded() pushManager.refresh(true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index 228392ad76..ea0510d579 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -45,10 +45,10 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme // Set up FCM toggle String fcmKey = "pref_key_use_fcm"; - ((SwitchPreferenceCompat)findPreference(fcmKey)).setChecked(TextSecurePreferences.isUsingFCM(getContext())); + ((SwitchPreferenceCompat)findPreference(fcmKey)).setChecked(TextSecurePreferences.isPushEnabled(getContext())); this.findPreference(fcmKey) .setOnPreferenceChangeListener((preference, newValue) -> { - TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue); + TextSecurePreferences.setPushEnabled(getContext(), (boolean) newValue); pushManager.refresh(true); return true; }); diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt index 074ef379f8..8bd19de61d 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt @@ -27,7 +27,9 @@ class FirebasePushManager @Inject constructor( firebaseInstanceIdJob = getFcmInstanceId { task -> when { - task.isSuccessful -> try { task.result?.token?.let { genericPushManager.refresh(it, force).get() } } catch(e: Exception) { Log.e(TAG, "refresh() failed", e) } + task.isSuccessful -> try { task.result?.token?.let { + genericPushManager.refresh(it, force).get() + } } catch(e: Exception) { Log.e(TAG, "refresh() failed", e) } else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt index 3e82bda4a3..d1a822ae3d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt @@ -28,7 +28,7 @@ object PushManagerV1 { fun register( device: Device, - isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), + isUsingFCM: Boolean = TextSecurePreferences.isPushEnabled(context), token: String? = TextSecurePreferences.getFCMToken(context), publicKey: String? = TextSecurePreferences.getLocalNumber(context), legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() @@ -94,7 +94,7 @@ object PushManagerV1 { sendOnionRequest(request) success { when (it.code) { - null, 0 -> throw Exception("error: ${it.message}.") + null, 0 -> Log.d(TAG, "error: ${it.message}.") else -> Log.d(TAG, "unregisterV1 success") } } @@ -105,7 +105,7 @@ object PushManagerV1 { fun subscribeGroup( closedGroupPublicKey: String, - isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), + isUsingFCM: Boolean = TextSecurePreferences.isPushEnabled(context), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! ) = if (isUsingFCM) { performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey) @@ -113,7 +113,7 @@ object PushManagerV1 { fun unsubscribeGroup( closedGroupPublicKey: String, - isUsingFCM: Boolean = TextSecurePreferences.isUsingFCM(context), + isUsingFCM: Boolean = TextSecurePreferences.isPushEnabled(context), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! ) = if (isUsingFCM) { performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey) diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 349546838d..ee17fb2c26 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -37,12 +37,12 @@ interface TextSecurePreferences { fun setLastConfigurationSyncTime(value: Long) fun getConfigurationMessageSynced(): Boolean fun setConfigurationMessageSynced(value: Boolean) - fun isUsingFCM(): Boolean - fun setIsUsingFCM(value: Boolean) - fun getFCMToken(): String? - fun setFCMToken(value: String) - fun getLastFCMUploadTime(): Long - fun setLastFCMUploadTime(value: Long) + fun isPushEnabled(): Boolean + fun setPushEnabled(value: Boolean) + fun getPushToken(): String? + fun setPushToken(value: String) + fun getPushRegisterTime(): Long + fun setPushRegisterTime(value: Long) fun isScreenLockEnabled(): Boolean fun setScreenLockEnabled(value: Boolean) fun getScreenLockTimeout(): Long @@ -251,9 +251,9 @@ interface TextSecurePreferences { const val LINK_PREVIEWS = "pref_link_previews" const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning" const val GIF_GRID_LAYOUT = "pref_gif_grid_layout" - const val IS_USING_FCM = "pref_is_using_fcm" + const val IS_PUSH_ENABLED = "pref_is_using_fcm" const val FCM_TOKEN = "pref_fcm_token_2" - const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2" + const val PUSH_REGISTER_TIME = "pref_last_fcm_token_upload_time_2" const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time" const val CONFIGURATION_SYNCED = "pref_configuration_synced" const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time" @@ -309,13 +309,13 @@ interface TextSecurePreferences { } @JvmStatic - fun isUsingFCM(context: Context): Boolean { - return getBooleanPreference(context, IS_USING_FCM, false) + fun isPushEnabled(context: Context): Boolean { + return getBooleanPreference(context, IS_PUSH_ENABLED, false) } @JvmStatic - fun setIsUsingFCM(context: Context, value: Boolean) { - setBooleanPreference(context, IS_USING_FCM, value) + fun setPushEnabled(context: Context, value: Boolean) { + setBooleanPreference(context, IS_PUSH_ENABLED, value) } @JvmStatic @@ -329,11 +329,11 @@ interface TextSecurePreferences { } fun getLastFCMUploadTime(context: Context): Long { - return getLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, 0) + return getLongPreference(context, PUSH_REGISTER_TIME, 0) } fun setLastFCMUploadTime(context: Context, value: Long) { - setLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, value) + setLongPreference(context, PUSH_REGISTER_TIME, value) } // endregion @@ -1032,28 +1032,28 @@ class AppTextSecurePreferences @Inject constructor( TextSecurePreferences._events.tryEmit(TextSecurePreferences.CONFIGURATION_SYNCED) } - override fun isUsingFCM(): Boolean { - return getBooleanPreference(TextSecurePreferences.IS_USING_FCM, false) + override fun isPushEnabled(): Boolean { + return getBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, false) } - override fun setIsUsingFCM(value: Boolean) { - setBooleanPreference(TextSecurePreferences.IS_USING_FCM, value) + override fun setPushEnabled(value: Boolean) { + setBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, value) } - override fun getFCMToken(): String? { + override fun getPushToken(): String? { return getStringPreference(TextSecurePreferences.FCM_TOKEN, "") } - override fun setFCMToken(value: String) { + override fun setPushToken(value: String) { setStringPreference(TextSecurePreferences.FCM_TOKEN, value) } - override fun getLastFCMUploadTime(): Long { - return getLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, 0) + override fun getPushRegisterTime(): Long { + return getLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, 0) } - override fun setLastFCMUploadTime(value: Long) { - setLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, value) + override fun setPushRegisterTime(value: Long) { + setLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, value) } override fun isScreenLockEnabled(): Boolean { From bcf925c132fcbded7b225f25536c47afb4504acb Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 6 Aug 2023 22:22:39 +0930 Subject: [PATCH 083/112] Cleanup --- app/src/huawei/AndroidManifest.xml | 2 +- .../notifications/HuaweiPushManager.kt | 52 ------------------- .../notifications/HuaweiPushModule.kt | 7 +-- ...icationService.kt => HuaweiPushService.kt} | 17 +++--- .../notifications/HuaweiTokenFetcher.kt | 27 ++++++++++ .../securesms/ApplicationContext.java | 6 +-- .../securesms/database/Storage.kt | 4 +- .../securesms/dependencies/PushComponent.kt | 2 +- .../securesms/groups/ClosedGroupManager.kt | 4 +- .../securesms/home/HomeActivity.kt | 6 +-- .../securesms/notifications/ExpiryManager.kt | 4 +- .../{PushHandler.kt => PushReceiver.kt} | 2 +- ...FcmTokenManager.kt => PushTokenManager.kt} | 11 ++-- .../securesms/notifications/TokenFetcher.kt | 7 +++ .../securesms/onboarding/PNModeActivity.kt | 5 +- .../NotificationsPreferenceFragment.java | 6 +-- ...{GenericPushManager.kt => PushRegistry.kt} | 30 ++++++++--- .../{PushManagerV2.kt => PushRegistryV2.kt} | 4 +- app/src/play/AndroidManifest.xml | 2 +- .../securesms/notifications/FcmUtils.kt | 16 ------ .../notifications/FirebasePushManager.kt | 37 ------------- .../notifications/FirebasePushModule.kt | 2 +- ...ationService.kt => FirebasePushService.kt} | 19 ++++--- .../notifications/FirebaseTokenFetcher.kt | 45 ++++++++++++++++ .../notifications/NoOpPushManager.kt | 10 ---- .../securesms/notifications/NoOpPushModule.kt | 4 +- .../MessageSenderClosedGroupHandler.kt | 4 +- .../ReceivedMessageHandler.kt | 6 +-- .../{PushManagerV1.kt => PushRegistryV1.kt} | 8 +-- .../utilities/TextSecurePreferences.kt | 18 +++---- .../utilities/ExternalStorageUtil.kt | 13 ++--- 31 files changed, 176 insertions(+), 204 deletions(-) delete mode 100644 app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt rename app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/{HuaweiPushNotificationService.kt => HuaweiPushService.kt} (74%) create mode 100644 app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt rename app/src/main/java/org/thoughtcrime/securesms/notifications/{PushHandler.kt => PushReceiver.kt} (98%) rename app/src/main/java/org/thoughtcrime/securesms/notifications/{FcmTokenManager.kt => PushTokenManager.kt} (68%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt rename app/src/main/kotlin/org/thoughtcrime/securesms/notifications/{GenericPushManager.kt => PushRegistry.kt} (79%) rename app/src/main/kotlin/org/thoughtcrime/securesms/notifications/{PushManagerV2.kt => PushRegistryV2.kt} (96%) delete mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt delete mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt rename app/src/play/kotlin/org/thoughtcrime/securesms/notifications/{FirebasePushNotificationService.kt => FirebasePushService.kt} (60%) create mode 100644 app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt delete mode 100644 app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt rename libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/{PushManagerV1.kt => PushRegistryV1.kt} (96%) diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml index 57279978c1..0483deac1c 100644 --- a/app/src/huawei/AndroidManifest.xml +++ b/app/src/huawei/AndroidManifest.xml @@ -4,7 +4,7 @@ diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt deleted file mode 100644 index c6f0161a43..0000000000 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushManager.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import android.content.Context -import android.util.Log -import com.huawei.hmf.tasks.Tasks -import com.huawei.hms.aaid.HmsInstanceId -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.session.libsession.utilities.TextSecurePreferences -import javax.inject.Inject -import javax.inject.Singleton - -private val TAG = HuaweiPushManager::class.java.name - -@Singleton -class HuaweiPushManager @Inject constructor( - @ApplicationContext private val context: Context, - private val genericPushManager: GenericPushManager -): PushManager { - private var huaweiPushInstanceIdJob: Job? = null - - @Synchronized - override fun refresh(force: Boolean) { - Log.d(TAG, "refresh() called with: force = $force") - - val huaweiPushInstanceIdJob = huaweiPushInstanceIdJob - - huaweiPushInstanceIdJob?.apply { - if (force) cancel() else if (isActive) return - } - - val appId = "107205081" - val tokenScope = "HCM" - val hmsInstanceId = HmsInstanceId.getInstance(context) - - Log.d(TAG, "hmsInstanceId: $hmsInstanceId") - -// genericPushManager.refresh(TextSecurePreferences.getFCMToken(context), force) - - MainScope().launch(Dispatchers.IO) { - Log.d(TAG, "hmInstanceId getting token...") - val token = hmsInstanceId.getToken(appId, tokenScope) - Log.d(TAG, "refresh() hmsInstanceId => huawei token: $token") -// -//// genericPushManager.refresh(token, force) - } - } -} diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt index c1be786afc..26a484df16 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushModule.kt @@ -1,16 +1,13 @@ package org.thoughtcrime.securesms.notifications -import android.content.Context import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton + @Module @InstallIn(SingletonComponent::class) abstract class HuaweiBindingModule { @Binds - abstract fun bindPushManager(pushManager: HuaweiPushManager): PushManager + abstract fun bindTokenFetcher(tokenFetcher: HuaweiTokenFetcher): TokenFetcher } diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt similarity index 74% rename from app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt rename to app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt index a0ce0283d4..2c3512acc7 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushNotificationService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt @@ -10,18 +10,17 @@ import org.session.libsignal.utilities.Log import java.lang.Exception import javax.inject.Inject -private val TAG = HuaweiPushNotificationService::class.java.simpleName +private val TAG = HuaweiPushService::class.java.simpleName @AndroidEntryPoint -class HuaweiPushNotificationService: HmsMessageService() { +class HuaweiPushService: HmsMessageService() { init { Log.d(TAG, "init Huawei Service") } - @Inject lateinit var pushManager: PushManager - @Inject lateinit var genericPushManager: GenericPushManager - @Inject lateinit var pushHandler: PushHandler + @Inject lateinit var pushRegistry: PushRegistry + @Inject lateinit var pushReceiver: PushReceiver override fun onCreate() { Log.d(TAG, "onCreate Huawei Service") @@ -30,7 +29,7 @@ class HuaweiPushNotificationService: HmsMessageService() { override fun onMessageReceived(message: RemoteMessage?) { Log.d(TAG, "onMessageReceived: $message.") - pushHandler.onPush(message?.data?.let(Base64::decode)) + pushReceiver.onPush(message?.data?.let(Base64::decode)) } override fun onMessageSent(p0: String?) { @@ -57,12 +56,12 @@ class HuaweiPushNotificationService: HmsMessageService() { override fun onNewToken(token: String?, bundle: Bundle?) { Log.d(TAG, "New HCM token: $token.") - TextSecurePreferences.setFCMToken(this, token) + TextSecurePreferences.setPushToken(this, token) - genericPushManager.refresh(token, true) + pushRegistry.refresh(token, true) } override fun onDeletedMessages() { - pushManager.refresh(true) + pushRegistry.refresh(true) } } diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt new file mode 100644 index 0000000000..e3b6afe4cc --- /dev/null +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.notifications + +import android.content.Context +import com.huawei.hms.aaid.HmsInstanceId +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class HuaweiTokenFetcher @Inject constructor( + @ApplicationContext private val context: Context +): TokenFetcher { + override fun fetch(): Job { + val hmsInstanceId = HmsInstanceId.getInstance(context) + + return MainScope().launch(Dispatchers.IO) { + val appId = "107205081" + val tokenScope = "HCM" + // getToken returns an empty string, but triggers the service to initialize. + hmsInstanceId.getToken(appId, tokenScope) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index f3cf59255f..73b0af55f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -76,7 +76,7 @@ import org.thoughtcrime.securesms.notifications.BackgroundPollWorker; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; -import org.thoughtcrime.securesms.notifications.PushManager; +import org.thoughtcrime.securesms.notifications.PushRegistry; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -146,7 +146,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO @Inject Device device; @Inject MessageDataProvider messageDataProvider; @Inject TextSecurePreferences textSecurePreferences; - @Inject PushManager pushManager; + @Inject PushRegistry pushRegistry; @Inject ConfigFactory configFactory; CallMessageProcessor callMessageProcessor; MessagingModuleConfiguration messagingModuleConfiguration; @@ -429,7 +429,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO private static class ProviderInitializationException extends RuntimeException { } public void registerForPnIfNeeded(final Boolean force) { - pushManager.refresh(force); + pushRegistry.refresh(force); } private void setUpPollingIfNeeded() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 5a95ed64f6..9b038b9955 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -50,7 +50,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SessionId @@ -591,7 +591,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co val expireTimer = group.disappearingTimer setExpirationTimer(groupId, expireTimer.toInt()) // Notify the PN server - PushManagerV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey) + PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey) // Notify the user val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId)) threadDb.setDate(threadID, formationTimestamp) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt index cd4e00416c..69baf1f208 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt @@ -8,5 +8,5 @@ import org.thoughtcrime.securesms.notifications.PushManager @EntryPoint @InstallIn(SingletonComponent::class) interface PushComponent { - fun providePushManager(): PushManager + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt index 46142a4c58..f8e64dd381 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.groups import android.content.Context import network.loki.messenger.libsession_util.ConfigBase import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupRecord @@ -24,7 +24,7 @@ object ClosedGroupManager { storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) // Notify the PN server - PushManagerV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey) + PushRegistryV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey) // Stop polling ClosedGroupPollerV2.shared.stopPolling(groupPublicKey) storage.cancelPendingMessageSendJobs(threadId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index c64d6ef071..59f324b525 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -67,7 +67,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.notifications.PushManager +import org.thoughtcrime.securesms.notifications.PushRegistry import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.permissions.Permissions @@ -107,7 +107,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), @Inject lateinit var groupDatabase: GroupDatabase @Inject lateinit var textSecurePreferences: TextSecurePreferences @Inject lateinit var configFactory: ConfigFactory - @Inject lateinit var pushManager: PushManager + @Inject lateinit var pushRegistry: PushRegistry private val globalSearchViewModel by viewModels() private val homeViewModel by viewModels() @@ -232,7 +232,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), (applicationContext as ApplicationContext).startPollingIfNeeded() // update things based on TextSecurePrefs (profile info etc) // Set up remaining components if needed - pushManager.refresh(false) + pushRegistry.refresh(false) if (textSecurePreferences.getLocalNumber() != null) { OpenGroupManager.startPolling() JobQueue.shared.resumePendingJobs() diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt index 45318d43ad..2b3f0aa860 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt @@ -21,8 +21,8 @@ class ExpiryManager( } private var time - get() = TextSecurePreferences.getLastFCMUploadTime(context) - set(value) = TextSecurePreferences.setLastFCMUploadTime(context, value) + get() = TextSecurePreferences.getPushRegisterTime(context) + set(value) = TextSecurePreferences.setPushRegisterTime(context, value) private fun currentTime() = System.currentTimeMillis() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index 087d1ee982..56871ba64e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -26,7 +26,7 @@ import javax.inject.Inject private const val TAG = "PushHandler" -class PushHandler @Inject constructor(@ApplicationContext val context: Context) { +class PushReceiver @Inject constructor(@ApplicationContext val context: Context) { private val sodium = LazySodiumAndroid(SodiumAndroid()) fun onPush(dataMap: Map?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushTokenManager.kt similarity index 68% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/PushTokenManager.kt index cfccb7f721..45469a3dd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/FcmTokenManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushTokenManager.kt @@ -2,22 +2,24 @@ package org.thoughtcrime.securesms.notifications import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Job import org.session.libsession.utilities.TextSecurePreferences import javax.inject.Inject import javax.inject.Singleton @Singleton -class FcmTokenManager @Inject constructor( +class PushTokenManager @Inject constructor( @ApplicationContext private val context: Context, + private val tokenFetcher: TokenFetcher ) { private val expiryManager = ExpiryManager(context) - val isUsingFCM get() = TextSecurePreferences.isPushEnabled(context) + val isPushEnabled get() = TextSecurePreferences.isPushEnabled(context) var fcmToken - get() = TextSecurePreferences.getFCMToken(context) + get() = TextSecurePreferences.getPushToken(context) set(value) { - TextSecurePreferences.setFCMToken(context, value) + TextSecurePreferences.setPushToken(context, value) if (value != null) markTime() else clearTime() } @@ -28,4 +30,5 @@ class FcmTokenManager @Inject constructor( private fun isExpired() = expiryManager.isExpired() fun isInvalid(): Boolean = fcmToken == null || isExpired() + fun fetchToken(): Job = tokenFetcher.fetch() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt new file mode 100644 index 0000000000..d8657e95d3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.notifications + +import kotlinx.coroutines.Job + +interface TokenFetcher { + fun fetch(): Job +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index c6de3cd676..e4e8e6a9a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.notifications.PushManager +import org.thoughtcrime.securesms.notifications.PushRegistry import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.PNModeView @@ -34,7 +35,7 @@ import javax.inject.Inject @AndroidEntryPoint class PNModeActivity : BaseActionBarActivity() { - @Inject lateinit var pushManager: PushManager + @Inject lateinit var pushRegistry: PushRegistry private lateinit var binding: ActivityPnModeBinding private var selectedOptionView: PNModeView? = null @@ -168,7 +169,7 @@ class PNModeActivity : BaseActionBarActivity() { TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView)) val application = ApplicationContext.getInstance(this) application.startPollingIfNeeded() - pushManager.refresh(true) + pushRegistry.refresh(true) val intent = Intent(this, HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.putExtra(HomeActivity.FROM_ONBOARDING, true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index ea0510d579..4cb316f40f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -23,7 +23,7 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.notifications.PushManager; +import org.thoughtcrime.securesms.notifications.PushRegistry; import javax.inject.Inject; @@ -37,7 +37,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName(); @Inject - PushManager pushManager; + PushRegistry pushRegistry; @Override public void onCreate(Bundle paramBundle) { @@ -49,7 +49,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme this.findPreference(fcmKey) .setOnPreferenceChangeListener((preference, newValue) -> { TextSecurePreferences.setPushEnabled(getContext(), (boolean) newValue); - pushManager.refresh(true); + pushRegistry.refresh(true); return true; }); diff --git a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistry.kt similarity index 79% rename from app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistry.kt index f3e0443eed..872b2956a3 100644 --- a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/GenericPushManager.kt +++ b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistry.kt @@ -3,9 +3,10 @@ package org.thoughtcrime.securesms.notifications import android.content.Context import com.goterl.lazysodium.utils.KeyPair import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Job import nl.komponents.kovenant.Promise import nl.komponents.kovenant.combine.and -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.utilities.Device import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log @@ -18,12 +19,25 @@ import javax.inject.Singleton private const val TAG = "GenericPushManager" @Singleton -class GenericPushManager @Inject constructor( +class PushRegistry @Inject constructor( @ApplicationContext private val context: Context, private val device: Device, - private val tokenManager: FcmTokenManager, - private val pushManagerV2: PushManagerV2, + private val tokenManager: PushTokenManager, + private val pushRegistryV2: PushRegistryV2, ) { + + private var firebaseInstanceIdJob: Job? = null + + fun refresh(force: Boolean) { + Log.d(TAG, "refresh() called with: force = $force") + + firebaseInstanceIdJob?.apply { + if (force) cancel() else if (isActive) return + } + + firebaseInstanceIdJob = tokenManager.fetchToken() + } + fun refresh(token: String?, force: Boolean): Promise<*, Exception> { Log.d(TAG, "refresh($token, $force) called") @@ -32,7 +46,7 @@ class GenericPushManager @Inject constructor( val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() return when { - tokenManager.isUsingFCM -> register(force, token, userPublicKey, userEdKey) + tokenManager.isPushEnabled -> register(force, token, userPublicKey, userEdKey) tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey) else -> emptyPromise() } @@ -68,7 +82,7 @@ class GenericPushManager @Inject constructor( "register() called with: token = $token, publicKey = $publicKey, userEd25519Key = $userEd25519Key, namespaces = $namespaces" ) - val v1 = PushManagerV1.register( + val v1 = PushRegistryV1.register( device = device, token = token, publicKey = publicKey @@ -76,7 +90,7 @@ class GenericPushManager @Inject constructor( Log.e(TAG, "register v1 failed", it) } - val v2 = pushManagerV2.register( + val v2 = pushRegistryV2.register( device, token, publicKey, userEd25519Key, namespaces ) fail { Log.e(TAG, "register v2 failed", it) @@ -92,7 +106,7 @@ class GenericPushManager @Inject constructor( token: String, userPublicKey: String, userEdKey: KeyPair - ): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister( + ): Promise<*, Exception> = PushRegistryV1.unregister() and pushRegistryV2.unregister( device, token, userPublicKey, userEdKey ) fail { Log.e(TAG, "unregisterBoth failed", it) diff --git a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt similarity index 96% rename from app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt rename to app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt index 93e6fc6819..d03af92e6d 100644 --- a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushManagerV2.kt +++ b/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt @@ -28,7 +28,7 @@ private const val TAG = "PushManagerV2" private const val maxRetryCount = 4 @Singleton -class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) { +class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) { private val sodium = LazySodiumAndroid(SodiumAndroid()) fun register( @@ -38,7 +38,7 @@ class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) { userEd25519Key: KeyPair, namespaces: List ): Promise { - val pnKey = pushHandler.getOrCreateNotificationKey() + val pnKey = pushReceiver.getOrCreateNotificationKey() val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s // if we want to support passing namespace list, here is the place to do it diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml index 8b975b0634..b3e5ead475 100644 --- a/app/src/play/AndroidManifest.xml +++ b/app/src/play/AndroidManifest.xml @@ -4,7 +4,7 @@ diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt deleted file mode 100644 index af3f49a26a..0000000000 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FcmUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -@file:JvmName("FcmUtils") -package org.thoughtcrime.securesms.notifications - -import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.Tasks -import com.google.firebase.iid.FirebaseInstanceId -import com.google.firebase.iid.InstanceIdResult -import kotlinx.coroutines.* - - -fun getFcmInstanceId(body: (Task)->Unit): Job = MainScope().launch(Dispatchers.IO) { - val task = FirebaseInstanceId.getInstance().instanceId - Tasks.await(task) - if (!isActive) return@launch // don't 'complete' task if we were canceled - body(task) -} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt deleted file mode 100644 index 8bd19de61d..0000000000 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushManager.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import kotlinx.coroutines.Job -import org.session.libsignal.utilities.Log -import javax.inject.Inject -import javax.inject.Singleton - -private const val TAG = "FirebasePushManager" - -@Singleton -class FirebasePushManager @Inject constructor( - private val genericPushManager: GenericPushManager -): PushManager { - - private var firebaseInstanceIdJob: Job? = null - - @Synchronized - override fun refresh(force: Boolean) { - Log.d(TAG, "refresh() called with: force = $force") - - firebaseInstanceIdJob?.apply { - when { - force -> cancel() - isActive -> return - } - } - - firebaseInstanceIdJob = getFcmInstanceId { task -> - when { - task.isSuccessful -> try { task.result?.token?.let { - genericPushManager.refresh(it, force).get() - } } catch(e: Exception) { Log.e(TAG, "refresh() failed", e) } - else -> Log.w(TAG, "getFcmInstanceId failed." + task.exception) - } - } - } -} diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt index 925ad9d4d7..2dac25072e 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushModule.kt @@ -9,5 +9,5 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) abstract class FirebaseBindingModule { @Binds - abstract fun bindPushManager(firebasePushManager: FirebasePushManager): PushManager + abstract fun bindTokenFetcher(tokenFetcher: FirebaseTokenFetcher): TokenFetcher } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushNotificationService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt similarity index 60% rename from app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushNotificationService.kt rename to app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt index b8640296fa..1ebf405a28 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushNotificationService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt @@ -9,26 +9,25 @@ import javax.inject.Inject private const val TAG = "FirebasePushNotificationService" @AndroidEntryPoint -class FirebasePushNotificationService : FirebaseMessagingService() { +class FirebasePushService : FirebaseMessagingService() { - @Inject lateinit var pushManager: PushManager - @Inject lateinit var pushHandler: PushHandler + @Inject lateinit var prefs: TextSecurePreferences + @Inject lateinit var pushReceiver: PushReceiver + @Inject lateinit var pushRegistry: PushRegistry override fun onNewToken(token: String) { - super.onNewToken(token) - TextSecurePreferences.getLocalNumber(this) ?: return - if (TextSecurePreferences.getFCMToken(this) != token) { - pushManager.refresh(true) - } + if (token == prefs.getPushToken()) return + + pushRegistry.refresh(token, true) } override fun onMessageReceived(message: RemoteMessage) { Log.d(TAG, "Received a push notification.") - pushHandler.onPush(message.data) + pushReceiver.onPush(message.data) } override fun onDeletedMessages() { Log.d(TAG, "Called onDeletedMessages.") - pushManager.refresh(true) + pushRegistry.refresh(true) } } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt new file mode 100644 index 0000000000..e0aa14f6d2 --- /dev/null +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt @@ -0,0 +1,45 @@ +package org.thoughtcrime.securesms.notifications + +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.iid.InstanceIdResult +import dagger.Lazy +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.session.libsignal.utilities.Log +import javax.inject.Inject +import javax.inject.Singleton + +private val TAG = FirebaseTokenFetcher::class.java.name + +@Singleton +class FirebaseTokenFetcher @Inject constructor( + private val pushRegistry: Lazy, +): TokenFetcher { + override fun fetch(): Job = MainScope().launch(Dispatchers.IO) { + FirebaseInstanceId.getInstance().instanceId + .also(Tasks::await) + .also { if (!isActive) return@launch } // don't 'complete' task if we were canceled + .process() + } + + private fun Task.process() { + when { + isSuccessful -> try { + result?.token?.let { + pushRegistry.get().refresh(it, force = true).get() + } + } catch (e: Exception) { + onFail(e) + } + else -> exception?.let(::onFail) + } + } + + private fun onFail(e: Exception) = Log.e(TAG, "fetch failed", e) +} + diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt deleted file mode 100644 index a817e59d5d..0000000000 --- a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushManager.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import org.session.libsignal.utilities.Log - -class NoOpPushManager: PushManager { - - override fun refresh(force: Boolean) { - Log.d("NoOpPushManager", "Push notifications not supported, not registering for push notifications") - } -} diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt index 1c8f2f936c..af907b51ca 100644 --- a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt @@ -9,7 +9,5 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class NoOpPushModule { - @Provides - @Singleton - fun provideNoOpManager(): PushManager = NoOpPushManager() + } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index 022d32f170..1a7e3d8eaa 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -8,7 +8,7 @@ import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.sending_receiving.MessageSender.Error -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address @@ -94,7 +94,7 @@ fun MessageSender.create( // Add the group to the config now that it was successfully created storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair) // Notify the PN server - PushManagerV1.register(device = device, publicKey = userPublicKey) + PushRegistryV1.register(device = device, publicKey = userPublicKey) // Start polling ClosedGroupPollerV2.shared.startPolling(groupPublicKey) // Fulfill the promise diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 7c5092e5ea..b101e9db0b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -23,7 +23,7 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview -import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1 +import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SessionId @@ -556,7 +556,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli // Set expiration timer storage.setExpirationTimer(groupID, expireTimer) // Notify the PN server - PushManagerV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey) + PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey) // Notify the user if (userPublicKey == sender && !groupExists) { val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) @@ -869,7 +869,7 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou storage.setActive(groupID, false) storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) // Notify the PN server - PushManagerV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey) + PushRegistryV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey) // Stop polling ClosedGroupPollerV2.shared.stopPolling(groupPublicKey) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt similarity index 96% rename from libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt rename to libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt index d1a822ae3d..d7d84b5c61 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushManagerV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt @@ -18,8 +18,8 @@ import org.session.libsignal.utilities.retryIfNeeded import org.session.libsignal.utilities.sideEffect @SuppressLint("StaticFieldLeak") -object PushManagerV1 { - private const val TAG = "PushManagerV1" +object PushRegistryV1 { + private val TAG = PushRegistryV1::class.java.name val context = MessagingModuleConfiguration.shared.context private const val maxRetryCount = 4 @@ -29,7 +29,7 @@ object PushManagerV1 { fun register( device: Device, isUsingFCM: Boolean = TextSecurePreferences.isPushEnabled(context), - token: String? = TextSecurePreferences.getFCMToken(context), + token: String? = TextSecurePreferences.getPushToken(context), publicKey: String? = TextSecurePreferences.getLocalNumber(context), legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() ): Promise<*, Exception> = when { @@ -84,7 +84,7 @@ object PushManagerV1 { fun unregister(): Promise<*, Exception> { Log.d(TAG, "unregisterV1 requested") - val token = TextSecurePreferences.getFCMToken(context) ?: emptyPromise() + val token = TextSecurePreferences.getPushToken(context) ?: emptyPromise() return retryIfNeeded(maxRetryCount) { val parameters = mapOf("token" to token) diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index ee17fb2c26..2b92b9836e 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -252,7 +252,7 @@ interface TextSecurePreferences { const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning" const val GIF_GRID_LAYOUT = "pref_gif_grid_layout" const val IS_PUSH_ENABLED = "pref_is_using_fcm" - const val FCM_TOKEN = "pref_fcm_token_2" + const val PUSH_TOKEN = "pref_fcm_token_2" const val PUSH_REGISTER_TIME = "pref_last_fcm_token_upload_time_2" const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time" const val CONFIGURATION_SYNCED = "pref_configuration_synced" @@ -319,20 +319,20 @@ interface TextSecurePreferences { } @JvmStatic - fun getFCMToken(context: Context): String? { - return getStringPreference(context, FCM_TOKEN, "") + fun getPushToken(context: Context): String? { + return getStringPreference(context, PUSH_TOKEN, "") } @JvmStatic - fun setFCMToken(context: Context, value: String?) { - setStringPreference(context, FCM_TOKEN, value) + fun setPushToken(context: Context, value: String?) { + setStringPreference(context, PUSH_TOKEN, value) } - fun getLastFCMUploadTime(context: Context): Long { + fun getPushRegisterTime(context: Context): Long { return getLongPreference(context, PUSH_REGISTER_TIME, 0) } - fun setLastFCMUploadTime(context: Context, value: Long) { + fun setPushRegisterTime(context: Context, value: Long) { setLongPreference(context, PUSH_REGISTER_TIME, value) } @@ -1041,11 +1041,11 @@ class AppTextSecurePreferences @Inject constructor( } override fun getPushToken(): String? { - return getStringPreference(TextSecurePreferences.FCM_TOKEN, "") + return getStringPreference(TextSecurePreferences.PUSH_TOKEN, "") } override fun setPushToken(value: String) { - setStringPreference(TextSecurePreferences.FCM_TOKEN, value) + setStringPreference(TextSecurePreferences.PUSH_TOKEN, value) } override fun getPushRegisterTime(): Long { diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/ExternalStorageUtil.kt b/libsignal/src/main/java/org/session/libsignal/utilities/ExternalStorageUtil.kt index a8a8fd662f..f9ccb315cb 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/ExternalStorageUtil.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/ExternalStorageUtil.kt @@ -15,7 +15,7 @@ object ExternalStorageUtil { @Throws(NoExternalStorageException::class) fun getDir(context: Context, type: String?): File { return context.getExternalFilesDir(type) - ?: throw NoExternalStorageException("External storage dir is currently unavailable: $type") + ?: throw NoExternalStorageException("External storage dir is currently unavailable: $type") } @Throws(NoExternalStorageException::class) @@ -73,10 +73,7 @@ object ExternalStorageUtil { } @JvmStatic - fun getCleanFileName(fileName: String?): String? { - var fileName = fileName ?: return null - fileName = fileName.replace('\u202D', '\uFFFD') - fileName = fileName.replace('\u202E', '\uFFFD') - return fileName - } -} \ No newline at end of file + fun getCleanFileName(fileName: String?): String? = + fileName?.replace('\u202D', '\uFFFD') + ?.replace('\u202E', '\uFFFD') +} From 24f7bb2b45ff90add1ccb6697b73b1ff2e42e4a0 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 6 Aug 2023 22:39:49 +0930 Subject: [PATCH 084/112] Move files --- .../securesms/notifications/PushRegistry.kt | 10 +++++----- .../securesms/notifications/PushRegistryV2.kt | 11 ++++++++--- .../notifications/FirebaseTokenFetcher.kt | 16 +++++++--------- 3 files changed, 20 insertions(+), 17 deletions(-) rename app/src/main/{kotlin => java}/org/thoughtcrime/securesms/notifications/PushRegistry.kt (94%) rename app/src/main/{kotlin => java}/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt (90%) diff --git a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistry.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt similarity index 94% rename from app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistry.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt index 872b2956a3..9a625f133c 100644 --- a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistry.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.crypto.KeyPairUtilities import javax.inject.Inject import javax.inject.Singleton -private const val TAG = "GenericPushManager" +private val TAG = PushRegistry::class.java.name @Singleton class PushRegistry @Inject constructor( @@ -26,16 +26,16 @@ class PushRegistry @Inject constructor( private val pushRegistryV2: PushRegistryV2, ) { - private var firebaseInstanceIdJob: Job? = null + private var pushRegistrationJob: Job? = null fun refresh(force: Boolean) { Log.d(TAG, "refresh() called with: force = $force") - firebaseInstanceIdJob?.apply { + pushRegistrationJob?.apply { if (force) cancel() else if (isActive) return } - firebaseInstanceIdJob = tokenManager.fetchToken() + pushRegistrationJob = tokenManager.fetchToken() } fun refresh(token: String?, force: Boolean): Promise<*, Exception> { @@ -113,4 +113,4 @@ class PushRegistry @Inject constructor( } success { tokenManager.fcmToken = null } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt similarity index 90% rename from app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt index d03af92e6d..4bef45ff97 100644 --- a/app/src/main/kotlin/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt @@ -12,7 +12,12 @@ import nl.komponents.kovenant.functional.map import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody -import org.session.libsession.messaging.sending_receiving.notifications.* +import org.session.libsession.messaging.sending_receiving.notifications.Response +import org.session.libsession.messaging.sending_receiving.notifications.Server +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest +import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse +import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.Version @@ -24,7 +29,7 @@ import org.session.libsignal.utilities.retryIfNeeded import javax.inject.Inject import javax.inject.Singleton -private const val TAG = "PushManagerV2" +private val TAG = PushRegistryV2::class.java.name private const val maxRetryCount = 4 @Singleton @@ -108,4 +113,4 @@ class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) .also { if (it.isFailure()) throw Exception("error: ${it.message}.") } } } -} +} \ No newline at end of file diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt index e0aa14f6d2..70f3fb4522 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt @@ -27,17 +27,15 @@ class FirebaseTokenFetcher @Inject constructor( .process() } - private fun Task.process() { - when { - isSuccessful -> try { - result?.token?.let { - pushRegistry.get().refresh(it, force = true).get() - } - } catch (e: Exception) { - onFail(e) + private fun Task.process() = when { + isSuccessful -> try { + result?.token?.let { + pushRegistry.get().refresh(it, force = true).get() } - else -> exception?.let(::onFail) + } catch (e: Exception) { + onFail(e) } + else -> exception?.let(::onFail) } private fun onFail(e: Exception) = Log.e(TAG, "fetch failed", e) From d6380c5e6366a4f2f20d992d852834f6ff3fc1dc Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 7 Aug 2023 09:50:51 +0930 Subject: [PATCH 085/112] Fix website flavor --- .../securesms/notifications/NoOpPushModule.kt | 8 +++++--- .../securesms/notifications/NoOpTokenFetcher.kt | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt index af907b51ca..59873c82be 100644 --- a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpPushModule.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.notifications +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -8,6 +9,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class NoOpPushModule { - -} \ No newline at end of file +abstract class NoOpPushModule { + @Binds + abstract fun bindTokenFetcher(tokenFetcher: NoOpTokenFetcher): TokenFetcher +} diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt new file mode 100644 index 0000000000..1705eefe09 --- /dev/null +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.notifications + +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NoOpTokenFetcher @Inject constructor() : TokenFetcher { + override fun fetch(): Job = MainScope().launch { } +} From 9cf99480d6019e7400909424f8b85424b3b41db8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 7 Aug 2023 23:01:03 +0930 Subject: [PATCH 086/112] Fix shareLogs() canceled by early dismiss() (#1295) --- .../org/thoughtcrime/securesms/SessionDialogBuilder.kt | 10 +++++----- .../securesms/preferences/ShareLogsDialog.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt index 141a98e4ac..3fb5e2787c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt @@ -111,16 +111,16 @@ class SessionDialogBuilder(val context: Context) { text, contentDescription, R.style.Widget_Session_Button_Dialog_DestructiveText, - listener - ) + ) { listener() } - fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok, listener = listener) - fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button, listener = listener) + fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok) { listener() } + fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button) { listener() } fun button( @StringRes text: Int, @StringRes contentDescriptionRes: Int = text, @StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText, + dismiss: Boolean = false, listener: (() -> Unit) = {} ) = Button(context, null, 0, style).apply { setText(text) @@ -129,7 +129,7 @@ class SessionDialogBuilder(val context: Context) { .apply { setMargins(toPx(20, resources)) } setOnClickListener { listener.invoke() - dismiss() + if (dismiss) dismiss() } }.let(buttonLayout::addView) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt index e3be429e3a..2dc5e75d98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt @@ -40,7 +40,7 @@ class ShareLogsDialog : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { title(R.string.dialog_share_logs_title) text(R.string.dialog_share_logs_explanation) - button(R.string.share) { shareLogs() } + button(R.string.share, dismiss = false) { shareLogs() } cancelButton { dismiss() } } From 2f42fe9d0d80b5424bcf92473c0b4df277f88f43 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 8 Aug 2023 14:40:36 +0930 Subject: [PATCH 087/112] Remove unused classes (#1289) --- .../securesms/BindableConversationItem.java | 39 ------ .../MessageDetailsRecipientAdapter.java | 111 ------------------ .../thoughtcrime/securesms/Unbindable.java | 5 - .../securesms/components/Outliner.java | 50 -------- .../emoji/parsing/EmojiPageBitmap.java | 107 ----------------- .../SmoothScrollingLinearLayoutManager.java | 31 ----- .../FastCursorRecyclerViewAdapter.java | 110 ----------------- .../widgets/ContactPreference.java | 81 ------------- .../widgets/NotificationSettingsPreference.kt | 16 --- .../widgets/ProgressPreference.java | 61 ---------- .../any/ThisMessageEmojiPageModel.java | 63 ---------- .../securesms/sms/TelephonyServiceState.java | 92 --------------- .../securesms/util/ContextProvider.kt | 37 ------ .../util/LongClickMovementMethod.java | 104 ---------------- .../securesms/util/WakeLockUtil.java | 54 --------- .../securesms/webrtc/AudioEvent.kt | 5 - 16 files changed, 966 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/Unbindable.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/SmoothScrollingLinearLayoutManager.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/FastCursorRecyclerViewAdapter.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ContactPreference.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationSettingsPreference.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProgressPreference.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/sms/TelephonyServiceState.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/ContextProvider.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioEvent.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java deleted file mode 100644 index 93313e5270..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.thoughtcrime.securesms; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - - -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.session.libsignal.utilities.guava.Optional; - -import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; - -import java.util.Locale; -import java.util.Set; - -public interface BindableConversationItem extends Unbindable { - void bind(@NonNull MessageRecord messageRecord, - @NonNull Optional previousMessageRecord, - @NonNull Optional nextMessageRecord, - @NonNull GlideRequests glideRequests, - @NonNull Locale locale, - @NonNull Set batchSelected, - @NonNull Recipient recipients, - @Nullable String searchQuery, - boolean pulseHighlight); - - MessageRecord getMessageRecord(); - - void setEventListener(@Nullable EventListener listener); - - interface EventListener { - void onQuoteClicked(MmsMessageRecord messageRecord); - void onLinkPreviewClicked(@NonNull LinkPreview linkPreview); - void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java deleted file mode 100644 index ca6cf8f6c8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.thoughtcrime.securesms; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.BaseAdapter; - -import androidx.annotation.NonNull; - - -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.contacts.UserView; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.Conversions; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.List; - -class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener { - - private final Context context; - private final GlideRequests glideRequests; - private final MessageRecord record; - private final List members; - private final boolean isPushGroup; - - MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, - @NonNull MessageRecord record, @NonNull List members, - boolean isPushGroup) - { - this.context = context; - this.glideRequests = glideRequests; - this.record = record; - this.isPushGroup = isPushGroup; - this.members = members; - } - - @Override - public int getCount() { - return members.size(); - } - - @Override - public Object getItem(int position) { - return members.get(position); - } - - @Override - public long getItemId(int position) { - try { - return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(members.get(position).recipient.getAddress().serialize().getBytes())); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - UserView result = new UserView(context); - Recipient recipient = members.get(position).getRecipient(); - result.setOpenGroupThreadID(record.getThreadId()); - result.bind(recipient, glideRequests, UserView.ActionIndicator.None, false); - return result; - } - - @Override - public void onMovedToScrapHeap(View view) { - ((UserView)view).unbind(); - } - - - static class RecipientDeliveryStatus { - - enum Status { - UNKNOWN, PENDING, SENT, DELIVERED, READ - } - - private final Recipient recipient; - private final Status deliveryStatus; - private final boolean isUnidentified; - private final long timestamp; - - RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) { - this.recipient = recipient; - this.deliveryStatus = deliveryStatus; - this.isUnidentified = isUnidentified; - this.timestamp = timestamp; - } - - Status getDeliveryStatus() { - return deliveryStatus; - } - - boolean isUnidentified() { - return isUnidentified; - } - - public long getTimestamp() { - return timestamp; - } - - public Recipient getRecipient() { - return recipient; - } - - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/Unbindable.java b/app/src/main/java/org/thoughtcrime/securesms/Unbindable.java deleted file mode 100644 index 3dd5cd8cc0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/Unbindable.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.thoughtcrime.securesms; - -public interface Unbindable { - public void unbind(); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java b/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java deleted file mode 100644 index cb6cfc7abf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/Outliner.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.RectF; - -import androidx.annotation.ColorInt; - -public class Outliner { - - private final float[] radii = new float[8]; - private final Path corners = new Path(); - private final RectF bounds = new RectF(); - private final Paint outlinePaint = new Paint(); - { - outlinePaint.setStyle(Paint.Style.STROKE); - outlinePaint.setStrokeWidth(1f); - outlinePaint.setAntiAlias(true); - } - - public void setColor(@ColorInt int color) { - outlinePaint.setColor(color); - } - - public void draw(Canvas canvas) { - final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2; - - bounds.left = halfStrokeWidth; - bounds.top = halfStrokeWidth; - bounds.right = canvas.getWidth() - halfStrokeWidth; - bounds.bottom = canvas.getHeight() - halfStrokeWidth; - - corners.reset(); - corners.addRoundRect(bounds, radii, Path.Direction.CW); - - canvas.drawPath(corners, outlinePaint); - } - - public void setRadius(int radius) { - setRadii(radius, radius, radius, radius); - } - - public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) { - radii[0] = radii[1] = topLeft; - radii[2] = radii[3] = topRight; - radii[4] = radii[5] = bottomRight; - radii[6] = radii[7] = bottomLeft; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java deleted file mode 100644 index 3c3a4fa3eb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.thoughtcrime.securesms.components.emoji.parsing; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; -import androidx.annotation.NonNull; -import org.session.libsignal.utilities.Log; - -import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; -import org.thoughtcrime.securesms.util.Stopwatch; - -import org.session.libsession.utilities.ListenableFutureTask; -import org.session.libsession.utilities.Util; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.SoftReference; -import java.util.concurrent.Callable; - -public class EmojiPageBitmap { - - private static final String TAG = EmojiPageBitmap.class.getSimpleName(); - - private final Context context; - private final EmojiPageModel model; - private final float decodeScale; - - private SoftReference bitmapReference; - private ListenableFutureTask task; - - public EmojiPageBitmap(@NonNull Context context, @NonNull EmojiPageModel model, float decodeScale) { - this.context = context.getApplicationContext(); - this.model = model; - this.decodeScale = decodeScale; - } - - @SuppressLint("StaticFieldLeak") - public ListenableFutureTask get() { - Util.assertMainThread(); - - if (bitmapReference != null && bitmapReference.get() != null) { - return new ListenableFutureTask<>(bitmapReference.get()); - } else if (task != null) { - return task; - } else { - Callable callable = () -> { - try { - Log.i(TAG, "loading page " + model.getSpriteUri().toString()); - return loadPage(); - } catch (IOException ioe) { - Log.w(TAG, ioe); - } - return null; - }; - task = new ListenableFutureTask<>(callable); - new AsyncTask() { - @Override protected Void doInBackground(Void... params) { - task.run(); - return null; - } - - @Override protected void onPostExecute(Void aVoid) { - task = null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - return task; - } - - private Bitmap loadPage() throws IOException { - if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get(); - - - float scale = decodeScale; - AssetManager assetManager = context.getAssets(); - InputStream assetStream = assetManager.open(model.getSpriteUri().toString()); - BitmapFactory.Options options = new BitmapFactory.Options(); - - if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) { - Log.i(TAG, "Low memory detected. Changing sample size."); - options.inSampleSize = 2; - scale = decodeScale * 2; - } - - Stopwatch stopwatch = new Stopwatch(model.getSpriteUri().toString()); - Bitmap bitmap = BitmapFactory.decodeStream(assetStream, null, options); - stopwatch.split("decode"); - - Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int)(bitmap.getWidth() * scale), (int)(bitmap.getHeight() * scale), true); - stopwatch.split("scale"); - stopwatch.stop(TAG); - - bitmapReference = new SoftReference<>(scaledBitmap); - Log.i(TAG, "onPageLoaded(" + model.getSpriteUri().toString() + ") originalByteCount: " + bitmap.getByteCount() - + " scaledByteCount: " + scaledBitmap.getByteCount() - + " scaledSize: " + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight()); - return scaledBitmap; - } - - @Override - public @NonNull String toString() { - return model.getSpriteUri().toString(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/SmoothScrollingLinearLayoutManager.java b/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/SmoothScrollingLinearLayoutManager.java deleted file mode 100644 index a1b45ac2ae..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/SmoothScrollingLinearLayoutManager.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.thoughtcrime.securesms.components.recyclerview; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.LinearSmoothScroller; -import android.util.DisplayMetrics; - -public class SmoothScrollingLinearLayoutManager extends LinearLayoutManager { - - public SmoothScrollingLinearLayoutManager(Context context, boolean reverseLayout) { - super(context, LinearLayoutManager.VERTICAL, reverseLayout); - } - - public void smoothScrollToPosition(@NonNull Context context, int position, float millisecondsPerInch) { - final LinearSmoothScroller scroller = new LinearSmoothScroller(context) { - @Override - protected int getVerticalSnapPreference() { - return LinearSmoothScroller.SNAP_TO_END; - } - - @Override - protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { - return millisecondsPerInch / displayMetrics.densityDpi; - } - }; - - scroller.setTargetPosition(position); - startSmoothScroll(scroller); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/FastCursorRecyclerViewAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/database/FastCursorRecyclerViewAdapter.java deleted file mode 100644 index 4dfe6a20b5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/FastCursorRecyclerViewAdapter.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.thoughtcrime.securesms.database; - - -import android.content.Context; -import android.database.Cursor; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -public abstract class FastCursorRecyclerViewAdapter - extends CursorRecyclerViewAdapter -{ - private static final String TAG = FastCursorRecyclerViewAdapter.class.getSimpleName(); - - private final LinkedList fastRecords = new LinkedList<>(); - private final List releasedRecordIds = new LinkedList<>(); - - protected FastCursorRecyclerViewAdapter(Context context, Cursor cursor) { - super(context, cursor); - } - - public void addFastRecord(@NonNull T record) { - fastRecords.addFirst(record); - notifyDataSetChanged(); - } - - public void releaseFastRecord(long id) { - synchronized (releasedRecordIds) { - releasedRecordIds.add(id); - } - } - - protected void cleanFastRecords() { - synchronized (releasedRecordIds) { - Iterator releaseIdIterator = releasedRecordIds.iterator(); - - while (releaseIdIterator.hasNext()) { - long releasedId = releaseIdIterator.next(); - Iterator fastRecordIterator = fastRecords.iterator(); - - while (fastRecordIterator.hasNext()) { - if (isRecordForId(fastRecordIterator.next(), releasedId)) { - fastRecordIterator.remove(); - releaseIdIterator.remove(); - break; - } - } - } - } - } - - protected abstract T getRecordFromCursor(@NonNull Cursor cursor); - protected abstract void onBindItemViewHolder(VH viewHolder, @NonNull T record); - protected abstract long getItemId(@NonNull T record); - protected abstract int getItemViewType(@NonNull T record); - protected abstract boolean isRecordForId(@NonNull T record, long id); - - @Override - public int getItemViewType(@NonNull Cursor cursor) { - T record = getRecordFromCursor(cursor); - return getItemViewType(record); - } - - @Override - public void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor) { - T record = getRecordFromCursor(cursor); - onBindItemViewHolder(viewHolder, record); - } - - @Override - public void onBindFastAccessItemViewHolder(VH viewHolder, int position) { - int calculatedPosition = getCalculatedPosition(position); - onBindItemViewHolder(viewHolder, fastRecords.get(calculatedPosition)); - } - - @Override - protected int getFastAccessSize() { - return fastRecords.size(); - } - - protected T getRecordForPositionOrThrow(int position) { - if (isFastAccessPosition(position)) { - return fastRecords.get(getCalculatedPosition(position)); - } else { - Cursor cursor = getCursorAtPositionOrThrow(position); - return getRecordFromCursor(cursor); - } - } - - protected int getFastAccessItemViewType(int position) { - return getItemViewType(fastRecords.get(getCalculatedPosition(position))); - } - - protected boolean isFastAccessPosition(int position) { - position = getCalculatedPosition(position); - return position >= 0 && position < fastRecords.size(); - } - - protected long getFastAccessItemId(int position) { - return getItemId(fastRecords.get(getCalculatedPosition(position))); - } - - private int getCalculatedPosition(int position) { - return hasHeaderView() ? position - 1 : position; - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ContactPreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ContactPreference.java deleted file mode 100644 index f5417f3e91..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ContactPreference.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - - -import android.content.Context; -import android.graphics.PorterDuff; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageView; - -import network.loki.messenger.R; - -public class ContactPreference extends Preference { - - private ImageView messageButton; - - private Listener listener; - private boolean secure; - - public ContactPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initialize(); - } - - public ContactPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initialize(); - } - - public ContactPreference(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); - } - - public ContactPreference(Context context) { - super(context); - initialize(); - } - - private void initialize() { - setWidgetLayoutResource(R.layout.recipient_preference_contact_widget); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - this.messageButton = (ImageView) view.findViewById(R.id.message); - - if (listener != null) setListener(listener); - setSecure(secure); - } - - public void setSecure(boolean secure) { - this.secure = secure; - - int color; - - if (secure) { - color = getContext().getResources().getColor(R.color.textsecure_primary); - } else { - color = getContext().getResources().getColor(R.color.grey_600); - } - - if (messageButton != null) messageButton.setColorFilter(color, PorterDuff.Mode.SRC_IN); - } - - public void setListener(Listener listener) { - this.listener = listener; - - if (this.messageButton != null) this.messageButton.setOnClickListener(v -> listener.onMessageClicked()); - } - - public interface Listener { - public void onMessageClicked(); - public void onSecureCallClicked(); - public void onInSecureCallClicked(); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationSettingsPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationSettingsPreference.kt deleted file mode 100644 index 3c2e72779d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationSettingsPreference.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout - -class NotificationSettingsPreference @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null -) : FrameLayout(context, attrs) { - - override fun onFinishInflate() { - super.onFinishInflate() - // TODO: if we want do the spans - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProgressPreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProgressPreference.java deleted file mode 100644 index 52a88c5664..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProgressPreference.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.thoughtcrime.securesms.preferences.widgets; - - -import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TextView; - -import network.loki.messenger.R; - -public class ProgressPreference extends Preference { - - private View container; - private TextView progressText; - - public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initialize(); - } - - public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initialize(); - } - - public ProgressPreference(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); - } - - public ProgressPreference(Context context) { - super(context); - initialize(); - } - - private void initialize() { - setWidgetLayoutResource(R.layout.preference_widget_progress); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - this.container = view.findViewById(R.id.container); - this.progressText = (TextView) view.findViewById(R.id.progress_text); - - this.container.setVisibility(View.GONE); - } - - public void setProgress(int count) { - container.setVisibility(View.VISIBLE); - progressText.setText(getContext().getString(R.string.ProgressPreference_d_messages_so_far, count)); - } - - public void setProgressVisible(boolean visible) { - container.setVisibility(visible ? View.VISIBLE : View.GONE); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java deleted file mode 100644 index 0e70c41a90..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.thoughtcrime.securesms.reactions.any; - -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.annimon.stream.Stream; - -import org.thoughtcrime.securesms.components.emoji.Emoji; -import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; -import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; - -import java.util.List; - -import network.loki.messenger.R; - -/** - * Contains the Emojis that have been used in reactions for a given message. - */ -class ThisMessageEmojiPageModel implements EmojiPageModel { - - private final List emoji; - - ThisMessageEmojiPageModel(@NonNull List emoji) { - this.emoji = emoji; - } - - @Override - public String getKey() { - return RecentEmojiPageModel.KEY; - } - - @Override - public int getIconAttr() { - return R.attr.emoji_category_recent; - } - - @Override - public @NonNull List getEmoji() { - return emoji; - } - - @Override - public @NonNull List getDisplayEmoji() { - return Stream.of(getEmoji()).map(Emoji::new).toList(); - } - - @Override - public boolean hasSpriteMap() { - return false; - } - - @Override - public @Nullable Uri getSpriteUri() { - return null; - } - - @Override - public boolean isDynamic() { - return true; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/TelephonyServiceState.java b/app/src/main/java/org/thoughtcrime/securesms/sms/TelephonyServiceState.java deleted file mode 100644 index b6cb7e5e2a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/TelephonyServiceState.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.thoughtcrime.securesms.sms; - -import android.content.Context; -import android.os.Looper; -import android.telephony.PhoneStateListener; -import android.telephony.ServiceState; -import android.telephony.TelephonyManager; - -public class TelephonyServiceState { - - public boolean isConnected(Context context) { - ListenThread listenThread = new ListenThread(context); - listenThread.start(); - - return listenThread.get(); - } - - private static class ListenThread extends Thread { - - private final Context context; - - private boolean complete; - private boolean result; - - public ListenThread(Context context) { - this.context = context.getApplicationContext(); - } - - @Override - public void run() { - Looper looper = initializeLooper(); - ListenCallback callback = new ListenCallback(looper); - - TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); - telephonyManager.listen(callback, PhoneStateListener.LISTEN_SERVICE_STATE); - - Looper.loop(); - - telephonyManager.listen(callback, PhoneStateListener.LISTEN_NONE); - - set(callback.isConnected()); - } - - private Looper initializeLooper() { - Looper looper = Looper.myLooper(); - - if (looper == null) { - Looper.prepare(); - } - - return Looper.myLooper(); - } - - public synchronized boolean get() { - while (!complete) { - try { - wait(); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - } - - return result; - } - - private synchronized void set(boolean result) { - this.result = result; - this.complete = true; - notifyAll(); - } - } - - private static class ListenCallback extends PhoneStateListener { - - private final Looper looper; - private volatile boolean connected; - - public ListenCallback(Looper looper) { - this.looper = looper; - } - - @Override - public void onServiceStateChanged(ServiceState serviceState) { - this.connected = (serviceState.getState() == ServiceState.STATE_IN_SERVICE); - looper.quit(); - } - - public boolean isConnected() { - return connected; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ContextProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ContextProvider.kt deleted file mode 100644 index 4bc8e104dd..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ContextProvider.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.thoughtcrime.securesms.util - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.fragment.app.Fragment - -/** - * A simplified version of [android.content.ContextWrapper], - * but properly supports [startActivityForResult] for the implementations. - */ -interface ContextProvider { - fun getContext(): Context - fun startActivityForResult(intent: Intent, requestCode: Int) -} - -class ActivityContextProvider(private val activity: Activity): ContextProvider { - - override fun getContext(): Context { - return activity - } - - override fun startActivityForResult(intent: Intent, requestCode: Int) { - activity.startActivityForResult(intent, requestCode) - } -} - -class FragmentContextProvider(private val fragment: Fragment): ContextProvider { - - override fun getContext(): Context { - return fragment.requireContext() - } - - override fun startActivityForResult(intent: Intent, requestCode: Int) { - fragment.startActivityForResult(intent, requestCode) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java deleted file mode 100644 index 82077f474d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Color; -import androidx.core.content.ContextCompat; -import android.text.Layout; -import android.text.Selection; -import android.text.Spannable; -import android.text.method.LinkMovementMethod; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; -import android.widget.TextView; - -import network.loki.messenger.R; - -public class LongClickMovementMethod extends LinkMovementMethod { - @SuppressLint("StaticFieldLeak") - private static LongClickMovementMethod sInstance; - - private final GestureDetector gestureDetector; - private View widget; - private LongClickCopySpan currentSpan; - - private LongClickMovementMethod(final Context context) { - gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { - @Override - public void onLongPress(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onLongClick(widget); - widget = null; - currentSpan = null; - } - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onClick(widget); - widget = null; - currentSpan = null; - } - return true; - } - }); - } - - @Override - public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { - int action = event.getAction(); - - if (action == MotionEvent.ACTION_UP || - action == MotionEvent.ACTION_DOWN) { - int x = (int) event.getX(); - int y = (int) event.getY(); - - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); - - x += widget.getScrollX(); - y += widget.getScrollY(); - - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - int off = layout.getOffsetForHorizontal(line, x); - - LongClickCopySpan longClickCopySpan[] = buffer.getSpans(off, off, LongClickCopySpan.class); - if (longClickCopySpan.length != 0) { - LongClickCopySpan aSingleSpan = longClickCopySpan[0]; - if (action == MotionEvent.ACTION_DOWN) { - Selection.setSelection(buffer, buffer.getSpanStart(aSingleSpan), - buffer.getSpanEnd(aSingleSpan)); - aSingleSpan.setHighlighted(true, - ContextCompat.getColor(widget.getContext(), R.color.touch_highlight)); - } else { - Selection.removeSelection(buffer); - aSingleSpan.setHighlighted(false, Color.TRANSPARENT); - } - - this.currentSpan = aSingleSpan; - this.widget = widget; - return gestureDetector.onTouchEvent(event); - } - } else if (action == MotionEvent.ACTION_CANCEL) { - // Remove Selections. - LongClickCopySpan[] spans = buffer.getSpans(Selection.getSelectionStart(buffer), - Selection.getSelectionEnd(buffer), LongClickCopySpan.class); - for (LongClickCopySpan aSpan : spans) { - aSpan.setHighlighted(false, Color.TRANSPARENT); - } - Selection.removeSelection(buffer); - return gestureDetector.onTouchEvent(event); - } - return super.onTouchEvent(widget, buffer, event); - } - - public static LongClickMovementMethod getInstance(Context context) { - if (sInstance == null) { - sInstance = new LongClickMovementMethod(context.getApplicationContext()); - } - return sInstance; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java deleted file mode 100644 index bba19f1fc2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/WakeLockUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.content.Context; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import androidx.annotation.NonNull; - -import org.session.libsession.utilities.ServiceUtil; -import org.session.libsignal.utilities.Log; - -public class WakeLockUtil { - - private static final String TAG = WakeLockUtil.class.getSimpleName(); - - /** - * @param tag will be prefixed with "signal:" if it does not already start with it. - */ - public static WakeLock acquire(@NonNull Context context, int lockType, long timeout, @NonNull String tag) { - tag = prefixTag(tag); - try { - PowerManager powerManager = ServiceUtil.getPowerManager(context); - WakeLock wakeLock = powerManager.newWakeLock(lockType, tag); - - wakeLock.acquire(timeout); - Log.d(TAG, "Acquired wakelock with tag: " + tag); - - return wakeLock; - } catch (Exception e) { - Log.w(TAG, "Failed to acquire wakelock with tag: " + tag, e); - return null; - } - } - - /** - * @param tag will be prefixed with "signal:" if it does not already start with it. - */ - public static void release(@NonNull WakeLock wakeLock, @NonNull String tag) { - tag = prefixTag(tag); - try { - if (wakeLock.isHeld()) { - wakeLock.release(); - Log.d(TAG, "Released wakelock with tag: " + tag); - } else { - Log.d(TAG, "Wakelock wasn't held at time of release: " + tag); - } - } catch (Exception e) { - Log.w(TAG, "Failed to release wakelock with tag: " + tag, e); - } - } - - private static String prefixTag(@NonNull String tag) { - return tag.startsWith("signal:") ? tag : "signal:" + tag; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioEvent.kt deleted file mode 100644 index b01edfb492..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/AudioEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.thoughtcrime.securesms.webrtc - -enum class AudioEvent { - -} From 5a5b2f593f846faf0b1ca2900a02821b194e6cae Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 9 Aug 2023 10:08:42 +0930 Subject: [PATCH 088/112] Refactor to accept Huawei token from getToken() and/or onNewToken() --- .../notifications/HuaweiPushService.kt | 31 +---------- .../notifications/HuaweiTokenFetcher.kt | 22 ++++---- .../securesms/ApplicationContext.java | 5 -- .../securesms/notifications/PushReceiver.kt | 2 +- .../securesms/notifications/PushRegistry.kt | 55 +++++++++---------- .../notifications/PushTokenManager.kt | 34 ------------ .../securesms/notifications/TokenFetcher.kt | 4 +- .../{ExpiryManager.kt => TokenManager.kt} | 18 +++--- .../notifications/FirebasePushService.kt | 2 +- .../notifications/FirebaseTokenFetcher.kt | 34 ++---------- 10 files changed, 59 insertions(+), 148 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/notifications/PushTokenManager.kt rename app/src/main/java/org/thoughtcrime/securesms/notifications/{ExpiryManager.kt => TokenManager.kt} (59%) diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt index 2c3512acc7..fb4e262560 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt @@ -14,11 +14,6 @@ private val TAG = HuaweiPushService::class.java.simpleName @AndroidEntryPoint class HuaweiPushService: HmsMessageService() { - - init { - Log.d(TAG, "init Huawei Service") - } - @Inject lateinit var pushRegistry: PushRegistry @Inject lateinit var pushReceiver: PushReceiver @@ -32,33 +27,13 @@ class HuaweiPushService: HmsMessageService() { pushReceiver.onPush(message?.data?.let(Base64::decode)) } - override fun onMessageSent(p0: String?) { - Log.d(TAG, "onMessageSent() called with: p0 = $p0") - super.onMessageSent(p0) - } - - override fun onSendError(p0: String?, p1: Exception?) { - Log.d(TAG, "onSendError() called with: p0 = $p0, p1 = $p1") - super.onSendError(p0, p1) - } - - override fun onMessageDelivered(p0: String?, p1: Exception?) { - Log.d(TAG, "onMessageDelivered") - super.onMessageDelivered(p0, p1) - } - - - override fun onNewToken(p0: String?) { - Log.d(TAG, "onNewToken") - super.onNewToken(p0) + override fun onNewToken(token: String?) { + pushRegistry.register(token) } override fun onNewToken(token: String?, bundle: Bundle?) { Log.d(TAG, "New HCM token: $token.") - - TextSecurePreferences.setPushToken(this, token) - - pushRegistry.refresh(token, true) + onNewToken(token) } override fun onDeletedMessages() { diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt index e3b6afe4cc..9d9b61ce9a 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiTokenFetcher.kt @@ -2,26 +2,28 @@ package org.thoughtcrime.securesms.notifications import android.content.Context import com.huawei.hms.aaid.HmsInstanceId +import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.session.libsignal.utilities.Log import javax.inject.Inject import javax.inject.Singleton +private const val APP_ID = "107205081" +private const val TOKEN_SCOPE = "HCM" + @Singleton class HuaweiTokenFetcher @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val pushRegistry: Lazy, ): TokenFetcher { - override fun fetch(): Job { - val hmsInstanceId = HmsInstanceId.getInstance(context) - - return MainScope().launch(Dispatchers.IO) { - val appId = "107205081" - val tokenScope = "HCM" - // getToken returns an empty string, but triggers the service to initialize. - hmsInstanceId.getToken(appId, tokenScope) - } + override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run { + // https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370 + // getToken may return an empty string, if so HuaweiPushService#onNewToken will be called. + withContext(Dispatchers.IO) { getToken(APP_ID, TOKEN_SCOPE) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 73b0af55f7..75622c7bb6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -427,11 +427,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } private static class ProviderInitializationException extends RuntimeException { } - - public void registerForPnIfNeeded(final Boolean force) { - pushRegistry.refresh(force); - } - private void setUpPollingIfNeeded() { String userPublicKey = TextSecurePreferences.getLocalNumber(this); if (userPublicKey == null) return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index 56871ba64e..1d097c7059 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -75,7 +75,7 @@ class PushReceiver @Inject constructor(@ApplicationContext val context: Context) else -> this["ENCRYPTED_DATA"]?.let(Base64::decode) } - fun decrypt(encPayload: ByteArray): ByteArray? { + private fun decrypt(encPayload: ByteArray): ByteArray? { Log.d(TAG, "decrypt() called") val encKey = getOrCreateNotificationKey() diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt index 9a625f133c..dc50ad8bdd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt @@ -4,6 +4,8 @@ import android.content.Context import com.goterl.lazysodium.utils.KeyPair import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import nl.komponents.kovenant.Promise import nl.komponents.kovenant.combine.and import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 @@ -22,8 +24,10 @@ private val TAG = PushRegistry::class.java.name class PushRegistry @Inject constructor( @ApplicationContext private val context: Context, private val device: Device, - private val tokenManager: PushTokenManager, + private val tokenManager: TokenManager, private val pushRegistryV2: PushRegistryV2, + private val prefs: TextSecurePreferences, + private val tokenFetcher: TokenFetcher, ) { private var pushRegistrationJob: Job? = null @@ -32,42 +36,33 @@ class PushRegistry @Inject constructor( Log.d(TAG, "refresh() called with: force = $force") pushRegistrationJob?.apply { - if (force) cancel() else if (isActive) return + if (force) cancel() else if (isActive || !tokenManager.hasValidRegistration) return } - pushRegistrationJob = tokenManager.fetchToken() + pushRegistrationJob = MainScope().launch { + register(tokenFetcher.fetch()) fail { + Log.e(TAG, "register failed", it) + } + } } - fun refresh(token: String?, force: Boolean): Promise<*, Exception> { - Log.d(TAG, "refresh($token, $force) called") + fun register(token: String?): Promise<*, Exception> { + Log.d(TAG, "refresh($token) called") - token ?: return emptyPromise() - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return emptyPromise() + if (token?.isNotEmpty() != true) return emptyPromise() + + prefs.setPushToken(token) + + val userPublicKey = prefs.getLocalNumber() ?: return emptyPromise() val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise() return when { - tokenManager.isPushEnabled -> register(force, token, userPublicKey, userEdKey) - tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey) + prefs.isPushEnabled() -> register(token, userPublicKey, userEdKey) + tokenManager.isRegistered -> unregister(token, userPublicKey, userEdKey) else -> emptyPromise() } } - /** - * Register for push notifications if: - * force is true - * there is no FCM Token - * FCM Token has expired - */ - private fun register( - force: Boolean, - token: String, - publicKey: String, - userEd25519Key: KeyPair, - namespaces: List = listOf(Namespace.DEFAULT) - ): Promise<*, Exception> = if (force || tokenManager.isInvalid()) { - register(token, publicKey, userEd25519Key, namespaces) - } else emptyPromise() - /** * Register for push notifications. */ @@ -77,7 +72,7 @@ class PushRegistry @Inject constructor( userEd25519Key: KeyPair, namespaces: List = listOf(Namespace.DEFAULT) ): Promise<*, Exception> { - android.util.Log.d( + Log.d( TAG, "register() called with: token = $token, publicKey = $publicKey, userEd25519Key = $userEd25519Key, namespaces = $namespaces" ) @@ -97,8 +92,8 @@ class PushRegistry @Inject constructor( } return v1 and v2 success { - Log.d(TAG, "registerBoth success... saving token!!") - tokenManager.fcmToken = token + Log.d(TAG, "register v1 & v2 success") + tokenManager.register() } } @@ -111,6 +106,6 @@ class PushRegistry @Inject constructor( ) fail { Log.e(TAG, "unregisterBoth failed", it) } success { - tokenManager.fcmToken = null + tokenManager.unregister() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushTokenManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushTokenManager.kt deleted file mode 100644 index 45469a3dd9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushTokenManager.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Job -import org.session.libsession.utilities.TextSecurePreferences -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PushTokenManager @Inject constructor( - @ApplicationContext private val context: Context, - private val tokenFetcher: TokenFetcher -) { - private val expiryManager = ExpiryManager(context) - - val isPushEnabled get() = TextSecurePreferences.isPushEnabled(context) - - var fcmToken - get() = TextSecurePreferences.getPushToken(context) - set(value) { - TextSecurePreferences.setPushToken(context, value) - if (value != null) markTime() else clearTime() - } - - val requiresUnregister get() = fcmToken != null - - private fun clearTime() = expiryManager.clearTime() - private fun markTime() = expiryManager.markTime() - private fun isExpired() = expiryManager.isExpired() - - fun isInvalid(): Boolean = fcmToken == null || isExpired() - fun fetchToken(): Job = tokenFetcher.fetch() -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt index d8657e95d3..5bd9ce0d8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenFetcher.kt @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.notifications -import kotlinx.coroutines.Job - interface TokenFetcher { - fun fetch(): Job + suspend fun fetch(): String? } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenManager.kt similarity index 59% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/TokenManager.kt index 2b3f0aa860..b3db642b81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/ExpiryManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/TokenManager.kt @@ -6,17 +6,21 @@ import org.session.libsession.utilities.TextSecurePreferences import javax.inject.Inject import javax.inject.Singleton -class ExpiryManager( - private val context: Context, - private val interval: Int = 12 * 60 * 60 * 1000 -) { - fun isExpired() = currentTime() > time + interval +private const val INTERVAL: Int = 12 * 60 * 60 * 1000 - fun markTime() { +@Singleton +class TokenManager @Inject constructor( + @ApplicationContext private val context: Context, +) { + val hasValidRegistration get() = isRegistered && !isExpired + val isRegistered get() = time > 0 + private val isExpired get() = currentTime() > time + INTERVAL + + fun register() { time = currentTime() } - fun clearTime() { + fun unregister() { time = 0 } diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt index 1ebf405a28..2a3b054a58 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebasePushService.kt @@ -18,7 +18,7 @@ class FirebasePushService : FirebaseMessagingService() { override fun onNewToken(token: String) { if (token == prefs.getPushToken()) return - pushRegistry.refresh(token, true) + pushRegistry.register(token) } override fun onMessageReceived(message: RemoteMessage) { diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt index 70f3fb4522..d40f160d0b 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/notifications/FirebaseTokenFetcher.kt @@ -1,43 +1,19 @@ package org.thoughtcrime.securesms.notifications -import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.firebase.iid.FirebaseInstanceId -import com.google.firebase.iid.InstanceIdResult -import dagger.Lazy import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.MainScope import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.session.libsignal.utilities.Log +import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton -private val TAG = FirebaseTokenFetcher::class.java.name - @Singleton -class FirebaseTokenFetcher @Inject constructor( - private val pushRegistry: Lazy, -): TokenFetcher { - override fun fetch(): Job = MainScope().launch(Dispatchers.IO) { +class FirebaseTokenFetcher @Inject constructor(): TokenFetcher { + override suspend fun fetch() = withContext(Dispatchers.IO) { FirebaseInstanceId.getInstance().instanceId .also(Tasks::await) - .also { if (!isActive) return@launch } // don't 'complete' task if we were canceled - .process() + .takeIf { isActive } // don't 'complete' task if we were canceled + ?.run { result?.token ?: throw exception!! } } - - private fun Task.process() = when { - isSuccessful -> try { - result?.token?.let { - pushRegistry.get().refresh(it, force = true).get() - } - } catch (e: Exception) { - onFail(e) - } - else -> exception?.let(::onFail) - } - - private fun onFail(e: Exception) = Log.e(TAG, "fetch failed", e) } - From e60c05cee0827e05e111215af0a38e765906084f Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 9 Aug 2023 12:18:22 +0930 Subject: [PATCH 089/112] Fix Huawei message parsing --- .../securesms/notifications/HuaweiPushService.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt index fb4e262560..dc7bf893d7 100644 --- a/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt +++ b/app/src/huawei/kotlin/org/thoughtcrime/securesms/notifications/HuaweiPushService.kt @@ -4,6 +4,7 @@ import android.os.Bundle import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage import dagger.hilt.android.AndroidEntryPoint +import org.json.JSONException import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log @@ -17,13 +18,9 @@ class HuaweiPushService: HmsMessageService() { @Inject lateinit var pushRegistry: PushRegistry @Inject lateinit var pushReceiver: PushReceiver - override fun onCreate() { - Log.d(TAG, "onCreate Huawei Service") - super.onCreate() - } - override fun onMessageReceived(message: RemoteMessage?) { - Log.d(TAG, "onMessageReceived: $message.") + Log.d(TAG, "onMessageReceived") + message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?: pushReceiver.onPush(message?.data?.let(Base64::decode)) } @@ -33,10 +30,11 @@ class HuaweiPushService: HmsMessageService() { override fun onNewToken(token: String?, bundle: Bundle?) { Log.d(TAG, "New HCM token: $token.") - onNewToken(token) + pushRegistry.register(token) } override fun onDeletedMessages() { - pushRegistry.refresh(true) + Log.d(TAG, "onDeletedMessages") + pushRegistry.refresh(false) } } From e1e5c5937b60d934c92eade1b03e850ec56712ca Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 9 Aug 2023 12:42:53 +0930 Subject: [PATCH 090/112] Use live endpoint for legacy groups --- .../messaging/sending_receiving/notifications/Server.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt index 6fefd694e0..0497fe2220 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt @@ -2,5 +2,5 @@ package org.session.libsession.messaging.sending_receiving.notifications enum class Server(val url: String, val publicKey: String) { LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"), - LEGACY("https://dev.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049") + LEGACY("https://live.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049") } From 34fc6ee6cb097476e365e53a8ae19672c33906ff Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 9 Aug 2023 14:12:07 +0930 Subject: [PATCH 091/112] Remove contactshare package (#1288) --- .../ContactUtil.java | 4 +- .../contactshare/ContactModelMapper.java | 169 ------------------ .../conversation/v2/ConversationActivityV2.kt | 2 +- .../securesms/database/ThreadDatabase.java | 2 +- .../mediasend/MediaSendFragment.java | 2 +- .../notifications/DefaultMessageNotifier.java | 2 +- .../SimpleTextWatcher.java | 2 +- 7 files changed, 7 insertions(+), 176 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/{contactshare => contacts}/ContactUtil.java (89%) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactModelMapper.java rename app/src/main/java/org/thoughtcrime/securesms/{contactshare => util}/SimpleTextWatcher.java (90%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java similarity index 89% rename from app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java rename to app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java index 4a1059ffd9..5284fb0015 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactUtil.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.contactshare; +package org.thoughtcrime.securesms.contacts; import android.content.Context; import androidx.annotation.NonNull; @@ -24,7 +24,7 @@ public final class ContactUtil { return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message)); } - public static @NonNull String getDisplayName(@Nullable Contact contact) { + private static @NonNull String getDisplayName(@Nullable Contact contact) { if (contact == null) { return ""; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactModelMapper.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactModelMapper.java deleted file mode 100644 index ef783da791..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactModelMapper.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.thoughtcrime.securesms.contactshare; - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment; -import org.session.libsignal.utilities.guava.Optional; -import org.session.libsignal.messages.SharedContact; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import org.session.libsession.utilities.Contact; -import static org.session.libsession.utilities.Contact.*; - -public class ContactModelMapper { - - public static SharedContact.Builder localToRemoteBuilder(@NonNull Contact contact) { - List phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size()); - List emails = new ArrayList<>(contact.getEmails().size()); - List postalAddresses = new ArrayList<>(contact.getPostalAddresses().size()); - - for (Phone phone : contact.getPhoneNumbers()) { - phoneNumbers.add(new SharedContact.Phone.Builder().setValue(phone.getNumber()) - .setType(localToRemoteType(phone.getType())) - .setLabel(phone.getLabel()) - .build()); - } - - for (Email email : contact.getEmails()) { - emails.add(new SharedContact.Email.Builder().setValue(email.getEmail()) - .setType(localToRemoteType(email.getType())) - .setLabel(email.getLabel()) - .build()); - } - - for (PostalAddress postalAddress : contact.getPostalAddresses()) { - postalAddresses.add(new SharedContact.PostalAddress.Builder().setType(localToRemoteType(postalAddress.getType())) - .setLabel(postalAddress.getLabel()) - .setStreet(postalAddress.getStreet()) - .setPobox(postalAddress.getPoBox()) - .setNeighborhood(postalAddress.getNeighborhood()) - .setCity(postalAddress.getCity()) - .setRegion(postalAddress.getRegion()) - .setPostcode(postalAddress.getPostalCode()) - .setCountry(postalAddress.getCountry()) - .build()); - } - - SharedContact.Name name = new SharedContact.Name.Builder().setDisplay(contact.getName().getDisplayName()) - .setGiven(contact.getName().getGivenName()) - .setFamily(contact.getName().getFamilyName()) - .setPrefix(contact.getName().getPrefix()) - .setSuffix(contact.getName().getSuffix()) - .setMiddle(contact.getName().getMiddleName()) - .build(); - - return new SharedContact.Builder().setName(name) - .withOrganization(contact.getOrganization()) - .withPhones(phoneNumbers) - .withEmails(emails) - .withAddresses(postalAddresses); - } - - public static Contact remoteToLocal(@NonNull SharedContact sharedContact) { - Name name = new Name(sharedContact.getName().getDisplay().orNull(), - sharedContact.getName().getGiven().orNull(), - sharedContact.getName().getFamily().orNull(), - sharedContact.getName().getPrefix().orNull(), - sharedContact.getName().getSuffix().orNull(), - sharedContact.getName().getMiddle().orNull()); - - List phoneNumbers = new LinkedList<>(); - if (sharedContact.getPhone().isPresent()) { - for (SharedContact.Phone phone : sharedContact.getPhone().get()) { - phoneNumbers.add(new Phone(phone.getValue(), - remoteToLocalType(phone.getType()), - phone.getLabel().orNull())); - } - } - - List emails = new LinkedList<>(); - if (sharedContact.getEmail().isPresent()) { - for (SharedContact.Email email : sharedContact.getEmail().get()) { - emails.add(new Email(email.getValue(), - remoteToLocalType(email.getType()), - email.getLabel().orNull())); - } - } - - List postalAddresses = new LinkedList<>(); - if (sharedContact.getAddress().isPresent()) { - for (SharedContact.PostalAddress postalAddress : sharedContact.getAddress().get()) { - postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()), - postalAddress.getLabel().orNull(), - postalAddress.getStreet().orNull(), - postalAddress.getPobox().orNull(), - postalAddress.getNeighborhood().orNull(), - postalAddress.getCity().orNull(), - postalAddress.getRegion().orNull(), - postalAddress.getPostcode().orNull(), - postalAddress.getCountry().orNull())); - } - } - - Avatar avatar = null; - if (sharedContact.getAvatar().isPresent()) { - Attachment attachment = PointerAttachment.forPointer(Optional.of(sharedContact.getAvatar().get().getAttachment().asPointer())).get(); - boolean isProfile = sharedContact.getAvatar().get().isProfile(); - - avatar = new Avatar(null, attachment, isProfile); - } - - return new Contact(name, sharedContact.getOrganization().orNull(), phoneNumbers, emails, postalAddresses, avatar); - } - - private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) { - switch (type) { - case HOME: return Phone.Type.HOME; - case MOBILE: return Phone.Type.MOBILE; - case WORK: return Phone.Type.WORK; - default: return Phone.Type.CUSTOM; - } - } - - private static Email.Type remoteToLocalType(SharedContact.Email.Type type) { - switch (type) { - case HOME: return Email.Type.HOME; - case MOBILE: return Email.Type.MOBILE; - case WORK: return Email.Type.WORK; - default: return Email.Type.CUSTOM; - } - } - - private static PostalAddress.Type remoteToLocalType(SharedContact.PostalAddress.Type type) { - switch (type) { - case HOME: return PostalAddress.Type.HOME; - case WORK: return PostalAddress.Type.WORK; - default: return PostalAddress.Type.CUSTOM; - } - } - - private static SharedContact.Phone.Type localToRemoteType(Phone.Type type) { - switch (type) { - case HOME: return SharedContact.Phone.Type.HOME; - case MOBILE: return SharedContact.Phone.Type.MOBILE; - case WORK: return SharedContact.Phone.Type.WORK; - default: return SharedContact.Phone.Type.CUSTOM; - } - } - - private static SharedContact.Email.Type localToRemoteType(Email.Type type) { - switch (type) { - case HOME: return SharedContact.Email.Type.HOME; - case MOBILE: return SharedContact.Email.Type.MOBILE; - case WORK: return SharedContact.Email.Type.WORK; - default: return SharedContact.Email.Type.CUSTOM; - } - } - - private static SharedContact.PostalAddress.Type localToRemoteType(PostalAddress.Type type) { - switch (type) { - case HOME: return SharedContact.PostalAddress.Type.HOME; - case WORK: return SharedContact.PostalAddress.Type.WORK; - default: return SharedContact.PostalAddress.Type.CUSTOM; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 233d43eaeb..64bc0a4e71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -105,7 +105,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.attachments.ScreenshotObserver import org.thoughtcrime.securesms.audio.AudioRecorder import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey -import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher +import org.thoughtcrime.securesms.util.SimpleTextWatcher import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 5044529981..921b1c06b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -50,7 +50,7 @@ import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Pair; import org.session.libsignal.utilities.guava.Optional; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.contactshare.ContactUtil; +import org.thoughtcrime.securesms.contacts.ContactUtil; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index eac40f6818..cba1529a51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiEventListener; import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; -import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; +import org.thoughtcrime.securesms.util.SimpleTextWatcher; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 0157d8ad41..2c70bff637 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -54,7 +54,7 @@ import org.session.libsignal.utilities.IdPrefix; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Util; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.contactshare.ContactUtil; +import org.thoughtcrime.securesms.contacts.ContactUtil; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities; diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SimpleTextWatcher.java b/app/src/main/java/org/thoughtcrime/securesms/util/SimpleTextWatcher.java similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/contactshare/SimpleTextWatcher.java rename to app/src/main/java/org/thoughtcrime/securesms/util/SimpleTextWatcher.java index b2448b8f85..512748bae7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SimpleTextWatcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SimpleTextWatcher.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.contactshare; +package org.thoughtcrime.securesms.util; import android.text.Editable; import android.text.TextWatcher; From 309293df63763c2bd5e42fa8409315795dd2ab45 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 10 Aug 2023 10:36:52 +0930 Subject: [PATCH 092/112] Improve logs in SingleRecipientNotificationBuilder --- .../notifications/SingleRecipientNotificationBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index da0896d05a..891d0bb2df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -99,7 +99,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil .get(); setLargeIcon(iconBitmap); } catch (InterruptedException | ExecutionException e) { - Log.w(TAG, e); + Log.w(TAG, "get iconBitmap in getThread failed", e); setLargeIcon(getPlaceholderDrawable(context, recipient)); } } else { @@ -298,7 +298,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil .submit(64, 64) .get(); } catch (InterruptedException | ExecutionException e) { - Log.w(TAG, e); + Log.w(TAG, "getBigPicture failed", e); return Bitmap.createBitmap(64, 64, Bitmap.Config.RGB_565); } } From c9417b2fec800c8f1e074ccdd1a89ce24f3c139b Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 10 Aug 2023 13:17:28 +0930 Subject: [PATCH 093/112] Fix Dialog#button default (#1299) --- .../java/org/thoughtcrime/securesms/SessionDialogBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt index 3fb5e2787c..44c30741ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt @@ -120,7 +120,7 @@ class SessionDialogBuilder(val context: Context) { @StringRes text: Int, @StringRes contentDescriptionRes: Int = text, @StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText, - dismiss: Boolean = false, + dismiss: Boolean = true, listener: (() -> Unit) = {} ) = Button(context, null, 0, style).apply { setText(text) From 16177d5cb12275d65a3c1fbcd393e4d9e91f7858 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Aug 2023 17:36:55 +0930 Subject: [PATCH 094/112] Fix logs --- .../securesms/dependencies/PushComponent.kt | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt deleted file mode 100644 index 69baf1f208..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PushComponent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.thoughtcrime.securesms.dependencies - -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import org.thoughtcrime.securesms.notifications.PushManager - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface PushComponent { - -} \ No newline at end of file From 77100231d2c3b2bf26d7e005db634aa3adec9550 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Aug 2023 17:37:35 +0930 Subject: [PATCH 095/112] Fix logs --- app/src/main/AndroidManifest.xml | 10 -------- .../securesms/notifications/PushReceiver.kt | 23 ++++++------------- .../securesms/notifications/PushRegistry.kt | 11 ++++----- .../notifications/PushRegistryV1.kt | 12 +++------- 4 files changed, 14 insertions(+), 42 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ed51a7ea8d..cc63ab6a87 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -78,16 +78,6 @@ android:theme="@style/Theme.Session.DayNight" tools:replace="android:allowBackup"> - - - - - - = 2) (expectedList[1] as? BencodeString)?.value else null - // null content is valid only if we got a "data_too_long" flag - if (content == null) - check(metadata.data_too_long) { "missing message data, but no too-long flag" } - else - check(metadata.data_len == content.size) { "wrong message data size" } - - Log.d( - TAG, - "Received push for ${metadata.account}/${metadata.namespace}, msg ${metadata.msg_hash}, ${metadata.data_len}B" - ) - - return content + return (expectedList.getOrNull(1) as? BencodeString)?.value.also { + // null content is valid only if we got a "data_too_long" flag + it?.let { check(metadata.data_len == it.size) { "wrong message data size" } } + ?: check(metadata.data_too_long) { "missing message data, but no too-long flag" } + } } fun getOrCreateNotificationKey(): Key { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt index dc50ad8bdd..718b2b29ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt @@ -40,14 +40,14 @@ class PushRegistry @Inject constructor( } pushRegistrationJob = MainScope().launch { - register(tokenFetcher.fetch()) fail { - Log.e(TAG, "register failed", it) + register(tokenFetcher.fetch()) fail { e -> + Log.e(TAG, "register failed", e) } } } fun register(token: String?): Promise<*, Exception> { - Log.d(TAG, "refresh($token) called") + Log.d(TAG, "refresh() called") if (token?.isNotEmpty() != true) return emptyPromise() @@ -72,10 +72,7 @@ class PushRegistry @Inject constructor( userEd25519Key: KeyPair, namespaces: List = listOf(Namespace.DEFAULT) ): Promise<*, Exception> { - Log.d( - TAG, - "register() called with: token = $token, publicKey = $publicKey, userEd25519Key = $userEd25519Key, namespaces = $namespaces" - ) + Log.d(TAG, "register() called") val v1 = PushRegistryV1.register( device = device, diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt index d7d84b5c61..ac5fce6d50 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt @@ -34,23 +34,17 @@ object PushRegistryV1 { legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() ): Promise<*, Exception> = when { isUsingFCM -> retryIfNeeded(maxRetryCount) { - android.util.Log.d( - TAG, - "register() called with: device = $device, isUsingFCM = $isUsingFCM, token = $token, publicKey = $publicKey, legacyGroupPublicKeys = $legacyGroupPublicKeys" - ) + Log.d(TAG, "register() called") doRegister(token, publicKey, device, legacyGroupPublicKeys) } fail { exception -> - Log.d(TAG, "Couldn't register for FCM due to error: $exception... $device $token $publicKey $legacyGroupPublicKeys") + Log.d(TAG, "Couldn't register for FCM due to error", exception) } else -> emptyPromise() } private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection): Promise<*, Exception> { - android.util.Log.d( - TAG, - "doRegister() called with: token = $token, publicKey = $publicKey, device = $device, legacyGroupPublicKeys = $legacyGroupPublicKeys" - ) + Log.d(TAG, "doRegister() called") token ?: return emptyPromise() publicKey ?: return emptyPromise() From 9899b37f43072c5fcc80921c33cbafb7a1e305a3 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Aug 2023 17:45:56 +0930 Subject: [PATCH 096/112] Move huawei manifest metadata to huawei flavor --- app/src/huawei/AndroidManifest.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml index 0483deac1c..dad7ab3ac6 100644 --- a/app/src/huawei/AndroidManifest.xml +++ b/app/src/huawei/AndroidManifest.xml @@ -3,6 +3,16 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + + + Date: Fri, 11 Aug 2023 19:26:53 +0930 Subject: [PATCH 097/112] Move plugin to huawei flavor --- app/build.gradle | 4 ++++ app/{ => src/huawei}/agconnect-services.json | 0 libsession/build.gradle | 1 - 3 files changed, 4 insertions(+), 1 deletion(-) rename app/{ => src/huawei}/agconnect-services.json (100%) diff --git a/app/build.gradle b/app/build.gradle index 91ee23fbb4..6d6f0c9b0c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,6 +27,10 @@ apply plugin: 'kotlin-parcelize' apply plugin: 'kotlinx-serialization' apply plugin: 'dagger.hilt.android.plugin' +if (getGradle().getStartParameter().getTaskRequests().toString().contains("Huawei")){ + apply plugin: 'com.huawei.agconnect' +} + configurations.all { exclude module: "commons-logging" } diff --git a/app/agconnect-services.json b/app/src/huawei/agconnect-services.json similarity index 100% rename from app/agconnect-services.json rename to app/src/huawei/agconnect-services.json diff --git a/libsession/build.gradle b/libsession/build.gradle index 5dc67f525f..3cfb66cd32 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -2,7 +2,6 @@ plugins { id 'com.android.library' id 'kotlin-android' id 'kotlinx-serialization' - id 'com.huawei.agconnect' } android { From ed7ce364021ff97b303e6af8e18d6f5ecf040c60 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 14 Aug 2023 10:31:22 +0930 Subject: [PATCH 098/112] Cleanup gradle --- app/build.gradle | 4 ---- build.gradle | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6d6f0c9b0c..91ee23fbb4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,10 +27,6 @@ apply plugin: 'kotlin-parcelize' apply plugin: 'kotlinx-serialization' apply plugin: 'dagger.hilt.android.plugin' -if (getGradle().getStartParameter().getTaskRequests().toString().contains("Huawei")){ - apply plugin: 'com.huawei.agconnect' -} - configurations.all { exclude module: "commons-logging" } diff --git a/build.gradle b/build.gradle index 8e344c5e14..8e26563ee3 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { repositories { google() mavenCentral() - maven {url 'https://developer.huawei.com/repo/'} + maven { url 'https://developer.huawei.com/repo/' } } dependencies { classpath "com.android.tools.build:gradle:$gradlePluginVersion" From 4c8f38df729f97d8b733d687e6f2e93a820bc97d Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 15 Aug 2023 13:51:21 +0930 Subject: [PATCH 099/112] Require command line arg to include huawei dependencies --- app/build.gradle | 19 ++++++++++++++++++- build.gradle | 5 +++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 91ee23fbb4..d176cccdf1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,16 @@ def abiPostFix = ['armeabi-v7a' : 1, 'x86_64' : 4, 'universal' : 5] +tasks.register('checkHuaweiEnabled') { + doFirst { + if (!project.hasProperty('huawei')) { + def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See README.' + logger.error(message) + throw new GradleException(message) + } + } +} + android { compileSdkVersion androidCompileSdkVersion namespace 'network.loki.messenger' @@ -143,6 +153,7 @@ android { buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" + } website { @@ -180,6 +191,12 @@ android { dataBinding true viewBinding true } + + applicationVariants.all { variant -> + if (variant.flavorName == 'huawei') { + variant.preBuild.dependsOn checkHuaweiEnabled + } + } } dependencies { @@ -213,7 +230,7 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } - huaweiImplementation 'com.huawei.hms:push:6.7.0.300' + if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300' implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1' implementation 'org.conscrypt:conscrypt-android:2.0.0' diff --git a/build.gradle b/build.gradle index 8e26563ee3..ebb3f5f250 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ buildscript { mavenCentral() maven { url 'https://developer.huawei.com/repo/' } } + dependencies { classpath "com.android.tools.build:gradle:$gradlePluginVersion" classpath files('libs/gradle-witness.jar') @@ -13,7 +14,7 @@ buildscript { classpath files('libs/gradle-witness.jar') classpath "com.squareup:javapoet:1.13.0" classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" - classpath 'com.huawei.agconnect:agcp:1.6.0.300' + if (project.hasProperty('huawei')) classpath 'com.huawei.agconnect:agcp:1.6.0.300' } } @@ -57,7 +58,7 @@ allprojects { } jcenter() maven { url "https://jitpack.io" } - maven { url 'https://developer.huawei.com/repo/' } + if (project.hasProperty('huawei')) maven { url 'https://developer.huawei.com/repo/' } } project.ext { From b09b6836d49a568663473d6ba6c45c535992e0b5 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 15 Aug 2023 14:52:40 +0930 Subject: [PATCH 100/112] Add huawei info to BUILDING.md --- BUILDING.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BUILDING.md b/BUILDING.md index f88509c680..48b4412ddd 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -34,6 +34,12 @@ Setting up a development environment and building from Android Studio 6. Project initialization and building should proceed. 7. Clone submodules with `git submodule update --init --recursive` +If you would like to build the Huawei Flavor with Huawei HMS push notifications you will need to pass 'huawei' as a command line arg to include the required dependencies. + +e.g. `./gradlew assembleHuaweiDebug -Phuawei` + +If you are building in Android Studio then add `-Phuawei` to `Preferences > Build, Execution, Deployment > Gradle-Android Compiler > Command-line Options` + Contributing code ----------------- From 7c8882e1f395cba648adfd68fa887c6852b54fe8 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 15 Aug 2023 18:42:49 +0930 Subject: [PATCH 101/112] Fix project reference in task --- app/build.gradle | 5 +++-- build.gradle | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d176cccdf1..d15bca89da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,8 +42,9 @@ def abiPostFix = ['armeabi-v7a' : 1, 'universal' : 5] tasks.register('checkHuaweiEnabled') { + ext.huaweiEnabled = project.hasProperty('huawei') doFirst { - if (!project.hasProperty('huawei')) { + if (!huaweiEnabled) { def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See README.' logger.error(message) throw new GradleException(message) @@ -194,7 +195,7 @@ android { applicationVariants.all { variant -> if (variant.flavorName == 'huawei') { - variant.preBuild.dependsOn checkHuaweiEnabled + variant.getPreBuildProvider().get().dependsOn checkHuaweiEnabled } } } diff --git a/build.gradle b/build.gradle index ebb3f5f250..90266d56cb 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { classpath files('libs/gradle-witness.jar') classpath "com.squareup:javapoet:1.13.0" classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" - if (project.hasProperty('huawei')) classpath 'com.huawei.agconnect:agcp:1.6.0.300' + if (project.hasProperty('huawei')) classpath 'com.huawei.agconnect:agcp:1.9.1.300' } } From 0aa5dc79693f4ade52212755185b727d7f469357 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Aug 2023 13:25:45 +0930 Subject: [PATCH 102/112] Convert NotificationPreferenceFragment to Kotlin --- .../NotificationsPreferenceFragment.java | 190 ------------------ .../NotificationsPreferenceFragment.kt | 172 ++++++++++++++++ 2 files changed, 172 insertions(+), 190 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java deleted file mode 100644 index 4cb316f40f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ /dev/null @@ -1,190 +0,0 @@ -package org.thoughtcrime.securesms.preferences; - -import static android.app.Activity.RESULT_OK; - -import static org.thoughtcrime.securesms.preferences.ListPreferenceDialogKt.listPreferenceDialog; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.Settings; -import android.text.TextUtils; - -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.Preference; - -import org.session.libsession.utilities.TextSecurePreferences; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; -import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.notifications.PushRegistry; - -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; -import network.loki.messenger.R; - -@AndroidEntryPoint -public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment { - - @SuppressWarnings("unused") - private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName(); - - @Inject - PushRegistry pushRegistry; - - @Override - public void onCreate(Bundle paramBundle) { - super.onCreate(paramBundle); - - // Set up FCM toggle - String fcmKey = "pref_key_use_fcm"; - ((SwitchPreferenceCompat)findPreference(fcmKey)).setChecked(TextSecurePreferences.isPushEnabled(getContext())); - this.findPreference(fcmKey) - .setOnPreferenceChangeListener((preference, newValue) -> { - TextSecurePreferences.setPushEnabled(getContext(), (boolean) newValue); - pushRegistry.refresh(true); - return true; - }); - - if (NotificationChannels.supported()) { - TextSecurePreferences.setNotificationRingtone(getContext(), NotificationChannels.getMessageRingtone(getContext()).toString()); - TextSecurePreferences.setNotificationVibrateEnabled(getContext(), NotificationChannels.getMessageVibrate(getContext())); - } - this.findPreference(TextSecurePreferences.RINGTONE_PREF) - .setOnPreferenceChangeListener(new RingtoneSummaryListener()); - this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) - .setOnPreferenceChangeListener(new NotificationPrivacyListener()); - this.findPreference(TextSecurePreferences.VIBRATE_PREF) - .setOnPreferenceChangeListener((preference, newValue) -> { - NotificationChannels.updateMessageVibrate(getContext(), (boolean) newValue); - return true; - }); - - this.findPreference(TextSecurePreferences.RINGTONE_PREF) - .setOnPreferenceClickListener(preference -> { - Uri current = TextSecurePreferences.getNotificationRingtone(getContext()); - - Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current); - - startActivityForResult(intent, 1); - - return true; - }); - - this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) - .setOnPreferenceClickListener(preference -> { - ListPreference listPreference = (ListPreference) preference; - listPreference.setDialogMessage(R.string.preferences_notifications__content_message); - listPreferenceDialog(getContext(), listPreference, () -> { - initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)); - return null; - }); - return true; - }); - - initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)); - - if (NotificationChannels.supported()) { - this.findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF) - .setOnPreferenceClickListener(preference -> { - Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(getContext())); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName()); - startActivity(intent); - return true; - }); - } - - initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)); - initializeMessageVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.VIBRATE_PREF)); - } - - @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences_notifications); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == 1 && resultCode == RESULT_OK && data != null) { - Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); - - if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) { - NotificationChannels.updateMessageRingtone(getContext(), uri); - TextSecurePreferences.removeNotificationRingtone(getContext()); - } else { - uri = uri == null ? Uri.EMPTY : uri; - NotificationChannels.updateMessageRingtone(getContext(), uri); - TextSecurePreferences.setNotificationRingtone(getContext(), uri.toString()); - } - - initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)); - } - } - - private class RingtoneSummaryListener implements Preference.OnPreferenceChangeListener { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Uri value = (Uri) newValue; - - if (value == null || TextUtils.isEmpty(value.toString())) { - preference.setSummary(R.string.preferences__silent); - } else { - Ringtone tone = RingtoneManager.getRingtone(getActivity(), value); - - if (tone != null) { - preference.setSummary(tone.getTitle(getActivity())); - } - } - - return true; - } - } - - private void initializeRingtoneSummary(Preference pref) { - RingtoneSummaryListener listener = (RingtoneSummaryListener) pref.getOnPreferenceChangeListener(); - Uri uri = TextSecurePreferences.getNotificationRingtone(getContext()); - - listener.onPreferenceChange(pref, uri); - } - - private void initializeMessageVibrateSummary(SwitchPreferenceCompat pref) { - pref.setChecked(TextSecurePreferences.isNotificationVibrateEnabled(getContext())); - } - - public static CharSequence getSummary(Context context) { - final int onCapsResId = R.string.ApplicationPreferencesActivity_On; - final int offCapsResId = R.string.ApplicationPreferencesActivity_Off; - - return context.getString(TextSecurePreferences.isNotificationsEnabled(context) ? onCapsResId : offCapsResId); - } - - private class NotificationPrivacyListener extends ListSummaryListener { - @SuppressLint("StaticFieldLeak") - @Override - public boolean onPreferenceChange(Preference preference, Object value) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - ApplicationContext.getInstance(getActivity()).messageNotifier.updateNotification(getActivity()); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - return super.onPreferenceChange(preference, value); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt new file mode 100644 index 0000000000..933f13b9ec --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt @@ -0,0 +1,172 @@ +package org.thoughtcrime.securesms.preferences + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import android.os.AsyncTask +import android.os.Bundle +import android.provider.Settings +import android.text.TextUtils +import androidx.lifecycle.lifecycleScope +import androidx.preference.ListPreference +import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences.Companion.isNotificationsEnabled +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.components.SwitchPreferenceCompat +import org.thoughtcrime.securesms.notifications.NotificationChannels +import org.thoughtcrime.securesms.notifications.PushRegistry +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() { + @Inject + lateinit var pushRegistry: PushRegistry + @Inject + lateinit var prefs: TextSecurePreferences + + override fun onCreate(paramBundle: Bundle?) { + super.onCreate(paramBundle) + + // Set up FCM toggle + val fcmKey = "pref_key_use_fcm" + val fcmPreference: SwitchPreferenceCompat = findPreference(fcmKey)!! + fcmPreference.isChecked = prefs.isPushEnabled() + fcmPreference.setOnPreferenceChangeListener { _: Preference, newValue: Any -> + prefs.setPushEnabled(newValue as Boolean) + pushRegistry.refresh(true) + true + } + if (NotificationChannels.supported()) { + prefs.setNotificationRingtone( + NotificationChannels.getMessageRingtone(requireContext()).toString() + ) + prefs.setNotificationVibrateEnabled( + NotificationChannels.getMessageVibrate(requireContext()) + ) + } + findPreference(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceChangeListener = RingtoneSummaryListener() + findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceChangeListener = NotificationPrivacyListener() + findPreference(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean) + true + } + findPreference(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val current = prefs.getNotificationRingtone() + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_TYPE, + RingtoneManager.TYPE_NOTIFICATION + ) + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + Settings.System.DEFAULT_NOTIFICATION_URI + ) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current) + startActivityForResult(intent, 1) + true + } + findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { preference: Preference -> + val listPreference = preference as ListPreference + listPreference.setDialogMessage(R.string.preferences_notifications__content_message) + listPreferenceDialog(requireContext(), listPreference) { + initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)) + } + true + } + initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) as ListPreference?) + if (NotificationChannels.supported()) { + findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + intent.putExtra( + Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(requireContext()) + ) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) + startActivity(intent) + true + } + } + initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)) + initializeMessageVibrateSummary(findPreference(TextSecurePreferences.VIBRATE_PREF) as SwitchPreferenceCompat?) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_notifications) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == 1 && resultCode == Activity.RESULT_OK && data != null) { + var uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + if (Settings.System.DEFAULT_NOTIFICATION_URI == uri) { + NotificationChannels.updateMessageRingtone(requireContext(), uri) + prefs.removeNotificationRingtone() + } else { + uri = uri ?: Uri.EMPTY + NotificationChannels.updateMessageRingtone(requireContext(), uri) + prefs.setNotificationRingtone(uri.toString()) + } + initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF)) + } + } + + private inner class RingtoneSummaryListener : Preference.OnPreferenceChangeListener { + override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean { + val value = newValue as? Uri + if (value == null || TextUtils.isEmpty(value.toString())) { + preference.setSummary(R.string.preferences__silent) + } else { + RingtoneManager.getRingtone(activity, value) + ?.getTitle(activity) + ?.let { preference.summary = it } + + } + return true + } + } + + private fun initializeRingtoneSummary(pref: Preference?) { + val listener = pref!!.onPreferenceChangeListener as RingtoneSummaryListener? + val uri = prefs.getNotificationRingtone() + listener!!.onPreferenceChange(pref, uri) + } + + private fun initializeMessageVibrateSummary(pref: SwitchPreferenceCompat?) { + pref!!.isChecked = prefs.isNotificationVibrateEnabled() + } + + private inner class NotificationPrivacyListener : ListSummaryListener() { + @SuppressLint("StaticFieldLeak") + override fun onPreferenceChange(preference: Preference, value: Any): Boolean { + object : AsyncTask() { + override fun doInBackground(vararg params: Void?): Void? { + ApplicationContext.getInstance(activity).messageNotifier.updateNotification(activity!!) + return null + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + return super.onPreferenceChange(preference, value) + } + } + + companion object { + @Suppress("unused") + private val TAG = NotificationsPreferenceFragment::class.java.simpleName + fun getSummary(context: Context): CharSequence = when (isNotificationsEnabled(context)) { + true -> R.string.ApplicationPreferencesActivity_On + false -> R.string.ApplicationPreferencesActivity_Off + }.let(context::getString) + } +} From d308f381d9e9554b1cc9d6948c9483677e0bbdf9 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Aug 2023 13:27:28 +0930 Subject: [PATCH 103/112] Disable fcm preference while register request is in flight --- .../securesms/notifications/PushRegistry.kt | 13 ++++++++----- .../preferences/NotificationsPreferenceFragment.kt | 13 ++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt index 718b2b29ca..b0954f2327 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.notifications import android.content.Context import com.goterl.lazysodium.utils.KeyPair import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch @@ -32,18 +33,20 @@ class PushRegistry @Inject constructor( private var pushRegistrationJob: Job? = null - fun refresh(force: Boolean) { + fun refresh(force: Boolean): Job { Log.d(TAG, "refresh() called with: force = $force") pushRegistrationJob?.apply { - if (force) cancel() else if (isActive || !tokenManager.hasValidRegistration) return + if (force) cancel() else if (isActive) return MainScope().launch {} } - pushRegistrationJob = MainScope().launch { - register(tokenFetcher.fetch()) fail { e -> + return MainScope().launch(Dispatchers.IO) { + try { + register(tokenFetcher.fetch()).get() + } catch (e: Exception) { Log.e(TAG, "register failed", e) } - } + }.also { pushRegistrationJob = it } } fun register(token: String?): Promise<*, Exception> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt index 933f13b9ec..fa6461acc4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.kt @@ -42,7 +42,18 @@ class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() { fcmPreference.isChecked = prefs.isPushEnabled() fcmPreference.setOnPreferenceChangeListener { _: Preference, newValue: Any -> prefs.setPushEnabled(newValue as Boolean) - pushRegistry.refresh(true) + val job = pushRegistry.refresh(true) + + fcmPreference.isEnabled = false + + lifecycleScope.launch(Dispatchers.IO) { + job.join() + + withContext(Dispatchers.Main) { + fcmPreference.isEnabled = true + } + } + true } if (NotificationChannels.supported()) { From 62cd0f68f01b64da3683ec95539f5d53aa32d7a6 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Aug 2023 19:23:13 +0930 Subject: [PATCH 104/112] Improve huawei guarding --- app/build.gradle | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d15bca89da..61f5601891 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,17 +41,6 @@ def abiPostFix = ['armeabi-v7a' : 1, 'x86_64' : 4, 'universal' : 5] -tasks.register('checkHuaweiEnabled') { - ext.huaweiEnabled = project.hasProperty('huawei') - doFirst { - if (!huaweiEnabled) { - def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See README.' - logger.error(message) - throw new GradleException(message) - } - } -} - android { compileSdkVersion androidCompileSdkVersion namespace 'network.loki.messenger' @@ -193,9 +182,19 @@ android { viewBinding true } - applicationVariants.all { variant -> + def huaweiEnabled = project.properties['huawei'] != null + + applicationVariants.configureEach { variant -> if (variant.flavorName == 'huawei') { - variant.getPreBuildProvider().get().dependsOn checkHuaweiEnabled + variant.getPreBuildProvider().configure { task -> + task.doFirst { + if (!huaweiEnabled) { + def message = 'Huawei is not enabled. Please add -Phuawei command line arg. See BUILDING.md' + logger.error(message) + throw new GradleException(message) + } + } + } } } } From 550955f5307ecede4240d263013c0f30046bba3a Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 16 Aug 2023 20:54:07 +0930 Subject: [PATCH 105/112] Add huawei strings --- app/src/huawei/res/values/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/src/huawei/res/values/strings.xml diff --git a/app/src/huawei/res/values/strings.xml b/app/src/huawei/res/values/strings.xml new file mode 100644 index 0000000000..78d42b3e30 --- /dev/null +++ b/app/src/huawei/res/values/strings.xml @@ -0,0 +1,5 @@ + + + You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers. + You\'ll be notified of new messages reliably and immediately using Huawei’s notification servers. + \ No newline at end of file From ae9d3810e156ce20c1522212369e506cefc0f089 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Aug 2023 14:02:22 +0930 Subject: [PATCH 106/112] Fix website flavor TokenFetcher --- .../thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt index 1705eefe09..875518354b 100644 --- a/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt +++ b/app/src/website/kotlin/org/thoughtcrime/securesms/notifications/NoOpTokenFetcher.kt @@ -1,12 +1,9 @@ package org.thoughtcrime.securesms.notifications -import kotlinx.coroutines.Job -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @Singleton class NoOpTokenFetcher @Inject constructor() : TokenFetcher { - override fun fetch(): Job = MainScope().launch { } + override suspend fun fetch(): String? = null } From 296c5d743f825122cb6ec003a442be35f9895aeb Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Aug 2023 22:34:13 +0930 Subject: [PATCH 107/112] Limit scope of huawei dependencies --- build.gradle | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 90266d56cb..2cb531a7c1 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,12 @@ buildscript { repositories { google() mavenCentral() - maven { url 'https://developer.huawei.com/repo/' } + if (project.hasProperty('huawei')) maven { + url 'https://developer.huawei.com/repo/' + content { + includeGroup 'com.huawei.agconnect' + } + } } dependencies { @@ -58,7 +63,15 @@ allprojects { } jcenter() maven { url "https://jitpack.io" } - if (project.hasProperty('huawei')) maven { url 'https://developer.huawei.com/repo/' } + if (project.hasProperty('huawei')) maven { + url 'https://developer.huawei.com/repo/' + content { + includeGroup 'com.huawei.android.hms' + includeGroup 'com.huawei.agconnect' + includeGroup 'com.huawei.hmf' + includeGroup 'com.huawei.hms' + } + } } project.ext { From 7861eb25c2cf45c9da68460a6b73a5aefa11c487 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 17 Aug 2023 22:36:05 +0930 Subject: [PATCH 108/112] Improve param naming for isPushEnabled --- .../notifications/PushRegistryV1.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt index ac5fce6d50..1599dd93d5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt @@ -28,12 +28,12 @@ object PushRegistryV1 { fun register( device: Device, - isUsingFCM: Boolean = TextSecurePreferences.isPushEnabled(context), + isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context), token: String? = TextSecurePreferences.getPushToken(context), publicKey: String? = TextSecurePreferences.getLocalNumber(context), legacyGroupPublicKeys: Collection = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys() ): Promise<*, Exception> = when { - isUsingFCM -> retryIfNeeded(maxRetryCount) { + isPushEnabled -> retryIfNeeded(maxRetryCount) { Log.d(TAG, "register() called") doRegister(token, publicKey, device, legacyGroupPublicKeys) } fail { exception -> @@ -99,17 +99,17 @@ object PushRegistryV1 { fun subscribeGroup( closedGroupPublicKey: String, - isUsingFCM: Boolean = TextSecurePreferences.isPushEnabled(context), + isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = if (isUsingFCM) { + ) = if (isPushEnabled) { performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey) } else emptyPromise() fun unsubscribeGroup( closedGroupPublicKey: String, - isUsingFCM: Boolean = TextSecurePreferences.isPushEnabled(context), + isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context), publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! - ) = if (isUsingFCM) { + ) = if (isPushEnabled) { performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey) } else emptyPromise() From 2466d9b4c03f1b9f9422e87d23838b7bb87a7bf1 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Mon, 28 Aug 2023 09:51:48 +1000 Subject: [PATCH 109/112] [SES-1002] Synced blind requests (#1303) * feat: update config to use blinded-msg-requests pr * feat: add block community message requests bool to protos * feat: add everything needed for recipientDB to have blocked community requests potentially * feat: add db migrations * feat: add sending community block flags and preference options * feat: add parsing block request flag * fix: open group message requests were broken anyway * fix: delete all encoded open group inbox ID bs, fix privacy settings using user config as privacy store * feat: initial creation sets flag, rename to match libsession implementation value * fix: recipient blinded checks from open group message for blocking community requests on blinded ID version of recipient, use correct (inverted) values from before for checking polling and empty states etc * fix: pr comments for view model factory context ref, simplified user config object check for category in PrivacySettingsPreferenceFragment * fix: pr comments * fix: migrate some dependencies and functionality out of VM into repository to remove content resolver and context dependecy so tests pass again * refactor: better naming for hidesInputBar and add more tests for expected recipient view states * fix: use contact information as opposed to active conversations * fix: PR comments --- .../components/ProfilePictureView.kt | 3 +- .../conversation/v2/ConversationActivityV2.kt | 42 +++-- .../conversation/v2/ConversationViewModel.kt | 40 +++-- .../securesms/database/RecipientDatabase.java | 19 ++- .../securesms/database/Storage.kt | 27 ++-- .../database/helpers/SQLCipherOpenHelper.java | 8 +- .../securesms/dependencies/ContentModule.kt | 17 ++ .../securesms/home/HomeActivity.kt | 19 ++- .../preferences/PrivacySettingsActivity.kt | 2 + .../PrivacySettingsPreferenceFragment.kt | 36 +++++ .../repository/ConversationRepository.kt | 28 +++- app/src/main/res/values/strings.xml | 4 + .../res/xml/preferences_app_protection.xml | 6 + .../v2/ConversationViewModelTest.kt | 50 ++++-- libsession-util/libsession-util | 2 +- libsession-util/src/main/cpp/user_profile.cpp | 29 ++++ .../loki/messenger/libsession_util/Config.kt | 3 + .../libsession/database/StorageProtocol.kt | 2 + .../messages/visible/VisibleMessage.kt | 8 +- .../messaging/open_groups/OpenGroupApi.kt | 3 +- .../sending_receiving/MessageSender.kt | 7 + .../ReceivedMessageHandler.kt | 4 + .../session/libsession/utilities/GroupUtil.kt | 16 +- .../utilities/TextSecurePreferences.kt | 2 + .../utilities/recipients/Recipient.java | 153 ++++++++++++------ .../recipients/RecipientProvider.java | 2 + libsignal/protobuf/SignalService.proto | 29 ++-- .../libsignal/protos/SignalServiceProtos.java | 118 +++++++++++--- 28 files changed, 522 insertions(+), 157 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/dependencies/ContentModule.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 6044224601..7635ad40e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -10,7 +10,6 @@ import androidx.annotation.DimenRes import com.bumptech.glide.load.engine.DiskCacheStrategy import network.loki.messenger.R import network.loki.messenger.databinding.ViewProfilePictureBinding -import network.loki.messenger.databinding.ViewUserBinding import org.session.libsession.avatars.ContactColors import org.session.libsession.avatars.PlaceholderAvatarPhoto import org.session.libsession.avatars.ProfileContactPhoto @@ -74,7 +73,7 @@ class ProfilePictureView @JvmOverloads constructor( additionalDisplayName = getUserDisplayName(apk) } } else if(recipient.isOpenGroupInboxRecipient) { - val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize()) + val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize()) this.publicKey = publicKey displayName = getUserDisplayName(publicKey) additionalPublicKey = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 64bc0a4e71..e1515109f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -40,6 +40,7 @@ import androidx.annotation.DimenRes import androidx.core.text.set import androidx.core.text.toSpannable import androidx.core.view.drawToBitmap +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.lifecycle.Lifecycle @@ -78,7 +79,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview @@ -105,13 +105,12 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.attachments.ScreenshotObserver import org.thoughtcrime.securesms.audio.AudioRecorder import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey -import org.thoughtcrime.securesms.util.SimpleTextWatcher import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP +import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND -import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog @@ -174,6 +173,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.SaveAttachmentTask +import org.thoughtcrime.securesms.util.SimpleTextWatcher import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.toPx @@ -240,11 +240,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) { storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let { fromSerialized(it) - } ?: run { - val openGroupInboxId = - "${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray() - fromSerialized(GroupUtil.getEncodedOpenGroupInboxID(openGroupInboxId)) - } + } ?: GroupUtil.getEncodedOpenGroupInboxID(openGroup, sessionId) } else { it } @@ -253,7 +249,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } ?: finish() } - viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair(), contentResolver) + viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair()) } private var actionMode: ActionMode? = null private var unreadCount = 0 @@ -312,8 +308,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe handleSwipeToReply(message) }, onItemLongPress = { message, position, view -> - if (!isMessageRequestThread() && - (viewModel.openGroup == null || Capability.REACTIONS.name.lowercase() in viewModel.serverCapabilities) + if (!viewModel.isMessageRequestThread && + viewModel.canReactToMessages ) { showEmojiPicker(message, view) } else { @@ -596,26 +592,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // called from onCreate private fun setUpInputBar() { - binding!!.inputBar.isVisible = viewModel.openGroup == null || viewModel.openGroup?.canWrite == true - binding!!.inputBar.delegate = this - binding!!.inputBarRecordingView.delegate = this + val binding = binding ?: return + binding.inputBar.isGone = viewModel.hidesInputBar() + binding.inputBar.delegate = this + binding.inputBarRecordingView.delegate = this // GIF button - binding!!.gifButtonContainer.addView(gifButton) + binding.gifButtonContainer.addView(gifButton) gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) gifButton.onUp = { showGIFPicker() } gifButton.snIsEnabled = false // Document button - binding!!.documentButtonContainer.addView(documentButton) + binding.documentButtonContainer.addView(documentButton) documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) documentButton.onUp = { showDocumentPicker() } documentButton.snIsEnabled = false // Library button - binding!!.libraryButtonContainer.addView(libraryButton) + binding.libraryButtonContainer.addView(libraryButton) libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) libraryButton.onUp = { pickFromLibrary() } libraryButton.snIsEnabled = false // Camera button - binding!!.cameraButtonContainer.addView(cameraButton) + binding.cameraButtonContainer.addView(cameraButton) cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) cameraButton.onUp = { showCamera() } cameraButton.snIsEnabled = false @@ -764,7 +761,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun onPrepareOptionsMenu(menu: Menu): Boolean { val recipient = viewModel.recipient ?: return false - if (!isMessageRequestThread()) { + if (!viewModel.isMessageRequestThread) { ConversationMenuHelper.onPrepareOptionsMenu( menu, menuInflater, @@ -850,11 +847,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } - private fun isMessageRequestThread(): Boolean { - val recipient = viewModel.recipient ?: return false - return !recipient.isGroupRecipient && !recipient.isApproved - } - private fun isOutgoingMessageRequestThread(): Boolean { val recipient = viewModel.recipient ?: return false return !recipient.isGroupRecipient && @@ -1069,11 +1061,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private fun updatePlaceholder() { val recipient = viewModel.recipient ?: return Log.w("Loki", "recipient was null in placeholder update") + val blindedRecipient = viewModel.blindedRecipient val binding = binding ?: return val openGroup = viewModel.openGroup val (textResource, insertParam) = when { recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString() + blindedRecipient?.blocksCommunityMessageRequests == true -> R.string.activity_conversation_empty_state_blocks_community_requests to recipient.toShortString() else -> R.string.activity_conversation_empty_state_default to recipient.toShortString() } val showPlaceholder = adapter.itemCount == 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 13736974b1..532e65e196 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -1,10 +1,8 @@ package org.thoughtcrime.securesms.conversation.v2 -import android.content.ContentResolver import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import app.cash.copper.flow.observeQuery import com.goterl.lazysodium.utils.KeyPair import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -21,7 +19,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.repository.ConversationRepository @@ -30,7 +27,6 @@ import java.util.UUID class ConversationViewModel( val threadId: Long, val edKeyPair: KeyPair?, - private val contentResolver: ContentResolver, private val repository: ConversationRepository, private val storage: Storage ) : ViewModel() { @@ -47,6 +43,15 @@ class ConversationViewModel( val recipient: Recipient? get() = _recipient.value + val blindedRecipient: Recipient? + get() = _recipient.value?.let { recipient -> + when { + recipient.isOpenGroupOutboxRecipient -> recipient + recipient.isOpenGroupInboxRecipient -> repository.maybeGetBlindedRecipient(recipient) + else -> null + } + } + private var _openGroup: RetrieveOnce = RetrieveOnce { storage.getOpenGroup(threadId) } @@ -62,12 +67,22 @@ class ConversationViewModel( ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString } + val isMessageRequestThread : Boolean + get() { + val recipient = recipient ?: return false + return !recipient.isLocalNumber && !recipient.isGroupRecipient && !recipient.isApproved + } + + val canReactToMessages: Boolean + // allow reactions if the open group is null (normal conversations) or the open group's capabilities include reactions + get() = (openGroup == null || OpenGroupApi.Capability.REACTIONS.name.lowercase() in serverCapabilities) + + init { viewModelScope.launch(Dispatchers.IO) { - contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)) - .collect { - val recipientExists = storage.getRecipientForThread(threadId) != null - if (!recipientExists && _uiState.value.conversationExists) { + repository.recipientUpdateFlow(threadId) + .collect { recipient -> + if (recipient == null && _uiState.value.conversationExists) { _uiState.update { it.copy(conversationExists = false) } } } @@ -199,22 +214,25 @@ class ConversationViewModel( _recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId)) } + fun hidesInputBar(): Boolean = openGroup?.canWrite != true && + blindedRecipient?.blocksCommunityMessageRequests == true + + @dagger.assisted.AssistedFactory interface AssistedFactory { - fun create(threadId: Long, edKeyPair: KeyPair?, contentResolver: ContentResolver): Factory + fun create(threadId: Long, edKeyPair: KeyPair?): Factory } @Suppress("UNCHECKED_CAST") class Factory @AssistedInject constructor( @Assisted private val threadId: Long, @Assisted private val edKeyPair: KeyPair?, - @Assisted private val contentResolver: ContentResolver, private val repository: ConversationRepository, private val storage: Storage ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T + return ConversationViewModel(threadId, edKeyPair, repository, storage) as T } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index b7b8364184..dde5847ffe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -63,13 +63,14 @@ public class RecipientDatabase extends Database { private static final String FORCE_SMS_SELECTION = "force_sms_selection"; private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none private static final String WRAPPER_HASH = "wrapper_hash"; + private static final String BLOCKS_COMMUNITY_MESSAGE_REQUESTS = "blocks_community_message_requests"; private static final String[] RECIPIENT_PROJECTION = new String[] { BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED, PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI, SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL, UNIDENTIFIED_ACCESS_MODE, - FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH + FORCE_SMS_SELECTION, NOTIFY_TYPE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS }; static final List TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) @@ -142,6 +143,11 @@ public class RecipientDatabase extends Database { "ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;"; } + public static String getAddBlocksCommunityMessageRequests() { + return "ALTER TABLE "+TABLE_NAME+" "+ + "ADD COLUMN "+BLOCKS_COMMUNITY_MESSAGE_REQUESTS+" INT DEFAULT 0;"; + } + public static final int NOTIFY_TYPE_ALL = 0; public static final int NOTIFY_TYPE_MENTIONS = 1; public static final int NOTIFY_TYPE_NONE = 2; @@ -197,6 +203,7 @@ public class RecipientDatabase extends Database { int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE)); boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1; String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH)); + boolean blocksCommunityMessageRequests = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKS_COMMUNITY_MESSAGE_REQUESTS)) == 1; MaterialColor color; byte[] profileKey = null; @@ -228,7 +235,7 @@ public class RecipientDatabase extends Database { systemPhoneLabel, systemContactUri, signalProfileName, signalProfileAvatar, profileSharing, notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode), - forceSmsSelection, wrapperHash)); + forceSmsSelection, wrapperHash, blocksCommunityMessageRequests)); } public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) { @@ -395,6 +402,14 @@ public class RecipientDatabase extends Database { notifyRecipientListeners(); } + public void setBlocksCommunityMessageRequests(@NonNull Recipient recipient, boolean isBlocked) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(BLOCKS_COMMUNITY_MESSAGE_REQUESTS, isBlocked ? 1 : 0); + updateOrInsert(recipient.getAddress(), contentValues); + recipient.resolve().setBlocksCommunityMessageRequests(isBlocked); + notifyRecipientListeners(); + } + private void updateOrInsert(Address address, ContentValues contentValues) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 9b038b9955..481a4fa06e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -190,6 +190,11 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co db.setProfileKey(recipient, newProfileKey) } + override fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean) { + val db = DatabaseComponent.get(context).recipientDatabase() + db.setBlocksCommunityMessageRequests(recipient, blocksMessageRequests) + } + override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) { val ourRecipient = fromSerialized(getUserPublicKey()!!).let { Recipient.from(context, it, false) @@ -430,6 +435,10 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co return configFactory.canPerformChange(variant, publicKey, changeTimestampMs) } + override fun isCheckingCommunityRequests(): Boolean { + return configFactory.user?.getCommunityMessageRequests() == true + } + fun notifyUpdates(forConfigObject: ConfigBase) { when (forConfigObject) { is UserProfile -> updateUser(forConfigObject) @@ -1405,7 +1414,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co val blindedId = when { recipient.isGroupRecipient -> null recipient.isOpenGroupInboxRecipient -> { - GroupUtil.getDecodedOpenGroupInbox(address) + GroupUtil.getDecodedOpenGroupInboxSessionId(address) } else -> { if (SessionId(address).prefix == IdPrefix.BLINDED) { @@ -1524,16 +1533,12 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co if (mapping.sessionId != null) { return mapping } - val threadDb = DatabaseComponent.get(context).threadDatabase() - threadDb.readerFor(threadDb.conversationList).use { reader -> - while (reader.next != null) { - val recipient = reader.current.recipient - val sessionId = recipient.address.serialize() - if (!recipient.isGroupRecipient && SodiumUtilities.sessionId(sessionId, blindedId, serverPublicKey)) { - val contactMapping = mapping.copy(sessionId = sessionId) - db.addBlindedIdMapping(contactMapping) - return contactMapping - } + getAllContacts().forEach { contact -> + val sessionId = SessionId(contact.sessionID) + if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString, blindedId, serverPublicKey)) { + val contactMapping = mapping.copy(sessionId = sessionId.hexString) + db.addBlindedIdMapping(contactMapping) + return contactMapping } } db.getBlindedIdMappingsExceptFor(server).forEach { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 89bda09948..1b65706a5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -88,9 +88,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV40 = 61; private static final int lokiV41 = 62; private static final int lokiV42 = 63; + private static final int lokiV43 = 64; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV42; + private static final int DATABASE_VERSION = lokiV43; private static final int MIN_DATABASE_VERSION = lokiV7; private static final String CIPHER3_DATABASE_NAME = "signal.db"; public static final String DATABASE_NAME = "signal_v4.db"; @@ -356,6 +357,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS); db.execSQL(RecipientDatabase.getAddWrapperHash()); + db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests()); } @Override @@ -598,6 +600,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(RecipientDatabase.getAddWrapperHash()); } + if (oldVersion < lokiV43) { + db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests()); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ContentModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ContentModule.kt new file mode 100644 index 0000000000..89098a0f16 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ContentModule.kt @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.dependencies + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object ContentModule { + + @Provides + fun providesContentResolver(@ApplicationContext context: Context) =context.contentResolver + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 59f324b525..ba519f0a79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -72,8 +72,8 @@ import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.preferences.SettingsActivity -import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showMuteDialog +import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.IP2Country @@ -299,12 +299,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } EventBus.getDefault().register(this@HomeActivity) if (intent.hasExtra(FROM_ONBOARDING) - && intent.getBooleanExtra(FROM_ONBOARDING, false) - && !(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled() - ) { - Permissions.with(this) - .request(Manifest.permission.POST_NOTIFICATIONS) - .execute() + && intent.getBooleanExtra(FROM_ONBOARDING, false)) { + if ((getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) { + Permissions.with(this) + .request(Manifest.permission.POST_NOTIFICATIONS) + .execute() + } + configFactory.user?.let { user -> + if (!user.isBlockCommunityMessageRequestsSet()) { + user.setCommunityMessageRequests(false) + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsActivity.kt index b8606a3d54..de136694ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsActivity.kt @@ -1,9 +1,11 @@ package org.thoughtcrime.securesms.preferences import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +@AndroidEntryPoint class PrivacySettingsActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt index eaf48f8688..a04e71ddf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt @@ -8,6 +8,9 @@ import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceDataStore +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.BuildConfig import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences @@ -15,13 +18,19 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.isPasswo import org.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.components.SwitchPreferenceCompat +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled import org.thoughtcrime.securesms.util.IntentUtils +import javax.inject.Inject +@AndroidEntryPoint class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() { + + @Inject lateinit var configFactory: ConfigFactory + override fun onCreate(paramBundle: Bundle?) { super.onCreate(paramBundle) findPreference(TextSecurePreferences.SCREEN_LOCK)!! @@ -30,6 +39,33 @@ class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() { .onPreferenceChangeListener = TypingIndicatorsToggleListener() findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!! .onPreferenceChangeListener = CallToggleListener(this) { setCall(it) } + findPreference(getString(R.string.preferences__message_requests_category))?.let { category -> + when (val user = configFactory.user) { + null -> category.isVisible = false + else -> SwitchPreferenceCompat(requireContext()).apply { + key = TextSecurePreferences.ALLOW_MESSAGE_REQUESTS + preferenceDataStore = object : PreferenceDataStore() { + + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { + return user.getCommunityMessageRequests() + } + return super.getBoolean(key, defValue) + } + + override fun putBoolean(key: String?, value: Boolean) { + if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { + user.setCommunityMessageRequests(value) + return + } + super.putBoolean(key, value) + } + } + title = getString(R.string.preferences__message_requests_title) + summary = getString(R.string.preferences__message_requests_summary) + }.let(category::addPreference) + } + } initializeVisibility() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index dd013afa74..2c0b39d69d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -1,5 +1,11 @@ package org.thoughtcrime.securesms.repository +import android.content.ContentResolver +import android.content.Context +import app.cash.copper.flow.observeQuery +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.session.libsession.database.MessageDataProvider import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.MessageRequestResponse @@ -15,6 +21,7 @@ import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase @@ -35,6 +42,8 @@ import kotlin.coroutines.suspendCoroutine interface ConversationRepository { fun maybeGetRecipientForThreadId(threadId: Long): Recipient? + fun maybeGetBlindedRecipient(recipient: Recipient): Recipient? + fun recipientUpdateFlow(threadId: Long): Flow fun saveDraft(threadId: Long, text: String) fun getDraft(threadId: Long): String? fun clearDrafts(threadId: Long) @@ -75,6 +84,7 @@ interface ConversationRepository { } class DefaultConversationRepository @Inject constructor( + @ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val messageDataProvider: MessageDataProvider, private val threadDb: ThreadDatabase, @@ -87,13 +97,29 @@ class DefaultConversationRepository @Inject constructor( private val storage: Storage, private val lokiMessageDb: LokiMessageDatabase, private val sessionJobDb: SessionJobDatabase, - private val configFactory: ConfigFactory + private val configFactory: ConfigFactory, + private val contentResolver: ContentResolver, ) : ConversationRepository { override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? { return threadDb.getRecipientForThreadId(threadId) } + override fun maybeGetBlindedRecipient(recipient: Recipient): Recipient? { + if (!recipient.isOpenGroupInboxRecipient) return null + return Recipient.from( + context, + Address.fromSerialized(GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())), + false + ) + } + + override fun recipientUpdateFlow(threadId: Long): Flow { + return contentResolver.observeQuery(DatabaseContentProviders.Conversation.getUriForThread(threadId)).map { + maybeGetRecipientForThreadId(threadId) + } + } + override fun saveDraft(threadId: Long, text: String) { if (text.isEmpty()) return val drafts = DraftDatabase.Drafts() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d46f68605..64c2421dc3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -627,6 +627,9 @@ Priority Screenshot Notifications Receive a notification when a contact takes a screenshot of a one-to-one chat. + Message Requests + Community Message Requests + Allow message requests from Community conversations @@ -1033,6 +1036,7 @@ Some of your devices are using outdated versions. Syncing may be unreliable until they are updated. There are no messages in %s. + %s has message requests from Community conversations turned off, so you cannot send them a message. You have no messages in Note to Self. You have no messages from %s.\nSend a message to start the conversation! diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml index 12607ee769..6e0cd98c2f 100644 --- a/app/src/main/res/xml/preferences_app_protection.xml +++ b/app/src/main/res/xml/preferences_app_protection.xml @@ -20,6 +20,12 @@ + + + () + private val storage = mock() private val threadId = 123L - private val edKeyPair = mock(KeyPair::class.java) + private val edKeyPair = mock() private lateinit var recipient: Recipient private val viewModel: ConversationViewModel by lazy { - ConversationViewModel(threadId, edKeyPair, mock(), repository, storage) + ConversationViewModel(threadId, edKeyPair, repository, storage) } @Before fun setUp() { - recipient = mock(Recipient::class.java) + recipient = mock() whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient) + whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow()) } @Test @@ -79,7 +83,7 @@ class ConversationViewModelTest: BaseViewModelTest() { @Test fun `should delete locally`() { - val message = mock(MessageRecord::class.java) + val message = mock() viewModel.deleteLocally(message) @@ -88,7 +92,7 @@ class ConversationViewModelTest: BaseViewModelTest() { @Test fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest { - val message = mock(MessageRecord::class.java) + val message = mock() val error = Throwable() whenever(repository.deleteForEveryone(anyLong(), any(), any())) .thenReturn(ResultOf.Failure(error)) @@ -101,7 +105,7 @@ class ConversationViewModelTest: BaseViewModelTest() { @Test fun `should emit error message on failure to delete messages without unsend request`() = runBlockingTest { - val message = mock(MessageRecord::class.java) + val message = mock() val error = Throwable() whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet())) .thenReturn(ResultOf.Failure(error)) @@ -181,4 +185,30 @@ class ConversationViewModelTest: BaseViewModelTest() { assertThat(viewModel.uiState.value.uiMessages.size, equalTo(0)) } + @Test + fun `open group recipient should have no blinded recipient`() { + whenever(recipient.isOpenGroupRecipient).thenReturn(true) + whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false) + whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false) + assertThat(viewModel.blindedRecipient, nullValue()) + } + + @Test + fun `local recipient should have input and no blinded recipient`() { + whenever(recipient.isLocalNumber).thenReturn(true) + assertThat(viewModel.hidesInputBar(), equalTo(false)) + assertThat(viewModel.blindedRecipient, nullValue()) + } + + @Test + fun `contact recipient should hide input bar if not accepting requests`() { + whenever(recipient.isOpenGroupInboxRecipient).thenReturn(true) + val blinded = mock { + whenever(it.blocksCommunityMessageRequests).thenReturn(true) + } + whenever(repository.maybeGetBlindedRecipient(recipient)).thenReturn(blinded) + assertThat(viewModel.blindedRecipient, notNullValue()) + assertThat(viewModel.hidesInputBar(), equalTo(true)) + } + } \ No newline at end of file diff --git a/libsession-util/libsession-util b/libsession-util/libsession-util index 7eb8702835..e3ccf29db0 160000 --- a/libsession-util/libsession-util +++ b/libsession-util/libsession-util @@ -1 +1 @@ -Subproject commit 7eb87028355bfc89950102c52d5b2927a25b2e22 +Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d diff --git a/libsession-util/src/main/cpp/user_profile.cpp b/libsession-util/src/main/cpp/user_profile.cpp index 78b671ef0d..5b3980e634 100644 --- a/libsession-util/src/main/cpp/user_profile.cpp +++ b/libsession-util/src/main/cpp/user_profile.cpp @@ -95,4 +95,33 @@ Java_network_loki_messenger_libsession_1util_UserProfile_getNtsPriority(JNIEnv * std::lock_guard lock{util::util_mutex_}; auto profile = ptrToProfile(env, thiz); return profile->get_nts_priority(); +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getCommunityMessageRequests( + JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + auto blinded_msg_requests = profile->get_blinded_msgreqs(); + if (blinded_msg_requests.has_value()) { + return *blinded_msg_requests; + } + return true; +} + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_setCommunityMessageRequests( + JNIEnv *env, jobject thiz, jboolean blocks) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + profile->set_blinded_msgreqs(std::optional{(bool)blocks}); +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_isBlockCommunityMessageRequestsSet( + JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + return profile->get_blinded_msgreqs().has_value(); } \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index 52fb541d7d..7dc41d7fdc 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -126,6 +126,9 @@ class UserProfile(pointer: Long) : ConfigBase(pointer) { external fun setPic(userPic: UserPic) external fun setNtsPriority(priority: Int) external fun getNtsPriority(): Int + external fun getCommunityMessageRequests(): Boolean + external fun setCommunityMessageRequests(blocks: Boolean) + external fun isBlockCommunityMessageRequestsSet(): Boolean } class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 4fff833835..1de94cfe3d 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -42,6 +42,7 @@ interface StorageProtocol { fun getUserProfile(): Profile fun setProfileAvatar(recipient: Recipient, profileAvatar: String?) fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) + fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean) fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) fun clearUserPic() // Signal @@ -228,4 +229,5 @@ interface StorageProtocol { fun notifyConfigUpdates(forConfigObject: ConfigBase) fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean + fun isCheckingCommunityRequests(): Boolean } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index 705fc7ce45..d6c067930d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -25,7 +25,8 @@ class VisibleMessage( var profile: Profile? = null, var openGroupInvitation: OpenGroupInvitation? = null, var reaction: Reaction? = null, - var hasMention: Boolean = false + var hasMention: Boolean = false, + var blocksMessageRequests: Boolean = false ) : Message() { override val isSelfSendValid: Boolean = true @@ -74,6 +75,9 @@ class VisibleMessage( val reaction = Reaction.fromProto(reactionProto) result.reaction = reaction } + + result.blocksMessageRequests = with (dataMessage) { hasBlocksCommunityMessageRequests() && blocksCommunityMessageRequests } + return result } } @@ -141,6 +145,8 @@ class VisibleMessage( return null } } + // Community blocked message requests flag + dataMessage.blocksCommunityMessageRequests = blocksMessageRequests // Sync target if (syncTarget != null) { dataMessage.syncTarget = syncTarget diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index dc6d1475f8..c6b6186c9c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -753,7 +753,8 @@ object OpenGroupApi { ) } val serverCapabilities = storage.getServerCapabilities(server) - if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) { + val isAcceptingCommunityRequests = storage.isCheckingCommunityRequests() + if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && isAcceptingCommunityRequests) { requests.add( if (lastInboxMessageId == null) { BatchRequestInfo( diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index be7075dc54..e7929bc350 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -242,9 +242,16 @@ object MessageSender { private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { val deferred = deferred() val storage = MessagingModuleConfiguration.shared.storage + val configFactory = MessagingModuleConfiguration.shared.configFactory if (message.sentTimestamp == null) { message.sentTimestamp = SnodeAPI.nowWithOffset } + // Attach the blocks message requests info + configFactory.user?.let { user -> + if (message is VisibleMessage) { + message.blocksMessageRequests = !user.getCommunityMessageRequests() + } + } val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!! var serverCapabilities = listOf() var blindedPublicKey: ByteArray? = null diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index b101e9db0b..8745418e77 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -304,6 +304,10 @@ fun MessageReceiver.handleVisibleMessage( profileManager.setProfilePicture(context, recipient, null, null) } } + + if (userPublicKey != messageSender && !isUserBlindedSender) { + storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests) + } } // Parse quote if needed var quoteModel: QuoteModel? = null diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt index bfab2585de..5d6741d30e 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt @@ -1,6 +1,7 @@ package org.session.libsession.utilities -import network.loki.messenger.libsession_util.util.GroupInfo +import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.messaging.utilities.SessionId import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.Hex import java.io.IOException @@ -16,8 +17,15 @@ object GroupUtil { } @JvmStatic - fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): String { - return OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID) + fun getEncodedOpenGroupInboxID(openGroup: OpenGroup, sessionId: SessionId): Address { + val openGroupInboxId = + "${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray() + return getEncodedOpenGroupInboxID(openGroupInboxId) + } + + @JvmStatic + fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address { + return Address.fromSerialized(OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID)) } @JvmStatic @@ -52,7 +60,7 @@ object GroupUtil { } @JvmStatic - fun getDecodedOpenGroupInbox(groupID: String): String { + fun getDecodedOpenGroupInboxSessionId(groupID: String): String { val decodedGroupId = getDecodedGroupID(groupID) if (decodedGroupId.split("!").count() > 2) { return decodedGroupId.split("!", limit = 3)[2] diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 2b92b9836e..9f00047ac2 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -287,6 +287,8 @@ interface TextSecurePreferences { const val OCEAN_DARK = "ocean.dark" const val OCEAN_LIGHT = "ocean.light" + const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS" + @JvmStatic fun getLastConfigurationSyncTime(context: Context): Long { return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0) diff --git a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java index e2d193a934..20f285b59c 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java +++ b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java @@ -100,6 +100,7 @@ public class Recipient implements RecipientModifiedListener { private String notificationChannel; private boolean forceSmsSelection; private String wrapperHash; + private boolean blocksCommunityMessageRequests; private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED; @@ -192,6 +193,7 @@ public class Recipient implements RecipientModifiedListener { this.unidentifiedAccessMode = details.get().unidentifiedAccessMode; this.forceSmsSelection = details.get().forceSmsSelection; this.notifyType = details.get().notifyType; + this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests; this.participants.clear(); this.participants.addAll(details.get().participants); @@ -228,6 +230,7 @@ public class Recipient implements RecipientModifiedListener { Recipient.this.unidentifiedAccessMode = result.unidentifiedAccessMode; Recipient.this.forceSmsSelection = result.forceSmsSelection; Recipient.this.notifyType = result.notifyType; + Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests; Recipient.this.participants.clear(); Recipient.this.participants.addAll(result.participants); @@ -281,6 +284,7 @@ public class Recipient implements RecipientModifiedListener { this.unidentifiedAccessMode = details.unidentifiedAccessMode; this.forceSmsSelection = details.forceSmsSelection; this.wrapperHash = details.wrapperHash; + this.blocksCommunityMessageRequests = details.blocksCommunityMessageRequests; this.participants.addAll(details.participants); this.resolving = false; @@ -321,7 +325,7 @@ public class Recipient implements RecipientModifiedListener { return this.name; } } else if (isOpenGroupInboxRecipient()){ - String inboxID = GroupUtil.getDecodedOpenGroupInbox(sessionID); + String inboxID = GroupUtil.getDecodedOpenGroupInboxSessionId(sessionID); Contact contact = storage.getContactWithSessionID(inboxID); if (contact == null) { return sessionID; } return contact.displayName(Contact.ContactContext.REGULAR); @@ -345,6 +349,18 @@ public class Recipient implements RecipientModifiedListener { if (notify) notifyListeners(); } + public boolean getBlocksCommunityMessageRequests() { + return blocksCommunityMessageRequests; + } + + public void setBlocksCommunityMessageRequests(boolean blocksCommunityMessageRequests) { + synchronized (this) { + this.blocksCommunityMessageRequests = blocksCommunityMessageRequests; + } + + notifyListeners(); + } + public synchronized @NonNull MaterialColor getColor() { if (isGroupRecipient()) return MaterialColor.GROUP; else if (color != null) return color; @@ -759,12 +775,43 @@ public class Recipient implements RecipientModifiedListener { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Recipient recipient = (Recipient) o; - return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar) && Objects.equals(wrapperHash, recipient.wrapperHash); + return resolving == recipient.resolving + && mutedUntil == recipient.mutedUntil + && notifyType == recipient.notifyType + && blocked == recipient.blocked + && approved == recipient.approved + && approvedMe == recipient.approvedMe + && expireMessages == recipient.expireMessages + && address.equals(recipient.address) + && Objects.equals(name, recipient.name) + && Objects.equals(customLabel, recipient.customLabel) + && Objects.equals(groupAvatarId, recipient.groupAvatarId) + && Arrays.equals(profileKey, recipient.profileKey) + && Objects.equals(profileName, recipient.profileName) + && Objects.equals(profileAvatar, recipient.profileAvatar) + && Objects.equals(wrapperHash, recipient.wrapperHash) + && blocksCommunityMessageRequests == recipient.blocksCommunityMessageRequests; } @Override public int hashCode() { - int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar, wrapperHash); + int result = Objects.hash( + address, + name, + customLabel, + resolving, + groupAvatarId, + mutedUntil, + notifyType, + blocked, + approved, + approvedMe, + expireMessages, + profileName, + profileAvatar, + wrapperHash, + blocksCommunityMessageRequests + ); result = 31 * result + Arrays.hashCode(profileKey); return result; } @@ -869,55 +916,59 @@ public class Recipient implements RecipientModifiedListener { private final UnidentifiedAccessMode unidentifiedAccessMode; private final boolean forceSmsSelection; private final String wrapperHash; + private final boolean blocksCommunityMessageRequests; public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil, - int notifyType, - @NonNull VibrateState messageVibrateState, - @NonNull VibrateState callVibrateState, - @Nullable Uri messageRingtone, - @Nullable Uri callRingtone, - @Nullable MaterialColor color, - int defaultSubscriptionId, - int expireMessages, - @NonNull RegisteredState registered, - @Nullable byte[] profileKey, - @Nullable String systemDisplayName, - @Nullable String systemContactPhoto, - @Nullable String systemPhoneLabel, - @Nullable String systemContactUri, - @Nullable String signalProfileName, - @Nullable String signalProfileAvatar, - boolean profileSharing, - @Nullable String notificationChannel, - @NonNull UnidentifiedAccessMode unidentifiedAccessMode, - boolean forceSmsSelection, - String wrapperHash) + int notifyType, + @NonNull VibrateState messageVibrateState, + @NonNull VibrateState callVibrateState, + @Nullable Uri messageRingtone, + @Nullable Uri callRingtone, + @Nullable MaterialColor color, + int defaultSubscriptionId, + int expireMessages, + @NonNull RegisteredState registered, + @Nullable byte[] profileKey, + @Nullable String systemDisplayName, + @Nullable String systemContactPhoto, + @Nullable String systemPhoneLabel, + @Nullable String systemContactUri, + @Nullable String signalProfileName, + @Nullable String signalProfileAvatar, + boolean profileSharing, + @Nullable String notificationChannel, + @NonNull UnidentifiedAccessMode unidentifiedAccessMode, + boolean forceSmsSelection, + String wrapperHash, + boolean blocksCommunityMessageRequests + ) { - this.blocked = blocked; - this.approved = approved; - this.approvedMe = approvedMe; - this.muteUntil = muteUntil; - this.notifyType = notifyType; - this.messageVibrateState = messageVibrateState; - this.callVibrateState = callVibrateState; - this.messageRingtone = messageRingtone; - this.callRingtone = callRingtone; - this.color = color; - this.defaultSubscriptionId = defaultSubscriptionId; - this.expireMessages = expireMessages; - this.registered = registered; - this.profileKey = profileKey; - this.systemDisplayName = systemDisplayName; - this.systemContactPhoto = systemContactPhoto; - this.systemPhoneLabel = systemPhoneLabel; - this.systemContactUri = systemContactUri; - this.signalProfileName = signalProfileName; - this.signalProfileAvatar = signalProfileAvatar; - this.profileSharing = profileSharing; - this.notificationChannel = notificationChannel; - this.unidentifiedAccessMode = unidentifiedAccessMode; - this.forceSmsSelection = forceSmsSelection; - this.wrapperHash = wrapperHash; + this.blocked = blocked; + this.approved = approved; + this.approvedMe = approvedMe; + this.muteUntil = muteUntil; + this.notifyType = notifyType; + this.messageVibrateState = messageVibrateState; + this.callVibrateState = callVibrateState; + this.messageRingtone = messageRingtone; + this.callRingtone = callRingtone; + this.color = color; + this.defaultSubscriptionId = defaultSubscriptionId; + this.expireMessages = expireMessages; + this.registered = registered; + this.profileKey = profileKey; + this.systemDisplayName = systemDisplayName; + this.systemContactPhoto = systemContactPhoto; + this.systemPhoneLabel = systemPhoneLabel; + this.systemContactUri = systemContactUri; + this.signalProfileName = signalProfileName; + this.signalProfileAvatar = signalProfileAvatar; + this.profileSharing = profileSharing; + this.notificationChannel = notificationChannel; + this.unidentifiedAccessMode = unidentifiedAccessMode; + this.forceSmsSelection = forceSmsSelection; + this.wrapperHash = wrapperHash; + this.blocksCommunityMessageRequests = blocksCommunityMessageRequests; } public @Nullable MaterialColor getColor() { @@ -1020,6 +1071,10 @@ public class Recipient implements RecipientModifiedListener { return wrapperHash; } + public boolean getBlocksCommunityMessageRequests() { + return blocksCommunityMessageRequests; + } + } diff --git a/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java b/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java index 75ebd837b6..195250cb37 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java +++ b/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java @@ -178,6 +178,7 @@ class RecipientProvider { @NonNull final UnidentifiedAccessMode unidentifiedAccessMode; final boolean forceSmsSelection; final String wrapperHash; + final boolean blocksCommunityMessageRequests; RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId, boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings, @@ -211,6 +212,7 @@ class RecipientProvider { this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED; this.forceSmsSelection = settings != null && settings.isForceSmsSelection(); this.wrapperHash = settings != null ? settings.getWrapperHash() : null; + this.blocksCommunityMessageRequests = settings != null && settings.getBlocksCommunityMessageRequests(); if (name == null && settings != null) this.name = settings.getSystemDisplayName(); else this.name = name; diff --git a/libsignal/protobuf/SignalService.proto b/libsignal/protobuf/SignalService.proto index 68dd35ce61..a2448604f3 100644 --- a/libsignal/protobuf/SignalService.proto +++ b/libsignal/protobuf/SignalService.proto @@ -163,20 +163,21 @@ message DataMessage { required Action action = 4; } - optional string body = 1; - repeated AttachmentPointer attachments = 2; - optional GroupContext group = 3; - optional uint32 flags = 4; - optional uint32 expireTimer = 5; - optional bytes profileKey = 6; - optional uint64 timestamp = 7; - optional Quote quote = 8; - repeated Preview preview = 10; - optional Reaction reaction = 11; - optional LokiProfile profile = 101; - optional OpenGroupInvitation openGroupInvitation = 102; - optional ClosedGroupControlMessage closedGroupControlMessage = 104; - optional string syncTarget = 105; + optional string body = 1; + repeated AttachmentPointer attachments = 2; + optional GroupContext group = 3; + optional uint32 flags = 4; + optional uint32 expireTimer = 5; + optional bytes profileKey = 6; + optional uint64 timestamp = 7; + optional Quote quote = 8; + repeated Preview preview = 10; + optional Reaction reaction = 11; + optional LokiProfile profile = 101; + optional OpenGroupInvitation openGroupInvitation = 102; + optional ClosedGroupControlMessage closedGroupControlMessage = 104; + optional string syncTarget = 105; + optional bool blocksCommunityMessageRequests = 106; } message CallMessage { diff --git a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java index 8e26b05d92..895575b09c 100644 --- a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java +++ b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java @@ -5890,6 +5890,16 @@ public final class SignalServiceProtos { */ com.google.protobuf.ByteString getSyncTargetBytes(); + + // optional bool blocksCommunityMessageRequests = 106; + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + boolean hasBlocksCommunityMessageRequests(); + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + boolean getBlocksCommunityMessageRequests(); } /** * Protobuf type {@code signalservice.DataMessage} @@ -6066,6 +6076,11 @@ public final class SignalServiceProtos { syncTarget_ = input.readBytes(); break; } + case 848: { + bitField0_ |= 0x00001000; + blocksCommunityMessageRequests_ = input.readBool(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -14336,6 +14351,22 @@ public final class SignalServiceProtos { } } + // optional bool blocksCommunityMessageRequests = 106; + public static final int BLOCKSCOMMUNITYMESSAGEREQUESTS_FIELD_NUMBER = 106; + private boolean blocksCommunityMessageRequests_; + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + public boolean hasBlocksCommunityMessageRequests() { + return ((bitField0_ & 0x00001000) == 0x00001000); + } + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + public boolean getBlocksCommunityMessageRequests() { + return blocksCommunityMessageRequests_; + } + private void initFields() { body_ = ""; attachments_ = java.util.Collections.emptyList(); @@ -14351,6 +14382,7 @@ public final class SignalServiceProtos { openGroupInvitation_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.getDefaultInstance(); closedGroupControlMessage_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.getDefaultInstance(); syncTarget_ = ""; + blocksCommunityMessageRequests_ = false; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -14448,6 +14480,9 @@ public final class SignalServiceProtos { if (((bitField0_ & 0x00000800) == 0x00000800)) { output.writeBytes(105, getSyncTargetBytes()); } + if (((bitField0_ & 0x00001000) == 0x00001000)) { + output.writeBool(106, blocksCommunityMessageRequests_); + } getUnknownFields().writeTo(output); } @@ -14513,6 +14548,10 @@ public final class SignalServiceProtos { size += com.google.protobuf.CodedOutputStream .computeBytesSize(105, getSyncTargetBytes()); } + if (((bitField0_ & 0x00001000) == 0x00001000)) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(106, blocksCommunityMessageRequests_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -14697,6 +14736,8 @@ public final class SignalServiceProtos { bitField0_ = (bitField0_ & ~0x00001000); syncTarget_ = ""; bitField0_ = (bitField0_ & ~0x00002000); + blocksCommunityMessageRequests_ = false; + bitField0_ = (bitField0_ & ~0x00004000); return this; } @@ -14815,6 +14856,10 @@ public final class SignalServiceProtos { to_bitField0_ |= 0x00000800; } result.syncTarget_ = syncTarget_; + if (((from_bitField0_ & 0x00004000) == 0x00004000)) { + to_bitField0_ |= 0x00001000; + } + result.blocksCommunityMessageRequests_ = blocksCommunityMessageRequests_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -14923,6 +14968,9 @@ public final class SignalServiceProtos { syncTarget_ = other.syncTarget_; onChanged(); } + if (other.hasBlocksCommunityMessageRequests()) { + setBlocksCommunityMessageRequests(other.getBlocksCommunityMessageRequests()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -16457,6 +16505,39 @@ public final class SignalServiceProtos { return this; } + // optional bool blocksCommunityMessageRequests = 106; + private boolean blocksCommunityMessageRequests_ ; + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + public boolean hasBlocksCommunityMessageRequests() { + return ((bitField0_ & 0x00004000) == 0x00004000); + } + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + public boolean getBlocksCommunityMessageRequests() { + return blocksCommunityMessageRequests_; + } + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + public Builder setBlocksCommunityMessageRequests(boolean value) { + bitField0_ |= 0x00004000; + blocksCommunityMessageRequests_ = value; + onChanged(); + return this; + } + /** + * optional bool blocksCommunityMessageRequests = 106; + */ + public Builder clearBlocksCommunityMessageRequests() { + bitField0_ = (bitField0_ & ~0x00004000); + blocksCommunityMessageRequests_ = false; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:signalservice.DataMessage) } @@ -27160,7 +27241,7 @@ public final class SignalServiceProtos { "actionNotification\022<\n\004type\030\001 \002(\0162..signa" + "lservice.DataExtractionNotification.Type" + "\022\021\n\ttimestamp\030\002 \001(\004\"\'\n\004Type\022\016\n\nSCREENSHO" + - "T\020\001\022\017\n\013MEDIA_SAVED\020\002\"\361\r\n\013DataMessage\022\014\n\004", + "T\020\001\022\017\n\013MEDIA_SAVED\020\002\"\231\016\n\013DataMessage\022\014\n\004", "body\030\001 \001(\t\0225\n\013attachments\030\002 \003(\0132 .signal" + "service.AttachmentPointer\022*\n\005group\030\003 \001(\013" + "2\033.signalservice.GroupContext\022\r\n\005flags\030\004" + @@ -27175,12 +27256,13 @@ public final class SignalServiceProtos { "ice.DataMessage.OpenGroupInvitation\022W\n\031c" + "losedGroupControlMessage\030h \001(\01324.signals" + "ervice.DataMessage.ClosedGroupControlMes" + - "sage\022\022\n\nsyncTarget\030i \001(\t\032\225\002\n\005Quote\022\n\n\002id" + + "sage\022\022\n\nsyncTarget\030i \001(\t\022&\n\036blocksCommun" + + "ityMessageRequests\030j \001(\010\032\225\002\n\005Quote\022\n\n\002id" + "\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\022\014\n\004text\030\003 \001(\t\022F\n\013" + "attachments\030\004 \003(\01321.signalservice.DataMe" + "ssage.Quote.QuotedAttachment\032\231\001\n\020QuotedA" + - "ttachment\022\023\n\013contentType\030\001 \001(\t\022\020\n\010fileNa" + - "me\030\002 \001(\t\0223\n\tthumbnail\030\003 \001(\0132 .signalserv", + "ttachment\022\023\n\013contentType\030\001 \001(\t\022\020\n\010fileNa", + "me\030\002 \001(\t\0223\n\tthumbnail\030\003 \001(\0132 .signalserv" + "ice.AttachmentPointer\022\r\n\005flags\030\004 \001(\r\"\032\n\005" + "Flags\022\021\n\rVOICE_MESSAGE\020\001\032V\n\007Preview\022\013\n\003u" + "rl\030\001 \002(\t\022\r\n\005title\030\002 \001(\t\022/\n\005image\030\003 \001(\0132 " + @@ -27189,8 +27271,8 @@ public final class SignalServiceProtos { "icture\030\002 \001(\t\0320\n\023OpenGroupInvitation\022\013\n\003u" + "rl\030\001 \002(\t\022\014\n\004name\030\003 \002(\t\032\374\003\n\031ClosedGroupCo" + "ntrolMessage\022G\n\004type\030\001 \002(\01629.signalservi" + - "ce.DataMessage.ClosedGroupControlMessage" + - ".Type\022\021\n\tpublicKey\030\002 \001(\014\022\014\n\004name\030\003 \001(\t\0221", + "ce.DataMessage.ClosedGroupControlMessage", + ".Type\022\021\n\tpublicKey\030\002 \001(\014\022\014\n\004name\030\003 \001(\t\0221" + "\n\021encryptionKeyPair\030\004 \001(\0132\026.signalservic" + "e.KeyPair\022\017\n\007members\030\005 \003(\014\022\016\n\006admins\030\006 \003" + "(\014\022U\n\010wrappers\030\007 \003(\0132C.signalservice.Dat" + @@ -27199,8 +27281,8 @@ public final class SignalServiceProtos { "yPairWrapper\022\021\n\tpublicKey\030\001 \002(\014\022\030\n\020encry" + "ptedKeyPair\030\002 \002(\014\"r\n\004Type\022\007\n\003NEW\020\001\022\027\n\023EN" + "CRYPTION_KEY_PAIR\020\003\022\017\n\013NAME_CHANGE\020\004\022\021\n\r" + - "MEMBERS_ADDED\020\005\022\023\n\017MEMBERS_REMOVED\020\006\022\017\n\013" + - "MEMBER_LEFT\020\007\032\222\001\n\010Reaction\022\n\n\002id\030\001 \002(\004\022\016", + "MEMBERS_ADDED\020\005\022\023\n\017MEMBERS_REMOVED\020\006\022\017\n\013", + "MEMBER_LEFT\020\007\032\222\001\n\010Reaction\022\n\n\002id\030\001 \002(\004\022\016" + "\n\006author\030\002 \002(\t\022\r\n\005emoji\030\003 \001(\t\022:\n\006action\030" + "\004 \002(\0162*.signalservice.DataMessage.Reacti" + "on.Action\"\037\n\006Action\022\t\n\005REACT\020\000\022\n\n\006REMOVE" + @@ -27209,8 +27291,8 @@ public final class SignalServiceProtos { "ervice.CallMessage.Type\022\014\n\004sdps\030\002 \003(\t\022\027\n" + "\017sdpMLineIndexes\030\003 \003(\r\022\017\n\007sdpMids\030\004 \003(\t\022" + "\014\n\004uuid\030\005 \002(\t\"f\n\004Type\022\r\n\tPRE_OFFER\020\006\022\t\n\005" + - "OFFER\020\001\022\n\n\006ANSWER\020\002\022\026\n\022PROVISIONAL_ANSWE" + - "R\020\003\022\022\n\016ICE_CANDIDATES\020\004\022\014\n\010END_CALL\020\005\"\245\004", + "OFFER\020\001\022\n\n\006ANSWER\020\002\022\026\n\022PROVISIONAL_ANSWE", + "R\020\003\022\022\n\016ICE_CANDIDATES\020\004\022\014\n\010END_CALL\020\005\"\245\004" + "\n\024ConfigurationMessage\022E\n\014closedGroups\030\001" + " \003(\0132/.signalservice.ConfigurationMessag" + "e.ClosedGroup\022\022\n\nopenGroups\030\002 \003(\t\022\023\n\013dis" + @@ -27219,8 +27301,8 @@ public final class SignalServiceProtos { "ignalservice.ConfigurationMessage.Contac" + "t\032\233\001\n\013ClosedGroup\022\021\n\tpublicKey\030\001 \001(\014\022\014\n\004" + "name\030\002 \001(\t\0221\n\021encryptionKeyPair\030\003 \001(\0132\026." + - "signalservice.KeyPair\022\017\n\007members\030\004 \003(\014\022\016" + - "\n\006admins\030\005 \003(\014\022\027\n\017expirationTimer\030\006 \001(\r\032", + "signalservice.KeyPair\022\017\n\007members\030\004 \003(\014\022\016", + "\n\006admins\030\005 \003(\014\022\027\n\017expirationTimer\030\006 \001(\r\032" + "\223\001\n\007Contact\022\021\n\tpublicKey\030\001 \002(\014\022\014\n\004name\030\002" + " \002(\t\022\026\n\016profilePicture\030\003 \001(\t\022\022\n\nprofileK" + "ey\030\004 \001(\014\022\022\n\nisApproved\030\005 \001(\010\022\021\n\tisBlocke" + @@ -27229,8 +27311,8 @@ public final class SignalServiceProtos { "rofileKey\030\002 \001(\014\0227\n\007profile\030\003 \001(\0132&.signa" + "lservice.DataMessage.LokiProfile\"\375\001\n\023Sha" + "redConfigMessage\0225\n\004kind\030\001 \002(\0162\'.signals" + - "ervice.SharedConfigMessage.Kind\022\r\n\005seqno" + - "\030\002 \002(\003\022\014\n\004data\030\003 \002(\014\"\221\001\n\004Kind\022\020\n\014USER_PR", + "ervice.SharedConfigMessage.Kind\022\r\n\005seqno", + "\030\002 \002(\003\022\014\n\004data\030\003 \002(\014\"\221\001\n\004Kind\022\020\n\014USER_PR" + "OFILE\020\001\022\014\n\010CONTACTS\020\002\022\027\n\023CONVO_INFO_VOLA" + "TILE\020\003\022\n\n\006GROUPS\020\004\022\025\n\021CLOSED_GROUP_INFO\020" + "\005\022\030\n\024CLOSED_GROUP_MEMBERS\020\006\022\023\n\017ENCRYPTIO" + @@ -27239,8 +27321,8 @@ public final class SignalServiceProtos { "timestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022\010\n" + "\004READ\020\001\"\354\001\n\021AttachmentPointer\022\n\n\002id\030\001 \002(" + "\006\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004s" + - "ize\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest\030\006" + - " \001(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n", + "ize\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest\030\006", + " \001(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n" + "\005width\030\t \001(\r\022\016\n\006height\030\n \001(\r\022\017\n\007caption\030" + "\013 \001(\t\022\013\n\003url\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_MES" + "SAGE\020\001\"\365\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022.\n\004t" + @@ -27249,7 +27331,7 @@ public final class SignalServiceProtos { "atar\030\005 \001(\0132 .signalservice.AttachmentPoi" + "nter\022\016\n\006admins\030\006 \003(\t\"H\n\004Type\022\013\n\007UNKNOWN\020" + "\000\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020\n\014" + - "REQUEST_INFO\020\004B3\n\034org.session.libsignal." + + "REQUEST_INFO\020\004B3\n\034org.session.libsignal.", "protosB\023SignalServiceProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = @@ -27298,7 +27380,7 @@ public final class SignalServiceProtos { internal_static_signalservice_DataMessage_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_DataMessage_descriptor, - new java.lang.String[] { "Body", "Attachments", "Group", "Flags", "ExpireTimer", "ProfileKey", "Timestamp", "Quote", "Preview", "Reaction", "Profile", "OpenGroupInvitation", "ClosedGroupControlMessage", "SyncTarget", }); + new java.lang.String[] { "Body", "Attachments", "Group", "Flags", "ExpireTimer", "ProfileKey", "Timestamp", "Quote", "Preview", "Reaction", "Profile", "OpenGroupInvitation", "ClosedGroupControlMessage", "SyncTarget", "BlocksCommunityMessageRequests", }); internal_static_signalservice_DataMessage_Quote_descriptor = internal_static_signalservice_DataMessage_descriptor.getNestedTypes().get(0); internal_static_signalservice_DataMessage_Quote_fieldAccessorTable = new From 0e0cbf112b1869b808909461068b87ca9d02cc3e Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 28 Aug 2023 11:18:51 +0930 Subject: [PATCH 110/112] Use separate keys for huawei shared prefs --- app/build.gradle | 4 +++- .../org/thoughtcrime/securesms/ApplicationContext.java | 3 +++ .../libsession/utilities/TextSecurePreferences.kt | 9 ++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 61f5601891..6a46e41ad8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -135,6 +135,7 @@ android { buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" + buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"' } huawei { @@ -143,7 +144,7 @@ android { buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" - + buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"_HUAWEI\"' } website { @@ -152,6 +153,7 @@ android { buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID" buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" + buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"' } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 75622c7bb6..8715042c8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -109,6 +109,7 @@ import dagger.hilt.EntryPoints; import dagger.hilt.android.HiltAndroidApp; import kotlin.Unit; import kotlinx.coroutines.Job; +import network.loki.messenger.BuildConfig; import network.loki.messenger.libsession_util.ConfigBase; import network.loki.messenger.libsession_util.UserProfile; @@ -206,6 +207,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO @Override public void onCreate() { + TextSecurePreferences.setPushSuffix(BuildConfig.PUSH_KEY_SUFFIX); + DatabaseModule.init(this); MessagingModuleConfiguration.configure(this); super.onCreate(); diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 9f00047ac2..1b431d62bb 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -189,6 +189,9 @@ interface TextSecurePreferences { internal val _events = MutableSharedFlow(0, 64, BufferOverflow.DROP_OLDEST) val events get() = _events.asSharedFlow() + @JvmStatic + var pushSuffix = "" + const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase" const val LANGUAGE_PREF = "pref_language" const val THREAD_TRIM_NOW = "pref_trim_now" @@ -251,9 +254,9 @@ interface TextSecurePreferences { const val LINK_PREVIEWS = "pref_link_previews" const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning" const val GIF_GRID_LAYOUT = "pref_gif_grid_layout" - const val IS_PUSH_ENABLED = "pref_is_using_fcm" - const val PUSH_TOKEN = "pref_fcm_token_2" - const val PUSH_REGISTER_TIME = "pref_last_fcm_token_upload_time_2" + val IS_PUSH_ENABLED get() = "pref_is_using_fcm$pushSuffix" + val PUSH_TOKEN get() = "pref_fcm_token_2$pushSuffix" + val PUSH_REGISTER_TIME get() = "pref_last_fcm_token_upload_time_2$pushSuffix" const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time" const val CONFIGURATION_SYNCED = "pref_configuration_synced" const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time" From 4daa3e69236e26afd57d65294035d8ecece90046 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:43:48 +1000 Subject: [PATCH 111/112] feat: add huawei build folder to gitignore (#1308) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1fe35a0e7b..023fc81010 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ ffpr *.sh pkcs11.password app/play +app/huawei \ No newline at end of file From 29275cef518a77de4e1d6d6d8dd5dad057634b78 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:42:23 +1000 Subject: [PATCH 112/112] build: update build number (#1310) --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6a46e41ad8..2bf496cd49 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 354 -def canonicalVersionName = "1.17.0" +def canonicalVersionCode = 355 +def canonicalVersionName = "1.17.1" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1,