Merge branch 'dev' into disappear-2

This commit is contained in:
andrew
2023-08-28 17:13:00 +09:30
105 changed files with 2323 additions and 2058 deletions

View File

@@ -24,15 +24,181 @@ 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'
configurations.all {
exclude module: "commons-logging"
}
def canonicalVersionCode = 354
def canonicalVersionName = "1.17.0"
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"
}
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"
}
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\""
}
}
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")
@@ -59,11 +225,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'
@@ -176,144 +343,6 @@ dependencies {
implementation 'androidx.compose.material:material:1.4.3'
}
def canonicalVersionCode = 354
def canonicalVersionName = "1.17.0"
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 {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
website {
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
buildFeatures {
dataBinding true
viewBinding true
}
}
static def getLastCommitTimestamp() {
new ByteArrayOutputStream().withStream { os ->
return os.toString() + "000"

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

@@ -313,14 +313,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;
@@ -143,8 +143,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;
@@ -207,8 +209,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
DatabaseModule.init(this);
MessagingModuleConfiguration.configure(this);
super.onCreate();
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
messagingModuleConfiguration = new MessagingModuleConfiguration(
this,
storage,
device,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
configFactory
@@ -226,10 +230,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 +427,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 +497,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

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

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

@@ -10,7 +10,6 @@ import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
import network.loki.messenger.databinding.ViewUserBinding
import org.session.libsession.avatars.ContactColors
import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsession.avatars.ProfileContactPhoto
@@ -74,7 +73,7 @@ class ProfilePictureView @JvmOverloads constructor(
additionalDisplayName = getUserDisplayName(apk)
}
} else if(recipient.isOpenGroupInboxRecipient) {
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
val publicKey = GroupUtil.getDecodedOpenGroupInboxSessionId(recipient.address.serialize())
this.publicKey = publicKey
displayName = getUserDisplayName(publicKey)
additionalPublicKey = null

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

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

@@ -38,6 +38,7 @@ import androidx.activity.viewModels
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
@@ -77,7 +78,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.Reaction
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
@@ -105,7 +105,6 @@ 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.ConversationActionBarDelegate
import org.thoughtcrime.securesms.conversation.expiration.ExpirationSettingsActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
@@ -172,6 +171,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.show
@@ -237,11 +237,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
}
@@ -250,7 +246,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
@@ -309,8 +305,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 {
@@ -593,26 +589,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate
private fun setUpInputBar() {
binding!!.inputBar.isVisible = viewModel.openGroup == null || viewModel.openGroup?.canWrite == true
binding!!.inputBar.delegate = this
binding!!.inputBarRecordingView.delegate = this
val binding = binding ?: return
binding.inputBar.isGone = viewModel.hidesInputBar()
binding.inputBar.delegate = this
binding.inputBarRecordingView.delegate = this
// GIF button
binding!!.gifButtonContainer.addView(gifButton)
binding.gifButtonContainer.addView(gifButton)
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
gifButton.onUp = { showGIFPicker() }
gifButton.snIsEnabled = false
// Document button
binding!!.documentButtonContainer.addView(documentButton)
binding.documentButtonContainer.addView(documentButton)
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
documentButton.onUp = { showDocumentPicker() }
documentButton.snIsEnabled = false
// Library button
binding!!.libraryButtonContainer.addView(libraryButton)
binding.libraryButtonContainer.addView(libraryButton)
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
libraryButton.onUp = { pickFromLibrary() }
libraryButton.snIsEnabled = false
// Camera button
binding!!.cameraButtonContainer.addView(cameraButton)
binding.cameraButtonContainer.addView(cameraButton)
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
cameraButton.onUp = { showCamera() }
cameraButton.snIsEnabled = false
@@ -776,7 +773,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,
@@ -1080,11 +1077,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

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
@@ -22,7 +20,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
@@ -31,7 +28,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() {
@@ -51,6 +47,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)
}
@@ -66,12 +71,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) }
}
}
@@ -203,22 +218,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

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

