Merge branch 'dev' into disappearing-messages

This commit is contained in:
charles 2022-12-19 20:07:27 +11:00
commit 3c6b93b2f8
75 changed files with 830 additions and 873 deletions

View File

@ -4,11 +4,11 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.2' classpath "com.android.tools.build:gradle:$gradlePluginVersion"
classpath files('libs/gradle-witness.jar') classpath files('libs/gradle-witness.jar')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
classpath "com.google.gms:google-services:4.3.10" classpath "com.google.gms:google-services:$googleServicesVersion"
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
} }
} }
@ -27,26 +27,27 @@ configurations.all {
} }
dependencies { dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1' implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'com.google.android.material:material:1.2.1' implementation "com.google.android.material:material:$materialVersion"
implementation 'com.google.android:flexbox:2.0.1' implementation 'com.google.android:flexbox:2.0.1'
implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.exifinterface:exifinterface:1.3.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
implementation 'androidx.activity:activity-ktx:1.2.2' implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
implementation 'androidx.fragment:fragment-ktx:1.3.2' implementation 'androidx.activity:activity-ktx:1.5.1'
implementation "androidx.core:core-ktx:1.3.2" implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation "androidx.work:work-runtime-ktx:2.4.0" implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation ("com.google.firebase:firebase-messaging:18.0.0") { implementation ("com.google.firebase:firebase-messaging:18.0.0") {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
@ -119,7 +120,7 @@ dependencies {
implementation "com.github.tbruyelle:rxpermissions:0.10.2" implementation "com.github.tbruyelle:rxpermissions:0.10.2"
implementation "com.github.ybq:Android-SpinKit:1.4.0" implementation "com.github.ybq:Android-SpinKit:1.4.0"
implementation "com.opencsv:opencsv:4.6" implementation "com.opencsv:opencsv:4.6"
testImplementation 'junit:junit:4.12' testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1' testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation "org.mockito:mockito-inline:4.0.0" testImplementation "org.mockito:mockito-inline:4.0.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
@ -127,7 +128,7 @@ dependencies {
testImplementation 'org.powermock:powermock-module-junit4:1.6.1' testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1' testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1' testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.3.0' testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
@ -141,7 +142,7 @@ dependencies {
// Assertions // Assertions
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.ext:truth:1.4.0' androidTestImplementation 'androidx.test.ext:truth:1.4.0'
androidTestImplementation 'com.google.truth:truth:1.0' androidTestImplementation 'com.google.truth:truth:1.1.3'
// Espresso dependencies // Espresso dependencies
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@ -151,14 +152,14 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0' androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
androidTestUtil 'androidx.test:orchestrator:1.4.0' androidTestUtil 'androidx.test:orchestrator:1.4.1'
testImplementation 'org.robolectric:robolectric:4.4' testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:shadows-multidex:4.4' testImplementation 'org.robolectric:shadows-multidex:4.4'
} }
def canonicalVersionCode = 309 def canonicalVersionCode = 321
def canonicalVersionName = "1.16.0" def canonicalVersionName = "1.16.3"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -169,13 +170,9 @@ def abiPostFix = ['armeabi-v7a' : 1,
android { android {
compileSdkVersion androidCompileSdkVersion compileSdkVersion androidCompileSdkVersion
buildToolsVersion '29.0.3' namespace 'network.loki.messenger'
useLibrary 'org.apache.http.legacy' useLibrary 'org.apache.http.legacy'
dexOptions {
javaMaxHeapSize "4g"
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
@ -209,7 +206,7 @@ android {
versionName canonicalVersionName versionName canonicalVersionName
minSdkVersion androidMinimumSdkVersion minSdkVersion androidMinimumSdkVersion
targetSdkVersion androidCompileSdkVersion targetSdkVersion androidTargetSdkVersion
multiDexEnabled = true multiDexEnabled = true

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="network.loki.messenger">
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" /> <uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
@ -31,6 +30,7 @@
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" /> <uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
@ -177,6 +177,7 @@
android:theme="@style/Theme.Session.DayNight.NoActionBar" /> android:theme="@style/Theme.Session.DayNight.NoActionBar" />
<activity <activity
android:exported="true"
android:name="org.thoughtcrime.securesms.ShareActivity" android:name="org.thoughtcrime.securesms.ShareActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:excludeFromRecents="true" android:excludeFromRecents="true"
@ -324,6 +325,7 @@
android:exported="false" /> android:exported="false" />
<service <service
android:name="org.thoughtcrime.securesms.service.DirectShareService" android:name="org.thoughtcrime.securesms.service.DirectShareService"
android:exported="true"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE"> android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<intent-filter> <intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" /> <action android:name="android.service.chooser.ChooserTargetService" />
@ -401,42 +403,48 @@
android:authorities="network.loki.securesms.database.recipient" android:authorities="network.loki.securesms.database.recipient"
android:exported="false" /> android:exported="false" />
<receiver android:name="org.thoughtcrime.securesms.service.BootReceiver"> <receiver android:name="org.thoughtcrime.securesms.service.BootReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="network.loki.securesms.RESTART" /> <action android:name="network.loki.securesms.RESTART" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener"> <receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener"
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"> <receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver"> <receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" /> <action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver"> <receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="network.loki.securesms.DELETE_NOTIFICATION" /> <action android:name="network.loki.securesms.DELETE_NOTIFICATION" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver <receiver
android:name="org.thoughtcrime.securesms.service.PanicResponderListener" android:name="org.thoughtcrime.securesms.service.PanicResponderListener"
android:exported="true"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" /> <action android:name="info.guardianproject.panic.action.TRIGGER" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver <receiver
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver" android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
android:enabled="true"> android:enabled="true"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>

View File

@ -481,6 +481,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return; if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
ThreadUtils.queue(() -> { ThreadUtils.queue(() -> {
// Don't generate a new profile key here; we do that when the user changes their profile picture // Don't generate a new profile key here; we do that when the user changes their profile picture
Log.d("Loki-Avatar", "Uploading Avatar Started");
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this); String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
try { try {
// Read the file into a byte array // Read the file into a byte array
@ -497,6 +498,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> { ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
// Update the last profile picture upload date // Update the last profile picture upload date
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime()); TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
Log.d("Loki-Avatar", "Uploading Avatar Finished");
return Unit.INSTANCE; return Unit.INSTANCE;
}); });
} catch (Exception exception) { } catch (Exception exception) {

View File

@ -1,104 +0,0 @@
package org.thoughtcrime.securesms.backup;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.util.BackupDirSelector;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.session.libsession.utilities.Util;
import java.io.IOException;
import network.loki.messenger.R;
public class BackupDialog {
private static final String TAG = "BackupDialog";
public static void showEnableBackupDialog(
@NonNull Context context,
@NonNull SwitchPreferenceCompat preference,
@NonNull BackupDirSelector backupDirSelector) {
String[] password = BackupUtil.generateBackupPassphrase();
String passwordSt = Util.join(password, "");
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_enable_local_backups)
.setView(R.layout.backup_enable_dialog)
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(created -> {
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(v -> {
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
if (confirmationCheckBox.isChecked()) {
backupDirSelector.selectBackupDir(true, uri -> {
try {
BackupUtil.enableBackups(context, passwordSt);
} catch (IOException e) {
Log.e(TAG, "Failed to activate backups.", e);
Toast.makeText(context,
context.getString(R.string.dialog_backup_activation_failed),
Toast.LENGTH_LONG)
.show();
return;
}
preference.setChecked(true);
created.dismiss();
});
} else {
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
}
});
});
dialog.show();
CheckBox checkBox = dialog.findViewById(R.id.confirmation_check);
TextView textView = dialog.findViewById(R.id.confirmation_text);
((TextView)dialog.findViewById(R.id.code_first)).setText(password[0]);
((TextView)dialog.findViewById(R.id.code_second)).setText(password[1]);
((TextView)dialog.findViewById(R.id.code_third)).setText(password[2]);
((TextView)dialog.findViewById(R.id.code_fourth)).setText(password[3]);
((TextView)dialog.findViewById(R.id.code_fifth)).setText(password[4]);
((TextView)dialog.findViewById(R.id.code_sixth)).setText(password[5]);
textView.setOnClickListener(v -> checkBox.toggle());
dialog.findViewById(R.id.number_table).setOnClickListener(v -> {
((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", passwordSt));
Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_SHORT).show();
});
}
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_delete_backups)
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
BackupUtil.disableBackups(context, true);
preference.setChecked(false);
})
.create()
.show();
}
}

View File

@ -1,206 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.app.Activity
import android.app.Application
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ClickableSpan
import android.text.style.StyleSpan
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.android.gms.common.util.Strings
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.libsignal.utilities.Log
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.util.show
class BackupRestoreActivity : BaseActionBarActivity() {
companion object {
private const val TAG = "BackupRestoreActivity"
}
private val viewModel by viewModels<BackupRestoreViewModel>()
private val fileSelectionResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK && result.data != null && result.data!!.data != null) {
viewModel.backupFile.value = result.data!!.data!!
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpActionBarSessionLogo()
// val viewBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
// viewBinding.lifecycleOwner = this
// viewBinding.viewModel = viewModel
// viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() }
// viewBinding.buttonSelectFile.setOnClickListener {
// fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
// //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly
// // and the backup files are unavailable for selection.
//// type = BackupUtil.BACKUP_FILE_MIME_TYPE
// type = "*/*"
// })
// }
// viewBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
// Focus passphrase text edit when backup file is selected.
// viewModel.backupFile.observe(this, { backupFile ->
// if (backupFile != null) viewBinding.backupCode.post {
// viewBinding.backupCode.requestFocus()
// (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
// .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT)
// }
// })
// React to backup import result.
viewModel.backupImportResult.observe(this) { result ->
if (result != null) when (result) {
BackupRestoreViewModel.BackupRestoreResult.SUCCESS -> {
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
this.show(intent)
}
BackupRestoreViewModel.BackupRestoreResult.FAILURE_VERSION_DOWNGRADE ->
Toast.makeText(this, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
BackupRestoreViewModel.BackupRestoreResult.FAILURE_UNKNOWN ->
Toast.makeText(this, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
}
}
//region Legal info views
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
openURL("https://getsession.org/terms-of-service/")
}
}, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
termsExplanation.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
openURL("https://getsession.org/privacy-policy/")
}
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
// viewBinding.termsTextView.text = termsExplanation
//endregion
}
private fun openURL(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
}
}
}
class BackupRestoreViewModel(application: Application): AndroidViewModel(application) {
companion object {
private const val TAG = "BackupRestoreViewModel"
@JvmStatic
fun uriToFileName(view: View, fileUri: Uri?): String? {
fileUri ?: return null
view.context.contentResolver.query(fileUri, null, null, null, null).use {
val nameIndex = it!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
it.moveToFirst()
return it.getString(nameIndex)
}
}
@JvmStatic
fun validateData(fileUri: Uri?, passphrase: String?): Boolean {
return fileUri != null &&
!Strings.isEmptyOrWhitespace(passphrase) &&
passphrase!!.length == BackupUtil.BACKUP_PASSPHRASE_LENGTH
}
}
val backupFile = MutableLiveData<Uri>(null)
val backupPassphrase = MutableLiveData<String>(null)
val processingBackupFile = MutableLiveData<Boolean>(false)
val backupImportResult = MutableLiveData<BackupRestoreResult>(null)
fun tryRestoreBackup() = viewModelScope.launch {
if (processingBackupFile.value == true) return@launch
if (backupImportResult.value == BackupRestoreResult.SUCCESS) return@launch
if (!validateData(backupFile.value, backupPassphrase.value)) return@launch
val context = getApplication<Application>()
val backupFile = backupFile.value!!
val passphrase = backupPassphrase.value!!
val result: BackupRestoreResult
processingBackupFile.value = true
withContext(Dispatchers.IO) {
result = try {
val database = DatabaseComponent.get(context).openHelper().readableDatabase
FullBackupImporter.importFromUri(
context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
backupFile,
passphrase
)
DatabaseFactory.upgradeRestored(context, database)
NotificationChannels.restoreContactNotificationChannels(context)
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
TextSecurePreferences.setHasViewedSeed(context, true)
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
BackupRestoreResult.SUCCESS
} catch (e: DatabaseDowngradeException) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
BackupRestoreResult.FAILURE_VERSION_DOWNGRADE
} catch (e: Exception) {
Log.w(TAG, e)
BackupRestoreResult.FAILURE_UNKNOWN
}
}
processingBackupFile.value = false
backupImportResult.value = result
}
enum class BackupRestoreResult {
SUCCESS, FAILURE_VERSION_DOWNGRADE, FAILURE_UNKNOWN
}
}

View File

@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -58,7 +57,6 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.recyclerView.layoutManager = LinearLayoutManager(activity) binding.recyclerView.layoutManager = LinearLayoutManager(activity)
binding.recyclerView.adapter = listAdapter binding.recyclerView.adapter = listAdapter
binding.swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
} }
override fun onStop() { override fun onStop() {
@ -73,15 +71,6 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
fun resetQueryFilter() { fun resetQueryFilter() {
setQueryFilter(null) setQueryFilter(null)
binding.swipeRefreshLayout.isRefreshing = false
}
fun setRefreshing(refreshing: Boolean) {
binding.swipeRefreshLayout.isRefreshing = refreshing
}
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
binding.swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
} }
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> { override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
@ -106,7 +95,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
return return
} }
listAdapter.items = items listAdapter.items = items
binding.mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE binding.recyclerView.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
} }

