Merge remote-tracking branch 'upstream/dev'

This commit is contained in:
hjubb 2023-09-01 13:50:33 +10:00
commit 99e5ed3db7
159 changed files with 3545 additions and 2351 deletions

3
.gitignore vendored
View File

@ -15,4 +15,5 @@ signing.properties
ffpr
*.sh
pkcs11.password
play
app/play
app/huawei

View File

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

View File

@ -1,3 +1,4 @@
buildscript {
repositories {
google()
@ -13,12 +14,16 @@ buildscript {
}
}
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
apply plugin: 'com.android.application'
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'
@ -26,7 +31,181 @@ configurations.all {
exclude module: "commons-logging"
}
def canonicalVersionCode = 355
def canonicalVersionName = "1.17.1"
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
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.4.7'
}
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 "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 {
dimension "distribution"
ext.websiteUpdateUrl = "null"
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 {
dimension "distribution"
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
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', '\"\"'
}
}
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
}
def huaweiEnabled = project.properties['huawei'] != null
applicationVariants.configureEach { variant ->
if (variant.flavorName == 'huawei') {
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)
}
}
}
}
}
}
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"
@ -39,7 +218,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"
@ -49,11 +227,12 @@ 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'
}
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'
@ -154,137 +333,16 @@ dependencies {
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:shadows-multidex:4.4'
}
def canonicalVersionCode = 354
def canonicalVersionName = "1.17.0"
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.31.5-beta"
implementation "com.google.accompanist:accompanist-pager-indicators:0.31.5-beta"
implementation "androidx.compose.runtime:runtime-livedata:1.4.3"
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
}
implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02'
implementation 'androidx.compose.material:material:1.5.0-alpha02'
}
static def getLastCommitTimestamp() {
@ -305,3 +363,8 @@ def autoResConfig() {
.collect { matcher -> matcher.group(1) }
.sort()
}
// Allow references to generated code
kapt {
correctErrorTypes = true
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:node="merge">
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="appid=107205081">
</meta-data>
<meta-data
android:name="com.huawei.hms.client.cpid"
android:value="cpid=30061000024605000">
</meta-data>
<service
android:name="org.thoughtcrime.securesms.notifications.HuaweiPushService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -0,0 +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":"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"
}
}
]
}

View File

@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.notifications
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class HuaweiBindingModule {
@Binds
abstract fun bindTokenFetcher(tokenFetcher: HuaweiTokenFetcher): TokenFetcher
}

View File

@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.notifications
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
import java.lang.Exception
import javax.inject.Inject
private val TAG = HuaweiPushService::class.java.simpleName
@AndroidEntryPoint
class HuaweiPushService: HmsMessageService() {
@Inject lateinit var pushRegistry: PushRegistry
@Inject lateinit var pushReceiver: PushReceiver
override fun onMessageReceived(message: RemoteMessage?) {
Log.d(TAG, "onMessageReceived")
message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?:
pushReceiver.onPush(message?.data?.let(Base64::decode))
}
override fun onNewToken(token: String?) {
pushRegistry.register(token)
}
override fun onNewToken(token: String?, bundle: Bundle?) {
Log.d(TAG, "New HCM token: $token.")
pushRegistry.register(token)
}
override fun onDeletedMessages() {
Log.d(TAG, "onDeletedMessages")
pushRegistry.refresh(false)
}
}

View File

@ -0,0 +1,29 @@
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,
private val pushRegistry: Lazy<PushRegistry>,
): TokenFetcher {
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) }
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="preferences_notifications_strategy_category_fast_mode_summary">You\'ll be notified of new messages reliably and immediately using Huaweis notification servers.</string>
<string name="activity_pn_mode_fast_mode_explanation">You\'ll be notified of new messages reliably and immediately using Huaweis notification servers.</string>
</resources>

View File

@ -310,14 +310,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
</activity>
<service
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:exported="false" />
<service

View File

@ -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;
@ -73,10 +74,9 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
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.NotificationChannels;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
import org.thoughtcrime.securesms.notifications.PushRegistry;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;
@ -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;
@ -143,8 +144,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject LokiAPIDatabase lokiAPIDatabase;
@Inject public Storage storage;
@Inject Device device;
@Inject MessageDataProvider messageDataProvider;
@Inject TextSecurePreferences textSecurePreferences;
@Inject PushRegistry pushRegistry;
@Inject ConfigFactory configFactory;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@ -204,11 +207,15 @@ 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();
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
messagingModuleConfiguration = new MessagingModuleConfiguration(
this,
storage,
device,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
configFactory
@ -226,10 +233,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) {
registerForFCMIfNeeded(false);
}
initializeExpiringMessageManager();
initializeTypingStatusRepository();
initializeTypingStatusSender();
@ -427,33 +430,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
private static class ProviderInitializationException extends RuntimeException { }
public void registerForFCMIfNeeded(final Boolean force) {
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
if (force && firebaseInstanceIdJob != null) {
firebaseInstanceIdJob.cancel(null);
}
firebaseInstanceIdJob = FcmUtils.getFcmInstanceId(task->{
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)) {
LokiPushNotificationManager.register(token, userPublicKey, this, force);
} else {
LokiPushNotificationManager.unregister(token, this);
}
});
return Unit.INSTANCE;
});
}
private void setUpPollingIfNeeded() {
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return;
@ -524,18 +500,14 @@ 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);
}
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
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();

View File