@@ -64,13 +64,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, DISAPPEARING_STATE, WRAPPER_HASH
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS
};
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
@@ -148,6 +149,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;
@@ -204,6 +210,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;
@@ -236,7 +243,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) {
@@ -393,6 +400,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

@@ -51,7 +51,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
@@ -92,8 +92,11 @@ import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
open class Storage(context: Context, helper: SQLCipherOpenHelper, private val configFactory: ConfigFactory) : Database(context, helper), StorageProtocol,
ThreadDatabase.ConversationThreadUpdateListener {
open class Storage(
context: Context,
helper: SQLCipherOpenHelper,
private val configFactory: ConfigFactory
) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener {
override fun threadCreated(address: Address, threadId: Long) {
val localUserAddress = getUserPublicKey() ?: return
@@ -191,6 +194,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)
@@ -446,6 +454,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
}
private fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) {
when (forConfigObject) {
is UserProfile -> updateUser(forConfigObject, messageTimestamp)
@@ -618,7 +630,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)
@@ -1486,7 +1498,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) {
@@ -1614,16 +1626,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

@@ -90,9 +90,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV41 = 62;
private static final int lokiV42 = 63;
private static final int lokiV43 = 64;
private static final int lokiV44 = 65;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV43;
private static final int DATABASE_VERSION = lokiV44;
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";
@@ -360,6 +361,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
db.execSQL(RecipientDatabase.getAddWrapperHash());
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
}
@Override
@@ -603,6 +605,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
if (oldVersion < lokiV43) {
db.execSQL(RecipientDatabase.getAddBlocksCommunityMessageRequests());
}
if (oldVersion < lokiV44) {
db.execSQL(RecipientDatabase.getCreateDisappearingStateCommand());
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
db.execSQL(ExpirationConfigurationDatabase.MIGRATE_GROUP_CONVERSATION_EXPIRY_TYPE_COMMAND);

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

@@ -67,6 +67,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.notifications.PushRegistry
import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.permissions.Permissions
@@ -106,6 +107,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
@Inject lateinit var groupDatabase: GroupDatabase
@Inject lateinit var textSecurePreferences: TextSecurePreferences
@Inject lateinit var configFactory: ConfigFactory
@Inject lateinit var pushRegistry: PushRegistry
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
private val homeViewModel by viewModels<HomeViewModel>()
@@ -230,8 +232,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
(applicationContext as ApplicationContext).startPollingIfNeeded()
// update things based on TextSecurePrefs (profile info etc)
// Set up remaining components if needed
val application = ApplicationContext.getInstance(this@HomeActivity)
application.registerForFCMIfNeeded(false)
pushRegistry.refresh(false)
if (textSecurePreferences.getLocalNumber() != null) {
OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs()
@@ -298,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)
}
}
}
}

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

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

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

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