View File

@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.contacts
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySelectContactsBinding import network.loki.messenger.databinding.ActivitySelectContactsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
@ -49,7 +49,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
LoaderManager.getInstance(this).initLoader(0, null, this) LoaderManager.getInstance(this).initLoader(0, null, this)
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_done, menu) menuInflater.inflate(R.menu.menu_done, menu)
return members.isNotEmpty() return members.isNotEmpty()
} }
@ -70,7 +70,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
private fun update(members: List<String>) { private fun update(members: List<String>) {
this.members = members this.members = members
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE binding.recyclerView.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu() invalidateOptionsMenu()
} }

View File

@ -0,0 +1,129 @@
package org.thoughtcrime.securesms.conversation.paging
import androidx.annotation.WorkerThread
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.recyclerview.widget.DiffUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.session.libsession.messaging.contacts.Contact
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
private const val TIME_BUCKET = 600000L // bucket into 10 minute increments
private fun config() = PagingConfig(
pageSize = 25,
maxSize = 100,
enablePlaceholders = false
)
fun Long.bucketed(): Long = (TIME_BUCKET - this % TIME_BUCKET) + this
fun conversationPager(threadId: Long, initialKey: PageLoad? = null, db: MmsSmsDatabase, contactDb: SessionContactDatabase) = Pager(config(), initialKey = initialKey) {
ConversationPagingSource(threadId, db, contactDb)
}
class ConversationPagerDiffCallback: DiffUtil.ItemCallback<MessageAndContact>() {
override fun areItemsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
oldItem.message.id == newItem.message.id && oldItem.message.isMms == newItem.message.isMms
override fun areContentsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
oldItem == newItem
}
data class MessageAndContact(val message: MessageRecord,
val contact: Contact?)
data class PageLoad(val fromTime: Long, val toTime: Long? = null)
class ConversationPagingSource(
private val threadId: Long,
private val messageDb: MmsSmsDatabase,
private val contactDb: SessionContactDatabase
): PagingSource<PageLoad, MessageAndContact>() {
override fun getRefreshKey(state: PagingState<PageLoad, MessageAndContact>): PageLoad? {
val anchorPosition = state.anchorPosition ?: return null
val anchorPage = state.closestPageToPosition(anchorPosition) ?: return null
val next = anchorPage.nextKey?.fromTime
val previous = anchorPage.prevKey?.fromTime ?: anchorPage.data.firstOrNull()?.message?.dateSent ?: return null
return PageLoad(previous, next)
}
private val contactCache = mutableMapOf<String, Contact>()
@WorkerThread
private fun getContact(sessionId: String): Contact? {
contactCache[sessionId]?.let { contact ->
return contact
} ?: run {
contactDb.getContactWithSessionID(sessionId)?.let { contact ->
contactCache[sessionId] = contact
return contact
}
}
return null
}
override suspend fun load(params: LoadParams<PageLoad>): LoadResult<PageLoad, MessageAndContact> {
val pageLoad = params.key ?: withContext(Dispatchers.IO) {
messageDb.getConversationSnippet(threadId).use {
val reader = messageDb.readerFor(it)
var record: MessageRecord? = null
if (reader != null) {
record = reader.next
while (record != null && record.isDeleted) {
record = reader.next
}
}
record?.dateSent?.let { fromTime ->
PageLoad(fromTime)
}
}
} ?: return LoadResult.Page(emptyList(), null, null)
val result = withContext(Dispatchers.IO) {
val cursor = messageDb.getConversationPage(
threadId,
pageLoad.fromTime,
pageLoad.toTime ?: -1L,
params.loadSize
)
val processedList = mutableListOf<MessageAndContact>()
val reader = messageDb.readerFor(cursor)
while (reader.next != null && !invalid) {
reader.current?.let { item ->
val contact = getContact(item.individualRecipient.address.serialize())
processedList += MessageAndContact(item, contact)
}
}
reader.close()
processedList.toMutableList()
}
val hasNext = withContext(Dispatchers.IO) {
if (result.isEmpty()) return@withContext false
val lastTime = result.last().message.dateSent
messageDb.hasNextPage(threadId, lastTime)
}
val nextCheckTime = if (hasNext) {
val lastSent = result.last().message.dateSent
if (lastSent == pageLoad.fromTime) null else lastSent
} else null
val hasPrevious = withContext(Dispatchers.IO) { messageDb.hasPreviousPage(threadId, pageLoad.fromTime) }
val nextKey = if (!hasNext) null else nextCheckTime
val prevKey = if (!hasPrevious) null else messageDb.getPreviousPage(threadId, pageLoad.fromTime, params.loadSize)
return LoadResult.Page(
data = result, // next check time is not null if drop is true
prevKey = prevKey?.let { PageLoad(it, pageLoad.fromTime) },
nextKey = nextKey?.let { PageLoad(it) }
)
}
}

View File

@ -3,21 +3,13 @@ package org.thoughtcrime.securesms.conversation.v2
import android.Manifest import android.Manifest
import android.animation.FloatEvaluator import android.animation.FloatEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.ClipData import android.content.*
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.AsyncTask import android.os.*
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.text.TextUtils import android.text.TextUtils
import android.util.Pair import android.util.Pair
@ -66,12 +58,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.utilities.Address import org.session.libsession.utilities.*
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.Stub
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.Recipient.DisappearingState import org.session.libsession.utilities.recipients.Recipient.DisappearingState
@ -134,14 +122,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.AudioSlide import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
@ -633,7 +614,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
) )
} }
viewModel.recipient?.let { maybeUpdateToolbar(it) } viewModel.recipient?.let { maybeUpdateToolbar(it) }
super.onPrepareOptionsMenu(menu)
return true return true
} }
@ -1760,6 +1740,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
endActionMode() endActionMode()
} }
override fun destroyActionMode() {
this.actionMode = null
}
private fun sendScreenshotNotification() { private fun sendScreenshotNotification() {
val recipient = viewModel.recipient ?: return val recipient = viewModel.recipient ?: return
if (recipient.isGroupRecipient) return if (recipient.isGroupRecipient) return
@ -1805,7 +1789,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (result == null) return@Observer if (result == null) return@Observer
if (result.getResults().isNotEmpty()) { if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let { result.getResults()[result.position]?.let {
jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs) { jumpToMessage(it.messageRecipient.address, it.sentTimestampMs) {
searchViewModel.onMissingResult() } searchViewModel.onMissingResult() }
} }
} }
@ -1871,7 +1855,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems) ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems)
ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems) ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems)
ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems) ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems)
ConversationReactionOverlay.Action.COPY_SESSION_ID -> TODO() ConversationReactionOverlay.Action.COPY_SESSION_ID -> copySessionID(selectedItems)
} }
} }
} }

View File

@ -186,7 +186,6 @@ class ConversationViewModel(
data class UiMessage(val id: Long, val message: String) data class UiMessage(val id: Long, val message: String)
data class ConversationUiState( data class ConversationUiState(
val isOxenHostedOpenGroup: Boolean = false,
val uiMessages: List<UiMessage> = emptyList(), val uiMessages: List<UiMessage> = emptyList(),
val isMessageRequestAccepted: Boolean? = null val isMessageRequestAccepted: Boolean? = null
) )

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityMessageDetailBinding import network.loki.messenger.databinding.ActivityMessageDetailBinding
@ -20,8 +21,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.*
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -48,7 +48,10 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
// We only show this screen for messages fail to send, // We only show this screen for messages fail to send,
// so the author of the messages must be the current user. // so the author of the messages must be the current user.
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run {
finish()
return
}
val threadId = messageRecord!!.threadId val threadId = messageRecord!!.threadId
val openGroup = storage.getOpenGroup(threadId) val openGroup = storage.getOpenGroup(threadId)
val blindedKey = openGroup?.let { group -> val blindedKey = openGroup?.let { group ->
@ -71,8 +74,15 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale) val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent)) binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send." val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId())
binding.errorMessage.text = errorMessage if (errorMessage != null) {
binding.errorMessage.text = errorMessage
binding.resendContainer.isVisible = true
binding.errorContainer.isVisible = true
} else {
binding.errorContainer.isVisible = false
binding.resendContainer.isVisible = false
}
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) { if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
binding.expiresContainer.visibility = View.GONE binding.expiresContainer.visibility = View.GONE

View File

@ -65,9 +65,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
// Copy Session ID // Copy Session ID
menu.findItem(R.id.menu_context_copy_public_key).isVisible = menu.findItem(R.id.menu_context_copy_public_key).isVisible =
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.recipient.address.toString() != userPublicKey) (thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
// Message detail // Message detail
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isFailed) menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
// Resend // Resend
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed) menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
// Save media // Save media
@ -101,6 +101,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
override fun onDestroyActionMode(mode: ActionMode) { override fun onDestroyActionMode(mode: ActionMode) {
adapter.selectedItems.clear() adapter.selectedItems.clear()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
delegate?.destroyActionMode()
} }
} }
@ -116,4 +117,5 @@ interface ConversationActionModeCallbackDelegate {
fun showMessageDetail(messages: Set<MessageRecord>) fun showMessageDetail(messages: Set<MessageRecord>)
fun saveAttachment(messages: Set<MessageRecord>) fun saveAttachment(messages: Set<MessageRecord>)
fun reply(messages: Set<MessageRecord>) fun reply(messages: Set<MessageRecord>)
fun destroyActionMode()
} }

View File

@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
import java.util.Locale import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
@ -86,6 +86,14 @@ class VisibleMessageContentView : LinearLayout {
if (message.isDeleted) { if (message.isDeleted) {
binding.deletedMessageView.root.isVisible = true binding.deletedMessageView.root.isVisible = true
binding.deletedMessageView.root.bind(message, getTextColor(context, message)) binding.deletedMessageView.root.bind(message, getTextColor(context, message))
binding.bodyTextView.isVisible = false
binding.quoteView.root.isVisible = false
binding.linkPreviewView.isVisible = false
binding.untrustedView.root.isVisible = false
binding.voiceMessageView.root.isVisible = false
binding.documentView.root.isVisible = false
binding.albumThumbnailView.isVisible = false
binding.openGroupInvitationView.root.isVisible = false
return return
} else { } else {
binding.deletedMessageView.root.isVisible = false binding.deletedMessageView.root.isVisible = false

View File

@ -78,7 +78,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d", binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs), TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs)) TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs) % 60)
} }
} }
} }
@ -102,7 +102,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
this.progress = progress this.progress = progress
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d", binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()), TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong())) TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()) % 60)
val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt() layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
binding.progressView.layoutParams = layoutParams binding.progressView.layoutParams = layoutParams

View File