@ -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<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> 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);
}
}

View File

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

View File

@ -147,6 +147,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) {
@ -524,7 +528,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> 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);

View File

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

View File

@ -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<RecipientDeliveryStatus> members;
private final boolean isPushGroup;
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> 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;
}
}
}

View File

@ -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 = true,
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)

View File

@ -1,5 +0,0 @@
package org.thoughtcrime.securesms;
public interface Unbindable {
public void unbind();
}

View File

@ -249,18 +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
}
else -> { /* do nothing */ }
CALL_CONNECTED -> wantsToAnswer = false
else -> {}
}
updateControls(state)
}

View File

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

View File

@ -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
@ -18,13 +19,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 by lazy { ViewProfilePictureBinding.bind(this) }
lateinit var glide: GlideRequests
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
private val glide: GlideRequests = GlideApp.with(this)
var publicKey: String? = null
var displayName: String? = null
var additionalPublicKey: String? = null
@ -37,8 +39,13 @@ class ProfilePictureView @JvmOverloads constructor(
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
// endregion
constructor(context: Context, sender: Recipient): this(context) {
update(sender)
}
// region Updating
fun update(recipient: Recipient) {
fun getUserDisplayName(publicKey: String): String {
@ -66,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
@ -80,7 +87,6 @@ class ProfilePictureView @JvmOverloads constructor(
}
fun update() {
if (!this::glide.isInitialized) return
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
if (additionalPublicKey != null) {

View File

@ -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<Bitmap> bitmapReference;
private ListenableFutureTask<Bitmap> 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<Bitmap> get() {
Util.assertMainThread();
if (bitmapReference != null && bitmapReference.get() != null) {
return new ListenableFutureTask<>(bitmapReference.get());
} else if (task != null) {
return task;
} else {
Callable<Bitmap> 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<Void, Void, Void>() {
@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();
}
}

View File

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

View File

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

View File

@ -55,8 +55,7 @@ class UserView : LinearLayout {
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
val address = user.address.serialize()
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(user)
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) {
@ -88,7 +87,7 @@ class UserView : LinearLayout {
}
fun unbind() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
}
// endregion
}

View File

@ -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<SharedContact.Phone> phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
List<SharedContact.Email> emails = new ArrayList<>(contact.getEmails().size());
List<SharedContact.PostalAddress> 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<Phone> 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<Email> 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<PostalAddress> 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;
}
}
}

View File

@ -32,14 +32,13 @@ 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.update(contact.recipient)
binding.nameTextView.text = contact.displayName
binding.root.setOnClickListener { listener(contact.recipient) }
}
fun unbind() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
}
}

View File

@ -33,11 +33,14 @@ import android.view.WindowManager
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.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
@ -76,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
@ -103,9 +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.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_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.dialogs.BlockedDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
@ -168,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
@ -234,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
}
@ -247,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
@ -306,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 {
@ -582,35 +584,35 @@ 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)
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
val profilePictureView = binding.toolbarContent.profilePictureView.root
val profilePictureView = binding.toolbarContent.profilePictureView
viewModel.recipient?.let(profilePictureView::update)
}
// 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
@ -759,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,
@ -795,7 +797,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()
@ -845,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 &&
@ -1064,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
@ -1926,10 +1925,24 @@ 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<MessageRecord>) {
val intent = Intent(this, MessageDetailActivity::class.java)
intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp)
push(intent)
Intent(this, MessageDetailActivity::class.java)
.apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) }
.let { handleMessageDetail.launch(it) }
endActionMode()
}
@ -1968,7 +1981,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun reply(messages: Set<MessageRecord>) {
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
messages.firstOrNull()?.let { binding?.inputBar?.draftQuote(recipient, it, glide) }
endActionMode()
}

View File

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

View File

@ -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<OpenGroup> = 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 <T : ViewModel> create(modelClass: Class<T>): T {
return ConversationViewModel(threadId, edKeyPair, contentResolver, repository, storage) as T
return ConversationViewModel(threadId, edKeyPair, repository, storage) as T
}
}
}

View File