@@ -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,6 +1,12 @@
package org.thoughtcrime.securesms.repository
import network.loki.messenger.libsession_util.util.ExpiryMode
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
@@ -16,6 +22,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.ExpirationConfigurationDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase
@@ -37,6 +44,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)
@@ -77,6 +86,7 @@ interface ConversationRepository {
}
class DefaultConversationRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val textSecurePreferences: TextSecurePreferences,
private val messageDataProvider: MessageDataProvider,
private val threadDb: ThreadDatabase,
@@ -90,13 +100,29 @@ class DefaultConversationRepository @Inject constructor(
private val lokiMessageDb: LokiMessageDatabase,
private val sessionJobDb: SessionJobDatabase,
private val configDb: ExpirationConfigurationDatabase,
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

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

@@ -644,6 +644,9 @@
<string name="preferences_notifications__priority">Priority</string>
<string name="preferences_app_protection__screenshot_notifications">Screenshot Notifications</string>
<string name="preferences_app_protected__screenshot_notifications_summary">Receive a notification when a contact takes a screenshot of a one-to-one chat.</string>
<string name="preferences__message_requests_category">Message Requests</string>
<string name="preferences__message_requests_title">Community Message Requests</string>
<string name="preferences__message_requests_summary">Allow message requests from Community conversations</string>
<!-- **************************************** -->
<!-- menus -->
<!-- **************************************** -->
@@ -1066,6 +1069,7 @@
<string name="activity_home_outdated_client_config">Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.</string>
<string name="activity_conversation_empty_state_read_only">There are no messages in <b>%s</b>.</string>
<string name="activity_conversation_empty_state_blocks_community_requests"><b>%s</b> has message requests from Community conversations turned off, so you cannot send them a message.</string>
<string name="activity_conversation_empty_state_note_to_self">You have no messages in Note to Self.</string>
<string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string>

View File

@@ -20,6 +20,12 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/preferences__message_requests_category"
android:key="@string/preferences__message_requests_category"
android:persistent="false">
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences__read_receipts">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="false"

View File

@@ -0,0 +1,16 @@
<?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">
<service
android:name="org.thoughtcrime.securesms.notifications.FirebasePushService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

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 FirebaseBindingModule {
@Binds
abstract fun bindTokenFetcher(tokenFetcher: FirebaseTokenFetcher): TokenFetcher
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.notifications
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import javax.inject.Inject
private const val TAG = "FirebasePushNotificationService"
@AndroidEntryPoint
class FirebasePushService : FirebaseMessagingService() {
@Inject lateinit var prefs: TextSecurePreferences
@Inject lateinit var pushReceiver: PushReceiver
@Inject lateinit var pushRegistry: PushRegistry
override fun onNewToken(token: String) {
if (token == prefs.getPushToken()) return
pushRegistry.register(token)
}
override fun onMessageReceived(message: RemoteMessage) {
Log.d(TAG, "Received a push notification.")
pushReceiver.onPush(message.data)
}
override fun onDeletedMessages() {
Log.d(TAG, "Called onDeletedMessages.")
pushRegistry.refresh(true)
}
}

View File

@@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.notifications
import com.google.android.gms.tasks.Tasks
import com.google.firebase.iid.FirebaseInstanceId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FirebaseTokenFetcher @Inject constructor(): TokenFetcher {
override suspend fun fetch() = withContext(Dispatchers.IO) {
FirebaseInstanceId.getInstance().instanceId
.also(Tasks::await)
.takeIf { isActive } // don't 'complete' task if we were canceled
?.run { result?.token ?: throw exception!! }
}
}

View File

@@ -1,42 +1,46 @@
package org.thoughtcrime.securesms.conversation.v2
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anySet
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.BaseViewModelTest
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.repository.ResultOf
import org.mockito.Mockito.`when` as whenever
class ConversationViewModelTest: BaseViewModelTest() {
private val repository = mock(ConversationRepository::class.java)
private val storage = mock(Storage::class.java)
private val repository = mock<ConversationRepository>()
private val storage = mock<Storage>()
private val threadId = 123L
private val edKeyPair = mock(KeyPair::class.java)
private val edKeyPair = mock<KeyPair>()
private lateinit var recipient: Recipient
private val viewModel: ConversationViewModel by lazy {
ConversationViewModel(threadId, edKeyPair, mock(), repository, storage)
ConversationViewModel(threadId, edKeyPair, repository, storage)
}
@Before
fun setUp() {
recipient = mock(Recipient::class.java)
recipient = mock()
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
whenever(repository.recipientUpdateFlow(anyLong())).thenReturn(emptyFlow())
}
@Test
@@ -79,7 +83,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test
fun `should delete locally`() {
val message = mock(MessageRecord::class.java)
val message = mock<MessageRecord>()
viewModel.deleteLocally(message)
@@ -88,7 +92,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test
fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest {
val message = mock(MessageRecord::class.java)
val message = mock<MessageRecord>()
val error = Throwable()
whenever(repository.deleteForEveryone(anyLong(), any(), any()))
.thenReturn(ResultOf.Failure(error))
@@ -101,7 +105,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Test
fun `should emit error message on failure to delete messages without unsend request`() =
runBlockingTest {
val message = mock(MessageRecord::class.java)
val message = mock<MessageRecord>()
val error = Throwable()
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
.thenReturn(ResultOf.Failure(error))
@@ -181,4 +185,30 @@ class ConversationViewModelTest: BaseViewModelTest() {
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(0))
}
@Test
fun `open group recipient should have no blinded recipient`() {
whenever(recipient.isOpenGroupRecipient).thenReturn(true)
whenever(recipient.isOpenGroupOutboxRecipient).thenReturn(false)
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(false)
assertThat(viewModel.blindedRecipient, nullValue())
}
@Test
fun `local recipient should have input and no blinded recipient`() {
whenever(recipient.isLocalNumber).thenReturn(true)
assertThat(viewModel.hidesInputBar(), equalTo(false))
assertThat(viewModel.blindedRecipient, nullValue())
}
@Test
fun `contact recipient should hide input bar if not accepting requests`() {
whenever(recipient.isOpenGroupInboxRecipient).thenReturn(true)
val blinded = mock<Recipient> {
whenever(it.blocksCommunityMessageRequests).thenReturn(true)
}
whenever(repository.maybeGetBlindedRecipient(recipient)).thenReturn(blinded)
assertThat(viewModel.blindedRecipient, notNullValue())
assertThat(viewModel.hidesInputBar(), equalTo(true))
}
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.notifications
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class NoOpPushModule {
@Binds
abstract fun bindTokenFetcher(tokenFetcher: NoOpTokenFetcher): TokenFetcher
}

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.notifications
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NoOpTokenFetcher @Inject constructor() : TokenFetcher {
override suspend fun fetch(): String? = null
}