@ -136,6 +136,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString())) database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
} }
fun clearErrorMessage(messageID: Long) {
val database = databaseHelper.writableDatabase
database.delete(errorMessageTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
}
fun deleteThread(threadId: Long) { fun deleteThread(threadId: Long) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
try { try {

View File

@ -112,6 +112,64 @@ public class MmsSmsDatabase extends Database {
return getMessageFor(timestamp, author.serialize()); return getMessageFor(timestamp, author.serialize());
} }
public long getPreviousPage(long threadId, long fromTime, int limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" ASC";
String selection = MmsSmsColumns.THREAD_ID+" = "+threadId
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" > "+fromTime;
String limitStr = ""+limit;
long sent = -1;
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
if (cursor == null) return sent;
Reader reader = readerFor(cursor);
if (!cursor.move(limit)) {
cursor.moveToLast();
}
MessageRecord record = reader.getCurrent();
sent = record.getDateSent();
reader.close();
return sent;
}
public Cursor getConversationPage(long threadId, long fromTime, long toTime, int limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" DESC";
String selection = MmsSmsColumns.THREAD_ID + " = "+threadId
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" <= " + fromTime;
String limitStr = null;
if (toTime != -1L) {
selection += " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" > "+toTime;
} else {
limitStr = ""+limit;
}
return queryTables(PROJECTION, selection, order, limitStr);
}
public boolean hasNextPage(long threadId, long toTime) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" DESC";
String selection = MmsSmsColumns.THREAD_ID + " = "+threadId
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" < " + toTime; // check if there's at least one message before the `toTime`
Cursor cursor = queryTables(PROJECTION, selection, order, null);
boolean hasNext = false;
if (cursor != null) {
hasNext = cursor.getCount() > 0;
cursor.close();
}
return hasNext;
}
public boolean hasPreviousPage(long threadId, long fromTime) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" DESC";
String selection = MmsSmsColumns.THREAD_ID + " = "+threadId
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" > " + fromTime; // check if there's at least one message after the `fromTime`
Cursor cursor = queryTables(PROJECTION, selection, order, null);
boolean hasNext = false;
if (cursor != null) {
hasNext = cursor.getCount() > 0;
cursor.close();
}
return hasNext;
}
public Cursor getConversation(long threadId, boolean reverse, long offset, long limit) { public Cursor getConversation(long threadId, boolean reverse, long offset, long limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC"); String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC");
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
@ -199,16 +257,16 @@ public class MmsSmsDatabase extends Database {
return -1; return -1;
} }
public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull Address address) { public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.ADDRESS }, selection, order, null)) { try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
String serializedAddress = address.serialize(); String serializedAddress = address.serialize();
boolean isOwnNumber = Util.isOwnNumber(context, address.serialize()); boolean isOwnNumber = Util.isOwnNumber(context, address.serialize());
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
boolean timestampMatches = cursor.getLong(0) == receivedTimestamp; boolean timestampMatches = cursor.getLong(0) == sentTimestamp;
boolean addressMatches = serializedAddress.equals(cursor.getString(1)); boolean addressMatches = serializedAddress.equals(cursor.getString(1));
if (timestampMatches && (addressMatches || isOwnNumber)) { if (timestampMatches && (addressMatches || isOwnNumber)) {

View File

@ -63,7 +63,7 @@ public class SearchDatabase extends Database {
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + "snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + SmsDatabase.TABLE_NAME + " " + "FROM " + SmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " + "INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
@ -74,13 +74,13 @@ public class SearchDatabase extends Database {
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + "snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + MmsDatabase.TABLE_NAME + " " + "FROM " + MmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + "INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " + "WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " + "ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC " +
"LIMIT ?"; "LIMIT ?";
private static final String MESSAGES_FOR_THREAD_QUERY = private static final String MESSAGES_FOR_THREAD_QUERY =
@ -88,7 +88,7 @@ public class SearchDatabase extends Database {
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + "snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + SmsDatabase.TABLE_NAME + " " + "FROM " + SmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " + "INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
@ -99,13 +99,13 @@ public class SearchDatabase extends Database {
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " + "snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + MmsDatabase.TABLE_NAME + " " + "FROM " + MmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + "INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? AND " + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? " + "WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? AND " + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? " +
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " + "ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC " +
"LIMIT 500"; "LIMIT 500";
public SearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) { public SearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {

View File

@ -17,13 +17,7 @@ import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage import org.session.libsession.messaging.messages.signal.*
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.Reaction
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
@ -38,12 +32,8 @@ import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.*
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentPointer
@ -439,6 +429,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
} }
} }
override fun clearErrorMessage(messageID: Long) {
val db = DatabaseComponent.get(context).lokiMessageDatabase()
db.clearErrorMessage(messageID)
}
override fun setMessageServerHash(messageID: Long, serverHash: String) { override fun setMessageServerHash(messageID: Long, serverHash: String) {
DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(messageID, serverHash) DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(messageID, serverHash)
} }

View File

@ -502,15 +502,23 @@ public class ThreadDatabase extends Database {
return db.rawQuery(query, null); return db.rawQuery(query, null);
} }
public void setLastSeen(long threadId) { public void setLastSeen(long threadId, long timestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(LAST_SEEN, System.currentTimeMillis()); if (timestamp == -1) {
contentValues.put(LAST_SEEN, System.currentTimeMillis());
} else {
contentValues.put(LAST_SEEN, timestamp);
}
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)}); db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
notifyConversationListListeners(); notifyConversationListListeners();
} }
public void setLastSeen(long threadId) {
setLastSeen(threadId, -1);
}
public Pair<Long, Boolean> getLastSeenAndHasSent(long threadId) { public Pair<Long, Boolean> getLastSeenAndHasSent(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);

View File

@ -33,6 +33,7 @@ import org.session.libsession.utilities.NetworkFailure;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* The base class for message record models that are displayed in * The base class for message record models that are displayed in
@ -140,14 +141,16 @@ public abstract class MessageRecord extends DisplayRecord {
return spannable; return spannable;
} }
@Override
public boolean equals(Object other) { public boolean equals(Object other) {
return other instanceof MessageRecord return other instanceof MessageRecord
&& ((MessageRecord) other).getId() == getId() && ((MessageRecord) other).getId() == getId()
&& ((MessageRecord) other).isMms() == isMms(); && ((MessageRecord) other).isMms() == isMms();
} }
@Override
public int hashCode() { public int hashCode() {
return (int)getId(); return Objects.hash(id, isMms());
} }
public @NonNull List<ReactionRecord> getReactions() { public @NonNull List<ReactionRecord> getReactions() {

View File

@ -2,13 +2,15 @@ package org.thoughtcrime.securesms.database.model;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.session.libsession.utilities.Contact;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.Contact;
import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.IdentityKeyMismatch;
import org.session.libsession.utilities.NetworkFailure; import org.session.libsession.utilities.NetworkFailure;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;

View File

@ -8,6 +8,8 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import java.util.Objects;
public class Quote { public class Quote {
private final long id; private final long id;
@ -47,4 +49,17 @@ public class Quote {
public QuoteModel getQuoteModel() { public QuoteModel getQuoteModel() {
return new QuoteModel(id, author, text, missing, attachment.asAttachments()); return new QuoteModel(id, author, text, missing, attachment.asAttachments());
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Quote quote = (Quote) o;
return id == quote.id && missing == quote.missing && Objects.equals(author, quote.author) && Objects.equals(text, quote.text) && Objects.equals(attachment, quote.attachment);
}
@Override
public int hashCode() {
return Objects.hash(id, author, text, missing, attachment);
}
} }

View File

@ -102,7 +102,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
when (model) { when (model) {
is GlobalSearchAdapter.Model.Message -> { is GlobalSearchAdapter.Model.Message -> {
val threadId = model.messageResult.threadId val threadId = model.messageResult.threadId
val timestamp = model.messageResult.receivedTimestampMs val timestamp = model.messageResult.sentTimestampMs
val author = model.messageResult.messageRecipient.address val author = model.messageResult.messageRecipient.address
val intent = Intent(this, ConversationActivityV2::class.java) val intent = Intent(this, ConversationActivityV2::class.java)

View File

@ -134,7 +134,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
// if (hasUnreads) { // if (hasUnreads) {
// binding.unreadCountTextView.text = model.unread.toString() // binding.unreadCountTextView.text = model.unread.toString()
// } // }
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs) binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient) binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
val textSpannable = SpannableStringBuilder() val textSpannable = SpannableStringBuilder()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {

View File

@ -6,17 +6,19 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ApplicationContext;
import network.loki.messenger.BuildConfig;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import network.loki.messenger.BuildConfig;
/** /**
* Schedules tasks using the {@link AlarmManager}. * Schedules tasks using the {@link AlarmManager}.
* *
@ -51,7 +53,7 @@ public class AlarmManagerScheduler implements Scheduler {
Intent intent = new Intent(context, RetryReceiver.class); Intent intent = new Intent(context, RetryReceiver.class);
intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString()); intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString());
alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, 0)); alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms."); Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms.");
} }

View File

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.forceShowIcon
class MessageRequestsAdapter( class MessageRequestsAdapter(
context: Context, context: Context,
@ -64,7 +63,7 @@ class MessageRequestsAdapter(
item.iconTintList = ColorStateList.valueOf(context.getColor(R.color.destructive)) item.iconTintList = ColorStateList.valueOf(context.getColor(R.color.destructive))
item.title = s item.title = s
} }
popupMenu.forceShowIcon() popupMenu.setForceShowIcon(true)
popupMenu.show() popupMenu.show()
} }

View File

@ -17,17 +17,19 @@
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
public class SlideDeck { public class SlideDeck {
@ -138,4 +140,17 @@ public class SlideDeck {
return null; return null;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SlideDeck slideDeck = (SlideDeck) o;
return Objects.equals(slides, slideDeck.slides);
}
@Override
public int hashCode() {
return Objects.hash(slides);
}
} }

View File

@ -40,7 +40,6 @@ import com.annimon.stream.Optional;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.goterl.lazysodium.utils.KeyPair; import com.goterl.lazysodium.utils.KeyPair;
import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.open_groups.OpenGroup; import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.utilities.SessionId; import org.session.libsession.messaging.utilities.SessionId;
@ -453,8 +452,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
NotificationState notificationState = new NotificationState(); NotificationState notificationState = new NotificationState();
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor); MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase(); ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
LokiThreadDatabase lokiThreadDatabase= DatabaseComponent.get(context).lokiThreadDatabase();
KeyPair edKeyPair = MessagingModuleConfiguration.getShared().getGetUserED25519KeyPair().invoke();
MessageRecord record; MessageRecord record;
Map<Long, String> cache = new HashMap<Long, String>(); Map<Long, String> cache = new HashMap<Long, String>();
@ -575,7 +573,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION); Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
alarmIntent.putExtra("reminder_count", count); alarmIntent.putExtra("reminder_count", count);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
long timeout = TimeUnit.MINUTES.toMillis(2); long timeout = TimeUnit.MINUTES.toMillis(2);
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent); alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent);
@ -584,7 +582,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
@Override @Override
public void clearReminder(Context context) { public void clearReminder(Context context) {
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION); Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(pendingIntent); alarmManager.cancel(pendingIntent);
} }

View File

@ -5,9 +5,10 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import network.loki.messenger.R;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.NotificationPrivacyPreference; import org.session.libsession.utilities.NotificationPrivacyPreference;
import org.session.libsession.utilities.recipients.Recipient;
import network.loki.messenger.R;
public class FailedNotificationBuilder extends AbstractNotificationBuilder { public class FailedNotificationBuilder extends AbstractNotificationBuilder {
@ -20,7 +21,7 @@ public class FailedNotificationBuilder extends AbstractNotificationBuilder {
setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed)); setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed));
setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_message)); setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_message));
setTicker(context.getString(R.string.MessageNotifier_error_delivering_message)); setTicker(context.getString(R.string.MessageNotifier_error_delivering_message));
setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
setAutoCancel(true); setAutoCancel(true);
setAlarms(null, Recipient.VibrateState.DEFAULT); setAlarms(null, Recipient.VibrateState.DEFAULT);
setChannelId(NotificationChannels.FAILURES); setChannelId(NotificationChannels.FAILURES);

View File

@ -34,7 +34,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
setColor(context.getResources().getColor(R.color.textsecure_primary)); setColor(context.getResources().getColor(R.color.textsecure_primary));
setSmallIcon(R.drawable.ic_notification); setSmallIcon(R.drawable.ic_notification);
setContentTitle(context.getString(R.string.app_name)); setContentTitle(context.getString(R.string.app_name));
setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, HomeActivity.class), 0)); setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, HomeActivity.class), PendingIntent.FLAG_IMMUTABLE));
setCategory(NotificationCompat.CATEGORY_MESSAGE); setCategory(NotificationCompat.CATEGORY_MESSAGE);
setGroupSummary(true); setGroupSummary(true);

View File

@ -4,14 +4,15 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.utilities.recipients.Recipient;
public class NotificationItem { public class NotificationItem {
@ -75,9 +76,14 @@ public class NotificationItem {
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
return TaskStackBuilder.create(context) return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(intent) .addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); .getPendingIntent(0, intentFlags);
} }
public long getId() { public long getId() {

View File

@ -4,12 +4,14 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.session.libsignal.utilities.Log;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.Recipient.*; import org.session.libsession.utilities.recipients.Recipient.VibrateState;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -114,7 +116,12 @@ public class NotificationState {
intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray); intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray);
intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId); intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
} }
public PendingIntent getRemoteReplyIntent(Context context, Recipient recipient, ReplyMethod replyMethod) { public PendingIntent getRemoteReplyIntent(Context context, Recipient recipient, ReplyMethod replyMethod) {
@ -127,7 +134,12 @@ public class NotificationState {
intent.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod); intent.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod);
intent.setPackage(context.getPackageName()); intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
} }
public PendingIntent getAndroidAutoReplyIntent(Context context, Recipient recipient) { public PendingIntent getAndroidAutoReplyIntent(Context context, Recipient recipient) {
@ -141,7 +153,12 @@ public class NotificationState {
intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]); intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]);
intent.setPackage(context.getPackageName()); intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
} }
public PendingIntent getAndroidAutoHeardIntent(Context context, int notificationId) { public PendingIntent getAndroidAutoHeardIntent(Context context, int notificationId) {
@ -160,7 +177,12 @@ public class NotificationState {
intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId); intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId);
intent.setPackage(context.getPackageName()); intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
} }
public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) { public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) {
@ -171,7 +193,12 @@ public class NotificationState {
intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]); intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
return PendingIntent.getActivity(context, 0, intent, intentFlags);
} }
public PendingIntent getDeleteIntent(Context context) { public PendingIntent getDeleteIntent(Context context) {
@ -190,7 +217,12 @@ public class NotificationState {
intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms); intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
} }

View File

@ -4,12 +4,13 @@ package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.session.libsession.utilities.NotificationPrivacyPreference; import org.session.libsession.utilities.NotificationPrivacyPreference;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.home.HomeActivity;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -28,7 +29,7 @@ public class PendingMessageNotificationBuilder extends AbstractNotificationBuild
setContentText(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages)); setContentText(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages));
setTicker(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages)); setTicker(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages));
setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
setAutoCancel(true); setAutoCancel(true);
setAlarms(null, Recipient.VibrateState.DEFAULT); setAlarms(null, Recipient.VibrateState.DEFAULT);

View File

@ -52,7 +52,7 @@ class PNModeActivity : BaseActionBarActivity() {
toggleFCM() toggleFCM()
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_pn_mode, menu) menuInflater.inflate(R.menu.menu_pn_mode, menu)
return true return true
} }

View File

@ -41,8 +41,7 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() {
addPreferencesFromResource(R.xml.preferences_help) addPreferencesFromResource(R.xml.preferences_help)
} }
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
preference ?: return false
return when (preference.key) { return when (preference.key) {
EXPORT_LOGS -> { EXPORT_LOGS -> {
shareLogs() shareLogs()

View File

@ -301,10 +301,10 @@ public class SearchRepository {
Recipient conversationRecipient = Recipient.from(context, conversationAddress, false); Recipient conversationRecipient = Recipient.from(context, conversationAddress, false);
Recipient messageRecipient = Recipient.from(context, messageAddress, false); Recipient messageRecipient = Recipient.from(context, messageAddress, false);
String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET)); String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET));
long receivedMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)); long sentMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID));
return new MessageResult(conversationRecipient, messageRecipient, body, threadId, receivedMs); return new MessageResult(conversationRecipient, messageRecipient, body, threadId, sentMs);
} }
} }

View File

@ -13,18 +13,18 @@ public class MessageResult {
public final Recipient messageRecipient; public final Recipient messageRecipient;
public final String bodySnippet; public final String bodySnippet;
public final long threadId; public final long threadId;
public final long receivedTimestampMs; public final long sentTimestampMs;
public MessageResult(@NonNull Recipient conversationRecipient, public MessageResult(@NonNull Recipient conversationRecipient,
@NonNull Recipient messageRecipient, @NonNull Recipient messageRecipient,
@NonNull String bodySnippet, @NonNull String bodySnippet,
long threadId, long threadId,
long receivedTimestampMs) long sentTimestampMs)
{ {
this.conversationRecipient = conversationRecipient; this.conversationRecipient = conversationRecipient;
this.messageRecipient = messageRecipient; this.messageRecipient = messageRecipient;
this.bodySnippet = bodySnippet; this.bodySnippet = bodySnippet;
this.threadId = threadId; this.threadId = threadId;
this.receivedTimestampMs = receivedTimestampMs; this.sentTimestampMs = sentTimestampMs;
} }
} }

View File

@ -6,14 +6,12 @@ import android.content.IntentFilter;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcel; import android.os.Parcel;
import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService; import android.service.chooser.ChooserTargetService;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
@ -28,7 +26,6 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@RequiresApi(api = Build.VERSION_CODES.M)
public class DirectShareService extends ChooserTargetService { public class DirectShareService extends ChooserTargetService {
private static final String TAG = DirectShareService.class.getSimpleName(); private static final String TAG = DirectShareService.class.getSimpleName();
@ -40,53 +37,50 @@ public class DirectShareService extends ChooserTargetService {
List<ChooserTarget> results = new LinkedList<>(); List<ChooserTarget> results = new LinkedList<>();
ComponentName componentName = new ComponentName(this, ShareActivity.class); ComponentName componentName = new ComponentName(this, ShareActivity.class);
ThreadDatabase threadDatabase = DatabaseComponent.get(this).threadDatabase(); ThreadDatabase threadDatabase = DatabaseComponent.get(this).threadDatabase();
Cursor cursor = threadDatabase.getDirectShareList();
try { try (Cursor cursor = threadDatabase.getDirectShareList()) {
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor); ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor);
ThreadRecord record; ThreadRecord record;
while ((record = reader.getNext()) != null && results.size() < 10) { while ((record = reader.getNext()) != null && results.size() < 10) {
Recipient recipient = Recipient.from(this, record.getRecipient().getAddress(), false); Recipient recipient = Recipient.from(this, record.getRecipient().getAddress(), false);
String name = recipient.toShortString(); String name = recipient.toShortString();
Bitmap avatar; Bitmap avatar;
if (recipient.getContactPhoto() != null) {
try {
avatar = GlideApp.with(this)
.asBitmap()
.load(recipient.getContactPhoto())
.circleCrop()
.submit(getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width))
.get();
} catch (InterruptedException | ExecutionException e) {
Log.w(TAG, e);
avatar = getFallbackDrawable(recipient);
}
} else {
avatar = getFallbackDrawable(recipient);
}
Parcel parcel = Parcel.obtain();
parcel.writeParcelable(recipient.getAddress(), 0);
Bundle bundle = new Bundle();
bundle.putLong(ShareActivity.EXTRA_THREAD_ID, record.getThreadId());
bundle.putByteArray(ShareActivity.EXTRA_ADDRESS_MARSHALLED, parcel.marshall());
bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, record.getDistributionType());
bundle.setClassLoader(getClassLoader());
results.add(new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle));
parcel.recycle();
if (recipient.getContactPhoto() != null) {
try {
avatar = GlideApp.with(this)
.asBitmap()
.load(recipient.getContactPhoto())
.circleCrop()
.submit(getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width))
.get();
} catch (InterruptedException | ExecutionException e) {
Log.w(TAG, e);
avatar = getFallbackDrawable(recipient);
}
} else {
avatar = getFallbackDrawable(recipient);
} }
Parcel parcel = Parcel.obtain(); return results;
parcel.writeParcelable(recipient.getAddress(), 0);
Bundle bundle = new Bundle();
bundle.putLong(ShareActivity.EXTRA_THREAD_ID, record.getThreadId());
bundle.putByteArray(ShareActivity.EXTRA_ADDRESS_MARSHALLED, parcel.marshall());
bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, record.getDistributionType());
bundle.setClassLoader(getClassLoader());
results.add(new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle));
parcel.recycle();
} }
return results;
} finally {
if (cursor != null) cursor.close();
}
} }
private Bitmap getFallbackDrawable(@NonNull Recipient recipient) { private Bitmap getFallbackDrawable(@NonNull Recipient recipient) {

View File

@ -17,7 +17,7 @@ public class ExpirationListener extends BroadcastReceiver {
public static void setAlarm(Context context, long waitTimeMillis) { public static void setAlarm(Context context, long waitTimeMillis) {
Intent intent = new Intent(context, ExpirationListener.class); Intent intent = new Intent(context, ExpirationListener.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(pendingIntent); alarmManager.cancel(pendingIntent);

View File

@ -6,6 +6,7 @@ import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.IBinder; import android.os.IBinder;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -13,9 +14,9 @@ import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Preconditions;
import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.session.libsignal.utilities.guava.Preconditions;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -87,10 +88,10 @@ public class GenericForegroundService extends Service {
} }
private void postObligatoryForegroundNotification(String title, String channelId, @DrawableRes int iconRes) { private void postObligatoryForegroundNotification(String title, String channelId, @DrawableRes int iconRes) {
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, channelId) startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, channelId)
.setSmallIcon(iconRes) .setSmallIcon(iconRes)
.setContentTitle(title) .setContentTitle(title)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, HomeActivity.class), 0)) .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, HomeActivity.class), PendingIntent.FLAG_IMMUTABLE))
.build()); .build());
} }

View File

@ -29,18 +29,19 @@ import android.os.AsyncTask;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
import org.thoughtcrime.securesms.DummyActivity; import org.thoughtcrime.securesms.DummyActivity;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -255,18 +256,18 @@ public class KeyCachingService extends Service {
private PendingIntent buildLockIntent() { private PendingIntent buildLockIntent() {
Intent intent = new Intent(this, KeyCachingService.class); Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(PASSPHRASE_EXPIRED_EVENT); intent.setAction(PASSPHRASE_EXPIRED_EVENT);
return PendingIntent.getService(getApplicationContext(), 0, intent, 0); return PendingIntent.getService(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
} }
private PendingIntent buildLaunchIntent() { private PendingIntent buildLaunchIntent() {
Intent intent = new Intent(this, HomeActivity.class); Intent intent = new Intent(this, HomeActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); return PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
} }
private static PendingIntent buildExpirationPendingIntent(@NonNull Context context) { private static PendingIntent buildExpirationPendingIntent(@NonNull Context context) {
Intent expirationIntent = new Intent(PASSPHRASE_EXPIRED_EVENT, null, context, KeyCachingService.class); Intent expirationIntent = new Intent(PASSPHRASE_EXPIRED_EVENT, null, context, KeyCachingService.class);
return PendingIntent.getService(context, 0, expirationIntent, 0); return PendingIntent.getService(context, 0, expirationIntent, PendingIntent.FLAG_IMMUTABLE);
} }
@Override @Override

View File

@ -6,6 +6,7 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
public abstract class PersistentAlarmManagerListener extends BroadcastReceiver { public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
@ -21,7 +22,7 @@ public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
long scheduledTime = getNextScheduledExecutionTime(context); long scheduledTime = getNextScheduledExecutionTime(context);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent(context, getClass()); Intent alarmIntent = new Intent(context, getClass());
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_IMMUTABLE);
if (System.currentTimeMillis() >= scheduledTime) { if (System.currentTimeMillis() >= scheduledTime) {
scheduledTime = onAlarm(context, scheduledTime); scheduledTime = onAlarm(context, scheduledTime);

View File

@ -12,21 +12,22 @@ import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import org.session.libsignal.utilities.Log;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.session.libsession.utilities.FileUtils; import org.session.libsession.utilities.FileUtils;
import org.session.libsignal.utilities.Hex;
import org.session.libsession.utilities.ServiceUtil; import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.FileProviderUtil;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest; import java.security.MessageDigest;
import network.loki.messenger.R;
public class UpdateApkReadyListener extends BroadcastReceiver { public class UpdateApkReadyListener extends BroadcastReceiver {
private static final String TAG = UpdateApkReadyListener.class.getSimpleName(); private static final String TAG = UpdateApkReadyListener.class.getSimpleName();
@ -61,7 +62,7 @@ public class UpdateApkReadyListener extends BroadcastReceiver {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setData(uri); intent.setData(uri);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.APP_UPDATES) Notification notification = new NotificationCompat.Builder(context, NotificationChannels.APP_UPDATES)
.setOngoing(true) .setOngoing(true)

View File

@ -7,10 +7,13 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT import android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioManager import android.media.AudioManager
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.ResultReceiver import android.os.ResultReceiver
import android.telephony.PhoneStateListener import android.telephony.PhoneStateListener
import android.telephony.PhoneStateListener.LISTEN_NONE
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
@ -28,30 +31,13 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_IN
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import org.thoughtcrime.securesms.webrtc.*
import org.thoughtcrime.securesms.webrtc.CallManager
import org.thoughtcrime.securesms.webrtc.CallViewModel
import org.thoughtcrime.securesms.webrtc.HangUpRtcOnPstnCallAnsweredListener
import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver
import org.thoughtcrime.securesms.webrtc.NetworkChangeReceiver
import org.thoughtcrime.securesms.webrtc.PeerConnectionException
import org.thoughtcrime.securesms.webrtc.PowerButtonReceiver
import org.thoughtcrime.securesms.webrtc.ProximityLockRelease
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager
import org.thoughtcrime.securesms.webrtc.WiredHeadsetStateReceiver
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.data.Event import org.thoughtcrime.securesms.webrtc.data.Event
import org.thoughtcrime.securesms.webrtc.locks.LockManager import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.webrtc.DataChannel import org.webrtc.*
import org.webrtc.IceCandidate import org.webrtc.PeerConnection.IceConnectionState.*
import org.webrtc.MediaStream import java.util.*
import org.webrtc.PeerConnection
import org.webrtc.PeerConnection.IceConnectionState.CONNECTED
import org.webrtc.PeerConnection.IceConnectionState.DISCONNECTED
import org.webrtc.PeerConnection.IceConnectionState.FAILED
import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription
import java.util.UUID
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledFuture
@ -60,7 +46,7 @@ import javax.inject.Inject
import org.thoughtcrime.securesms.webrtc.data.State as CallState import org.thoughtcrime.securesms.webrtc.data.State as CallState
@AndroidEntryPoint @AndroidEntryPoint
class WebRtcCallService: Service(), CallManager.WebRtcListener { class WebRtcCallService : Service(), CallManager.WebRtcListener {
companion object { companion object {
@ -108,62 +94,82 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private const val RECONNECT_SECONDS = 5L private const val RECONNECT_SECONDS = 5L
private const val MAX_RECONNECTS = 5 private const val MAX_RECONNECTS = 5
fun cameraEnabled(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java) fun cameraEnabled(context: Context, enabled: Boolean) =
Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_SET_MUTE_VIDEO) .setAction(ACTION_SET_MUTE_VIDEO)
.putExtra(EXTRA_MUTE, !enabled) .putExtra(EXTRA_MUTE, !enabled)
fun flipCamera(context: Context) = Intent(context, WebRtcCallService::class.java) fun flipCamera(context: Context) = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_FLIP_CAMERA) .setAction(ACTION_FLIP_CAMERA)
fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java) fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_ANSWER_CALL) .setAction(ACTION_ANSWER_CALL)
fun microphoneIntent(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java) fun microphoneIntent(context: Context, enabled: Boolean) =
.setAction(ACTION_SET_MUTE_AUDIO) Intent(context, WebRtcCallService::class.java)
.putExtra(EXTRA_MUTE, !enabled) .setAction(ACTION_SET_MUTE_AUDIO)
.putExtra(EXTRA_MUTE, !enabled)
fun createCall(context: Context, recipient: Recipient) = Intent(context, WebRtcCallService::class.java) fun createCall(context: Context, recipient: Recipient) =
Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_OUTGOING_CALL) .setAction(ACTION_OUTGOING_CALL)
.putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address) .putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address)
fun incomingCall(context: Context, address: Address, sdp: String, callId: UUID, callTime: Long) = fun incomingCall(
Intent(context, WebRtcCallService::class.java) context: Context,
.setAction(ACTION_INCOMING_RING) address: Address,
.putExtra(EXTRA_RECIPIENT_ADDRESS, address) sdp: String,
.putExtra(EXTRA_CALL_ID, callId) callId: UUID,
.putExtra(EXTRA_REMOTE_DESCRIPTION, sdp) callTime: Long
.putExtra(EXTRA_TIMESTAMP, callTime) ) =
Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_INCOMING_RING)
.putExtra(EXTRA_RECIPIENT_ADDRESS, address)
.putExtra(EXTRA_CALL_ID, callId)
.putExtra(EXTRA_REMOTE_DESCRIPTION, sdp)
.putExtra(EXTRA_TIMESTAMP, callTime)
fun incomingAnswer(context: Context, address: Address, sdp: String, callId: UUID) = fun incomingAnswer(context: Context, address: Address, sdp: String, callId: UUID) =
Intent(context, WebRtcCallService::class.java) Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_RESPONSE_MESSAGE) .setAction(ACTION_RESPONSE_MESSAGE)
.putExtra(EXTRA_RECIPIENT_ADDRESS, address) .putExtra(EXTRA_RECIPIENT_ADDRESS, address)
.putExtra(EXTRA_CALL_ID, callId) .putExtra(EXTRA_CALL_ID, callId)
.putExtra(EXTRA_REMOTE_DESCRIPTION, sdp) .putExtra(EXTRA_REMOTE_DESCRIPTION, sdp)
fun preOffer(context: Context, address: Address, callId: UUID, callTime: Long) = fun preOffer(context: Context, address: Address, callId: UUID, callTime: Long) =
Intent(context, WebRtcCallService::class.java) Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_PRE_OFFER) .setAction(ACTION_PRE_OFFER)
.putExtra(EXTRA_RECIPIENT_ADDRESS, address) .putExtra(EXTRA_RECIPIENT_ADDRESS, address)
.putExtra(EXTRA_CALL_ID, callId) .putExtra(EXTRA_CALL_ID, callId)
.putExtra(EXTRA_TIMESTAMP, callTime) .putExtra(EXTRA_TIMESTAMP, callTime)
fun iceCandidates(context: Context, address: Address, iceCandidates: List<IceCandidate>, callId: UUID) = fun iceCandidates(
Intent(context, WebRtcCallService::class.java) context: Context,
.setAction(ACTION_ICE_MESSAGE) address: Address,
.putExtra(EXTRA_CALL_ID, callId) iceCandidates: List<IceCandidate>,
.putExtra(EXTRA_ICE_SDP, iceCandidates.map(IceCandidate::sdp).toTypedArray()) callId: UUID
.putExtra(EXTRA_ICE_SDP_LINE_INDEX, iceCandidates.map(IceCandidate::sdpMLineIndex).toIntArray()) ) =
.putExtra(EXTRA_ICE_SDP_MID, iceCandidates.map(IceCandidate::sdpMid).toTypedArray()) Intent(context, WebRtcCallService::class.java)
.putExtra(EXTRA_RECIPIENT_ADDRESS, address) .setAction(ACTION_ICE_MESSAGE)
.putExtra(EXTRA_CALL_ID, callId)
.putExtra(EXTRA_ICE_SDP, iceCandidates.map(IceCandidate::sdp).toTypedArray())
.putExtra(
EXTRA_ICE_SDP_LINE_INDEX,
iceCandidates.map(IceCandidate::sdpMLineIndex).toIntArray()
)
.putExtra(EXTRA_ICE_SDP_MID, iceCandidates.map(IceCandidate::sdpMid).toTypedArray())
.putExtra(EXTRA_RECIPIENT_ADDRESS, address)
fun denyCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_DENY_CALL) fun denyCallIntent(context: Context) =
Intent(context, WebRtcCallService::class.java).setAction(ACTION_DENY_CALL)
fun remoteHangupIntent(context: Context, callId: UUID) = Intent(context, WebRtcCallService::class.java) fun remoteHangupIntent(context: Context, callId: UUID) =
Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_REMOTE_HANGUP) .setAction(ACTION_REMOTE_HANGUP)
.putExtra(EXTRA_CALL_ID, callId) .putExtra(EXTRA_CALL_ID, callId)
fun hangupIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_LOCAL_HANGUP) fun hangupIntent(context: Context) =
Intent(context, WebRtcCallService::class.java).setAction(ACTION_LOCAL_HANGUP)
fun sendAudioManagerCommand(context: Context, command: AudioManagerCommand) { fun sendAudioManagerCommand(context: Context, command: AudioManagerCommand) {
val intent = Intent(context, WebRtcCallService::class.java) val intent = Intent(context, WebRtcCallService::class.java)
@ -174,7 +180,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) { fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) {
val intent = Intent(ACTION_WANTS_TO_ANSWER) val intent = Intent(ACTION_WANTS_TO_ANSWER)
.putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer) .putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent) LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
} }
@ -182,13 +188,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
@JvmStatic @JvmStatic
fun isCallActive(context: Context, resultReceiver: ResultReceiver) { fun isCallActive(context: Context, resultReceiver: ResultReceiver) {
val intent = Intent(context, WebRtcCallService::class.java) val intent = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_IS_IN_CALL_QUERY) .setAction(ACTION_IS_IN_CALL_QUERY)
.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver) .putExtra(EXTRA_RESULT_RECEIVER, resultReceiver)
context.startService(intent) context.startService(intent)
} }
} }
@Inject lateinit var callManager: CallManager @Inject
lateinit var callManager: CallManager
private var wantsToAnswer = false private var wantsToAnswer = false
private var currentTimeouts = 0 private var currentTimeouts = 0
@ -199,8 +206,17 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private val lockManager by lazy { LockManager(this) } private val lockManager by lazy { LockManager(this) }
private val serviceExecutor = Executors.newSingleThreadExecutor() private val serviceExecutor = Executors.newSingleThreadExecutor()
private val timeoutExecutor = Executors.newScheduledThreadPool(1) private val timeoutExecutor = Executors.newScheduledThreadPool(1)
private val hangupOnCallAnswered = HangUpRtcOnPstnCallAnsweredListener {
ContextCompat.startForegroundService(this, hangupIntent(this)) private val hangupOnCallAnswered by lazy {
HangUpRtcOnPstnCallAnsweredListener {
ContextCompat.startForegroundService(this, hangupIntent(this))
}
}
private val hangupTelephonyCallback by lazy {
HangUpRtcTelephonyCallback {
ContextCompat.startForegroundService(this, hangupIntent(this))
}
} }
private var networkChangedReceiver: NetworkChangeReceiver? = null private var networkChangedReceiver: NetworkChangeReceiver? = null
@ -258,7 +274,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
val action = intent.action val action = intent.action
Log.i("Loki", "Handling ${intent.action}") Log.i("Loki", "Handling ${intent.action}")
when { when {
action == ACTION_INCOMING_RING && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleNewOffer(intent) action == ACTION_INCOMING_RING && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleNewOffer(
intent
)
action == ACTION_PRE_OFFER && isIdle() -> handlePreOffer(intent) action == ACTION_PRE_OFFER && isIdle() -> handlePreOffer(intent)
action == ACTION_INCOMING_RING && isBusy(intent) -> handleBusyCall(intent) action == ACTION_INCOMING_RING && isBusy(intent) -> handleBusyCall(intent)
action == ACTION_INCOMING_RING && isPreOffer() -> handleIncomingRing(intent) action == ACTION_INCOMING_RING && isPreOffer() -> handleIncomingRing(intent)
@ -272,7 +290,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent) action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent)
action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent) action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent)
action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent) action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent)
action == ACTION_RESPONSE_MESSAGE && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleResponseMessage(intent) action == ACTION_RESPONSE_MESSAGE && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleResponseMessage(
intent
)
action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent) action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent)
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent) action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent) action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
@ -293,8 +313,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
registerIncomingPstnCallReceiver() registerIncomingPstnCallReceiver()
registerWiredHeadsetStateReceiver() registerWiredHeadsetStateReceiver()
registerWantsToAnswerReceiver() registerWantsToAnswerReceiver()
getSystemService(TelephonyManager::class.java) if (checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
getSystemService(TelephonyManager::class.java)
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
} else {
getSystemService(TelephonyManager::class.java)
.registerTelephonyCallback(serviceExecutor, hangupTelephonyCallback)
}
}
registerUncaughtExceptionHandler() registerUncaughtExceptionHandler()
networkChangedReceiver = NetworkChangeReceiver(::networkChange) networkChangedReceiver = NetworkChangeReceiver(::networkChange)
networkChangedReceiver!!.register(this) networkChangedReceiver!!.register(this)
@ -318,7 +345,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
} }
wantsToAnswerReceiver = receiver wantsToAnswerReceiver = receiver
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, IntentFilter(ACTION_WANTS_TO_ANSWER)) LocalBroadcastManager.getInstance(this)
.registerReceiver(receiver, IntentFilter(ACTION_WANTS_TO_ANSWER))
} }
private fun registerWiredHeadsetStateReceiver() { private fun registerWiredHeadsetStateReceiver() {
@ -339,7 +367,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun handleUpdateAudio(intent: Intent) { private fun handleUpdateAudio(intent: Intent) {
val audioCommand = intent.getParcelableExtra<AudioManagerCommand>(EXTRA_AUDIO_COMMAND)!! val audioCommand = intent.getParcelableExtra<AudioManagerCommand>(EXTRA_AUDIO_COMMAND)!!
if (callManager.currentConnectionState !in arrayOf(CallState.Connected, *CallState.PENDING_CONNECTION_STATES)) { if (callManager.currentConnectionState !in arrayOf(
CallState.Connected,
*CallState.PENDING_CONNECTION_STATES
)
) {
Log.w(TAG, "handling audio command not in call") Log.w(TAG, "handling audio command not in call")
return return
} }
@ -419,8 +451,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
callManager.initializeAudioForCall() callManager.initializeAudioForCall()
callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING) callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING)
setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient) setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
callManager.insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_OUTGOING) callManager.insertCallMessage(
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS) recipient.address.serialize(),
CallMessageType.CALL_OUTGOING
)
scheduledTimeout = timeoutExecutor.schedule(
TimeoutRunnable(callId, this),
TIMEOUT_SECONDS,
TimeUnit.SECONDS
)
callManager.setAudioEnabled(true) callManager.setAudioEnabled(true)
val expectedState = callManager.currentConnectionState val expectedState = callManager.currentConnectionState
@ -429,15 +468,21 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
try { try {
val offerFuture = callManager.onOutgoingCall(this) val offerFuture = callManager.onOutgoingCall(this)
offerFuture.fail { e -> offerFuture.fail { e ->
if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) { if (isConsistentState(
Log.e(TAG,e) expectedState,
expectedCallId,
callManager.currentConnectionState,
callManager.callId
)
) {
Log.e(TAG, e)
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE) callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
callManager.postConnectionError() callManager.postConnectionError()
terminate() terminate()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG,e) Log.e(TAG, e)
callManager.postConnectionError() callManager.postConnectionError()
terminate() terminate()
} }
@ -476,7 +521,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
callManager.silenceIncomingRinger() callManager.silenceIncomingRinger()
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING) callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS) scheduledTimeout = timeoutExecutor.schedule(
TimeoutRunnable(callId, this),
TIMEOUT_SECONDS,
TimeUnit.SECONDS
)
callManager.initializeAudioForCall() callManager.initializeAudioForCall()
callManager.initializeVideo(this) callManager.initializeVideo(this)
@ -487,7 +536,13 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
try { try {
val answerFuture = callManager.onIncomingCall(this) val answerFuture = callManager.onIncomingCall(this)
answerFuture.fail { e -> answerFuture.fail { e ->
if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) { if (isConsistentState(
expectedState,
expectedCallId,
callManager.currentConnectionState,
callManager.callId
)
) {
Log.e(TAG, e) Log.e(TAG, e)
insertMissedCall(recipient, true) insertMissedCall(recipient, true)
callManager.postConnectionError() callManager.postConnectionError()
@ -497,7 +552,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING) lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING)
callManager.setAudioEnabled(true) callManager.setAudioEnabled(true)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG,e) Log.e(TAG, e)
callManager.postConnectionError() callManager.postConnectionError()
terminate() terminate()
} }
@ -518,6 +573,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun handleRemoteHangup(intent: Intent) { private fun handleRemoteHangup(intent: Intent) {
if (callManager.callId != getCallId(intent)) { if (callManager.callId != getCallId(intent)) {
Log.e(TAG, "Hangup for non-active call...") Log.e(TAG, "Hangup for non-active call...")
stopForeground(true)
return return
} }
@ -555,7 +611,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
val callId = getCallId(intent) val callId = getCallId(intent)
val description = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) val description = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION)
callManager.handleResponseMessage(recipient, callId, SessionDescription(SessionDescription.Type.ANSWER, description)) callManager.handleResponseMessage(
recipient,
callId,
SessionDescription(SessionDescription.Type.ANSWER, description)
)
} catch (e: PeerConnectionException) { } catch (e: PeerConnectionException) {
terminate() terminate()
} }
@ -567,14 +627,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
val sdpLineIndexes = intent.getIntArrayExtra(EXTRA_ICE_SDP_LINE_INDEX) ?: return val sdpLineIndexes = intent.getIntArrayExtra(EXTRA_ICE_SDP_LINE_INDEX) ?: return
val sdps = intent.getStringArrayExtra(EXTRA_ICE_SDP) ?: return val sdps = intent.getStringArrayExtra(EXTRA_ICE_SDP) ?: return
if (sdpMids.size != sdpLineIndexes.size || sdpLineIndexes.size != sdps.size) { if (sdpMids.size != sdpLineIndexes.size || sdpLineIndexes.size != sdps.size) {
Log.w(TAG,"sdp info not of equal length") Log.w(TAG, "sdp info not of equal length")
return return
} }
val iceCandidates = sdpMids.indices.map { index -> val iceCandidates = sdpMids.indices.map { index ->
IceCandidate( IceCandidate(
sdpMids[index], sdpMids[index],
sdpLineIndexes[index], sdpLineIndexes[index],
sdps[index] sdps[index]
) )
} }
callManager.handleRemoteIceCandidate(iceCandidates, callId) callManager.handleRemoteIceCandidate(iceCandidates, callId)
@ -597,7 +657,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun handleIsInCallQuery(intent: Intent) { private fun handleIsInCallQuery(intent: Intent) {
val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return
val currentState = callManager.currentConnectionState val currentState = callManager.currentConnectionState
val isInCall = if (currentState in arrayOf(*CallState.PENDING_CONNECTION_STATES, CallState.Connected)) 1 else 0 val isInCall = if (currentState in arrayOf(
*CallState.PENDING_CONNECTION_STATES,
CallState.Connected
)
) 1 else 0
listener.send(isInCall, bundleOf()) listener.send(isInCall, bundleOf())
} }
@ -616,10 +680,21 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
if (callId == getCallId(intent) && isNetworkAvailable && numTimeouts <= MAX_RECONNECTS) { if (callId == getCallId(intent) && isNetworkAvailable && numTimeouts <= MAX_RECONNECTS) {
Log.i("Loki", "Trying to re-connect") Log.i("Loki", "Trying to re-connect")
callManager.networkReestablished() callManager.networkReestablished()
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS) scheduledTimeout = timeoutExecutor.schedule(
TimeoutRunnable(callId, this),
TIMEOUT_SECONDS,
TimeUnit.SECONDS
)
} else if (numTimeouts < MAX_RECONNECTS) { } else if (numTimeouts < MAX_RECONNECTS) {
Log.i("Loki", "Network isn't available, timeouts == $numTimeouts out of $MAX_RECONNECTS") Log.i(
scheduledReconnect = timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), RECONNECT_SECONDS, TimeUnit.SECONDS) "Loki",
"Network isn't available, timeouts == $numTimeouts out of $MAX_RECONNECTS"
)
scheduledReconnect = timeoutExecutor.schedule(
CheckReconnectedRunnable(callId, this),
RECONNECT_SECONDS,
TimeUnit.SECONDS
)
} else { } else {
Log.i("Loki", "Network isn't available, timing out") Log.i("Loki", "Network isn't available, timing out")
handleLocalHangup(intent) handleLocalHangup(intent)
@ -627,12 +702,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
private fun handleCheckTimeout(intent: Intent) { private fun handleCheckTimeout(intent: Intent) {
val callId = callManager.callId ?: return val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState val callState = callManager.currentConnectionState
if (callId == getCallId(intent) && (callState !in arrayOf(CallState.Connected, CallState.Connecting))) { if (callId == getCallId(intent) && (callState !in arrayOf(
CallState.Connected,
CallState.Connecting
))
) {
Log.w(TAG, "Timing out call: $callId") Log.w(TAG, "Timing out call: $callId")
handleLocalHangup(intent) handleLocalHangup(intent)
} }
@ -640,8 +718,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun setCallInProgressNotification(type: Int, recipient: Recipient?) { private fun setCallInProgressNotification(type: Int, recipient: Recipient?) {
startForeground( startForeground(
CallNotificationBuilder.WEBRTC_NOTIFICATION, CallNotificationBuilder.WEBRTC_NOTIFICATION,
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient) CallNotificationBuilder.getCallInProgressNotification(this, type, recipient)
) )
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) { if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) {
// start an intent for the fullscreen // start an intent for the fullscreen
@ -661,14 +739,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun getRemoteRecipient(intent: Intent): Recipient { private fun getRemoteRecipient(intent: Intent): Recipient {
val remoteAddress = intent.getParcelableExtra<Address>(EXTRA_RECIPIENT_ADDRESS) val remoteAddress = intent.getParcelableExtra<Address>(EXTRA_RECIPIENT_ADDRESS)
?: throw AssertionError("No recipient in intent!") ?: throw AssertionError("No recipient in intent!")
return Recipient.from(this, remoteAddress, true) return Recipient.from(this, remoteAddress, true)
} }
private fun getCallId(intent: Intent) : UUID { private fun getCallId(intent: Intent): UUID {
return intent.getSerializableExtra(EXTRA_CALL_ID) as? UUID return intent.getSerializableExtra(EXTRA_CALL_ID) as? UUID
?: throw AssertionError("No callId in intent!") ?: throw AssertionError("No callId in intent!")
} }
private fun insertMissedCall(recipient: Recipient, signal: Boolean) { private fun insertMissedCall(recipient: Recipient, signal: Boolean) {
@ -680,10 +758,13 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
private fun isIncomingMessageExpired(intent: Intent) = private fun isIncomingMessageExpired(intent: Intent) =
System.currentTimeMillis() - intent.getLongExtra(EXTRA_TIMESTAMP, -1) > TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS) System.currentTimeMillis() - intent.getLongExtra(
EXTRA_TIMESTAMP,
-1
) > TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)
override fun onDestroy() { override fun onDestroy() {
Log.d(TAG,"onDestroy()") Log.d(TAG, "onDestroy()")
callManager.unregisterListener(this) callManager.unregisterListener(this)
callReceiver?.let { receiver -> callReceiver?.let { receiver ->
unregisterReceiver(receiver) unregisterReceiver(receiver)
@ -698,6 +779,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
wantsToAnswer = false wantsToAnswer = false
currentTimeouts = 0 currentTimeouts = 0
isNetworkAvailable = false isNetworkAvailable = false
if (checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
val telephonyManager = getSystemService(TelephonyManager::class.java)
with(telephonyManager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
this.listen(hangupOnCallAnswered, LISTEN_NONE)
} else {
this.unregisterTelephonyCallback(hangupTelephonyCallback)
}
}
}
super.onDestroy() super.onDestroy()
} }
@ -709,7 +800,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
} }
private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context): Runnable { private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context) :
Runnable {
override fun run() { override fun run() {
val intent = Intent(context, WebRtcCallService::class.java) val intent = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_CHECK_RECONNECT) .setAction(ACTION_CHECK_RECONNECT)
@ -718,7 +810,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
} }
private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context): Runnable { private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context) :
Runnable {
override fun run() { override fun run() {
val intent = Intent(context, WebRtcCallService::class.java) val intent = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_CHECK_RECONNECT_TIMEOUT) .setAction(ACTION_CHECK_RECONNECT_TIMEOUT)
@ -727,26 +820,29 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
} }
private class TimeoutRunnable(private val callId: UUID, private val context: Context): Runnable { private class TimeoutRunnable(private val callId: UUID, private val context: Context) :
Runnable {
override fun run() { override fun run() {
val intent = Intent(context, WebRtcCallService::class.java) val intent = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_CHECK_TIMEOUT) .setAction(ACTION_CHECK_TIMEOUT)
.putExtra(EXTRA_CALL_ID, callId) .putExtra(EXTRA_CALL_ID, callId)
context.startService(intent) context.startService(intent)
} }
} }
private abstract class FailureListener<V>( private abstract class FailureListener<V>(
expectedState: CallState, expectedState: CallState,
expectedCallId: UUID?, expectedCallId: UUID?,
getState: () -> Pair<CallState, UUID?>): StateAwareListener<V>(expectedState, expectedCallId, getState) { getState: () -> Pair<CallState, UUID?>
) : StateAwareListener<V>(expectedState, expectedCallId, getState) {
override fun onSuccessContinue(result: V) {} override fun onSuccessContinue(result: V) {}
} }
private abstract class SuccessOnlyListener<V>( private abstract class SuccessOnlyListener<V>(
expectedState: CallState, expectedState: CallState,
expectedCallId: UUID?, expectedCallId: UUID?,
getState: () -> Pair<CallState, UUID>): StateAwareListener<V>(expectedState, expectedCallId, getState) { getState: () -> Pair<CallState, UUID>
) : StateAwareListener<V>(expectedState, expectedCallId, getState) {
override fun onFailureContinue(throwable: Throwable?) { override fun onFailureContinue(throwable: Throwable?) {
Log.e(TAG, throwable) Log.e(TAG, throwable)
throw AssertionError(throwable) throw AssertionError(throwable)
@ -754,9 +850,10 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
private abstract class StateAwareListener<V>( private abstract class StateAwareListener<V>(
private val expectedState: CallState, private val expectedState: CallState,
private val expectedCallId: UUID?, private val expectedCallId: UUID?,
private val getState: ()->Pair<CallState, UUID?>): FutureTaskListener<V> { private val getState: () -> Pair<CallState, UUID?>
) : FutureTaskListener<V> {
companion object { companion object {
private val TAG = Log.tag(StateAwareListener::class.java) private val TAG = Log.tag(StateAwareListener::class.java)
@ -764,7 +861,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
override fun onSuccess(result: V) { override fun onSuccess(result: V) {
if (!isConsistentState()) { if (!isConsistentState()) {
Log.w(TAG,"State has changed since request, aborting success callback...") Log.w(TAG, "State has changed since request, aborting success callback...")
} else { } else {
onSuccessContinue(result) onSuccessContinue(result)
} }
@ -773,7 +870,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
override fun onFailure(exception: ExecutionException?) { override fun onFailure(exception: ExecutionException?) {
if (!isConsistentState()) { if (!isConsistentState()) {
Log.w(TAG, exception) Log.w(TAG, exception)
Log.w(TAG,"State has changed since request, aborting failure callback...") Log.w(TAG, "State has changed since request, aborting failure callback...")
} else { } else {
exception?.let { exception?.let {
onFailureContinue(it.cause) onFailureContinue(it.cause)
@ -792,10 +889,10 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
private fun isConsistentState( private fun isConsistentState(
expectedState: CallState, expectedState: CallState,
expectedCallId: UUID?, expectedCallId: UUID?,
currentState: CallState, currentState: CallState,
currentCallId: UUID? currentCallId: UUID?
): Boolean { ): Boolean {
return expectedState == currentState && expectedCallId == currentCallId return expectedState == currentState && expectedCallId == currentCallId
} }
@ -817,17 +914,29 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
val intent = Intent(this, WebRtcCallService::class.java) val intent = Intent(this, WebRtcCallService::class.java)
.setAction(ACTION_ICE_CONNECTED) .setAction(ACTION_ICE_CONNECTED)
startService(intent) startService(intent)
} else if (newState in arrayOf(FAILED, DISCONNECTED) && (scheduledReconnect == null && scheduledTimeout == null)) { } else if (newState in arrayOf(
FAILED,
DISCONNECTED
) && (scheduledReconnect == null && scheduledTimeout == null)
) {
callManager.callId?.let { callId -> callManager.callId?.let { callId ->
callManager.postConnectionEvent(Event.IceDisconnect) { callManager.postConnectionEvent(Event.IceDisconnect) {
callManager.postViewModelState(CallViewModel.State.CALL_RECONNECTING) callManager.postViewModelState(CallViewModel.State.CALL_RECONNECTING)
if (callManager.isInitiator()) { if (callManager.isInitiator()) {
Log.i("Loki", "Starting reconnect timer") Log.i("Loki", "Starting reconnect timer")
scheduledReconnect = timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), RECONNECT_SECONDS, TimeUnit.SECONDS) scheduledReconnect = timeoutExecutor.schedule(
CheckReconnectedRunnable(callId, this),
RECONNECT_SECONDS,
TimeUnit.SECONDS
)
} else { } else {
Log.i("Loki", "Starting timeout, awaiting new reconnect") Log.i("Loki", "Starting timeout, awaiting new reconnect")
callManager.postConnectionEvent(Event.PrepareForNewOffer) { callManager.postConnectionEvent(Event.PrepareForNewOffer) {
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS) scheduledTimeout = timeoutExecutor.schedule(
TimeoutRunnable(callId, this),
TIMEOUT_SECONDS,
TimeUnit.SECONDS
)
} }
} }
} }
@ -855,7 +964,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
override fun onDataChannel(p0: DataChannel?) {} override fun onDataChannel(p0: DataChannel?) {}
override fun onRenegotiationNeeded() { override fun onRenegotiationNeeded() {
Log.w(TAG,"onRenegotiationNeeded was called!") Log.w(TAG, "onRenegotiationNeeded was called!")
} }
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {} override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {}

View File

@ -1,20 +1,21 @@
package org.thoughtcrime.securesms.sskenvironment; package org.thoughtcrime.securesms.sskenvironment;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import android.content.Context;
import androidx.annotation.NonNull;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import java.util.ArrayList; import java.util.ArrayList;
@ -198,12 +199,12 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr
if (device != typist.device) return false; if (device != typist.device) return false;
if (threadId != typist.threadId) return false; if (threadId != typist.threadId) return false;
return author.equals(typist.author); return author.getAddress().equals(typist.author.getAddress());
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = author.hashCode(); int result = author.getAddress().hashCode();
result = 31 * result + device; result = 31 * result + device;
result = 31 * result + (int) (threadId ^ (threadId >>> 32)); result = 31 * result + (int) (threadId ^ (threadId >>> 32));
return result; return result;

View File

@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.BackupFileRecord import org.thoughtcrime.securesms.database.BackupFileRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.service.LocalBackupListener
import java.io.IOException import java.io.IOException
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
@ -74,44 +73,6 @@ object BackupUtil {
return prefList return prefList
} }
/**
* Set app-wide configuration to enable the backups and schedule them.
*
* Make sure that the backup dir is selected prior activating the backup.
* Use [BackupDirSelector] or [setBackupDirUri] manually.
*/
@JvmStatic
@Throws(IOException::class)
fun enableBackups(context: Context, password: String) {
val backupDir = getBackupDirUri(context)
if (backupDir == null || !validateDirAccess(context, backupDir)) {
throw IOException("Backup dir is not set or invalid.")
}
BackupPassphrase.set(context, password)
TextSecurePreferences.setBackupEnabled(context, true)
LocalBackupListener.schedule(context)
}
/**
* Set app-wide configuration to disable the backups.
*
* This call resets the backup dir value.
* Make sure to call [setBackupDirUri] prior next call to [enableBackups].
*
* @param deleteBackupFiles if true, deletes all the previously created backup files
* (if the app has access to them)
*/
@JvmStatic
fun disableBackups(context: Context, deleteBackupFiles: Boolean) {
BackupPassphrase.set(context, null)
TextSecurePreferences.setBackupEnabled(context, false)
if (deleteBackupFiles) {
deleteAllBackupFiles(context)
}
setBackupDirUri(context, null)
}
@JvmStatic @JvmStatic
fun getLastBackupTimeString(context: Context, locale: Locale): String { fun getLastBackupTimeString(context: Context, locale: Locale): String {
val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime() val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime()

View File

@ -4,8 +4,6 @@ import android.database.Cursor;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Optional;
public final class CursorUtil { public final class CursorUtil {
@ -19,71 +17,8 @@ public final class CursorUtil {
return cursor.getInt(cursor.getColumnIndexOrThrow(column)); return cursor.getInt(cursor.getColumnIndexOrThrow(column));
} }
public static float requireFloat(@NonNull Cursor cursor, @NonNull String column) {
return cursor.getFloat(cursor.getColumnIndexOrThrow(column));
}
public static long requireLong(@NonNull Cursor cursor, @NonNull String column) { public static long requireLong(@NonNull Cursor cursor, @NonNull String column) {
return cursor.getLong(cursor.getColumnIndexOrThrow(column)); return cursor.getLong(cursor.getColumnIndexOrThrow(column));
} }
public static boolean requireBoolean(@NonNull Cursor cursor, @NonNull String column) {
return requireInt(cursor, column) != 0;
}
public static byte[] requireBlob(@NonNull Cursor cursor, @NonNull String column) {
return cursor.getBlob(cursor.getColumnIndexOrThrow(column));
}
public static boolean isNull(@NonNull Cursor cursor, @NonNull String column) {
return cursor.isNull(cursor.getColumnIndexOrThrow(column));
}
public static Optional<String> getString(@NonNull Cursor cursor, @NonNull String column) {
if (cursor.getColumnIndex(column) < 0) {
return Optional.empty();
} else {
return Optional.ofNullable(requireString(cursor, column));
}
}
public static Optional<Integer> getInt(@NonNull Cursor cursor, @NonNull String column) {
if (cursor.getColumnIndex(column) < 0) {
return Optional.empty();
} else {
return Optional.of(requireInt(cursor, column));
}
}
public static Optional<Boolean> getBoolean(@NonNull Cursor cursor, @NonNull String column) {
if (cursor.getColumnIndex(column) < 0) {
return Optional.empty();
} else {
return Optional.of(requireBoolean(cursor, column));
}
}
public static Optional<byte[]> getBlob(@NonNull Cursor cursor, @NonNull String column) {
if (cursor.getColumnIndex(column) < 0) {
return Optional.empty();
} else {
return Optional.ofNullable(requireBlob(cursor, column));
}
}
/**
* Reads each column as a string, and concatenates them together into a single string separated by |
*/
public static String readRowAsString(@NonNull Cursor cursor) {
StringBuilder row = new StringBuilder();
for (int i = 0, len = cursor.getColumnCount(); i < len; i++) {
row.append(cursor.getString(i));
if (i < len - 1) {
row.append(" | ");
}
}
return row.toString();
}
} }

View File

@ -1,24 +0,0 @@
package org.thoughtcrime.securesms.util
import android.annotation.SuppressLint
import android.os.Build
import android.util.Log
import android.widget.PopupMenu
@SuppressLint("PrivateApi")
@Deprecated(message = "Not needed when using appcompat 1.4.1+", replaceWith = ReplaceWith("setForceShowIcon(true)"))
fun PopupMenu.forceShowIcon() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.setForceShowIcon(true)
} else {
try {
val popupField = PopupMenu::class.java.getDeclaredField("mPopup")
popupField.isAccessible = true
val menu = popupField.get(this)
menu.javaClass.getDeclaredMethod("setForceShowIcon", Boolean::class.java)
.invoke(menu, true)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't show message request popupmenu due to error: $exception.")
}
}
}

View File

@ -3,8 +3,11 @@ package org.thoughtcrime.securesms.webrtc
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.telephony.PhoneStateListener import android.telephony.PhoneStateListener
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import androidx.annotation.RequiresApi
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.webrtc.locks.LockManager import org.thoughtcrime.securesms.webrtc.locks.LockManager
@ -25,6 +28,21 @@ class HangUpRtcOnPstnCallAnsweredListener(private val hangupListener: ()->Unit):
} }
} }
@RequiresApi(Build.VERSION_CODES.S)
class HangUpRtcTelephonyCallback(private val hangupListener: ()->Unit): TelephonyCallback(), TelephonyCallback.CallStateListener {
companion object {
private val TAG = Log.tag(HangUpRtcTelephonyCallback::class.java)
}
override fun onCallStateChanged(state: Int) {
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
hangupListener()
Log.i(TAG, "Device phone call ended Session call.")
}
}
}
class PowerButtonReceiver : BroadcastReceiver() { class PowerButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_SCREEN_OFF == intent.action) { if (Intent.ACTION_SCREEN_OFF == intent.action) {

View File

@ -6,7 +6,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.media.AudioManager import android.media.AudioManager
import android.media.SoundPool import android.media.SoundPool
import android.os.Build
import android.os.HandlerThread import android.os.HandlerThread
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -108,7 +107,7 @@ class SignalAudioManager(private val context: Context,
updateAudioDeviceState() updateAudioDeviceState()
wiredHeadsetReceiver = WiredHeadsetReceiver() wiredHeadsetReceiver = WiredHeadsetReceiver()
context.registerReceiver(wiredHeadsetReceiver, IntentFilter(if (Build.VERSION.SDK_INT >= 21) AudioManager.ACTION_HEADSET_PLUG else Intent.ACTION_HEADSET_PLUG)) context.registerReceiver(wiredHeadsetReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG))
state = State.PREINITIALIZED state = State.PREINITIALIZED

View File

@ -2,14 +2,15 @@ package org.thoughtcrime.securesms.webrtc.audio
import android.Manifest import android.Manifest
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothHeadset import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioManager import android.media.AudioManager
import androidx.core.app.ActivityCompat
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -80,7 +81,6 @@ class SignalBluetoothManager(
bluetoothReceiver = BluetoothHeadsetBroadcastReceiver() bluetoothReceiver = BluetoothHeadsetBroadcastReceiver()
context.registerReceiver(bluetoothReceiver, bluetoothHeadsetFilter) context.registerReceiver(bluetoothReceiver, bluetoothHeadsetFilter)
Log.i(TAG, "Headset profile state: ${bluetoothAdapter?.getProfileConnectionState(BluetoothProfile.HEADSET)?.toStateString()}")
Log.i(TAG, "Bluetooth proxy for headset profile has started") Log.i(TAG, "Bluetooth proxy for headset profile has started")
state = State.UNAVAILABLE state = State.UNAVAILABLE
} }
@ -161,7 +161,8 @@ class SignalBluetoothManager(
Log.d(TAG, "updateDevice(): state: $state") Log.d(TAG, "updateDevice(): state: $state")
if (state == State.UNINITIALIZED || bluetoothHeadset == null) { if (state == State.UNINITIALIZED || bluetoothHeadset == null
|| ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return return
} }

View File

@ -49,7 +49,7 @@ public class LockManager {
partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:partial"); partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:partial");
proximityLock = new ProximityLock(pm); proximityLock = new ProximityLock(pm);
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "signal:wifi"); wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "signal:wifi");
fullLock.setReferenceCounted(false); fullLock.setReferenceCounted(false);

View File

@ -8,6 +8,6 @@
android:fillColor="?android:textColorPrimary" android:fillColor="?android:textColorPrimary"
android:pathData="M6.5,6.5m-6.5,0a6.5,6.5 0,1 1,13 0a6.5,6.5 0,1 1,-13 0"/> android:pathData="M6.5,6.5m-6.5,0a6.5,6.5 0,1 1,13 0a6.5,6.5 0,1 1,-13 0"/>
<path <path
android:fillColor="?android:textColorPrimaryInverse" android:fillColor="?colorPrimary"
android:pathData="M3.77,6.61c-0.15,-0.15 -0.38,-0.15 -0.53,0c-0.15,0.15 -0.15,0.38 0,0.53l1.88,1.88c0.15,0.15 0.38,0.15 0.53,0L9.78,4.9c0.15,-0.15 0.15,-0.38 0,-0.53c-0.15,-0.15 -0.38,-0.15 -0.53,0L5.38,8.22L3.77,6.61z"/> android:pathData="M3.77,6.61c-0.15,-0.15 -0.38,-0.15 -0.53,0c-0.15,0.15 -0.15,0.38 0,0.53l1.88,1.88c0.15,0.15 0.38,0.15 0.53,0L9.78,4.9c0.15,-0.15 0.15,-0.38 0,-0.53c-0.15,-0.15 -0.38,-0.15 -0.53,0L5.38,8.22L3.77,6.61z"/>
</vector> </vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1018.39685"
android:viewportHeight="1019.1061">
<group android:translateX="307.15576"
android:translateY="285.3497">
<group>
<clip-path android:pathData="M0,0L404.085,0L404.085,448.407L0,448.407Z M 0,0"/>
<path android:fillAlpha="1" android:fillColor="#000000"
android:fillType="nonZero"
android:pathData="m288.607,420.376l-196.335,-0c-33.576,-0 -62.508,-25.748 -64.164,-59.281 -1.771,-35.847 26.883,-65.576 62.353,-65.576l113.072,-0c6.919,-0 12.527,-5.608 12.527,-12.525l0,-92.305L327.307,252.333C356.723,268.633 375.241,299.335 376.027,332.848 377.161,380.975 336.746,420.376 288.607,420.376m-211.829,-224.303c-29.416,-16.3 -47.933,-47.001 -48.721,-80.515 -1.132,-48.127 39.283,-87.528 87.42,-87.528L311.811,28.031c33.576,-0 62.508,25.748 64.165,59.283 1.771,35.845 -26.883,65.575 -62.352,65.575 0,-0 -81.316,0.013 -113.077,0.019 -6.915,0.001 -12.499,5.608 -12.501,12.523l-0.021,92.289zM340.891,227.816 L256.254,180.919l57.371,-0c49.877,-0 90.46,-40.579 90.46,-90.457 0,-49.877 -40.583,-90.461 -90.46,-90.461l-200.299,-0c-62.485,-0 -113.327,50.841 -113.327,113.327 0,44.567 24.216,85.664 63.195,107.265l84.636,46.896l-57.368,-0c-49.88,-0 -90.463,40.58 -90.463,90.457 0,49.877 40.583,90.461 90.463,90.461L290.758,448.407c62.488,-0 113.327,-50.84 113.327,-113.327 0,-44.567 -24.216,-85.664 -63.193,-107.264" android:strokeColor="#00000000"/>
</group>
</group>
</vector>

View File

@ -69,6 +69,7 @@
</TableRow> </TableRow>
<TableRow <TableRow
android:id="@+id/error_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/small_spacing" android:paddingHorizontal="@dimen/small_spacing"
@ -94,6 +95,7 @@
</TableLayout> </TableLayout>
<LinearLayout <LinearLayout
android:id="@+id/resend_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/very_large_spacing" android:layout_marginTop="@dimen/very_large_spacing"

View File

@ -1,26 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainContentContainer" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/emptyStateContainer" android:id="@+id/emptyStateContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
android:layout_centerInParent="true"> android:layout_centerInParent="true">
@ -35,4 +28,4 @@
</LinearLayout> </LinearLayout>
</RelativeLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -18,35 +18,13 @@
</LinearLayout> </LinearLayout>
<LinearLayout <androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainContentContainer" android:id="@+id/recyclerView"
android:layout_gravity="center"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:clipToPadding="false"
android:scrollbars="vertical"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout tools:listitem="@layout/view_user"/>
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbars="vertical"
tools:listitem="@layout/view_user"/>
<TextView
android:id="@+id/loadingTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/contact_selection_group_activity__finding_contacts"
android:textSize="@dimen/large_font_size" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</FrameLayout> </FrameLayout>

View File

@ -95,7 +95,6 @@
<org.thoughtcrime.securesms.components.ComposeText <org.thoughtcrime.securesms.components.ComposeText
style="@style/Widget.Session.EditText.Compose" style="@style/Widget.Session.EditText.Compose"
android:textColor="?android:textColorPrimary"
android:id="@+id/mediasend_compose_text" android:id="@+id/mediasend_compose_text"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -11,7 +11,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:background="@color/transparent_black_6" /> android:background="@color/transparent_black_30" />
<View <View
android:layout_width="84dp" android:layout_width="84dp"

View File

@ -26,11 +26,6 @@
android:id="@+id/menu_message_details" android:id="@+id/menu_message_details"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:title="@string/conversation_context__menu_message_details"
android:id="@+id/menu_context_select_message"
app:showAsAction="never" />
<item <item
android:title="@string/conversation_context__menu_copy_text" android:title="@string/conversation_context__menu_copy_text"
android:id="@+id/menu_context_copy" android:id="@+id/menu_context_copy"

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@ -158,6 +158,7 @@
<style name="Widget.Session.EditText.Compose" parent="@style/Signal.Text.Body"> <style name="Widget.Session.EditText.Compose" parent="@style/Signal.Text.Body">
<item name="android:padding">2dp</item> <item name="android:padding">2dp</item>
<item name="android:background">@null</item> <item name="android:background">@null</item>
<item name="android:textColor">@color/white</item>
<item name="android:maxLines">4</item> <item name="android:maxLines">4</item>
<item name="android:maxLength">65536</item> <item name="android:maxLength">65536</item>
<item name="android:capitalize">sentences</item> <item name="android:capitalize">sentences</item>

View File

@ -37,15 +37,9 @@ class ConversationViewModelTest: BaseViewModelTest() {
@Before @Before
fun setUp() { fun setUp() {
recipient = mock(Recipient::class.java) recipient = mock(Recipient::class.java)
whenever(repository.isOxenHostedOpenGroup(anyLong())).thenReturn(true)
whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient) whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient)
} }
@Test
fun `should emit group type on init`() = runBlockingTest {
assertTrue(viewModel.uiState.first().isOxenHostedOpenGroup)
}
@Test @Test
fun `should save draft message`() { fun `should save draft message`() {
val draft = "Hi there" val draft = "Hi there"

View File

@ -4,9 +4,9 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.2' classpath "com.android.tools.build:gradle:$gradlePluginVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.google.gms:google-services:4.3.10" classpath "com.google.gms:google-services:$googleServicesVersion"
classpath files('libs/gradle-witness.jar') classpath files('libs/gradle-witness.jar')
} }
} }
@ -51,6 +51,7 @@ allprojects {
project.ext { project.ext {
androidMinimumSdkVersion = 23 androidMinimumSdkVersion = 23
androidCompileSdkVersion = 30 androidTargetSdkVersion = 31
androidCompileSdkVersion = 32
} }
} }