@ -1,99 +1,401 @@
package org.thoughtcrime.securesms.conversation.v2
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import android.view.LayoutInflater
import android.view.MotionEvent.ACTION_UP
import androidx.activity.viewModels
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.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
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
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.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
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 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.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 network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
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 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
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
import javax.inject.Inject
@AndroidEntryPoint
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
private lateinit var binding: ActivityMessageDetailBinding
var messageRecord: MessageRecord? = null
class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
@Inject
lateinit var storage: Storage
// region Settings
private val viewModel: MessageDetailsViewModel by viewModels()
companion object {
// Extras
const val MESSAGE_TIMESTAMP = "message_timestamp"
const val ON_REPLY = 1
const val ON_RESEND = 2
const val ON_DELETE = 3
}
// endregion
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()
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
ComposeView(this)
.apply { setContent { MessageDetailsScreen() } }
.let(::setContentView)
lifecycleScope.launch {
viewModel.eventFlow.collect {
when (it) {
Event.Finish -> finish()
is Event.StartMediaPreview -> startActivity(
getPreviewIntent(this@MessageDetailActivity, it.args)
)
}
}
}
}
fun updateContent() {
val dateLocale = Locale.getDefault()
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
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
}
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
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
binding.expiresIn.text = duration
@Composable
private fun MessageDetailsScreen() {
val state by viewModel.stateFlow.collectAsState()
AppTheme {
MessageDetails(
state = state,
onReply = { setResultAndFinish(ON_REPLY) },
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
onDelete = { setResultAndFinish(ON_DELETE) },
onClickImage = { viewModel.onClickImage(it) },
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
)
}
}
}
private fun setResultAndFinish(code: Int) {
Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) }
.let(Intent()::putExtras)
.let { setResult(code, it) }
finish()
}
}
@SuppressLint("ClickableViewAccessibility")
@Composable
fun MessageDetails(
state: MessageDetailsState,
onReply: () -> Unit = {},
onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {},
onClickImage: (Int) -> Unit = {},
onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> }
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
state.record?.let { message ->
AndroidView(
modifier = Modifier.padding(horizontal = 32.dp),
factory = {
ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
bind(
message,
thread = state.thread!!,
onAttachmentNeedsDownload = onAttachmentNeedsDownload,
suppressThumbnails = true
)
setOnTouchListener { _, event ->
if (event.actionMasked == ACTION_UP) onContentClick(event)
true
}
}
}
)
}
Carousel(state.imageAttachments) { onClickImage(it) }
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
CellMetadata(state)
CellButtons(
onReply,
onResend,
onDelete,
)
}
}
@Composable
fun CellMetadata(
state: MessageDetailsState,
) {
state.apply {
if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
CellWithPaddingAndMargin {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
TitledText(sent)
TitledText(received)
TitledErrorText(error)
senderInfo?.let {
TitledView(state.fromTitle) {
Row {
sender?.let { Avatar(it) }
TitledMonospaceText(it)
}
}
}
}
}
}
}
@Composable
fun CellButtons(
onReply: () -> Unit = {},
onResend: (() -> Unit)? = null,
onDelete: () -> Unit = {},
) {
Cell {
Column {
ItemButton(
stringResource(R.string.reply),
R.drawable.ic_message_details__reply,
onClick = onReply
)
Divider()
onResend?.let {
ItemButton(
stringResource(R.string.resend),
R.drawable.ic_message_details__refresh,
onClick = it
)
Divider()
}
ItemButton(
stringResource(R.string.delete),
R.drawable.ic_message_details__trash,
colors = destructiveButtonColors(),
onClick = onDelete
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Carousel(attachments: List<Attachment>, 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, attachments, onClick)
HorizontalPagerIndicator(pagerState)
ExpandButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(8.dp)
) { onClick(pagerState.currentPage) }
}
CarouselNextButton(pagerState)
}
attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) }
}
}
@OptIn(
ExperimentalFoundationApi::class,
ExperimentalGlideComposeApi::class
)
@Composable
private fun CellCarousel(
pagerState: PagerState,
attachments: List<Attachment>,
onClick: (Int) -> Unit
) {
CellNoMargin {
HorizontalPager(state = pagerState) { i ->
GlideImage(
contentScale = ContentScale.Crop,
modifier = Modifier
.aspectRatio(1f)
.clickable { onClick(i) },
model = attachments[i].uri,
contentDescription = attachments[i].fileName ?: 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 = stringResource(id = R.string.expand),
modifier = Modifier.clickable { onClick() },
)
}
}
@Preview
@Composable
fun PreviewMessageDetails(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
PreviewTheme(themeResId) {
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"),
)
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun FileDetails(fileDetails: List<TitledText>) {
if (fileDetails.isEmpty()) return
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)
)
}
}
}
}
}
@Composable
fun TitledErrorText(titledText: TitledText?) {
TitledText(
titledText,
valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
)
}
@Composable
fun TitledMonospaceText(titledText: TitledText?) {
TitledText(
titledText,
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
)
}
@Composable
fun TitledText(
titledText: TitledText?,
modifier: Modifier = Modifier,
valueStyle: TextStyle = LocalTextStyle.current,
) {
titledText?.apply {
TitledView(title, modifier) {
Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
}
}
}
@Composable
fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
Title(title)
content()
}
}
@Composable
fun Title(title: GetString) {
Text(title.string(), fontWeight = FontWeight.Bold)
}

View File

@ -0,0 +1,159 @@
package org.thoughtcrime.securesms.conversation.v2
import android.net.Uri
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
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
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
import org.thoughtcrime.securesms.mms.Slide
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
@HiltViewModel
class MessageDetailsViewModel @Inject constructor(
private val attachmentDb: AttachmentDatabase,
private val lokiMessageDatabase: LokiMessageDatabase,
private val mmsSmsDatabase: MmsSmsDatabase,
private val threadDb: ThreadDatabase,
) : ViewModel() {
private val state = MutableStateFlow(MessageDetailsState())
val stateFlow = state.asStateFlow()
private val event = Channel<Event>()
val eventFlow = event.receiveAsFlow()
var timestamp: Long = 0L
set(value) {
field = value
val record = mmsSmsDatabase.getMessageForTimestamp(timestamp)
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)!!,
)
}
}
private val Slide.details: List<TitledText>
get() = listOfNotNull(
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? =
slide.takeIf { it.hasAudio() }
?.run { asAttachment() as? DatabaseAttachment }
?.run { getAttachmentAudioExtras(attachmentId)?.durationMs }
?.takeIf { it > 0 }
?.let {
String.format(
"%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(it),
TimeUnit.MILLISECONDS.toSeconds(it) % 60
)
}
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
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))
}
}
}
data class MessageDetailsState(
val attachments: List<Attachment> = emptyList(),
val imageAttachments: List<Attachment> = attachments.filter { it.hasImage },
val nonImageAttachmentFileDetails: List<TitledText>? = 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 fileDetails: List<TitledText>,
val fileName: String?,
val uri: Uri?,
val hasImage: Boolean
)
sealed class Event {
object Finish: Event()
data class StartMediaPreview(val args: MediaPreviewArgs): Event()
}

View File

@ -28,11 +28,10 @@ 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.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE

View File

@ -28,11 +28,10 @@ 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.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE

View File

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

View File

@ -36,6 +36,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.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil
@ -45,7 +46,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
@ -59,13 +59,14 @@ 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,
onAttachmentNeedsDownload: (Long, Long) -> Unit
searchQuery: String? = null,
contactIsTrusted: Boolean = true,
onAttachmentNeedsDownload: (Long, Long) -> Unit,
suppressThumbnails: Boolean = false
) {
// Background
val color = if (message.isOutgoing) context.getAccentColor()
@ -184,7 +185,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
*/
@ -237,6 +238,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<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }

View File

@ -2,16 +2,17 @@ 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
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
@ -46,6 +47,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 +72,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
@ -114,6 +115,7 @@ class VisibleMessageView : LinearLayout {
binding.root.disableClipping()
binding.mainContainer.disableClipping()
binding.messageInnerContainer.disableClipping()
binding.messageInnerLayout.disableClipping()
binding.messageContentView.root.disableClipping()
}
// endregion
@ -121,14 +123,14 @@ 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,
lastSeen: Long,
delegate: VisibleMessageViewDelegate?,
delegate: VisibleMessageViewDelegate? = null,
onAttachmentNeedsDownload: (Long, Long) -> Unit
) {
val threadID = message.threadId
@ -139,7 +141,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
@ -148,22 +150,21 @@ 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.update(message.individualRecipient)
binding.profilePictureView.setOnClickListener {
if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
@ -342,11 +343,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
@ -392,7 +396,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
@ -412,7 +416,7 @@ class VisibleMessageView : LinearLayout {
}
fun recycle() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
binding.messageContentView.root.recycle()
}
@ -513,7 +517,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) {

View File

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

View File

@ -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<VH extends RecyclerView.ViewHolder, T>
extends CursorRecyclerViewAdapter<VH>
{
private static final String TAG = FastCursorRecyclerViewAdapter.class.getSimpleName();
private final LinkedList<T> fastRecords = new LinkedList<>();
private final List<Long> 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<Long> releaseIdIterator = releasedRecordIds.iterator();
while (releaseIdIterator.hasNext()) {
long releasedId = releaseIdIterator.next();
Iterator<T> 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;
}
}

View File

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

View File

@ -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.PushNotificationAPI
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
@ -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)
@ -591,7 +600,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co
val expireTimer = group.disappearingTimer
setExpirationTimer(groupId, expireTimer.toInt())
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
PushRegistryV1.subscribeGroup(group.sessionId, publicKey = localUserPublicKey)
// Notify the user
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
threadDb.setDate(threadID, formationTimestamp)
@ -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 {

View File

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

View File

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

View File

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

View File

@ -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.PushNotificationAPI
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
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
PushRegistryV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey)
// Stop polling
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
storage.cancelPendingMessageSendJobs(threadId)

View File

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

View File

@ -65,7 +65,6 @@ class ConversationView : LinearLayout {
} else {
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
}
binding.profilePictureView.root.glide = glide
val unreadCount = thread.unreadCount
if (thread.recipient.isBlocked) {
binding.accentView.setBackgroundResource(R.color.destructive)
@ -125,11 +124,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? {

View File

@ -67,12 +67,13 @@ 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.PushRegistry
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
@ -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 pushRegistry: PushRegistry
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
private val homeViewModel by viewModels<HomeViewModel>()
@ -168,8 +170,7 @@ 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.setOnClickListener { openSettings() }
binding.searchViewContainer.setOnClickListener {
binding.globalSearchInputLayout.requestFocus()
}
@ -231,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.registerForFCMIfNeeded(false)
pushRegistry.refresh(false)
if (textSecurePreferences.getLocalNumber() != null) {
OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs()
@ -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)
}
}
}
}
@ -364,8 +369,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
}
@ -440,10 +445,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

View File

@ -53,10 +53,9 @@ 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.isLarge = true
profilePictureView.update(recipient)
nameTextViewContainer.visibility = View.VISIBLE
nameTextViewContainer.setOnClickListener {
if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener

View File

@ -83,22 +83,20 @@ 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)
}
val binding = ViewGlobalSearchResultBinding.bind(view)
fun bindPayload(newQuery: String, model: Model) {
bindQuery(newQuery, model)
}
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)

View File

@ -87,12 +87,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)
@ -108,14 +108,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)
}
@ -124,12 +124,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
@ -138,7 +138,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

View File

@ -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<Attachment> thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaTypes.IMAGE_JPEG);

View File

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

View File