View File

@ -6,5 +6,5 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.android.tools.build:apksig:4.0.1' implementation 'com.android.tools.build:apksig:4.0.2'
} }

View File

@ -1,11 +1,13 @@
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx4g org.gradle.jvmargs=-Xmx8g
kotlinVersion=1.6.0 gradlePluginVersion=7.3.1
coroutinesVersion=1.6.0 googleServicesVersion=4.3.12
kotlinxJsonVersion=1.3.0 kotlinVersion=1.6.21
lifecycleVersion=2.3.1 coroutinesVersion=1.6.4
kotlinxJsonVersion=1.3.3
lifecycleVersion=2.5.1
daggerVersion=2.40.1 daggerVersion=2.40.1
glideVersion=4.11.0 glideVersion=4.11.0
kovenantVersion=3.3.0 kovenantVersion=3.3.0
@ -13,4 +15,12 @@ curve25519Version=0.6.0
protobufVersion=2.5.0 protobufVersion=2.5.0
okhttpVersion=3.12.1 okhttpVersion=3.12.1
jacksonDatabindVersion=2.9.8 jacksonDatabindVersion=2.9.8
appcompatVersion=1.5.1
materialVersion=1.7.0
preferenceVersion=1.2.0
coreVersion=1.8.0
junitVersion=4.13.2
mockitoKotlinVersion=4.0.0 mockitoKotlinVersion=4.0.0
testCoreVersion=1.4.0
pagingVersion=3.0.0