@ -34,7 +34,6 @@ class MessageRequestView : LinearLayout {
// region Updating
fun bind(thread: ThreadRecord, glide: GlideRequests) {
this.thread = thread
binding.profilePictureView.root.glide = glide
val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString()
binding.displayNameTextView.text = senderDisplayName
@ -44,12 +43,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? {

View File

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

View File

@ -1,19 +0,0 @@
@file:JvmName("FcmUtils")
package org.thoughtcrime.securesms.notifications
import com.google.android.gms.tasks.Task
import com.google.firebase.iid.FirebaseInstanceId
import com.google.firebase.iid.InstanceIdResult
import kotlinx.coroutines.*
fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) {
val task = FirebaseInstanceId.getInstance().instanceId
while (!task.isComplete && isActive) {
// 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)
}
}

View File

@ -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 LokiPushNotificationManager {
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<Map<*, *>, Exception> {
return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response ->
JsonUtil.fromJson(response.body, Map::class.java)
}
}
}

View File

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.notifications
interface PushManager {
fun refresh(force: Boolean)
}

View File

@ -1,58 +0,0 @@
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 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
class PushNotificationService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d("Loki", "New FCM token: $token.")
val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return
LokiPushNotificationManager.register(token, userPublicKey, this, false)
}
override fun onMessageReceived(message: RemoteMessage) {
Log.d("Loki", "Received a push notification.")
val base64EncodedData = message.data?.get("ENCRYPTED_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())
}
}
}
override fun onDeletedMessages() {
Log.d("Loki", "Called onDeletedMessages.")
super.onDeletedMessages()
val token = TextSecurePreferences.getFCMToken(this)!!
val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return
LokiPushNotificationManager.register(token, userPublicKey, this, true)
}
}

View File

@ -0,0 +1,114 @@
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 PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
private val sodium = LazySodiumAndroid(SodiumAndroid())
fun onPush(dataMap: Map<String, String>?) {
onPush(dataMap?.asByteArray())
}
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)
.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())
}
private fun Map<String, String>.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)
null
}
}
// old v1 push notification; we still need this for receiving legacy closed group notifications
else -> this["ENCRYPTED_DATA"]?.let(Base64::decode)
}
private 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))
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 {
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
)
)
}
}

View File

@ -0,0 +1,111 @@
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
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.combine.and
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
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 val TAG = PushRegistry::class.java.name
@Singleton
class PushRegistry @Inject constructor(
@ApplicationContext private val context: Context,
private val device: Device,
private val tokenManager: TokenManager,
private val pushRegistryV2: PushRegistryV2,
private val prefs: TextSecurePreferences,
private val tokenFetcher: TokenFetcher,
) {
private var pushRegistrationJob: Job? = null
fun refresh(force: Boolean): Job {
Log.d(TAG, "refresh() called with: force = $force")
pushRegistrationJob?.apply {
if (force) cancel() else if (isActive) return MainScope().launch {}
}
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> {
Log.d(TAG, "refresh() called")
if (token?.isNotEmpty() != true) return emptyPromise()
prefs.setPushToken(token)
val userPublicKey = prefs.getLocalNumber() ?: return emptyPromise()
val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise()
return when {
prefs.isPushEnabled() -> register(token, userPublicKey, userEdKey)
tokenManager.isRegistered -> unregister(token, userPublicKey, userEdKey)
else -> emptyPromise()
}
}
/**
* Register for push notifications.
*/
private fun register(
token: String,
publicKey: String,
userEd25519Key: KeyPair,
namespaces: List<Int> = listOf(Namespace.DEFAULT)
): Promise<*, Exception> {
Log.d(TAG, "register() called")
val v1 = PushRegistryV1.register(
device = device,
token = token,
publicKey = publicKey
) fail {
Log.e(TAG, "register v1 failed", it)
}
val v2 = pushRegistryV2.register(
device, token, publicKey, userEd25519Key, namespaces
) fail {
Log.e(TAG, "register v2 failed", it)
}
return v1 and v2 success {
Log.d(TAG, "register v1 & v2 success")
tokenManager.register()
}
}
private fun unregister(
token: String,
userPublicKey: String,
userEdKey: KeyPair
): Promise<*, Exception> = PushRegistryV1.unregister() and pushRegistryV2.unregister(
device, token, userPublicKey, userEdKey
) fail {
Log.e(TAG, "unregisterBoth failed", it)
} success {
tokenManager.unregister()
}
}

View File

@ -0,0 +1,116 @@
package org.thoughtcrime.securesms.notifications
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.KeyPair
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.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
import org.session.libsession.utilities.Device
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 javax.inject.Inject
import javax.inject.Singleton
private val TAG = PushRegistryV2::class.java.name
private const val maxRetryCount = 4
@Singleton
class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) {
private val sodium = LazySodiumAndroid(SodiumAndroid())
fun register(
device: Device,
token: String,
publicKey: String,
userEd25519Key: KeyPair,
namespaces: List<Int>
): Promise<SubscriptionResponse, Exception> {
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
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 = device.service,
sig_ts = timestamp,
signature = Base64.encodeBytes(signature),
service_info = mapOf("token" to token),
enc_key = pnKey.asHexString,
).let(Json::encodeToString)
return retryResponseBody<SubscriptionResponse>("subscribe", requestParameters) success {
Log.d(TAG, "registerV2 success")
}
}
fun unregister(
device: Device,
token: String,
userPublicKey: String,
userEdKey: KeyPair
): Promise<UnsubscribeResponse, Exception> {
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 = device.service,
sig_ts = timestamp,
signature = Base64.encodeBytes(signature),
service_info = mapOf("token" to token),
).let(Json::encodeToString)
return retryResponseBody<UnsubscribeResponse>("unsubscribe", requestParameters) success {
Log.d(TAG, "unregisterV2 success")
}
}
private inline fun <reified T: Response> retryResponseBody(path: String, requestParameters: String): Promise<T, Exception> =
retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) }
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
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,
server.url,
server.publicKey,
Version.V4
).map { response ->
response.body!!.inputStream()
.let { Json.decodeFromStream<T>(it) }
.also { if (it.isFailure()) throw Exception("error: ${it.message}.") }
}
}
}

View File

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

View File

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.notifications
interface TokenFetcher {
suspend fun fetch(): String?
}

View File

@ -0,0 +1,32 @@
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
private const val INTERVAL: Int = 12 * 60 * 60 * 1000
@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 unregister() {
time = 0
}
private var time
get() = TextSecurePreferences.getPushRegisterTime(context)
set(value) = TextSecurePreferences.setPushRegisterTime(context, value)
private fun currentTime() = System.currentTimeMillis()
}

View File

@ -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
@ -19,6 +20,8 @@ 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.notifications.PushRegistry
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.PNModeView
@ -27,8 +30,13 @@ 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
@AndroidEntryPoint
class PNModeActivity : BaseActionBarActivity() {
@Inject lateinit var pushRegistry: PushRegistry
private lateinit var binding: ActivityPnModeBinding
private var selectedOptionView: PNModeView? = null
@ -158,10 +166,10 @@ class PNModeActivity : BaseActionBarActivity() {
return
}
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView))
val application = ApplicationContext.getInstance(this)
application.startPollingIfNeeded()
application.registerForFCMIfNeeded(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)

View File

@ -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,8 +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) {
glide = this@ViewHolder.glide
with (binding.profilePictureView) {
update(selectable.item)
}
binding.root.setOnClickListener { toggle(selectable) }

View File

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

View File

@ -1,182 +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 network.loki.messenger.R;
public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment {
@SuppressWarnings("unused")
private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName();
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
// Set up FCM toggle
String fcmKey = "pref_key_use_fcm";
((SwitchPreferenceCompat)findPreference(fcmKey)).setChecked(TextSecurePreferences.isUsingFCM(getContext()));
this.findPreference(fcmKey)
.setOnPreferenceChangeListener((preference, newValue) -> {
TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue);
ApplicationContext.getInstance(getContext()).registerForFCMIfNeeded(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<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ApplicationContext.getInstance(getActivity()).messageNotifier.updateNotification(getActivity());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return super.onPreferenceChange(preference, value);
}
}
}

View File

@ -0,0 +1,183 @@
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)
val job = pushRegistry.refresh(true)
fcmPreference.isEnabled = false
lifecycleScope.launch(Dispatchers.IO) {
job.join()
withContext(Dispatchers.Main) {
fcmPreference.isEnabled = true
}
}
true
}
if (NotificationChannels.supported()) {
prefs.setNotificationRingtone(
NotificationChannels.getMessageRingtone(requireContext()).toString()
)
prefs.setNotificationVibrateEnabled(
NotificationChannels.getMessageVibrate(requireContext())
)
}
findPreference<Preference>(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceChangeListener = RingtoneSummaryListener()
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceChangeListener = NotificationPrivacyListener()
findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean)
true
}
findPreference<Preference>(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<Preference>(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<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) as ListPreference?)
if (NotificationChannels.supported()) {
findPreference<Preference>(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<Preference>(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<Uri>(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<Void?, Void?, Void?>() {
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)
}
}

View File

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

View File

@ -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<Preference>(TextSecurePreferences.SCREEN_LOCK)!!
@ -30,6 +39,33 @@ class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
.onPreferenceChangeListener = TypingIndicatorsToggleListener()
findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
.onPreferenceChangeListener = CallToggleListener(this) { setCall(it) }
findPreference<PreferenceCategory>(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()
}

View File

@ -88,10 +88,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
@ -116,7 +114,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
private fun setupProfilePictureView(view: ProfilePictureView) {
view.glide = glide
view.apply {
publicKey = hexEncodedPublicKey
displayName = getDisplayName()
@ -255,8 +252,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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -144,7 +144,6 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
super(itemView);
this.callback = callback;
avatar = itemView.findViewById(R.id.reactions_bottom_view_avatar);
avatar.glide = GlideApp.with(itemView);
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
remove = itemView.findViewById(R.id.reactions_bottom_view_recipient_remove);
}

View File

@ -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<String> emoji;
ThisMessageEmojiPageModel(@NonNull List<String> 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<String> getEmoji() {
return emoji;
}
@Override
public @NonNull List<Emoji> 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;
}
}

View File

@ -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<Recipient?>
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<Recipient?> {
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()

View File

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

View File

@ -0,0 +1,63 @@
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)
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
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)
@Composable
fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)
@Composable
fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive)

View File