View File

@ -1,6 +1,6 @@
#Thu Dec 30 07:09:53 SAST 2021 #Thu Dec 30 07:09:53 SAST 2021
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -19,17 +19,16 @@ android {
dependencies { dependencies {
implementation project(":libsignal") implementation project(":libsignal")
implementation project(":liblazysodium") implementation project(":liblazysodium")
// implementation 'com.goterl:lazysodium-android:5.0.2@aar'
implementation "net.java.dev.jna:jna:5.8.0@aar" implementation "net.java.dev.jna:jna:5.8.0@aar"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation 'androidx.core:core-ktx:1.3.2' implementation "androidx.core:core-ktx:$coreVersion"
implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.preference:preference-ktx:1.1.1' implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation 'com.google.android.material:material:1.2.1' implementation "com.google.android.material:material:$materialVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.google.dagger:hilt-android:$daggerVersion" implementation "com.google.dagger:hilt-android:$daggerVersion"
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.annimon:stream:1.1.8' implementation 'com.annimon:stream:1.1.8'
@ -43,7 +42,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
testImplementation 'junit:junit:4.12' testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1' testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation "org.mockito:mockito-inline:4.0.0" testImplementation "org.mockito:mockito-inline:4.0.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
@ -51,7 +50,7 @@ dependencies {
testImplementation 'org.powermock:powermock-module-junit4:1.6.1' testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1' testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1' testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.3.0' testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0" testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0"

View File

@ -12,7 +12,6 @@ import androidx.annotation.DrawableRes;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
import com.makeramen.roundedimageview.RoundedDrawable; import com.makeramen.roundedimageview.RoundedDrawable;
import org.session.libsession.R; import org.session.libsession.R;
import org.session.libsession.utilities.ThemeUtil; import org.session.libsession.utilities.ThemeUtil;
@ -34,7 +33,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color); Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId)); RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId));
foreground.setScaleType(ImageView.ScaleType.CENTER); foreground.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
if (inverted) { if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);