@ -0,0 +1,182 @@
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
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.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.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.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(
text: String,
@DrawableRes icon: Int,
colors: ButtonColors = transparentButtonColors(),
contentDescription: String = text,
onClick: () -> Unit
) {
TextButton(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
colors = colors,
onClick = onClick,
shape = RectangleShape,
) {
Box(modifier = Modifier
.width(80.dp)
.fillMaxHeight()) {
Icon(
painter = painterResource(id = icon),
contentDescription = contentDescription,
modifier = Modifier.align(Alignment.Center)
)
}
Text(text, modifier = Modifier.fillMaxWidth())
}
}
@Composable
fun Cell(content: @Composable () -> Unit) {
CellWithPaddingAndMargin(padding = 0.dp) { content() }
}
@Composable
fun CellNoMargin(content: @Composable () -> Unit) {
CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() }
}
@Composable
fun CellWithPaddingAndMargin(
padding: Dp = 24.dp,
margin: Dp = 32.dp,
content: @Composable () -> Unit
) {
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) {
if (pagerState.pageCount >= 2) Card(
shape = RoundedCornerShape(50.dp),
backgroundColor = Color.Black.copy(alpha = 0.4f),
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(8.dp)
) {
Box(modifier = Modifier.padding(8.dp)) {
HorizontalPagerIndicator(
pagerState = pagerState,
pageCount = pagerState.pageCount,
activeColor = Color.White,
inactiveColor = classicDarkColors[5])
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselPrevButton(pagerState: PagerState) {
CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselNextButton(pagerState: PagerState) {
CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.CarouselButton(
pagerState: PagerState,
enabled: Boolean,
@DrawableRes id: Int,
delta: Int
) {
if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp))
else {
val animationScope = rememberCoroutineScope()
IconButton(
modifier = Modifier
.width(40.dp)
.align(Alignment.CenterVertically),
enabled = enabled,
onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) {
Icon(
painter = painterResource(id = id),
contentDescription = "",
)
}
}
}
@Composable
fun Divider() {
androidx.compose.material.Divider(
modifier = Modifier.padding(horizontal = 16.dp),
)
}
@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)
)
}
}

View File

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

View File

@ -0,0 +1,76 @@
package org.thoughtcrime.securesms.ui
import android.content.Context
import androidx.annotation.AttrRes
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.PreviewParameterProvider
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
import com.google.android.material.color.MaterialColors
import network.loki.messenger.R
val LocalExtraColors = staticCompositionLocalOf<ExtraColors> { error("No Custom Attribute value provided") }
data class ExtraColors(
val settingsBackground: Color,
)
/**
* Converts current Theme to Compose Theme.
*/
@Composable
fun AppTheme(
content: @Composable () -> Unit
) {
val extraColors = LocalContext.current.run {
ExtraColors(
settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground),
)
}
CompositionLocalProvider(LocalExtraColors provides extraColors) {
AppCompatTheme {
content()
}
}
}
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 PreviewTheme(
themeResId: Int,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalContext provides ContextThemeWrapper(LocalContext.current, themeResId)
) {
AppTheme {
Box(modifier = Modifier.background(color = MaterialTheme.colors.background)) {
content()
}
}
}
}
class ThemeResPreviewParameterProvider : PreviewParameterProvider<Int> {
override val values = sequenceOf(
R.style.Classic_Dark,
R.style.Classic_Light,
R.style.Ocean_Dark,
R.style.Ocean_Light,
)
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.contactshare;
package org.thoughtcrime.securesms.util;
import android.text.Editable;
import android.text.TextWatcher;

View File

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

View File

@ -1,5 +0,0 @@
package org.thoughtcrime.securesms.webrtc
enum class AudioEvent {
}

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="27dp"
android:viewportHeight="27" android:viewportWidth="26"
android:width="26dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff" android:pathData="M7.38,12.265C7.792,12.265 8.093,11.962 8.093,11.55V11.079L7.957,9.008L9.494,10.629L11.394,12.542C11.528,12.682 11.703,12.746 11.893,12.746C12.336,12.746 12.654,12.448 12.654,12.009C12.654,11.807 12.58,11.627 12.441,11.489L10.533,9.588L8.911,8.052L10.995,8.188H11.497C11.909,8.188 12.217,7.892 12.217,7.476C12.217,7.058 11.915,6.758 11.497,6.758H7.849C7.097,6.758 6.662,7.193 6.662,7.944V11.55C6.662,11.957 6.969,12.265 7.38,12.265ZM14.497,19.444H18.146C18.897,19.444 19.338,19.009 19.338,18.257V14.65C19.338,14.245 19.031,13.937 18.614,13.937C18.208,13.937 17.901,14.24 17.901,14.65V15.123L18.043,17.193L16.5,15.572L14.605,13.66C14.472,13.52 14.291,13.456 14.101,13.456C13.664,13.456 13.34,13.754 13.34,14.191C13.34,14.394 13.42,14.574 13.559,14.712L15.461,16.613L17.089,18.149L15.005,18.013H14.497C14.086,18.013 13.777,18.309 13.777,18.726C13.777,19.144 14.086,19.444 14.497,19.444Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="21dp"
android:viewportWidth="26"
android:viewportHeight="21">
<path
android:pathData="M13.468,20.291C15.669,20.291 17.795,19.548 19.307,18.33C20.22,17.627 20.414,16.646 19.821,15.943C19.207,15.229 18.34,15.224 17.526,15.794C16.296,16.745 15.074,17.235 13.468,17.235C10.109,17.235 7.327,15 6.532,12.011H8.261C9.138,12.011 9.378,11.134 8.868,10.451L5.96,6.434C5.449,5.74 4.581,5.695 4.055,6.434L1.184,10.451C0.674,11.15 0.899,12.011 1.776,12.011H3.556C4.435,16.889 8.431,20.291 13.468,20.291ZM13.438,0.291C11.255,0.291 9.111,1.019 7.617,2.238C6.7,2.94 6.509,3.921 7.102,4.624C7.717,5.338 8.584,5.34 9.38,4.773C10.612,3.837 11.835,3.332 13.438,3.332C16.8,3.332 19.579,5.567 20.392,8.556H18.57C17.678,8.556 17.45,9.432 17.948,10.116L20.871,14.133C21.382,14.827 22.249,14.872 22.775,14.133L25.647,10.116C26.156,9.432 25.932,8.556 25.04,8.556H23.35C22.485,3.675 18.492,0.291 13.438,0.291Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<path
android:pathData="M10.847,3.572V7.974C20.432,8.707 23.521,16.919 23.868,20.933C19.76,14.869 13.476,14.412 10.847,14.942V19.466L2.962,11.702L10.847,3.572Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<path
android:pathData="M8.749,24.43H19.167C21.186,24.43 22.073,23.433 22.385,21.427L23.972,5.825L22.071,5.907L20.492,21.315C20.35,22.226 19.89,22.591 19.076,22.591H8.847C8.017,22.591 7.566,22.226 7.432,21.315L5.853,5.907L3.952,5.825L5.539,21.427C5.843,23.441 6.738,24.43 8.749,24.43ZM4.063,6.85H23.863C25.195,6.85 25.962,5.998 25.962,4.677V3.244C25.962,1.924 25.195,1.072 23.863,1.072H4.063C2.782,1.072 1.962,1.924 1.962,3.244V4.677C1.962,5.998 2.732,6.85 4.063,6.85ZM4.44,5.102C3.99,5.102 3.794,4.898 3.794,4.446V3.474C3.794,3.023 3.99,2.819 4.44,2.819H23.492C23.942,2.819 24.13,3.023 24.13,3.474V4.446C24.13,4.898 23.942,5.102 23.492,5.102H4.44Z"
android:fillColor="#FF3A3A"/>
</vector>

View File

@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="17dp"
android:viewportHeight="17" android:viewportWidth="13"
android:width="13dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M13,16.004l-13,-0l-0,-16l13,-0z"/>
<path android:fillColor="#ffffff" android:pathData="M0.646,1.736L10.112,7.933L0.444,14.268C0.323,14.343 0.222,14.438 0.144,14.547C0.067,14.657 0.015,14.779 -0.007,14.906C-0.029,15.033 -0.022,15.163 0.014,15.287C0.05,15.412 0.115,15.529 0.203,15.632C0.292,15.734 0.404,15.82 0.532,15.885C0.66,15.95 0.801,15.991 0.948,16.008C1.095,16.024 1.244,16.015 1.386,15.981C1.529,15.946 1.662,15.887 1.778,15.808L12.353,8.88C12.466,8.805 12.562,8.711 12.635,8.605C12.687,8.563 12.734,8.518 12.778,8.47C12.955,8.266 13.031,8.009 12.99,7.756C12.949,7.503 12.794,7.274 12.559,7.12L1.984,0.193C1.868,0.117 1.736,0.061 1.595,0.029C1.454,-0.003 1.307,-0.011 1.163,0.006C1.018,0.024 0.88,0.066 0.754,0.13C0.628,0.194 0.519,0.279 0.431,0.381C0.343,0.482 0.278,0.597 0.241,0.72C0.204,0.843 0.195,0.971 0.215,1.097C0.235,1.223 0.284,1.344 0.358,1.454C0.432,1.563 0.53,1.659 0.646,1.736Z"/>
</group>
</vector>

View File

@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="17dp"
android:viewportHeight="17" android:viewportWidth="12"
android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0.004h12v16h-12z"/>
<path android:fillColor="#ffffff" android:pathData="M11.403,14.272L2.666,8.075L11.59,1.74C11.701,1.665 11.795,1.57 11.867,1.46C11.938,1.351 11.986,1.229 12.006,1.102C12.027,0.975 12.02,0.845 11.987,0.721C11.954,0.596 11.894,0.479 11.812,0.376C11.73,0.274 11.627,0.187 11.509,0.123C11.391,0.058 11.26,0.016 11.125,0C10.989,-0.016 10.852,-0.007 10.72,0.027C10.589,0.062 10.466,0.12 10.359,0.2L0.597,7.127C0.493,7.203 0.405,7.297 0.337,7.403C0.289,7.444 0.245,7.49 0.205,7.538C0.042,7.742 -0.029,7.999 0.009,8.252C0.047,8.505 0.19,8.734 0.407,8.887L10.168,15.815C10.275,15.891 10.398,15.947 10.528,15.979C10.658,16.011 10.793,16.019 10.927,16.001C11.06,15.984 11.188,15.942 11.304,15.878C11.42,15.814 11.521,15.728 11.602,15.627C11.684,15.526 11.743,15.411 11.777,15.288C11.811,15.165 11.82,15.037 11.801,14.911C11.783,14.785 11.738,14.664 11.67,14.554C11.601,14.445 11.511,14.349 11.403,14.272Z"/>
</group>
</vector>

View File

@ -8,7 +8,7 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />

View File

@ -27,9 +27,8 @@
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp">
<include
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profileButton"
layout="@layout/view_profile_picture"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentLeft="true"

View File

@ -21,7 +21,7 @@
android:orientation="vertical"
android:gravity="center_horizontal">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"

View File

@ -9,7 +9,7 @@
android:gravity="center_vertical"
android:background="?selectableItemBackground"
android:id="@+id/backgroundContainer">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_width="@dimen/small_profile_picture_size"

Some files were not shown because too many files have changed in this diff Show More