View File

@ -109,6 +109,7 @@ interface StorageProtocol {
fun markAsSent(timestamp: Long, author: String) fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String) fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception) fun setErrorMessage(timestamp: Long, author: String, error: Exception)
fun clearErrorMessage(messageID: Long)
fun setMessageServerHash(messageID: Long, serverHash: String) fun setMessageServerHash(messageID: Long, serverHash: String)
// Closed Groups // Closed Groups

View File

@ -96,7 +96,10 @@ object FileServerApi {
) )
return send(request).map { response -> return send(request).map { response ->
val json = JsonUtil.fromJson(response, Map::class.java) val json = JsonUtil.fromJson(response, Map::class.java)
(json["id"] as? String)?.toLong() ?: throw Error.ParsingFailed val hasId = json.containsKey("id")
val id = json.getOrDefault("id", null)
Log.d("Loki-FS", "File Upload Response hasId: $hasId of type: ${id?.javaClass}")
(id as? String)?.toLong() ?: throw Error.ParsingFailed
} }
} }

View File

@ -34,12 +34,7 @@ import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.crypto.PushTransportDetails import org.session.libsignal.crypto.PushTransportDetails
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.defaultRequiresAuth
import org.session.libsignal.utilities.hasNamespaces
import org.session.libsignal.utilities.hexEncodedPublicKey
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
@ -361,6 +356,8 @@ object MessageSender {
message.serverHash?.let { message.serverHash?.let {
storage.setMessageServerHash(messageID, it) storage.setMessageServerHash(messageID, it)
} }
// in case any errors from previous sends
storage.clearErrorMessage(messageID)
// Track the open group server message ID // Track the open group server message ID
if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) { if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) {
val server: String val server: String

View File

@ -368,6 +368,8 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
return@mapNotNull attachment return@mapNotNull attachment
} }
} }
// Cancel any typing indicators if needed
cancelTypingIndicatorsIfNeeded(message.sender!!)
// Parse reaction if needed // Parse reaction if needed
val threadIsGroup = threadRecipient?.isGroupRecipient == true val threadIsGroup = threadRecipient?.isGroupRecipient == true
message.reaction?.let { reaction -> message.reaction?.let { reaction ->
@ -393,8 +395,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
} }
return messageID return messageID
} }
// Cancel any typing indicators if needed
cancelTypingIndicatorsIfNeeded(message.sender!!)
return null return null
} }
@ -510,7 +510,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
storage.updateTitle(groupID, name) storage.updateTitle(groupID, name)
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else { } else {
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), storage.createGroup(groupID, name, LinkedList(members.map { fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
} }
storage.setProfileSharing(Address.fromSerialized(groupID), true) storage.setProfileSharing(Address.fromSerialized(groupID), true)

View File

@ -13,6 +13,7 @@ import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.utilities.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
public class LinkPreview { public class LinkPreview {
@ -75,4 +76,17 @@ public class LinkPreview {
public static LinkPreview deserialize(@NonNull String serialized) throws IOException { public static LinkPreview deserialize(@NonNull String serialized) throws IOException {
return JsonUtil.fromJson(serialized, LinkPreview.class); return JsonUtil.fromJson(serialized, LinkPreview.class);
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LinkPreview that = (LinkPreview) o;
return Objects.equals(url, that.url) && Objects.equals(title, that.title) && Objects.equals(attachmentId, that.attachmentId) && Objects.equals(thumbnail, that.thumbnail);
}
@Override
public int hashCode() {
return Objects.hash(url, title, attachmentId, thumbnail);
}
} }

View File

@ -15,16 +15,16 @@ android {
} }
dependencies { dependencies {
implementation "androidx.annotation:annotation:1.2.0" implementation "androidx.annotation:annotation:1.5.0"
implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version" implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
testImplementation "junit:junit:3.8.2" testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:1.7.1" testImplementation "org.assertj:assertj-core:1.7.1"
testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0" testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0"
} }