diff --git a/app/build.gradle b/app/build.gradle index 49ecd7a8f5..3dcc197263 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,11 +4,11 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath "com.android.tools.build:gradle:$gradlePluginVersion" classpath files('libs/gradle-witness.jar') classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" - classpath "com.google.gms:google-services:4.3.10" + classpath "com.google.gms:google-services:$googleServicesVersion" classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" } } @@ -27,26 +27,27 @@ configurations.all { } dependencies { - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'com.google.android.material:material:1.2.1' + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation "com.google.android.material:material:$materialVersion" implementation 'com.google.android:flexbox:2.0.1' implementation 'androidx.legacy:legacy-support-v13: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.gridlayout:gridlayout:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.3.3' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.exifinterface:exifinterface:1.3.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" - implementation 'androidx.activity:activity-ktx:1.2.2' - implementation 'androidx.fragment:fragment-ktx:1.3.2' - implementation "androidx.core:core-ktx:1.3.2" - implementation "androidx.work:work-runtime-ktx:2.4.0" + implementation "androidx.paging:paging-runtime-ktx:$pagingVersion" + implementation 'androidx.activity:activity-ktx:1.5.1' + implementation 'androidx.fragment:fragment-ktx:1.5.3' + implementation "androidx.core:core-ktx:$coreVersion" + implementation "androidx.work:work-runtime-ktx:2.7.1" implementation ("com.google.firebase:firebase-messaging:18.0.0") { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' @@ -94,7 +95,8 @@ dependencies { implementation 'com.takisoft.fix:colorpicker:1.0.1' implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2' - implementation 'org.signal:android-database-sqlcipher:3.5.9-S3' + implementation 'androidx.sqlite:sqlite-ktx:2.2.0' + implementation 'net.zetetic:sqlcipher-android:4.5.3@aar' implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') { exclude group: 'com.fasterxml.jackson.core' exclude group: 'org.freemarker' @@ -119,7 +121,7 @@ dependencies { implementation "com.github.tbruyelle:rxpermissions:0.10.2" implementation "com.github.ybq:Android-SpinKit:1.4.0" 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.mockito:mockito-inline:4.0.0" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" @@ -127,7 +129,7 @@ dependencies { testImplementation 'org.powermock:powermock-module-junit4:1.6.1' testImplementation 'org.powermock:powermock-module-junit4-rule: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 "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" @@ -141,7 +143,7 @@ dependencies { // Assertions androidTestImplementation 'androidx.test.ext:junit:1.1.3' 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 androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' @@ -151,14 +153,14 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0' androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent: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:shadows-multidex:4.4' } -def canonicalVersionCode = 309 -def canonicalVersionName = "1.16.0" +def canonicalVersionCode = 323 +def canonicalVersionName = "1.16.3" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, @@ -169,13 +171,9 @@ def abiPostFix = ['armeabi-v7a' : 1, android { compileSdkVersion androidCompileSdkVersion - buildToolsVersion '29.0.3' + namespace 'network.loki.messenger' useLibrary 'org.apache.http.legacy' - dexOptions { - javaMaxHeapSize "4g" - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -209,7 +207,7 @@ android { versionName canonicalVersionName minSdkVersion androidMinimumSdkVersion - targetSdkVersion androidCompileSdkVersion + targetSdkVersion androidTargetSdkVersion multiDexEnabled = true diff --git a/app/proguard/proguard.pro b/app/proguard/proguard.pro index 863701970c..bcbc8dadaf 100644 --- a/app/proguard/proguard.pro +++ b/app/proguard/proguard.pro @@ -2,6 +2,7 @@ -keepattributes SourceFile,LineNumberTable -keep class org.whispersystems.** { *; } -keep class org.thoughtcrime.securesms.** { *; } +-keep class org.thoughtcrime.securesms.components.menu.** { *; } -keep class org.session.** { *; } -keepclassmembers class ** { public void onEvent*(**); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 681fc00c17..6755addc03 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,7 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -31,6 +30,7 @@ android:required="false" /> + @@ -174,6 +174,7 @@ android:screenOrientation="portrait"/> @@ -398,42 +400,48 @@ android:authorities="network.loki.securesms.database.recipient" android:exported="false" /> - + - + - + - + - + + android:exported="false"> + android:enabled="true" + android:exported="true"> diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 01bc1f38ae..ef4f5c46a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -47,6 +47,7 @@ import org.session.libsession.utilities.Util; import org.session.libsession.utilities.WindowDebouncer; import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper; import org.session.libsession.utilities.dynamiclanguage.LocaleParser; +import org.session.libsignal.utilities.HTTP; import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.ThreadUtils; @@ -57,6 +58,7 @@ import org.thoughtcrime.securesms.database.EmojiSearchDatabase; import org.thoughtcrime.securesms.database.JobDatabase; import org.thoughtcrime.securesms.database.LokiAPIDatabase; import org.thoughtcrime.securesms.database.Storage; +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.EmojiSearchData; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseModule; @@ -66,6 +68,7 @@ import org.thoughtcrime.securesms.groups.OpenGroupMigrator; import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobs.FastJobStorage; import org.thoughtcrime.securesms.jobs.JobManagerFactories; import org.thoughtcrime.securesms.logging.AndroidLogger; @@ -236,6 +239,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO resubmitProfilePictureIfNeeded(); loadEmojiSearchIndexIfNeeded(); EmojiSource.refresh(); + + NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create(); + HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet); } @Override @@ -244,6 +250,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO Log.i(TAG, "App is now visible."); KeyCachingService.onAppForegrounded(this); + // If the user account hasn't been created or onboarding wasn't finished then don't start + // the pollers + if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) { + return; + } + ThreadUtils.queue(()->{ if (poller != null) { poller.setCaughtUp(false); @@ -481,6 +493,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return; ThreadUtils.queue(() -> { // 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); try { // Read the file into a byte array @@ -497,6 +510,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> { // Update the last profile picture upload date TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime()); + Log.d("Loki-Avatar", "Uploading Avatar Finished"); return Unit.INSTANCE; }); } catch (Exception exception) { @@ -535,7 +549,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO TextSecurePreferences.setProfileName(this, displayName); } getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit(); - if (!deleteDatabase("signal.db")) { + if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) { Log.d("Loki", "Failed to delete database."); } Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java index aad4c17008..0fd813cf4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java @@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter { Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment()); if (slide != null) { - thumbnailView.setImageResource(glideRequests, slide, false, false); + thumbnailView.setImageResource(glideRequests, slide, false, null); } thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java index 7f7f6f75db..63b42c4936 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java @@ -148,12 +148,11 @@ public class PassphrasePromptActivity extends BaseActionBarActivity { // Finish and proceed with the next intent. Intent nextIntent = getIntent().getParcelableExtra("next_intent"); if (nextIntent != null) { - startActivity(nextIntent); -// try { -// startActivity(nextIntent); -// } catch (java.lang.SecurityException e) { -// Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing."); -// } + try { + startActivity(nextIntent); + } catch (java.lang.SecurityException e) { + Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.", e); + } } finish(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index fa0fce7bd3..6e9185094b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -176,6 +176,11 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return messageDB.getMessageID(serverId, threadId) } + override fun getMessageIDs(serverIds: List, threadId: Long): Pair, List> { + val messageDB = DatabaseComponent.get(context).lokiMessageDatabase() + return messageDB.getMessageIDs(serverIds, threadId) + } + override fun deleteMessage(messageID: Long, isSms: Boolean) { val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() else DatabaseComponent.get(context).mmsDatabase() @@ -184,6 +189,15 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID) } + override fun deleteMessages(messageIDs: List, threadId: Long, isSms: Boolean) { + val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() + else DatabaseComponent.get(context).mmsDatabase() + + messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId) + DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs) + DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs) + } + override fun updateMessageAsDeleted(timestamp: Long, author: String) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val address = Address.fromSerialized(author) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt index 94c7517eb0..84a9b6cfc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/ScreenshotObserver.kt @@ -13,6 +13,11 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) uri ?: return + + // There is an odd bug where we can get notified for changes to 'content://media/external' + // directly which is a protected folder, this code is to prevent that crash + if (uri.scheme == "content" && uri.host == "media" && uri.path == "/external") { return } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { queryRelativeDataColumn(uri) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java deleted file mode 100644 index 76342898b1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupDialog.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt deleted file mode 100644 index a94c866c09..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRestoreActivity.kt +++ /dev/null @@ -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() - - 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(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(null) - val backupPassphrase = MutableLiveData(null) - - val processingBackupFile = MutableLiveData(false) - val backupImportResult = MutableLiveData(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() - 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt index 33b8b67258..6b5d47a2e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt @@ -8,7 +8,7 @@ import androidx.annotation.WorkerThread import com.annimon.stream.function.Consumer import com.annimon.stream.function.Predicate import com.google.protobuf.ByteString -import net.sqlcipher.database.SQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteDatabase import org.greenrobot.eventbus.EventBus import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt index ba1df97d56..b40c049bc2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt @@ -5,7 +5,7 @@ import android.content.ContentValues import android.content.Context import android.net.Uri import androidx.annotation.WorkerThread -import net.sqlcipher.database.SQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteDatabase import org.greenrobot.eventbus.EventBus import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java deleted file mode 100644 index 5b2199896a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.TextView; - -import org.thoughtcrime.securesms.mms.GlideRequests; - -import org.thoughtcrime.securesms.mms.ImageSlide; -import org.thoughtcrime.securesms.mms.SlidesClickedListener; - -import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; - -import network.loki.messenger.R; -import okhttp3.HttpUrl; - -public class LinkPreviewView extends FrameLayout { - - private static final int TYPE_CONVERSATION = 0; - private static final int TYPE_COMPOSE = 1; - - private ViewGroup container; - private OutlinedThumbnailView thumbnail; - private TextView title; - private TextView site; - private View divider; - private View closeButton; - private View spinner; - - private int type; - private int defaultRadius; - private CornerMask cornerMask; - private Outliner outliner; - private CloseClickedListener closeClickedListener; - - public LinkPreviewView(Context context) { - super(context); - init(null); - } - - public LinkPreviewView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(attrs); - } - - private void init(@Nullable AttributeSet attrs) { - inflate(getContext(), R.layout.link_preview, this); - - container = findViewById(R.id.linkpreview_container); - thumbnail = findViewById(R.id.linkpreview_thumbnail); - title = findViewById(R.id.linkpreview_title); - site = findViewById(R.id.linkpreview_site); - divider = findViewById(R.id.linkpreview_divider); - spinner = findViewById(R.id.linkpreview_progress_wheel); - closeButton = findViewById(R.id.linkpreview_close); - defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius); - cornerMask = new CornerMask(this); - outliner = new Outliner(); - - outliner.setColor(getResources().getColor(R.color.transparent)); - - if (attrs != null) { - TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0); - type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0); - typedArray.recycle(); - } - - if (type == TYPE_COMPOSE) { - container.setBackgroundColor(Color.TRANSPARENT); - container.setPadding(0, 0, 0, 0); - divider.setVisibility(VISIBLE); - - closeButton.setOnClickListener(v -> { - if (closeClickedListener != null) { - closeClickedListener.onCloseClicked(); - } - }); - } - - setWillNotDraw(false); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - if (type == TYPE_COMPOSE) return; - - cornerMask.mask(canvas); - outliner.draw(canvas); - } - - public void setLoading() { - title.setVisibility(GONE); - site.setVisibility(GONE); - thumbnail.setVisibility(GONE); - spinner.setVisibility(VISIBLE); - closeButton.setVisibility(GONE); - } - - public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) { - setLinkPreview(glideRequests, linkPreview, showThumbnail); - if (showCloseButton) { - closeButton.setVisibility(VISIBLE); - } else { - closeButton.setVisibility(GONE); - } - } - - public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) { - title.setVisibility(VISIBLE); - site.setVisibility(VISIBLE); - thumbnail.setVisibility(VISIBLE); - spinner.setVisibility(GONE); - closeButton.setVisibility(VISIBLE); - - title.setText(linkPreview.getTitle()); - - HttpUrl url = HttpUrl.parse(linkPreview.getUrl()); - if (url != null) { - site.setText(url.topPrivateDomain()); - } - - if (showThumbnail && linkPreview.getThumbnail().isPresent()) { - thumbnail.setVisibility(VISIBLE); - thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false); - thumbnail.showDownloadText(false); - } else { - thumbnail.setVisibility(GONE); - } - } - - public void setCorners(int topLeft, int topRight) { - cornerMask.setRadii(topLeft, topRight, 0, 0); - outliner.setRadii(topLeft, topRight, 0, 0); - thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius); - postInvalidate(); - } - - public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) { - this.closeClickedListener = closeClickedListener; - } - - public void setDownloadClickedListener(SlidesClickedListener listener) { - thumbnail.setDownloadClickListener(listener); - } - - public interface CloseClickedListener { - void onCloseClicked(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java deleted file mode 100644 index 71bf8a2804..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import android.graphics.Canvas; -import android.util.AttributeSet; - -import org.session.libsession.utilities.ThemeUtil; -import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; - -import network.loki.messenger.R; - -public class OutlinedThumbnailView extends ThumbnailView { - - private CornerMask cornerMask; - private Outliner outliner; - - public OutlinedThumbnailView(Context context) { - super(context); - init(); - } - - public OutlinedThumbnailView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - private void init() { - cornerMask = new CornerMask(this); - outliner = new Outliner(); - - outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color)); - setWillNotDraw(false); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - cornerMask.mask(canvas); - outliner.draw(canvas); - } - - public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) { - cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft); - outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft); - postInvalidate(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 0ded9f346e..a827a7d260 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -34,6 +34,8 @@ class ProfilePictureView @JvmOverloads constructor( private val profilePicturesCache = mutableMapOf() private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default) .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) + private val unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification) + .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) // endregion @@ -43,10 +45,8 @@ class ProfilePictureView @JvmOverloads constructor( val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey) return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey } - fun isOpenGroupWithProfilePicture(recipient: Recipient): Boolean { - return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null - } - if (recipient.isGroupRecipient && !isOpenGroupWithProfilePicture(recipient)) { + + if (recipient.isClosedGroupRecipient) { val members = DatabaseComponent.get(context).groupDatabase() .getGroupMemberAddresses(recipient.address.toGroupString(), true) .sorted() @@ -107,7 +107,7 @@ class ProfilePictureView @JvmOverloads constructor( if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return val signalProfilePicture = recipient.contactPhoto val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject - val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}") + if (signalProfilePicture != null && avatar != "0" && avatar != "") { glide.clear(imageView) glide.load(signalProfilePicture) @@ -117,7 +117,12 @@ class ProfilePictureView @JvmOverloads constructor( .diskCacheStrategy(DiskCacheStrategy.NONE) .circleCrop() .into(imageView) + } else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) { + glide.clear(imageView) + imageView.setImageDrawable(unknownOpenGroupDrawable) } else { + val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}") + glide.clear(imageView) glide.load(placeholder) .placeholder(unknownRecipientDrawable) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java b/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java index 6214c58531..98a623eef3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/StickerView.java @@ -52,19 +52,4 @@ public class StickerView extends FrameLayout { public void setOnLongClickListener(@Nullable OnLongClickListener l) { image.setOnLongClickListener(l); } - - public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) { - boolean showControls = stickerSlide.asAttachment().getDataUri() == null; - - image.setImageResource(glideRequests, stickerSlide, showControls, false); - missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE); - } - - public void setThumbnailClickListener(@NonNull SlideClickListener listener) { - image.setThumbnailClickListener(listener); - } - - public void setDownloadClickListener(@NonNull SlidesClickedListener listener) { - image.setDownloadClickListener(listener); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java index d512e0924c..4f0072cc24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -24,7 +24,7 @@ public class EmojiTextView extends AppCompatTextView { private static final char ELLIPSIS = '…'; private CharSequence previousText; - private BufferType previousBufferType; + private BufferType previousBufferType = BufferType.NORMAL; private float originalFontSize; private boolean useSystemEmoji; private boolean sizeChangeInProgress; @@ -49,6 +49,15 @@ public class EmojiTextView extends AppCompatTextView { } @Override public void setText(@Nullable CharSequence text, BufferType type) { + // No need to do anything special if the text is null or empty + if (text == null || text.length() == 0) { + previousText = text; + previousOverflowText = overflowText; + previousBufferType = type; + super.setText(text, type); + return; + } + EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text); if (scaleEmojis && candidates != null && candidates.allEmojis) { @@ -149,10 +158,15 @@ public class EmojiTextView extends AppCompatTextView { } private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) { - return Util.equals(previousText, text) && - Util.equals(previousOverflowText, overflowText) && - Util.equals(previousBufferType, bufferType) && - useSystemEmoji == useSystemEmoji() && + CharSequence finalPrevText = (previousText == null || previousText.length() == 0 ? "" : previousText); + CharSequence finalText = (text == null || text.length() == 0 ? "" : text); + CharSequence finalPrevOverflowText = (previousOverflowText == null || previousOverflowText.length() == 0 ? "" : previousOverflowText); + CharSequence finalOverflowText = (overflowText == null || overflowText.length() == 0 ? "" : overflowText); + + return Util.equals(finalPrevText, finalText) && + Util.equals(finalPrevOverflowText, finalOverflowText) && + Util.equals(previousBufferType, bufferType) && + useSystemEmoji == useSystemEmoji() && !sizeChangeInProgress; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt index 24637c4341..0b0ddf4b3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt @@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.recyclerview.widget.LinearLayoutManager -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import network.loki.messenger.databinding.ContactSelectionListFragmentBinding import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log @@ -58,7 +57,6 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks> { @@ -106,7 +95,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks) { 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 invalidateOptionsMenu() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/paging/ConversationPager.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/paging/ConversationPager.kt new file mode 100644 index 0000000000..827c394546 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/paging/ConversationPager.kt @@ -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() { + 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() { + + override fun getRefreshKey(state: PagingState): 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() + + @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): LoadResult { + 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() + 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) } + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 7fe583ddbc..7adf5f7dd3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -3,31 +3,18 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest import android.animation.FloatEvaluator import android.animation.ValueAnimator -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.DialogInterface -import android.content.Intent +import android.content.* import android.content.res.Resources import android.database.Cursor import android.graphics.Rect import android.graphics.Typeface import android.net.Uri -import android.os.AsyncTask -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.os.* import android.provider.MediaStore import android.text.TextUtils import android.util.Pair import android.util.TypedValue -import android.view.ActionMode -import android.view.Menu -import android.view.MenuItem -import android.view.MotionEvent -import android.view.View -import android.view.WindowManager +import android.view.* import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.Toast @@ -53,6 +40,8 @@ import network.loki.messenger.databinding.ViewVisibleMessageBinding import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.MentionsManager import org.session.libsession.messaging.messages.control.DataExtractionNotification @@ -68,12 +57,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.quotes.QuoteModel 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.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.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientModifiedListener @@ -106,25 +91,10 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel -import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog -import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities -import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities -import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.* import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.database.GroupDatabase -import org.thoughtcrime.securesms.database.LokiAPIDatabase -import org.thoughtcrime.securesms.database.LokiMessageDatabase -import org.thoughtcrime.securesms.database.LokiThreadDatabase -import org.thoughtcrime.securesms.database.MmsDatabase -import org.thoughtcrime.securesms.database.MmsSmsDatabase -import org.thoughtcrime.securesms.database.ReactionDatabase -import org.thoughtcrime.securesms.database.RecipientDatabase -import org.thoughtcrime.securesms.database.SessionContactDatabase -import org.thoughtcrime.securesms.database.SmsDatabase -import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.* import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -137,25 +107,12 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivity -import org.thoughtcrime.securesms.mms.AudioSlide -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.mms.* import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment -import org.thoughtcrime.securesms.util.ActivityDispatcher -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -import org.thoughtcrime.securesms.util.DateUtils -import org.thoughtcrime.securesms.util.MediaUtil -import org.thoughtcrime.securesms.util.SaveAttachmentTask -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.toPx -import java.util.Locale +import org.thoughtcrime.securesms.util.* +import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference @@ -295,6 +252,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe onDeselect(message, position, it) } }, + onAttachmentNeedsDownload = { attachmentId, mmsId -> + // Start download (on IO thread) + lifecycleScope.launch(Dispatchers.IO) { + JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) + } + }, glide = glide, lifecycleCoroutineScope = lifecycleScope ) @@ -352,11 +315,24 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe restoreDraftIfNeeded() setUpUiStateObserver() binding!!.scrollToBottomButton.setOnClickListener { - val layoutManager = binding?.conversationRecyclerView?.layoutManager ?: return@setOnClickListener + val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener + if (layoutManager.isSmoothScrolling) { binding?.conversationRecyclerView?.scrollToPosition(0) } else { - binding?.conversationRecyclerView?.smoothScrollToPosition(0) + // It looks like 'smoothScrollToPosition' will actually load all intermediate items in + // order to do the scroll, this can be very slow if there are a lot of messages so + // instead we check the current position and if there are more than 10 items to scroll + // we jump instantly to the 10th item and scroll from there (this should happen quick + // enough to give a similar scroll effect without having to load everything) + val position = layoutManager.findFirstVisibleItemPosition() + if (position > 10) { + binding?.conversationRecyclerView?.scrollToPosition(10) + } + + binding?.conversationRecyclerView?.post { + binding?.conversationRecyclerView?.smoothScrollToPosition(0) + } } } unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId) @@ -388,7 +364,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe super.onResume() ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId) val recipient = viewModel.recipient ?: return - threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient) + + lifecycleScope.launch(Dispatchers.IO) { + threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient) + } + contentResolver.registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, @@ -481,6 +461,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // called from onCreate private fun setUpInputBar() { + binding!!.inputBar.isVisible = viewModel.openGroup == null || viewModel.openGroup?.canWrite == true binding!!.inputBar.delegate = this binding!!.inputBarRecordingView.delegate = this // GIF button @@ -635,7 +616,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe this ) { onOptionsItemSelected(it) } } - super.onPrepareOptionsMenu(menu) return true } @@ -660,7 +640,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe updateSubtitle() showOrHideInputIfNeeded() binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient) - binding!!.toolbarContent.conversationTitleView.text = when { + binding?.toolbarContent?.conversationTitleView?.text = when { threadRecipient.isLocalNumber -> getString(R.string.note_to_self) else -> threadRecipient.toShortString() } @@ -1789,6 +1769,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe endActionMode() } + override fun destroyActionMode() { + this.actionMode = null + } + private fun sendScreenshotNotification() { val recipient = viewModel.recipient ?: return if (recipient.isGroupRecipient) return @@ -1834,7 +1818,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (result == null) return@Observer if (result.getResults().isNotEmpty()) { result.getResults()[result.position]?.let { - jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs) { + jumpToMessage(it.messageRecipient.address, it.sentTimestampMs) { searchViewModel.onMissingResult() } } } @@ -1900,7 +1884,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems) ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems) ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems) - ConversationReactionOverlay.Action.COPY_SESSION_ID -> TODO() + ConversationReactionOverlay.Action.COPY_SESSION_ID -> copySessionID(selectedItems) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 17a47a843f..85d3c8e6de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -39,10 +39,10 @@ class ConversationAdapter( private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit, private val onDeselect: (MessageRecord, Int) -> Unit, + private val onAttachmentNeedsDownload: (Long, Long) -> Unit, private val glide: GlideRequests, lifecycleCoroutineScope: LifecycleCoroutineScope -) - : CursorRecyclerViewAdapter(context, cursor) { +) : CursorRecyclerViewAdapter(context, cursor) { private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() } private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() } var selectedItems = mutableSetOf() @@ -120,7 +120,18 @@ class ConversationAdapter( } val contact = contactCache[senderIdHash] - visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId, visibleMessageViewDelegate) + visibleMessageView.bind( + message, + messageBefore, + getMessageAfter(position, cursor), + glide, + searchQuery, + contact, + senderId, + visibleMessageViewDelegate, + onAttachmentNeedsDownload + ) + if (!message.isDeleted) { visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) } visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 4d78653abc..9a56108969 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope import com.goterl.lazysodium.utils.KeyPair import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -48,11 +50,19 @@ class ConversationViewModel( } fun saveDraft(text: String) { - repository.saveDraft(threadId, text) + GlobalScope.launch(Dispatchers.IO) { + repository.saveDraft(threadId, text) + } } fun getDraft(): String? { - return repository.getDraft(threadId) + val draft: String? = repository.getDraft(threadId) + + viewModelScope.launch(Dispatchers.IO) { + repository.clearDrafts(threadId) + } + + return draft } fun inviteContacts(contacts: List) { @@ -182,7 +192,6 @@ class ConversationViewModel( data class UiMessage(val id: Long, val message: String) data class ConversationUiState( - val isOxenHostedOpenGroup: Boolean = false, val uiMessages: List = emptyList(), val isMessageRequestAccepted: Boolean? = null ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 27f75701d9..104c9851b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R 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.util.DateUtils import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale +import java.util.* import javax.inject.Inject @AndroidEntryPoint @@ -48,7 +48,10 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { // We only show this screen for messages fail to send, // so the author of the messages must be the current user. val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) - messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) + messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run { + finish() + return + } val threadId = messageRecord!!.threadId val openGroup = storage.getOpenGroup(threadId) val blindedKey = openGroup?.let { group -> @@ -71,8 +74,15 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale) binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent)) - val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send." - binding.errorMessage.text = errorMessage + val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) + if (errorMessage != null) { + binding.errorMessage.text = errorMessage + binding.resendContainer.isVisible = true + binding.errorContainer.isVisible = true + } else { + binding.errorContainer.isVisible = false + binding.resendContainer.isVisible = false + } if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) { binding.expiresContainer.visibility = View.GONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index 8f0ddd8bef..330534e232 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -8,53 +8,39 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.ViewGroup import android.widget.FrameLayout +import android.widget.RelativeLayout import android.widget.TextView import androidx.core.view.children import androidx.core.view.isVisible import network.loki.messenger.R import network.loki.messenger.databinding.AlbumThumbnailViewBinding -import org.session.libsession.messaging.jobs.AttachmentDownloadJob -import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.components.CornerMask -import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView +import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.util.ActivityDispatcher -class AlbumThumbnailView : FrameLayout { - - private lateinit var binding: AlbumThumbnailViewBinding - +class AlbumThumbnailView : RelativeLayout { companion object { const val MAX_ALBUM_DISPLAY_SIZE = 3 } + private val binding: AlbumThumbnailViewBinding by lazy { AlbumThumbnailViewBinding.bind(this) } + // region Lifecycle - constructor(context: Context) : super(context) { - initialize() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - initialize() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - initialize() - } + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) private val cornerMask by lazy { CornerMask(this) } private var slides: List = listOf() private var slideSize: Int = 0 - private fun initialize() { - binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true) - } - override fun dispatchDraw(canvas: Canvas?) { super.dispatchDraw(canvas) cornerMask.mask(canvas) @@ -63,26 +49,25 @@ class AlbumThumbnailView : FrameLayout { // region Interaction - fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient) { + fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient, onAttachmentNeedsDownload: (Long, Long) -> Unit) { val rawXInt = event.rawX.toInt() val rawYInt = event.rawY.toInt() val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) val testRect = Rect() // test each album child - binding.albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> + binding.albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed forEach@{ index, child -> child.getGlobalVisibleRect(testRect) if (testRect.contains(eventRect)) { // hit intersects with this particular child - val slide = slides.getOrNull(index) ?: return + val slide = slides.getOrNull(index) ?: return@forEach // only open to downloaded images if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { - // restart download here + // Restart download here (on IO thread) (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - val attachmentId = attachment.attachmentId.rowId - JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mms.getId())) + onAttachmentNeedsDownload(attachment.attachmentId.rowId, mms.getId()) } } - if (slide.isInProgress) return + if (slide.isInProgress) return@forEach ActivityDispatcher.get(context)?.dispatchIntent { context -> MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient) @@ -133,7 +118,7 @@ class AlbumThumbnailView : FrameLayout { else -> R.layout.album_thumbnail_3 // three stacked with additional text } - fun getThumbnailView(position: Int): KThumbnailView = when (position) { + fun getThumbnailView(position: Int): ThumbnailView = when (position) { 0 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_1) 1 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_2) 2 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_3) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt index c1fce3f50b..66164f100f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt @@ -23,7 +23,7 @@ class LinkPreviewDraftView : LinearLayout { // Start out with the loader showing and the content view hidden binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true) binding.linkPreviewDraftContainer.isVisible = false - binding.thumbnailImageView.clipToOutline = true + binding.thumbnailImageView.root.clipToOutline = true binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() } } @@ -31,10 +31,10 @@ class LinkPreviewDraftView : LinearLayout { // Hide the loader and show the content view binding.linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftLoader.isVisible = false - binding.thumbnailImageView.radius = toPx(4, resources) + binding.thumbnailImageView.root.radius = toPx(4, resources) if (linkPreview.getThumbnail().isPresent) { // This internally fetches the thumbnail - binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false) + binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, null) } binding.linkPreviewDraftTitleTextView.text = linkPreview.title } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java deleted file mode 100644 index 826cfe7b3a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.components; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.PorterDuff; -import androidx.annotation.Nullable; -import android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; - -import network.loki.messenger.R; - -public class TypingIndicatorView extends LinearLayout { - private boolean isActive; - private long startTime; - - private static final long CYCLE_DURATION = 1500; - private static final long DOT_DURATION = 600; - private static final float MIN_ALPHA = 0.4f; - private static final float MIN_SCALE = 0.75f; - - private View dot1; - private View dot2; - private View dot3; - - public TypingIndicatorView(Context context) { - super(context); - initialize(null); - } - - public TypingIndicatorView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - initialize(attrs); - } - - private void initialize(@Nullable AttributeSet attrs) { - inflate(getContext(), R.layout.view_typing_indicator, this); - - setWillNotDraw(false); - - dot1 = findViewById(R.id.typing_dot1); - dot2 = findViewById(R.id.typing_dot2); - dot3 = findViewById(R.id.typing_dot3); - - if (attrs != null) { - TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0); - int tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE); - typedArray.recycle(); - - dot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY); - dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY); - dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY); - } - } - - @Override - protected void onDraw(Canvas canvas) { - if (!isActive) { - super.onDraw(canvas); - return; - } - - long timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION; - - render(dot1, timeInCycle, 0); - render(dot2, timeInCycle, 150); - render(dot3, timeInCycle, 300); - - super.onDraw(canvas); - postInvalidate(); - } - - private void render(View dot, long timeInCycle, long start) { - long end = start + DOT_DURATION; - long peak = start + (DOT_DURATION / 2); - - if (timeInCycle < start || timeInCycle > end) { - renderDefault(dot); - } else if (timeInCycle < peak) { - renderFadeIn(dot, timeInCycle, start); - } else { - renderFadeOut(dot, timeInCycle, peak); - } - } - - private void renderDefault(View dot) { - dot.setAlpha(MIN_ALPHA); - dot.setScaleX(MIN_SCALE); - dot.setScaleY(MIN_SCALE); - } - - private void renderFadeIn(View dot, long timeInCycle, long fadeInStart) { - float percent = (float) (timeInCycle - fadeInStart) / 300; - dot.setAlpha(MIN_ALPHA + (1 - MIN_ALPHA) * percent); - dot.setScaleX(MIN_SCALE + (1 - MIN_SCALE) * percent); - dot.setScaleY(MIN_SCALE + (1 - MIN_SCALE) * percent); - } - - private void renderFadeOut(View dot, long timeInCycle, long fadeOutStart) { - float percent = (float) (timeInCycle - fadeOutStart) / 300; - dot.setAlpha(1 - (1 - MIN_ALPHA) * percent); - dot.setScaleX(1 - (1 - MIN_SCALE) * percent); - dot.setScaleY(1 - (1 - MIN_SCALE) * percent); - } - - public void startAnimation() { - isActive = true; - startTime = System.currentTimeMillis(); - - postInvalidate(); - } - - public void stopAnimation() { - isActive = false; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.kt new file mode 100644 index 0000000000..d1310bffba --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorView.kt @@ -0,0 +1,105 @@ +package org.thoughtcrime.securesms.conversation.v2.components + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.PorterDuff +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import network.loki.messenger.R +import network.loki.messenger.databinding.ViewTypingIndicatorBinding + +class TypingIndicatorView : LinearLayout { + companion object { + private const val CYCLE_DURATION: Long = 1500 + private const val DOT_DURATION: Long = 600 + private const val MIN_ALPHA = 0.4f + private const val MIN_SCALE = 0.75f + } + + private val binding: ViewTypingIndicatorBinding by lazy { + val binding = ViewTypingIndicatorBinding.bind(this) + + if (tint != -1) { + binding.typingDot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY) + binding.typingDot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY) + binding.typingDot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY) + } + + return@lazy binding + } + + private var isActive = false + private var startTime: Long = 0 + private var tint: Int = -1 + + constructor(context: Context) : super(context) { initialize(null) } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) } + + private fun initialize(attrs: AttributeSet?) { + setWillNotDraw(false) + + if (attrs != null) { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0) + this.tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE) + typedArray.recycle() + } + } + + override fun onDraw(canvas: Canvas) { + if (!isActive) { + super.onDraw(canvas) + return + } + val timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION + render(binding.typingDot1, timeInCycle, 0) + render(binding.typingDot2, timeInCycle, 150) + render(binding.typingDot3, timeInCycle, 300) + super.onDraw(canvas) + postInvalidate() + } + + private fun render(dot: View?, timeInCycle: Long, start: Long) { + val end = start + DOT_DURATION + val peak = start + DOT_DURATION / 2 + if (timeInCycle < start || timeInCycle > end) { + renderDefault(dot) + } else if (timeInCycle < peak) { + renderFadeIn(dot, timeInCycle, start) + } else { + renderFadeOut(dot, timeInCycle, peak) + } + } + + private fun renderDefault(dot: View?) { + dot!!.alpha = MIN_ALPHA + dot.scaleX = MIN_SCALE + dot.scaleY = MIN_SCALE + } + + private fun renderFadeIn(dot: View?, timeInCycle: Long, fadeInStart: Long) { + val percent = (timeInCycle - fadeInStart).toFloat() / 300 + dot!!.alpha = MIN_ALPHA + (1 - MIN_ALPHA) * percent + dot.scaleX = MIN_SCALE + (1 - MIN_SCALE) * percent + dot.scaleY = MIN_SCALE + (1 - MIN_SCALE) * percent + } + + private fun renderFadeOut(dot: View?, timeInCycle: Long, fadeOutStart: Long) { + val percent = (timeInCycle - fadeOutStart).toFloat() / 300 + dot!!.alpha = 1 - (1 - MIN_ALPHA) * percent + dot.scaleX = 1 - (1 - MIN_SCALE) * percent + dot.scaleY = 1 - (1 - MIN_SCALE) * percent + } + + fun startAnimation() { + isActive = true + startTime = System.currentTimeMillis() + postInvalidate() + } + + fun stopAnimation() { + isActive = false + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt index 768d49146e..3077d227e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt @@ -19,7 +19,7 @@ class TypingIndicatorViewContainer : LinearLayout { } fun setTypists(typists: List) { - if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return } - binding.typingIndicator.startAnimation() + if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return } + binding.typingIndicator.root.startAnimation() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt index cab24ce8be..d475a64448 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt @@ -65,9 +65,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText // Copy Session ID 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 - 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 menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed) // Save media @@ -101,6 +101,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p override fun onDestroyActionMode(mode: ActionMode) { adapter.selectedItems.clear() adapter.notifyDataSetChanged() + delegate?.destroyActionMode() } } @@ -116,4 +117,5 @@ interface ConversationActionModeCallbackDelegate { fun showMessageDetail(messages: Set) fun saveAttachment(messages: Set) fun reply(messages: Set) + fun destroyActionMode() } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java deleted file mode 100644 index 6d16f1f421..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java +++ /dev/null @@ -1,346 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.messages; - -import android.content.Context; -import android.content.res.TypedArray; -import android.os.Handler; -import android.os.Looper; -import android.util.AttributeSet; -import android.view.HapticFeedbackConstants; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.Group; -import androidx.core.content.ContextCompat; - -import com.google.android.flexbox.FlexboxLayout; -import com.google.android.flexbox.JustifyContent; - -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.ThemeUtil; -import org.thoughtcrime.securesms.components.emoji.EmojiImageView; -import org.thoughtcrime.securesms.components.emoji.EmojiUtil; -import org.thoughtcrime.securesms.conversation.v2.ViewUtil; -import org.thoughtcrime.securesms.database.model.MessageId; -import org.thoughtcrime.securesms.database.model.ReactionRecord; -import org.thoughtcrime.securesms.util.NumberUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import network.loki.messenger.R; - -public class EmojiReactionsView extends LinearLayout implements View.OnTouchListener { - - // Normally 6dp, but we have 1dp left+right margin on the pills themselves - private final int OUTER_MARGIN = ViewUtil.dpToPx(2); - private static final int DEFAULT_THRESHOLD = 5; - - private List records; - private long messageId; - private ViewGroup container; - private Group showLess; - private VisibleMessageViewDelegate delegate; - private Handler gestureHandler = new Handler(Looper.getMainLooper()); - private Runnable pressCallback; - private Runnable longPressCallback; - private long onDownTimestamp = 0; - private static long longPressDurationThreshold = 250; - private static long maxDoubleTapInterval = 200; - private boolean extended = false; - - public EmojiReactionsView(Context context) { - super(context); - init(null); - } - - public EmojiReactionsView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(attrs); - } - - private void init(@Nullable AttributeSet attrs) { - inflate(getContext(), R.layout.view_emoji_reactions, this); - - this.container = findViewById(R.id.layout_emoji_container); - this.showLess = findViewById(R.id.group_show_less); - - records = new ArrayList<>(); - - if (attrs != null) { - TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0); - typedArray.recycle(); - } - } - - public void clear() { - this.records.clear(); - container.removeAllViews(); - } - - public void setReactions(long messageId, @NonNull List records, boolean outgoing, VisibleMessageViewDelegate delegate) { - this.delegate = delegate; - if (records.equals(this.records)) { - return; - } - - FlexboxLayout containerLayout = (FlexboxLayout) this.container; - containerLayout.setJustifyContent(outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START); - this.records.clear(); - this.records.addAll(records); - if (this.messageId != messageId) { - extended = false; - } - this.messageId = messageId; - - displayReactions(extended ? Integer.MAX_VALUE : DEFAULT_THRESHOLD); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (v.getTag() == null) return false; - - Reaction reaction = (Reaction) v.getTag(); - int action = event.getAction(); - if (action == MotionEvent.ACTION_DOWN) onDown(new MessageId(reaction.messageId, reaction.isMms)); - else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback(); - else if (action == MotionEvent.ACTION_UP) onUp(reaction); - return true; - } - - private void displayReactions(int threshold) { - String userPublicKey = TextSecurePreferences.getLocalNumber(getContext()); - List reactions = buildSortedReactionsList(records, userPublicKey, threshold); - - container.removeAllViews(); - LinearLayout overflowContainer = new LinearLayout(getContext()); - overflowContainer.setOrientation(LinearLayout.HORIZONTAL); - int innerPadding = ViewUtil.dpToPx(4); - overflowContainer.setPaddingRelative(innerPadding,innerPadding,innerPadding,innerPadding); - - int pixelSize = ViewUtil.dpToPx(1); - - for (Reaction reaction : reactions) { - if (container.getChildCount() + 1 >= DEFAULT_THRESHOLD && threshold != Integer.MAX_VALUE && reactions.size() > threshold) { - if (overflowContainer.getParent() == null) { - container.addView(overflowContainer); - MarginLayoutParams overflowParams = (MarginLayoutParams) overflowContainer.getLayoutParams(); - overflowParams.height = ViewUtil.dpToPx(26); - overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize); - overflowContainer.setLayoutParams(overflowParams); - overflowContainer.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.reaction_pill_background)); - } - View pill = buildPill(getContext(), this, reaction, true); - pill.setOnClickListener(v -> { - extended = true; - displayReactions(Integer.MAX_VALUE); - }); - pill.findViewById(R.id.reactions_pill_count).setVisibility(View.GONE); - pill.findViewById(R.id.reactions_pill_spacer).setVisibility(View.GONE); - overflowContainer.addView(pill); - } else { - View pill = buildPill(getContext(), this, reaction, false); - pill.setTag(reaction); - pill.setOnTouchListener(this); - MarginLayoutParams params = (MarginLayoutParams) pill.getLayoutParams(); - params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize); - pill.setLayoutParams(params); - container.addView(pill); - } - } - - int overflowChildren = overflowContainer.getChildCount(); - int negativeMargin = ViewUtil.dpToPx(-8); - for (int i = 0; i < overflowChildren; i++) { - View child = overflowContainer.getChildAt(i); - MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams(); - if ((i == 0 && overflowChildren > 1) || i + 1 < overflowChildren) { - // if first and there is more than one child, or we are not the last child then set negative right margin - childParams.setMargins(0,0, negativeMargin, 0); - child.setLayoutParams(childParams); - } - } - - if (threshold == Integer.MAX_VALUE) { - showLess.setVisibility(VISIBLE); - for (int id : showLess.getReferencedIds()) { - findViewById(id).setOnClickListener(view -> { - extended = false; - displayReactions(DEFAULT_THRESHOLD); - }); - } - } else { - showLess.setVisibility(GONE); - } - } - - private void onReactionClicked(Reaction reaction) { - if (reaction.messageId != 0) { - MessageId messageId = new MessageId(reaction.messageId, reaction.isMms); - delegate.onReactionClicked(reaction.emoji, messageId, reaction.userWasSender); - } - } - - private static @NonNull List buildSortedReactionsList(@NonNull List records, String userPublicKey, int threshold) { - Map counters = new LinkedHashMap<>(); - - for (ReactionRecord record : records) { - String baseEmoji = EmojiUtil.getCanonicalRepresentation(record.getEmoji()); - Reaction info = counters.get(baseEmoji); - - if (info == null) { - info = new Reaction(record.getMessageId(), record.isMms(), record.getEmoji(), record.getCount(), record.getSortId(), record.getDateReceived(), userPublicKey.equals(record.getAuthor())); - } else { - info.update(record.getEmoji(), record.getCount(), record.getDateReceived(), userPublicKey.equals(record.getAuthor())); - } - - counters.put(baseEmoji, info); - } - - List reactions = new ArrayList<>(counters.values()); - - Collections.sort(reactions, Collections.reverseOrder()); - - if (reactions.size() >= threshold + 2 && threshold != Integer.MAX_VALUE) { - List shortened = new ArrayList<>(threshold + 2); - shortened.addAll(reactions.subList(0, threshold + 2)); - return shortened; - } else { - return reactions; - } - } - - private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction, boolean isCompact) { - View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false); - EmojiImageView emojiView = root.findViewById(R.id.reactions_pill_emoji); - TextView countView = root.findViewById(R.id.reactions_pill_count); - View spacer = root.findViewById(R.id.reactions_pill_spacer); - - if (isCompact) { - root.setPaddingRelative(1,1,1,1); - ViewGroup.LayoutParams layoutParams = root.getLayoutParams(); - layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; - root.setLayoutParams(layoutParams); - } - - if (reaction.emoji != null) { - emojiView.setImageEmoji(reaction.emoji); - - if (reaction.count >= 1) { - countView.setText(NumberUtil.getFormattedNumber(reaction.count)); - } else { - countView.setVisibility(GONE); - spacer.setVisibility(GONE); - } - } else { - emojiView.setVisibility(GONE); - spacer.setVisibility(GONE); - countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.count)); - } - - if (reaction.userWasSender && !isCompact) { - root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); - countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor)); - } else { - if (!isCompact) { - root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)); - } - } - - return root; - } - - private void onDown(MessageId messageId) { - removeLongPressCallback(); - Runnable newLongPressCallback = () -> { - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - if (delegate != null) { - delegate.onReactionLongClicked(messageId); - } - }; - this.longPressCallback = newLongPressCallback; - gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold); - onDownTimestamp = new Date().getTime(); - } - - private void removeLongPressCallback() { - if (longPressCallback != null) { - gestureHandler.removeCallbacks(longPressCallback); - } - } - - private void onUp(Reaction reaction) { - if ((new Date().getTime() - onDownTimestamp) < longPressDurationThreshold) { - removeLongPressCallback(); - if (pressCallback != null) { - gestureHandler.removeCallbacks(pressCallback); - this.pressCallback = null; - } else { - Runnable newPressCallback = () -> { - onReactionClicked(reaction); - pressCallback = null; - }; - this.pressCallback = newPressCallback; - gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval); - } - } - } - - private static class Reaction implements Comparable { - private final long messageId; - private final boolean isMms; - private String emoji; - private long count; - private long sortIndex; - private long lastSeen; - private boolean userWasSender; - - Reaction(long messageId, boolean isMms, @Nullable String emoji, long count, long sortIndex, long lastSeen, boolean userWasSender) { - this.messageId = messageId; - this.isMms = isMms; - this.emoji = emoji; - this.count = count; - this.sortIndex = sortIndex; - this.lastSeen = lastSeen; - this.userWasSender = userWasSender; - } - - void update(@NonNull String emoji, long count, long lastSeen, boolean userWasSender) { - if (!this.userWasSender) { - if (userWasSender || lastSeen > this.lastSeen) { - this.emoji = emoji; - } - } - - this.count = this.count + count; - this.lastSeen = Math.max(this.lastSeen, lastSeen); - this.userWasSender = this.userWasSender || userWasSender; - } - - @NonNull Reaction merge(@NonNull Reaction other) { - this.count = this.count + other.count; - this.lastSeen = Math.max(this.lastSeen, other.lastSeen); - this.userWasSender = this.userWasSender || other.userWasSender; - return this; - } - - @Override - public int compareTo(Reaction rhs) { - Reaction lhs = this; - if (lhs.count == rhs.count ) { - return Long.compare(lhs.sortIndex, rhs.sortIndex); - } else { - return Long.compare(lhs.count, rhs.count); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.kt new file mode 100644 index 0000000000..49e4b1044f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.kt @@ -0,0 +1,291 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.* +import android.view.View.OnTouchListener +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import com.google.android.flexbox.JustifyContent +import network.loki.messenger.R +import network.loki.messenger.databinding.ViewEmojiReactionsBinding +import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber +import org.session.libsession.utilities.ThemeUtil +import org.thoughtcrime.securesms.components.emoji.EmojiImageView +import org.thoughtcrime.securesms.components.emoji.EmojiUtil +import org.thoughtcrime.securesms.conversation.v2.ViewUtil +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.util.NumberUtil.getFormattedNumber +import java.util.* + +class EmojiReactionsView : ConstraintLayout, OnTouchListener { + companion object { + private const val DEFAULT_THRESHOLD = 5 + private const val longPressDurationThreshold: Long = 250 + private const val maxDoubleTapInterval: Long = 200 + } + + private val binding: ViewEmojiReactionsBinding by lazy { ViewEmojiReactionsBinding.bind(this) } + + // Normally 6dp, but we have 1dp left+right margin on the pills themselves + private val OUTER_MARGIN = ViewUtil.dpToPx(2) + private var records: MutableList? = null + private var messageId: Long = 0 + private var delegate: VisibleMessageViewDelegate? = null + private val gestureHandler = Handler(Looper.getMainLooper()) + private var pressCallback: Runnable? = null + private var longPressCallback: Runnable? = null + private var onDownTimestamp: Long = 0 + private var extended = false + + constructor(context: Context) : super(context) { init(null) } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs) } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(attrs) } + + private fun init(attrs: AttributeSet?) { + records = ArrayList() + + if (attrs != null) { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0) + typedArray.recycle() + } + } + + fun clear() { + records!!.clear() + binding.layoutEmojiContainer.removeAllViews() + } + + fun setReactions(messageId: Long, records: List, outgoing: Boolean, delegate: VisibleMessageViewDelegate?) { + this.delegate = delegate + if (records == this.records) { + return + } + + binding.layoutEmojiContainer.justifyContent = if (outgoing) JustifyContent.FLEX_END else JustifyContent.FLEX_START + this.records!!.clear() + this.records!!.addAll(records) + if (this.messageId != messageId) { + extended = false + } + this.messageId = messageId + displayReactions(if (extended) Int.MAX_VALUE else DEFAULT_THRESHOLD) + } + + override fun onTouch(v: View, event: MotionEvent): Boolean { + if (v.tag == null) return false + val reaction = v.tag as Reaction + val action = event.action + if (action == MotionEvent.ACTION_DOWN) onDown(MessageId(reaction.messageId, reaction.isMms)) else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback() else if (action == MotionEvent.ACTION_UP) onUp(reaction) + return true + } + + private fun displayReactions(threshold: Int) { + val userPublicKey = getLocalNumber(context) + val reactions = buildSortedReactionsList(records!!, userPublicKey, threshold) + binding.layoutEmojiContainer.removeAllViews() + val overflowContainer = LinearLayout(context) + overflowContainer.orientation = LinearLayout.HORIZONTAL + val innerPadding = ViewUtil.dpToPx(4) + overflowContainer.setPaddingRelative(innerPadding, innerPadding, innerPadding, innerPadding) + val pixelSize = ViewUtil.dpToPx(1) + for (reaction in reactions) { + if (binding.layoutEmojiContainer.childCount + 1 >= DEFAULT_THRESHOLD && threshold != Int.MAX_VALUE && reactions.size > threshold) { + if (overflowContainer.parent == null) { + binding.layoutEmojiContainer.addView(overflowContainer) + val overflowParams = overflowContainer.layoutParams as MarginLayoutParams + overflowParams.height = ViewUtil.dpToPx(26) + overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize) + overflowContainer.layoutParams = overflowParams + overflowContainer.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background) + } + val pill = buildPill(context, this, reaction, true) + pill.setOnClickListener { v: View? -> + extended = true + displayReactions(Int.MAX_VALUE) + } + pill.findViewById(R.id.reactions_pill_count).visibility = GONE + pill.findViewById(R.id.reactions_pill_spacer).visibility = GONE + overflowContainer.addView(pill) + } else { + val pill = buildPill(context, this, reaction, false) + pill.tag = reaction + pill.setOnTouchListener(this) + val params = pill.layoutParams as MarginLayoutParams + params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize) + pill.layoutParams = params + binding.layoutEmojiContainer.addView(pill) + } + } + val overflowChildren = overflowContainer.childCount + val negativeMargin = ViewUtil.dpToPx(-8) + for (i in 0 until overflowChildren) { + val child = overflowContainer.getChildAt(i) + val childParams = child.layoutParams as MarginLayoutParams + if (i == 0 && overflowChildren > 1 || i + 1 < overflowChildren) { + // if first and there is more than one child, or we are not the last child then set negative right margin + childParams.setMargins(0, 0, negativeMargin, 0) + child.layoutParams = childParams + } + } + if (threshold == Int.MAX_VALUE) { + binding.groupShowLess.visibility = VISIBLE + for (id in binding.groupShowLess.referencedIds) { + findViewById(id).setOnClickListener { view: View? -> + extended = false + displayReactions(DEFAULT_THRESHOLD) + } + } + } else { + binding.groupShowLess.visibility = GONE + } + } + + private fun buildSortedReactionsList(records: List, userPublicKey: String?, threshold: Int): List { + val counters: MutableMap = LinkedHashMap() + + records.forEach { + val baseEmoji = EmojiUtil.getCanonicalRepresentation(it.emoji) + val info = counters[baseEmoji] + + if (info == null) { + counters[baseEmoji] = Reaction(messageId, it.isMms, it.emoji, it.count, it.sortId, it.dateReceived, userPublicKey == it.author) + } + else { + info.update(it.emoji, it.count, it.dateReceived, userPublicKey == it.author) + } + } + + val reactions: List = ArrayList(counters.values) + Collections.sort(reactions, Collections.reverseOrder()) + + return if (reactions.size >= threshold + 2 && threshold != Int.MAX_VALUE) { + val shortened: MutableList = ArrayList(threshold + 2) + shortened.addAll(reactions.subList(0, threshold + 2)) + shortened + } else { + reactions + } + } + + private fun buildPill(context: Context, parent: ViewGroup, reaction: Reaction, isCompact: Boolean): View { + val root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false) + val emojiView = root.findViewById(R.id.reactions_pill_emoji) + val countView = root.findViewById(R.id.reactions_pill_count) + val spacer = root.findViewById(R.id.reactions_pill_spacer) + if (isCompact) { + root.setPaddingRelative(1, 1, 1, 1) + val layoutParams = root.layoutParams + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + root.layoutParams = layoutParams + } + if (reaction.emoji != null) { + emojiView.setImageEmoji(reaction.emoji) + if (reaction.count >= 1) { + countView.text = getFormattedNumber(reaction.count) + } else { + countView.visibility = GONE + spacer.visibility = GONE + } + } else { + emojiView.visibility = GONE + spacer.visibility = GONE + countView.text = context.getString(R.string.ReactionsConversationView_plus, reaction.count) + } + if (reaction.userWasSender && !isCompact) { + root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected) + countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor)) + } else { + if (!isCompact) { + root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background) + } + } + return root + } + + private fun onReactionClicked(reaction: Reaction) { + if (reaction.messageId != 0L) { + val messageId = MessageId(reaction.messageId, reaction.isMms) + delegate!!.onReactionClicked(reaction.emoji!!, messageId, reaction.userWasSender) + } + } + + private fun onDown(messageId: MessageId) { + removeLongPressCallback() + val newLongPressCallback = Runnable { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + if (delegate != null) { + delegate!!.onReactionLongClicked(messageId) + } + } + longPressCallback = newLongPressCallback + gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold) + onDownTimestamp = Date().time + } + + private fun removeLongPressCallback() { + if (longPressCallback != null) { + gestureHandler.removeCallbacks(longPressCallback!!) + } + } + + private fun onUp(reaction: Reaction) { + if (Date().time - onDownTimestamp < longPressDurationThreshold) { + removeLongPressCallback() + if (pressCallback != null) { + gestureHandler.removeCallbacks(pressCallback!!) + pressCallback = null + } else { + val newPressCallback = Runnable { + onReactionClicked(reaction) + pressCallback = null + } + pressCallback = newPressCallback + gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval) + } + } + } + + internal class Reaction( + internal val messageId: Long, + internal val isMms: Boolean, + internal var emoji: String?, + internal var count: Long, + internal val sortIndex: Long, + internal var lastSeen: Long, + internal var userWasSender: Boolean + ) : Comparable { + fun update(emoji: String, count: Long, lastSeen: Long, userWasSender: Boolean) { + if (!this.userWasSender) { + if (userWasSender || lastSeen > this.lastSeen) { + this.emoji = emoji + } + } + this.count = this.count + count + this.lastSeen = Math.max(this.lastSeen, lastSeen) + this.userWasSender = this.userWasSender || userWasSender + } + + fun merge(other: Reaction): Reaction { + count = count + other.count + lastSeen = Math.max(lastSeen, other.lastSeen) + userWasSender = userWasSender || other.userWasSender + return this + } + + override fun compareTo(other: Reaction?): Int { + if (other == null) { return -1 } + + if (this.count == other.count) { + return this.sortIndex.compareTo(other.sortIndex) + } + + return this.count.compareTo(other.count) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index cb6bb536ff..45d353cc34 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -4,11 +4,9 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Rect import android.util.AttributeSet -import android.view.LayoutInflater import android.view.MotionEvent import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible import network.loki.messenger.R import network.loki.messenger.databinding.ViewLinkPreviewBinding @@ -19,21 +17,16 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtiliti import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.ImageSlide -import org.thoughtcrime.securesms.util.UiModeUtilities class LinkPreviewView : LinearLayout { - private lateinit var binding: ViewLinkPreviewBinding + private val binding: ViewLinkPreviewBinding by lazy { ViewLinkPreviewBinding.bind(this) } private val cornerMask by lazy { CornerMask(this) } private var url: String? = null // region Lifecycle - constructor(context: Context) : super(context) { initialize() } - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } - - private fun initialize() { - binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true) - } + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) // endregion // region Updating @@ -48,8 +41,8 @@ class LinkPreviewView : LinearLayout { // Thumbnail if (linkPreview.getThumbnail().isPresent) { // This internally fetches the thumbnail - binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) - binding.thumbnailImageView.loadIndicator.isVisible = false + binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) + binding.thumbnailImageView.root.loadIndicator.isVisible = false } // Title binding.titleTextView.text = linkPreview.title @@ -80,7 +73,7 @@ class LinkPreviewView : LinearLayout { val rawYInt = event.rawY.toInt() val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) val previewRect = Rect() - binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect) + binding.mainLinkPreviewContainer.getGlobalVisibleRect(previewRect) if (previewRect.contains(hitRect)) { openURL() return diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index 91ab4c106d..4e91400430 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -93,7 +93,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? val backgroundColor = context.getAccentColor() binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor) binding.quoteViewAttachmentPreviewImageView.isVisible = false - binding.quoteViewAttachmentThumbnailImageView.isVisible = false + binding.quoteViewAttachmentThumbnailImageView.root.isVisible = false when { attachments.audioSlide != null -> { binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) @@ -108,9 +108,9 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? attachments.thumbnailSlide != null -> { val slide = attachments.thumbnailSlide!! // This internally fetches the thumbnail - binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources) - binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false) - binding.quoteViewAttachmentThumbnailImageView.isVisible = true + binding.quoteViewAttachmentThumbnailImageView.root.radius = toPx(4, resources) + binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false, null) + binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 88045f8e47..d53ab45687 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -27,8 +27,6 @@ import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.jobs.AttachmentDownloadJob -import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.getColorFromAttr @@ -44,29 +42,33 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.getAccentColor -import java.util.Locale +import java.util.* import kotlin.math.roundToInt -class VisibleMessageContentView : LinearLayout { - private lateinit var binding: ViewVisibleMessageContentBinding +class VisibleMessageContentView : ConstraintLayout { + private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) } var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() var onContentDoubleTap: (() -> Unit)? = null var delegate: VisibleMessageViewDelegate? = null var indexInAdapter: Int = -1 // region Lifecycle - constructor(context: Context) : super(context) { initialize() } - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } - - private fun initialize() { - binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true) - } + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) // endregion // region Updating - fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, - glide: GlideRequests, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) { + fun bind( + message: MessageRecord, + isStartOfMessageCluster: Boolean, + isEndOfMessageCluster: Boolean, + glide: GlideRequests, + thread: Recipient, + searchQuery: String?, + contactIsTrusted: Boolean, + onAttachmentNeedsDownload: (Long, Long) -> Unit + ) { // Background val background = getBackground(message.isOutgoing) val color = if (message.isOutgoing) context.getAccentColor() @@ -80,28 +82,31 @@ class VisibleMessageContentView : LinearLayout { // reset visibilities / containers onContentClick.clear() - binding.albumThumbnailView.clearViews() + binding.albumThumbnailView.root.clearViews() onContentDoubleTap = null if (message.isDeleted) { binding.deletedMessageView.root.isVisible = true binding.deletedMessageView.root.bind(message, getTextColor(context, message)) + binding.bodyTextView.isVisible = false + binding.quoteView.root.isVisible = false + binding.linkPreviewView.root.isVisible = false + binding.untrustedView.root.isVisible = false + binding.voiceMessageView.root.isVisible = false + binding.documentView.root.isVisible = false + binding.albumThumbnailView.root.isVisible = false + binding.openGroupInvitationView.root.isVisible = false return } else { binding.deletedMessageView.root.isVisible = false } - // clear the - binding.bodyTextView.text = null - binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null - - binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty() - + binding.linkPreviewView.root.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty() binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty() binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null - binding.albumThumbnailView.isVisible = mediaThumbnailMessage + binding.albumThumbnailView.root.isVisible = mediaThumbnailMessage binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation var hideBody = false @@ -133,8 +138,7 @@ class VisibleMessageContentView : LinearLayout { val attachmentId = dbAttachment.attachmentId.rowId if (attach.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) { - // start download - JobQueue.shared.add(AttachmentDownloadJob(attachmentId, dbAttachment.mmsId)) + onAttachmentNeedsDownload(attachmentId, dbAttachment.mmsId) } } message.linkPreviews.forEach { preview -> @@ -142,15 +146,15 @@ class VisibleMessageContentView : LinearLayout { val attachmentId = previewThumbnail.attachmentId.rowId if (previewThumbnail.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) { - JobQueue.shared.add(AttachmentDownloadJob(attachmentId, previewThumbnail.mmsId)) + onAttachmentNeedsDownload(attachmentId, previewThumbnail.mmsId) } } } when { message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> { - binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) - onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) } + binding.linkPreviewView.root.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) + onContentClick.add { event -> binding.linkPreviewView.root.calculateHit(event) } // Body text view is inside the link preview for layout convenience } message is MmsMessageRecord && message.slideDeck.audioSlide != null -> { @@ -187,21 +191,21 @@ class VisibleMessageContentView : LinearLayout { if (contactIsTrusted || message.isOutgoing) { // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups // bind after add view because views are inflated and calculated during bind - binding.albumThumbnailView.bind( + binding.albumThumbnailView.root.bind( glideRequests = glide, message = message, isStart = isStartOfMessageCluster, isEnd = isEndOfMessageCluster ) - val layoutParams = binding.albumThumbnailView.layoutParams as ConstraintLayout.LayoutParams + val layoutParams = binding.albumThumbnailView.root.layoutParams as ConstraintLayout.LayoutParams layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f - binding.albumThumbnailView.layoutParams = layoutParams + binding.albumThumbnailView.root.layoutParams = layoutParams onContentClick.add { event -> - binding.albumThumbnailView.calculateHitObject(event, message, thread) + binding.albumThumbnailView.root.calculateHitObject(event, message, thread, onAttachmentNeedsDownload) } } else { hideBody = true - binding.albumThumbnailView.clearViews() + binding.albumThumbnailView.root.clearViews() binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } } @@ -233,7 +237,7 @@ class VisibleMessageContentView : LinearLayout { } private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean = - listOf(albumThumbnailView, linkPreviewView, voiceMessageView.root, quoteView.root).none { it.isVisible } + listOf(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible } private fun getBackground(isOutgoing: Boolean): Drawable { val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone @@ -248,8 +252,8 @@ class VisibleMessageContentView : LinearLayout { binding.openGroupInvitationView.root, binding.documentView.root, binding.quoteView.root, - binding.linkPreviewView, - binding.albumThumbnailView, + binding.linkPreviewView.root, + binding.albumThumbnailView.root, binding.bodyTextView ).forEach { view: View -> view.isVisible = false } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 0b4c1455fc..5805164623 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -85,7 +85,7 @@ class VisibleMessageView : LinearLayout { var onPress: ((event: MotionEvent) -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null - val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView } + val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView.root } companion object { const val swipeToReplyThreshold = 64.0f // dp @@ -108,7 +108,7 @@ class VisibleMessageView : LinearLayout { isHapticFeedbackEnabled = true setWillNotDraw(false) binding.messageInnerContainer.disableClipping() - binding.messageContentView.disableClipping() + binding.messageContentView.root.disableClipping() } // endregion @@ -122,6 +122,7 @@ class VisibleMessageView : LinearLayout { contact: Contact?, senderSessionID: String, delegate: VisibleMessageViewDelegate?, + onAttachmentNeedsDownload: (Long, Long) -> Unit ) { val threadID = message.threadId val thread = threadDb.getRecipientForThreadId(threadID) ?: return @@ -157,7 +158,8 @@ class VisibleMessageView : LinearLayout { binding.profilePictureView.root.update(message.individualRecipient) binding.profilePictureView.root.setOnClickListener { if (thread.isOpenGroupRecipient) { - if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) { + val openGroup = lokiThreadDb.getOpenGroupChat(threadID) + if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) { val intent = Intent(context, ConversationActivityV2::class.java) intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID) intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID)) @@ -190,50 +192,74 @@ class VisibleMessageView : LinearLayout { binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null binding.dateBreakTextView.isVisible = showDateBreak // Message status indicator - val (iconID, iconColor) = getMessageStatusImage(message) - if (iconID != null) { - val drawable = ContextCompat.getDrawable(context, iconID)?.mutate() - if (iconColor != null) { - drawable?.setTint(iconColor) - } - binding.messageStatusImageView.setImageDrawable(drawable) - } if (message.isOutgoing) { + val (iconID, iconColor, textId) = getMessageStatusImage(message) + if (textId != null) { + binding.messageStatusTextView.setText(textId) + + if (iconColor != null) { + binding.messageStatusTextView.setTextColor(iconColor) + } + } + if (iconID != null) { + val drawable = ContextCompat.getDrawable(context, iconID)?.mutate() + if (iconColor != null) { + drawable?.setTint(iconColor) + } + binding.messageStatusImageView.setImageDrawable(drawable) + } + val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId) - binding.messageStatusImageView.isVisible = - !message.isSent || message.id == lastMessageID + binding.messageStatusTextView.isVisible = ( + textId != null && ( + !message.isSent || + message.id == lastMessageID + ) + ) + binding.messageStatusImageView.isVisible = ( + iconID != null && ( + !message.isSent || + message.id == lastMessageID + ) + ) } else { + binding.messageStatusTextView.isVisible = false binding.messageStatusImageView.isVisible = false } // Expiration timer updateExpirationTimer(message) // Emoji Reactions - val emojiLayoutParams = binding.emojiReactionsView.layoutParams as ConstraintLayout.LayoutParams + val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f - binding.emojiReactionsView.layoutParams = emojiLayoutParams - val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) } - if (message.reactions.isNotEmpty() && - (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) - ) { - binding.emojiReactionsView.setReactions(message.id, message.reactions, message.isOutgoing, delegate) - binding.emojiReactionsView.isVisible = true - } else { - binding.emojiReactionsView.isVisible = false + binding.emojiReactionsView.root.layoutParams = emojiLayoutParams + + if (message.reactions.isNotEmpty()) { + val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) } + if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) { + binding.emojiReactionsView.root.setReactions(message.id, message.reactions, message.isOutgoing, delegate) + binding.emojiReactionsView.root.isVisible = true + } else { + binding.emojiReactionsView.root.isVisible = false + } + } + else { + binding.emojiReactionsView.root.isVisible = false } // Populate content view - binding.messageContentView.indexInAdapter = indexInAdapter - binding.messageContentView.bind( + binding.messageContentView.root.indexInAdapter = indexInAdapter + binding.messageContentView.root.bind( message, isStartOfMessageCluster, isEndOfMessageCluster, glide, thread, searchQuery, - message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false) + message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false), + onAttachmentNeedsDownload ) - binding.messageContentView.delegate = delegate - onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() } + binding.messageContentView.root.delegate = delegate + onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() } } private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean { @@ -256,19 +282,23 @@ class VisibleMessageView : LinearLayout { } } - private fun getMessageStatusImage(message: MessageRecord): Pair { + private fun getMessageStatusImage(message: MessageRecord): Triple { return when { - !message.isOutgoing -> null to null - message.isFailed -> R.drawable.ic_error to resources.getColor(R.color.destructive, context.theme) - message.isPending -> R.drawable.ic_circle_dot_dot_dot to null - message.isRead -> R.drawable.ic_filled_circle_check to null - else -> R.drawable.ic_circle_check to null + !message.isOutgoing -> Triple(null, null, null) + message.isFailed -> + Triple(R.drawable.ic_delivery_status_failed, resources.getColor(R.color.destructive, context.theme), R.string.delivery_status_failed) + message.isPending -> + Triple(R.drawable.ic_delivery_status_sending, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending) + message.isRead -> + Triple(R.drawable.ic_delivery_status_read, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read) + else -> + Triple(R.drawable.ic_delivery_status_sent, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sent) } } private fun updateExpirationTimer(message: MessageRecord) { val container = binding.messageInnerContainer - val content = binding.messageContentView + val content = binding.messageContentView.root val expiration = binding.expirationTimerView val spacing = binding.messageContentSpacing container.removeAllViewsInLayout() @@ -319,7 +349,7 @@ class VisibleMessageView : LinearLayout { override fun onDraw(canvas: Canvas) { val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val iconSize = toPx(24, context.resources) - val left = binding.messageInnerContainer.left + binding.messageContentView.right + spacing + val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2) val right = left + iconSize val bottom = top + iconSize @@ -341,7 +371,7 @@ class VisibleMessageView : LinearLayout { fun recycle() { binding.profilePictureView.root.recycle() - binding.messageContentView.recycle() + binding.messageContentView.root.recycle() } // endregion @@ -437,7 +467,7 @@ class VisibleMessageView : LinearLayout { } fun onContentClick(event: MotionEvent) { - binding.messageContentView.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) } + binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) } } private fun onPress(event: MotionEvent) { @@ -457,7 +487,7 @@ class VisibleMessageView : LinearLayout { } fun playVoiceMessage() { - binding.messageContentView.playVoiceMessage() + binding.messageContentView.root.playVoiceMessage() } // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index 451368e1cb..e1bf92c5f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -78,7 +78,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener { binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d", 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 binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d", 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 layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt() binding.progressView.layoutParams = layoutParams diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java deleted file mode 100644 index 912253ecd8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java +++ /dev/null @@ -1,425 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.utilities; - -import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; - -import android.content.Context; -import android.content.res.TypedArray; -import android.net.Uri; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.UiThread; - -import com.bumptech.glide.RequestBuilder; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; -import com.bumptech.glide.load.resource.bitmap.CenterCrop; -import com.bumptech.glide.load.resource.bitmap.FitCenter; -import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.bumptech.glide.request.RequestOptions; - -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsignal.utilities.ListenableFuture; -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.SettableFuture; -import org.session.libsignal.utilities.guava.Optional; -import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget; -import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget; -import org.thoughtcrime.securesms.components.TransferControlView; -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; -import org.thoughtcrime.securesms.mms.GlideRequest; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideClickListener; -import org.thoughtcrime.securesms.mms.SlidesClickedListener; - -import java.util.Collections; -import java.util.Locale; - -import network.loki.messenger.R; - -public class ThumbnailView extends FrameLayout { - - private static final String TAG = ThumbnailView.class.getSimpleName(); - private static final int WIDTH = 0; - private static final int HEIGHT = 1; - private static final int MIN_WIDTH = 0; - private static final int MAX_WIDTH = 1; - private static final int MIN_HEIGHT = 2; - private static final int MAX_HEIGHT = 3; - - private ImageView image; - private View playOverlay; - private View loadIndicator; - private OnClickListener parentClickListener; - - private final int[] dimens = new int[2]; - private final int[] bounds = new int[4]; - private final int[] measureDimens = new int[2]; - - private Optional transferControls = Optional.absent(); - private SlideClickListener thumbnailClickListener = null; - private SlidesClickedListener downloadClickListener = null; - private Slide slide = null; - - public int radius; - - public ThumbnailView(Context context) { - this(context, null); - } - - public ThumbnailView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - inflate(context, R.layout.thumbnail_view, this); - - this.image = findViewById(R.id.thumbnail_image); - this.playOverlay = findViewById(R.id.play_overlay); - this.loadIndicator = findViewById(R.id.thumbnail_load_indicator); - super.setOnClickListener(new ThumbnailClickDispatcher()); - - if (attrs != null) { - TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0); - bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0); - bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0); - bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0); - bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0); - radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0); - typedArray.recycle(); - } else { - radius = 0; - } - } - - @Override - protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasureSpec) { - fillTargetDimensions(measureDimens, dimens, bounds); - if (measureDimens[WIDTH] == 0 && measureDimens[HEIGHT] == 0) { - super.onMeasure(originalWidthMeasureSpec, originalHeightMeasureSpec); - return; - } - - int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight(); - int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom(); - - super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)); - } - - @SuppressWarnings("SuspiciousNameCombination") - private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) { - int dimensFilledCount = getNonZeroCount(dimens); - int boundsFilledCount = getNonZeroCount(bounds); - - if (dimensFilledCount == 0 || boundsFilledCount == 0) { - targetDimens[WIDTH] = 0; - targetDimens[HEIGHT] = 0; - return; - } - - double naturalWidth = dimens[WIDTH]; - double naturalHeight = dimens[HEIGHT]; - - int minWidth = bounds[MIN_WIDTH]; - int maxWidth = bounds[MAX_WIDTH]; - int minHeight = bounds[MIN_HEIGHT]; - int maxHeight = bounds[MAX_HEIGHT]; - - if (dimensFilledCount > 0 && dimensFilledCount < dimens.length) { - throw new IllegalStateException(String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %f x %f", - naturalWidth, naturalHeight)); - } - if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) { - throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]", - minWidth, maxWidth, minHeight, maxHeight)); - } - - double measuredWidth = naturalWidth; - double measuredHeight = naturalHeight; - - boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth; - boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight; - - if (!widthInBounds || !heightInBounds) { - double minWidthRatio = naturalWidth / minWidth; - double maxWidthRatio = naturalWidth / maxWidth; - double minHeightRatio = naturalHeight / minHeight; - double maxHeightRatio = naturalHeight / maxHeight; - - if (maxWidthRatio > 1 || maxHeightRatio > 1) { - if (maxWidthRatio >= maxHeightRatio) { - measuredWidth /= maxWidthRatio; - measuredHeight /= maxWidthRatio; - } else { - measuredWidth /= maxHeightRatio; - measuredHeight /= maxHeightRatio; - } - - measuredWidth = Math.max(measuredWidth, minWidth); - measuredHeight = Math.max(measuredHeight, minHeight); - - } else if (minWidthRatio < 1 || minHeightRatio < 1) { - if (minWidthRatio <= minHeightRatio) { - measuredWidth /= minWidthRatio; - measuredHeight /= minWidthRatio; - } else { - measuredWidth /= minHeightRatio; - measuredHeight /= minHeightRatio; - } - - measuredWidth = Math.min(measuredWidth, maxWidth); - measuredHeight = Math.min(measuredHeight, maxHeight); - } - } - - targetDimens[WIDTH] = (int) measuredWidth; - targetDimens[HEIGHT] = (int) measuredHeight; - } - - private int getNonZeroCount(int[] vals) { - int count = 0; - for (int val : vals) { - if (val > 0) { - count++; - } - } - return count; - } - - @Override - public void setOnClickListener(OnClickListener l) { - parentClickListener = l; - } - - @Override - public void setFocusable(boolean focusable) { - super.setFocusable(focusable); - if (transferControls.isPresent()) transferControls.get().setFocusable(focusable); - } - - @Override - public void setClickable(boolean clickable) { - super.setClickable(clickable); - if (transferControls.isPresent()) transferControls.get().setClickable(clickable); - } - - private TransferControlView getTransferControls() { - if (!transferControls.isPresent()) { - transferControls = Optional.of(ViewUtil.inflateStub(this, R.id.transfer_controls_stub)); - } - return transferControls.get(); - } - - public void setBounds(int minWidth, int maxWidth, int minHeight, int maxHeight) { - bounds[MIN_WIDTH] = minWidth; - bounds[MAX_WIDTH] = maxWidth; - bounds[MIN_HEIGHT] = minHeight; - bounds[MAX_HEIGHT] = maxHeight; - - forceLayout(); - } - - @UiThread - public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - boolean showControls, boolean isPreview) - { - return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0); - } - - @UiThread - public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - boolean showControls, boolean isPreview, - int naturalWidth, int naturalHeight) - { - if (showControls) { - getTransferControls().setSlide(slide); - getTransferControls().setDownloadClickListener(new DownloadClickDispatcher()); - } else if (transferControls.isPresent()) { - getTransferControls().setVisibility(View.GONE); - } - - if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() && - (slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) - { - this.playOverlay.setVisibility(View.VISIBLE); - } else { - this.playOverlay.setVisibility(View.GONE); - } - - if (Util.equals(slide, this.slide)) { - Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri()); - return new SettableFuture<>(false); - } - - if (this.slide != null && this.slide.getFastPreflightId() != null && - this.slide.getFastPreflightId().equals(slide.getFastPreflightId())) - { - Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId()); - this.slide = slide; - return new SettableFuture<>(false); - } - - Log.i(TAG, "loading part with id " + slide.asAttachment().getDataUri() - + ", progress " + slide.getTransferState() + ", fast preflight id: " + - slide.asAttachment().getFastPreflightId()); - - this.slide = slide; - - dimens[WIDTH] = naturalWidth; - dimens[HEIGHT] = naturalHeight; - invalidate(); - - SettableFuture result = new SettableFuture<>(); - - if (slide.getThumbnailUri() != null) { - buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result)); - } else if (slide.hasPlaceholder()) { - buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result)); - } else { - glideRequests.load(R.drawable.ic_image_white_24dp).centerInside().into(image); - result.set(false); - } - - return result; - } - - public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { - SettableFuture future = new SettableFuture<>(); - - if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); - - GlideRequest request = glideRequests.load(new DecryptableUri(uri)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .transition(withCrossFade()); - - if (radius > 0) { - request = request.transforms(new CenterCrop(), new RoundedCorners(radius)); - } else { - request = request.transforms(new CenterCrop()); - } - - request.into(new GlideDrawableListeningTarget(image, future)); - - return future; - } - - public void setThumbnailClickListener(SlideClickListener listener) { - this.thumbnailClickListener = listener; - } - - public void setDownloadClickListener(SlidesClickedListener listener) { - this.downloadClickListener = listener; - } - - public void clear(GlideRequests glideRequests) { - glideRequests.clear(image); - - if (transferControls.isPresent()) { - getTransferControls().clear(); - } - - slide = null; - } - - public void showDownloadText(boolean showDownloadText) { - getTransferControls().setShowDownloadText(showDownloadText); - } - - public void showProgressSpinner() { - getTransferControls().showProgressSpinner(); - } - - public void setLoadIndicatorVisibile(boolean visible) { - this.loadIndicator.setVisibility(visible ? VISIBLE : GONE); - } - - protected void setRadius(int radius) { - this.radius = radius; - } - - private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { - GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri())) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .transition(withCrossFade()), new CenterCrop()); - - if (slide.isInProgress()) return request; - else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)); - } - - private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { - return applySizing(glideRequests.asBitmap() - .load(slide.getPlaceholderRes(getContext().getTheme())) - .diskCacheStrategy(DiskCacheStrategy.NONE), new FitCenter()); - } - - private GlideRequest applySizing(@NonNull GlideRequest request, @NonNull BitmapTransformation fitting) { - int[] size = new int[2]; - fillTargetDimensions(size, dimens, bounds); - if (size[WIDTH] == 0 && size[HEIGHT] == 0) { - size[WIDTH] = getDefaultWidth(); - size[HEIGHT] = getDefaultHeight(); - } - - request = request.override(size[WIDTH], size[HEIGHT]); - - if (radius > 0) { - return request.transforms(fitting, new RoundedCorners(radius)); - } else { - return request.transforms(fitting); - } - } - - private int getDefaultWidth() { - ViewGroup.LayoutParams params = getLayoutParams(); - if (params != null) { - return Math.max(params.width, 0); - } - return 0; - } - - private int getDefaultHeight() { - ViewGroup.LayoutParams params = getLayoutParams(); - if (params != null) { - return Math.max(params.height, 0); - } - return 0; - } - - private class ThumbnailClickDispatcher implements View.OnClickListener { - - @Override - public void onClick(View view) { - if (thumbnailClickListener != null && - slide != null && - slide.asAttachment().getDataUri() != null && - slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) - { - thumbnailClickListener.onClick(view, slide); - } else if (parentClickListener != null) { - parentClickListener.onClick(view); - } - } - } - - private class DownloadClickDispatcher implements View.OnClickListener { - - @Override - public void onClick(View view) { - if (downloadClickListener != null && slide != null) { - downloadClickListener.onClick(view, Collections.singletonList(slide)); - } else { - Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + String.valueOf(slide) + " downloadClickListener: " + String.valueOf(downloadClickListener)); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt similarity index 82% rename from app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt index 1ae2902188..e158556675 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt @@ -2,14 +2,11 @@ package org.thoughtcrime.securesms.conversation.v2.utilities import android.content.Context import android.graphics.Bitmap -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri import android.util.AttributeSet -import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout -import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.bitmap.CenterCrop @@ -29,31 +26,33 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.GlideRequest import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.Slide +import kotlin.Boolean +import kotlin.Int +import kotlin.getValue +import kotlin.lazy +import kotlin.let -open class KThumbnailView: FrameLayout { - private lateinit var binding: ThumbnailViewBinding +open class ThumbnailView: FrameLayout { companion object { private const val WIDTH = 0 private const val HEIGHT = 1 } + private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) } + // region Lifecycle constructor(context: Context) : super(context) { initialize(null) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) } - private val image by lazy { binding.thumbnailImage } - private val playOverlay by lazy { binding.playOverlay } val loadIndicator: View by lazy { binding.thumbnailLoadIndicator } - val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon } private val dimensDelegate = ThumbnailDimensDelegate() private var slide: Slide? = null - private var radius: Int = 0 + var radius: Int = 0 private fun initialize(attrs: AttributeSet?) { - binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this) if (attrs != null) { val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0) @@ -66,8 +65,6 @@ open class KThumbnailView: FrameLayout { typedArray.recycle() } - val background = ContextCompat.getColor(context, R.color.transparent_black_6) - binding.root.background = ColorDrawable(background) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { @@ -80,8 +77,8 @@ open class KThumbnailView: FrameLayout { val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom super.onMeasure( - MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY) + MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY) ) } @@ -90,17 +87,17 @@ open class KThumbnailView: FrameLayout { // endregion // region Interaction - fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord): ListenableFuture { + fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord?): ListenableFuture { return setImageResource(glide, slide, isPreview, 0, 0, mms) } fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, naturalWidth: Int, - naturalHeight: Int, mms: MmsMessageRecord): ListenableFuture { + naturalHeight: Int, mms: MmsMessageRecord?): ListenableFuture { val currentSlide = this.slide - playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() && + binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() && (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) if (equals(currentSlide, slide)) { @@ -116,8 +113,8 @@ open class KThumbnailView: FrameLayout { this.slide = slide - loadIndicator.isVisible = slide.isInProgress - downloadIndicator.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED + binding.thumbnailLoadIndicator.isVisible = slide.isInProgress + binding.thumbnailDownloadIcon.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED dimensDelegate.setDimens(naturalWidth, naturalHeight) invalidate() @@ -126,13 +123,13 @@ open class KThumbnailView: FrameLayout { when { slide.thumbnailUri != null -> { - buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result)) + buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, result)) } slide.hasPlaceholder() -> { - buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result)) + buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, result)) } else -> { - glide.clear(image) + glide.clear(binding.thumbnailImage) result.set(false) } } @@ -176,7 +173,7 @@ open class KThumbnailView: FrameLayout { } open fun clear(glideRequests: GlideRequests) { - glideRequests.clear(image) + glideRequests.clear(binding.thumbnailImage) slide = null } @@ -193,11 +190,8 @@ open class KThumbnailView: FrameLayout { request.transforms(CenterCrop()) } - request.into(GlideDrawableListeningTarget(image, future)) + request.into(GlideDrawableListeningTarget(binding.thumbnailImage, future)) return future } - - // endregion - } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 8c9916b87c..45172e2f6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -33,8 +33,9 @@ import androidx.annotation.VisibleForTesting; import com.bumptech.glide.Glide; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; +import org.apache.commons.lang3.StringUtils; import org.json.JSONArray; import org.json.JSONException; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; @@ -318,6 +319,28 @@ public class AttachmentDatabase extends Database { notifyAttachmentListeners(); } + @SuppressWarnings("ResultOfMethodCallIgnored") + void deleteAttachmentsForMessages(long[] mmsIds) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + Cursor cursor = null; + String mmsIdString = StringUtils.join(mmsIds, ','); + + try { + cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE}, MMS_ID + " IN (?)", + new String[] {mmsIdString}, null, null, null); + + while (cursor != null && cursor.moveToNext()) { + deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1), cursor.getString(2)); + } + } finally { + if (cursor != null) + cursor.close(); + } + + database.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {mmsIdString}); + notifyAttachmentListeners(); + } + public void deleteAttachment(@NonNull AttachmentId id) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java index ce950214f0..b6b224589e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java @@ -23,7 +23,7 @@ import android.database.Cursor; import androidx.annotation.NonNull; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.session.libsession.utilities.WindowDebouncer; import org.thoughtcrime.securesms.ApplicationContext; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java index 74396e2a93..76fa8c5c0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -19,7 +19,7 @@ package org.thoughtcrime.securesms.database; import android.content.Context; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt index e6c9b9614e..f4d6530bbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt @@ -1,9 +1,9 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues +import android.database.Cursor import androidx.core.database.getStringOrNull -import net.sqlcipher.Cursor -import net.sqlcipher.database.SQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteDatabase import org.session.libsignal.utilities.Base64 fun SQLiteDatabase.get(table: String, query: String?, arguments: Array?, get: (Cursor) -> T): T? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java index 2dd8b2bf24..822e40129e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DraftDatabase.java @@ -6,7 +6,7 @@ import android.database.Cursor; import android.net.Uri; import androidx.annotation.Nullable; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import network.loki.messenger.R; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index feaccc3983..584bf3a71a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.database; - import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; @@ -12,7 +11,7 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.jetbrains.annotations.NotNull; import org.session.libsession.utilities.Address; @@ -319,6 +318,19 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt notifyConversationListListeners(); } + public boolean hasDownloadedProfilePicture(String groupId) { + try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{AVATAR}, GROUP_ID + " = ?", + new String[] {groupId}, + null, null, null)) + { + if (cursor != null && cursor.moveToNext()) { + return !cursor.isNull(0); + } + + return false; + } + } + public void updateMembers(String groupId, List
members) { Collections.sort(members); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index 81f8b62aa5..a6fed5be83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -1,14 +1,14 @@ package org.thoughtcrime.securesms.database; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; +import org.apache.commons.lang3.StringUtils; import org.session.libsession.utilities.Address; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; @@ -110,6 +110,11 @@ public class GroupReceiptDatabase extends Database { db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)}); } + void deleteRowsForMessages(long[] mmsIds) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {StringUtils.join(mmsIds, ',')}); + } + void deleteAllRows() { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.delete(TABLE_NAME, null, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.java index f878e3061a..ef4746923b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.java @@ -5,7 +5,7 @@ import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index 6aeadc2b7b..b0f6a676c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -300,6 +300,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val lastHash = database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() )) } + override fun clearAllLastMessageHashes() { + val database = databaseHelper.writableDatabase + database.delete(lastMessageHashValueTable2, null, null) + } + override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set? { val database = databaseHelper.readableDatabase val query = "${Companion.publicKey} = ? AND ${Companion.receivedMessageHashNamespace} = ?" @@ -321,6 +326,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(receivedMessageHashValuesTable, row, query, arrayOf( publicKey, namespace.toString() )) } + override fun clearReceivedMessageHashValues() { + val database = databaseHelper.writableDatabase + database.delete(receivedMessageHashValuesTable, null, null) + } + override fun getAuthToken(server: String): String? { val database = databaseHelper.readableDatabase return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor -> @@ -339,7 +349,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } override fun getLastMessageServerID(room: String, server: String): Long? { - val database = databaseHelper.writableDatabase + val database = databaseHelper.readableDatabase val index = "$server.$room" return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor -> cursor.getInt(lastMessageServerID) @@ -510,7 +520,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } fun getServerCapabilities(serverName: String): List { - val database = databaseHelper.writableDatabase + val database = databaseHelper.readableDatabase return database.get(serverCapabilitiesTable, "$server = ?", wrap(serverName)) { cursor -> cursor.getString(capabilities) }?.split(",") ?: emptyList() @@ -523,7 +533,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } fun getLastInboxMessageId(serverName: String): Long? { - val database = databaseHelper.writableDatabase + val database = databaseHelper.readableDatabase return database.get(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor -> cursor.getInt(lastInboxMessageServerId) }?.toLong() @@ -540,7 +550,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } fun getLastOutboxMessageId(serverName: String): Long? { - val database = databaseHelper.writableDatabase + val database = databaseHelper.readableDatabase return database.get(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor -> cursor.getInt(lastOutboxMessageServerId) }?.toLong() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt index 3cfdd13017..45184c2d23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context -import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE +import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE import org.session.libsignal.database.LokiMessageDatabaseProtocol import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper @@ -77,6 +77,25 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab database.endTransaction() } + fun deleteMessages(messageIDs: List) { + val database = databaseHelper.writableDatabase + database.beginTransaction() + + database.delete( + messageIDTable, + "${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})", + messageIDs.map { "$it" }.toTypedArray() + ) + database.delete( + messageThreadMappingTable, + "${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})", + messageIDs.map { "$it" }.toTypedArray() + ) + + database.setTransactionSuccessful() + database.endTransaction() + } + /** * @return pair of sms or mms table-specific ID and whether it is in SMS table */ @@ -96,6 +115,37 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab } } + fun getMessageIDs(serverIDs: List, threadID: Long): Pair, List> { + val database = databaseHelper.readableDatabase + + // Retrieve the message ids + val messageIdCursor = database + .rawQuery( + """ + SELECT ${messageThreadMappingTable}.${messageID}, ${messageIDTable}.${messageType} + FROM ${messageThreadMappingTable} + JOIN ${messageIDTable} ON ${messageIDTable}.message_id = ${messageThreadMappingTable}.${messageID} + WHERE ( + ${messageThreadMappingTable}.${Companion.threadID} = $threadID AND + ${messageThreadMappingTable}.${Companion.serverID} IN (${serverIDs.joinToString(",")}) + ) + """ + ) + + val smsMessageIds: MutableList = mutableListOf() + val mmsMessageIds: MutableList = mutableListOf() + while (messageIdCursor.moveToNext()) { + if (messageIdCursor.getInt(1) == SMS_TYPE) { + smsMessageIds.add(messageIdCursor.getLong(0)) + } + else { + mmsMessageIds.add(messageIdCursor.getLong(0)) + } + } + + return Pair(smsMessageIds, mmsMessageIds) + } + override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) { val database = databaseHelper.writableDatabase val contentValues = ContentValues(3) @@ -136,6 +186,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab 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) { val database = databaseHelper.writableDatabase try { @@ -178,6 +233,15 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) } + fun deleteMessageServerHashes(messageIDs: List) { + val database = databaseHelper.writableDatabase + database.delete( + messageHashTable, + "${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})", + messageIDs.map { "$it" }.toTypedArray() + ) + } + fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) { val database = databaseHelper.writableDatabase val contentValues = ContentValues(1) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index f16d663a10..1b273de929 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -7,7 +7,7 @@ import android.database.Cursor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.utilities.Address; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index ffde5ca029..2471db1cb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -5,7 +5,7 @@ import android.content.Context; import android.database.Cursor; import android.text.TextUtils; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Document; @@ -42,6 +42,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn public abstract void markAsDeleted(long messageId, boolean read); public abstract boolean deleteMessage(long messageId); + public abstract boolean deleteMessages(long[] messageId, long threadId); public abstract void updateThreadId(long fromId, long toId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index d82c6bb278..3f94dd6bcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -995,6 +995,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa return threadDeleted } + override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean { + val attachmentDatabase = get(context).attachmentDatabase() + val groupReceiptDatabase = get(context).groupReceiptDatabase() + + queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) }) + groupReceiptDatabase.deleteRowsForMessages(messageIds) + + val database = databaseHelper.writableDatabase + database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(","))) + + val threadDeleted = get(context).threadDatabase().update(threadId, false) + notifyConversationListeners(threadId) + notifyStickerListeners() + notifyStickerPackListeners() + return threadDeleted + } + override fun updateThreadId(fromId: Long, toId: Long) { val contentValues = ContentValues(1) contentValues.put(THREAD_ID, toId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 3fcb1e724c..73534aeb23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -22,8 +22,8 @@ import android.database.Cursor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import net.sqlcipher.database.SQLiteDatabase; -import net.sqlcipher.database.SQLiteQueryBuilder; +import net.zetetic.database.sqlcipher.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteQueryBuilder; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Util; @@ -112,6 +112,64 @@ public class MmsSmsDatabase extends Database { 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) { String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC"); String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; @@ -199,16 +257,16 @@ public class MmsSmsDatabase extends Database { return -1; } - public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull Address address) { - String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; + public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address) { + String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; 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(); boolean isOwnNumber = Util.isOwnNumber(context, address.serialize()); while (cursor != null && cursor.moveToNext()) { - boolean timestampMatches = cursor.getLong(0) == receivedTimestamp; + boolean timestampMatches = cursor.getLong(0) == sentTimestamp; boolean addressMatches = serializedAddress.equals(cursor.getString(1)); if (timestampMatches && (addressMatches || isOwnNumber)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java index d1ba25aa7e..b832d04dfc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java @@ -6,7 +6,7 @@ import android.database.Cursor; import androidx.annotation.NonNull; import org.session.libsignal.utilities.Log; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.session.libsignal.utilities.Base64; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt index 74e452db07..87c0b6c182 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt @@ -48,6 +48,14 @@ class ReactionDatabase(context: Context, helper: SQLCipherOpenHelper) : Database ) """.trimIndent() + @JvmField + val CREATE_INDEXS = arrayOf( + "CREATE INDEX IF NOT EXISTS reaction_message_id_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.MESSAGE_ID + ");", + "CREATE INDEX IF NOT EXISTS reaction_is_mms_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.IS_MMS + ");", + "CREATE INDEX IF NOT EXISTS reaction_message_id_is_mms_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.MESSAGE_ID + ", " + ReactionDatabase.IS_MMS + ");", + "CREATE INDEX IF NOT EXISTS reaction_sort_id_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.SORT_ID + ");", + ) + @JvmField val CREATE_REACTION_TRIGGERS = arrayOf( """ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 58693172ed..af2faaaca9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -11,7 +11,7 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.MaterialColor; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java index 37efc9a438..eac6a5fbc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java @@ -1,13 +1,13 @@ package org.thoughtcrime.securesms.database; import android.content.Context; +import android.database.Cursor; import androidx.annotation.NonNull; import com.annimon.stream.Stream; -import net.sqlcipher.Cursor; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.session.libsession.utilities.Util; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; @@ -63,7 +63,7 @@ public class SearchDatabase extends Database { ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + "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 + " " + "FROM " + SmsDatabase.TABLE_NAME + " " + "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 + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + "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 + " " + "FROM " + MmsDatabase.TABLE_NAME + " " + "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 + " " + "WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " + - "ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " + + "ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC " + "LIMIT ?"; 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 + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + "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 + " " + "FROM " + SmsDatabase.TABLE_NAME + " " + "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 + ", " + MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " + "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 + " " + "FROM " + MmsDatabase.TABLE_NAME + " " + "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 + " " + "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"; public SearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt index ef9f0cc383..40eee97428 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import androidx.core.database.getStringOrNull -import net.sqlcipher.Cursor +import android.database.Cursor import org.session.libsession.messaging.contacts.Contact import org.session.libsignal.utilities.Base64 import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper @@ -75,21 +75,6 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da } fun contactFromCursor(cursor: Cursor): Contact { - val sessionID = cursor.getString(sessionID) - val contact = Contact(sessionID) - contact.name = cursor.getStringOrNull(name) - contact.nickname = cursor.getStringOrNull(nickname) - contact.profilePictureURL = cursor.getStringOrNull(profilePictureURL) - contact.profilePictureFileName = cursor.getStringOrNull(profilePictureFileName) - cursor.getStringOrNull(profilePictureEncryptionKey)?.let { - contact.profilePictureEncryptionKey = Base64.decode(it) - } - contact.threadID = cursor.getLong(threadID) - contact.isTrusted = cursor.getInt(isTrusted) != 0 - return contact - } - - fun contactFromCursor(cursor: android.database.Cursor): Contact { val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID)) val contact = Contact(sessionID) contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt index 595168fdf7..4425e3d85d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context -import net.sqlcipher.Cursor +import android.database.Cursor import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 67243f73b6..320cee477c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -28,9 +28,10 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; -import net.sqlcipher.database.SQLiteDatabase; -import net.sqlcipher.database.SQLiteStatement; +import net.zetetic.database.sqlcipher.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteStatement; +import org.apache.commons.lang3.StringUtils; import org.session.libsession.messaging.calls.CallMessageType; import org.session.libsession.messaging.messages.signal.IncomingGroupMessage; import org.session.libsession.messaging.messages.signal.IncomingTextMessage; @@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import java.io.IOException; import java.security.SecureRandom; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -596,6 +598,30 @@ public class SmsDatabase extends MessagingDatabase { return threadDeleted; } + @Override + public boolean deleteMessages(long[] messageIds, long threadId) { + String[] argsArray = new String[messageIds.length]; + String[] argValues = new String[messageIds.length]; + Arrays.fill(argsArray, "?"); + + for (int i = 0; i < messageIds.length; i++) { + argValues[i] = (messageIds[i] + ""); + } + + String combinedMessageIdArgss = StringUtils.join(messageIds, ','); + String combinedMessageIds = StringUtils.join(messageIds, ','); + Log.i("MessageDatabase", "Deleting: " + combinedMessageIds); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.delete( + TABLE_NAME, + ID + " IN (" + StringUtils.join(argsArray, ',') + ")", + argValues + ); + boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false); + notifyConversationListeners(threadId); + return threadDeleted; + } + @Override public void updateThreadId(long fromId, long toId) { ContentValues contentValues = new ContentValues(1); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 76849b8af6..0fee84e8ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -2,31 +2,23 @@ package org.thoughtcrime.securesms.database import android.content.Context import android.net.Uri +import org.session.libsession.avatars.AvatarHelper import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.messaging.jobs.AttachmentUploadJob -import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob -import org.session.libsession.messaging.jobs.Job -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageReceiveJob -import org.session.libsession.messaging.jobs.MessageSendJob +import org.session.libsession.messaging.jobs.* import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.MessageRequestResponse -import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage -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.signal.* import org.session.libsession.messaging.messages.visible.Attachment +import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.GroupMember import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage @@ -36,11 +28,12 @@ import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.UpdateMessageData 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.GroupRecord import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.ProfileKeyUtil +import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.crypto.ecc.ECKeyPair @@ -58,6 +51,7 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.SessionMetaProtocol +import java.security.MessageDigest class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { @@ -69,16 +63,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).lokiAPIDatabase().getUserX25519KeyPair() } - override fun getUserDisplayName(): String? { - return TextSecurePreferences.getProfileName(context) - } - - override fun getUserProfileKey(): ByteArray? { - return ProfileKeyUtil.getProfileKey(context) - } - - override fun getUserProfilePictureURL(): String? { - return TextSecurePreferences.getProfilePictureURL(context) + override fun getUserProfile(): Profile { + val displayName = TextSecurePreferences.getProfileName(context)!! + val profileKey = ProfileKeyUtil.getProfileKey(context) + val profilePictureUrl = TextSecurePreferences.getProfilePictureURL(context) + return Profile(displayName, profileKey, profilePictureUrl) } override fun setUserProfilePictureURL(newValue: String) { @@ -335,6 +324,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue) } + override fun hasDownloadedProfilePicture(groupID: String): Boolean { + return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID) + } + override fun getReceivedMessageTimestamps(): Set { return SessionMetaProtocol.getTimestamps() } @@ -428,6 +421,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) { DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(messageID, serverHash) } @@ -562,8 +560,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).groupDatabase().allGroups } - override fun addOpenGroup(urlAsString: String) { - OpenGroupManager.addOpenGroup(urlAsString, context) + override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? { + return OpenGroupManager.addOpenGroup(urlAsString, context) } override fun onOpenGroupAdded(server: String) { @@ -759,6 +757,25 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val smsDb = DatabaseComponent.get(context).smsDatabase() val sender = Recipient.from(context, fromSerialized(senderPublicKey), false) val threadId = threadDB.getOrCreateThreadIdFor(sender) + val profile = response.profile + if (profile != null) { + val profileManager = SSKEnvironment.shared.profileManager + val name = profile.displayName!! + if (name.isNotEmpty()) { + profileManager.setName(context, sender, name) + } + val newProfileKey = profile.profileKey + + val needsProfilePicture = !AvatarHelper.avatarFileExists(context, sender.address) + val profileKeyValid = newProfileKey?.isNotEmpty() == true && (newProfileKey.size == 16 || newProfileKey.size == 32) && profile.profilePictureURL?.isNotEmpty() == true + val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey)) + + if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) { + profileManager.setProfileKey(context, sender, newProfileKey!!) + profileManager.setUnidentifiedAccessMode(context, sender, Recipient.UnidentifiedAccessMode.UNKNOWN) + profileManager.setProfilePictureURL(context, sender, profile.profilePictureURL!!) + } + } threadDB.setHasSent(threadId, true) val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val mappings = mutableMapOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 02a82de1d4..bf42b92ba5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -32,7 +32,7 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; -import net.sqlcipher.database.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.jetbrains.annotations.NotNull; import org.session.libsession.utilities.Address; @@ -502,15 +502,23 @@ public class ThreadDatabase extends Database { return db.rawQuery(query, null); } - public void setLastSeen(long threadId) { + public void setLastSeen(long threadId, long timestamp) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); 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)}); notifyConversationListListeners(); } + public void setLastSeen(long threadId) { + setLastSeen(threadId, -1); + } + public Pair getLastSeenAndHasSent(long threadId) { 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index d2266b3924..a6ac906980 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -1,14 +1,18 @@ package org.thoughtcrime.securesms.database.helpers; - +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; -import net.sqlcipher.database.SQLiteDatabase; -import net.sqlcipher.database.SQLiteDatabaseHook; -import net.sqlcipher.database.SQLiteOpenHelper; +import net.zetetic.database.sqlcipher.SQLiteConnection; +import net.zetetic.database.sqlcipher.SQLiteDatabase; +import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; +import net.zetetic.database.sqlcipher.SQLiteException; +import net.zetetic.database.sqlcipher.SQLiteOpenHelper; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsignal.utilities.Log; @@ -35,6 +39,11 @@ import org.thoughtcrime.securesms.database.SessionContactDatabase; import org.thoughtcrime.securesms.database.SessionJobDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.notifications.NotificationChannels; + +import java.io.File; + +import network.loki.messenger.R; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @@ -75,40 +84,157 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV36 = 57; private static final int lokiV37 = 58; private static final int lokiV38 = 59; + private static final int lokiV39 = 60; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV38; - private static final String DATABASE_NAME = "signal.db"; + private static final int DATABASE_VERSION = lokiV39; + private static final int MIN_DATABASE_VERSION = lokiV7; + private static final String CIPHER3_DATABASE_NAME = "signal.db"; + public static final String DATABASE_NAME = "signal_v4.db"; private final Context context; private final DatabaseSecret databaseSecret; public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) { - super(context, DATABASE_NAME, null, DATABASE_VERSION, new SQLiteDatabaseHook() { + super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, MIN_DATABASE_VERSION, null, new SQLiteDatabaseHook() { @Override - public void preKey(SQLiteDatabase db) { - db.rawExecSQL("PRAGMA cipher_default_kdf_iter = 1;"); - db.rawExecSQL("PRAGMA cipher_default_page_size = 4096;"); + public void preKey(SQLiteConnection connection) { + SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); } @Override - public void postKey(SQLiteDatabase db) { - db.rawExecSQL("PRAGMA kdf_iter = '1';"); - db.rawExecSQL("PRAGMA cipher_page_size = 4096;"); + public void postKey(SQLiteConnection connection) { + SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); + // if not vacuumed in a while, perform that operation long currentTime = System.currentTimeMillis(); // 7 days if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) { - db.rawExecSQL("VACUUM;"); + connection.execute("VACUUM;", null, null); TextSecurePreferences.setLastVacuumNow(context); } } - }); + }, true); this.context = context.getApplicationContext(); this.databaseSecret = databaseSecret; } + private static void applySQLCipherPragmas(SQLiteConnection connection, boolean useSQLCipher4) { + if (useSQLCipher4) { + connection.execute("PRAGMA kdf_iter = '256000';", null, null); + } + else { + connection.execute("PRAGMA cipher_compatibility = 3;", null, null); + connection.execute("PRAGMA kdf_iter = '1';", null, null); + } + + connection.execute("PRAGMA cipher_page_size = 4096;", null, null); + } + + private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException { + return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); } + + @Override + public void postKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); } + }); + } + + public static void migrateSqlCipher3To4IfNeeded(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) throws Exception { + String oldDbPath = context.getDatabasePath(CIPHER3_DATABASE_NAME).getPath(); + File oldDbFile = new File(oldDbPath); + + // If the old SQLCipher3 database file doesn't exist then no need to do anything + if (!oldDbFile.exists()) { return; } + + try { + // Define the location for the new database + String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath(); + File newDbFile = new File(newDbPath); + + // If the new database file already exists then check if it's valid first, if it's in an + // invalid state we should delete it and try to migrate again + if (newDbFile.exists()) { + // If the old database hasn't been modified since the new database was created, then we can + // assume the user hasn't downgraded for some reason and made changes to the old database and + // can remove the old database file (it won't be used anymore) + if (oldDbFile.lastModified() <= newDbFile.lastModified()) { + // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past +// //noinspection ResultOfMethodCallIgnored +// oldDbFile.delete(); + return; + } + + // If the old database does have newer changes then the new database could have stale/invalid + // data and we should re-migrate to avoid losing any data or issues + if (!newDbFile.delete()) { + throw new Exception("Failed to remove invalid new database"); + } + } + + if (!newDbFile.createNewFile()) { + throw new Exception("Failed to create new database"); + } + + // Open the old database and extract it's version + SQLiteDatabase oldDb = SQLCipherOpenHelper.open(oldDbPath, databaseSecret, false); + int oldDbVersion = oldDb.getVersion(); + + // Export the old database to the new one (will have the default 'kdf_iter' and 'page_size' settings) + oldDb.rawExecSQL( + String.format("ATTACH DATABASE '%s' AS sqlcipher4 KEY '%s'", newDbPath, databaseSecret.asString()) + ); + Cursor cursor = oldDb.rawQuery("SELECT sqlcipher_export('sqlcipher4')"); + cursor.moveToLast(); + cursor.close(); + oldDb.rawExecSQL("DETACH DATABASE sqlcipher4"); + oldDb.close(); + + // Open the newly migrated database (to ensure it works) and set it's version so we don't try + // to run any of our custom migrations + SQLiteDatabase newDb = SQLCipherOpenHelper.open(newDbPath, databaseSecret, true); + newDb.setVersion(oldDbVersion); + newDb.close(); + + // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past + // Remove the old database file since it will no longer be used +// //noinspection ResultOfMethodCallIgnored +// oldDbFile.delete(); + } + catch (Exception e) { + Log.e(TAG, "Migration from SQLCipher3 to SQLCipher4 failed", e); + + // Notify the user of the issue so they know they can downgrade until the issue is fixed + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + String channelId = context.getString(R.string.NotificationChannel_failures); + + if (NotificationChannels.supported()) { + NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH); + channel.enableVibration(true); + notificationManager.createNotificationChannel(channel); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_notification) + .setColor(context.getResources().getColor(R.color.textsecure_primary)) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setContentTitle(context.getString(R.string.ErrorNotifier_migration)) + .setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade)) + .setAutoCancel(true); + + if (!NotificationChannels.supported()) { + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + } + + notificationManager.notify(5874, builder.build()); + + // Throw the error (app will crash but there is nothing else we can do unfortunately) + throw e; + } + } + @Override public void onCreate(SQLiteDatabase db) { db.execSQL(SmsDatabase.CREATE_TABLE); @@ -188,6 +314,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { executeStatements(db, DraftDatabase.CREATE_INDEXS); executeStatements(db, GroupDatabase.CREATE_INDEXS); executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES); + executeStatements(db, ReactionDatabase.CREATE_INDEXS); executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS); } @@ -195,9 +322,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); - // Loki - Enable write ahead logging mode and increase the cache size. - // This should be disabled if we ever run into serious race condition bugs. - db.enableWriteAheadLogging(); + db.execSQL("PRAGMA cache_size = 10000"); } @@ -414,20 +539,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND); } + if (oldVersion < lokiV39) { + executeStatements(db, ReactionDatabase.CREATE_INDEXS); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); } } - public SQLiteDatabase getReadableDatabase() { - return getReadableDatabase(databaseSecret.asString()); - } - - public SQLiteDatabase getWritableDatabase() { - return getWritableDatabase(databaseSecret.asString()); - } - public void markCurrent(SQLiteDatabase db) { db.setVersion(DATABASE_VERSION); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index da71103753..1d78314dea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -33,6 +33,7 @@ import org.session.libsession.utilities.NetworkFailure; import org.session.libsession.utilities.recipients.Recipient; import java.util.List; +import java.util.Objects; /** * The base class for message record models that are displayed in @@ -140,14 +141,16 @@ public abstract class MessageRecord extends DisplayRecord { return spannable; } + @Override public boolean equals(Object other) { return other instanceof MessageRecord - && ((MessageRecord) other).getId() == getId() - && ((MessageRecord) other).isMms() == isMms(); + && ((MessageRecord) other).getId() == getId() + && ((MessageRecord) other).isMms() == isMms(); } + @Override public int hashCode() { - return (int)getId(); + return Objects.hash(id, isMms()); } public @NonNull List getReactions() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 46e4199622..b186e668eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -2,13 +2,15 @@ package org.thoughtcrime.securesms.database.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.session.libsession.utilities.Contact; + 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.NetworkFailure; +import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; + import java.util.LinkedList; import java.util.List; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java index e79626c66b..4fd22ce8a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java @@ -8,6 +8,8 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; import org.session.libsession.utilities.Address; import org.thoughtcrime.securesms.mms.SlideDeck; +import java.util.Objects; + public class Quote { private final long id; @@ -47,4 +49,17 @@ public class Quote { public QuoteModel getQuoteModel() { 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); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 6ce69a591a..1e5a2fef0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -50,7 +50,6 @@ public class ThreadRecord extends DisplayRecord { private final long expiresIn; private final long lastSeen; private final boolean pinned; - private final int recipientHash; public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri, @NonNull Recipient recipient, long date, long count, int unreadCount, @@ -67,17 +66,12 @@ public class ThreadRecord extends DisplayRecord { this.expiresIn = expiresIn; this.lastSeen = lastSeen; this.pinned = pinned; - this.recipientHash = recipient.hashCode(); } public @Nullable Uri getSnippetUri() { return snippetUri; } - public int getRecipientHash() { - return recipientHash; - } - @Override public SpannableString getDisplayBody(@NonNull Context context) { if (isGroupUpdateMessage()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt index 029daefbf1..fba7a7e503 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt @@ -6,7 +6,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import net.sqlcipher.database.SQLiteDatabase +import net.zetetic.database.sqlcipher.SQLiteDatabase import org.session.libsession.database.MessageDataProvider import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider import org.thoughtcrime.securesms.crypto.AttachmentSecret @@ -22,7 +22,7 @@ object DatabaseModule { @JvmStatic fun init(context: Context) { - SQLiteDatabase.loadLibs(context) + System.loadLibrary("sqlcipher") } @Provides @@ -33,6 +33,7 @@ object DatabaseModule { @Singleton fun provideOpenHelper(@ApplicationContext context: Context): SQLCipherOpenHelper { val dbSecret = DatabaseSecretProvider(context).orCreateDatabaseSecret + SQLCipherOpenHelper.migrateSqlCipher3To4IfNeeded(context, dbSecret) return SQLCipherOpenHelper(context, dbSecret) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index d39ba709df..ef47269107 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -58,14 +58,14 @@ object OpenGroupManager { } @WorkerThread - fun add(server: String, room: String, publicKey: String, context: Context) { + fun add(server: String, room: String, publicKey: String, context: Context): OpenGroupApi.RoomInfo? { val openGroupID = "$server.$room" var threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) val storage = MessagingModuleConfiguration.shared.storage val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() // Check it it's added already val existingOpenGroup = threadDB.getOpenGroupChat(threadID) - if (existingOpenGroup != null) { return } + if (existingOpenGroup != null) { return null } // Clear any existing data if needed storage.removeLastDeletionServerID(room, server) storage.removeLastMessageServerID(room, server) @@ -73,18 +73,17 @@ object OpenGroupManager { storage.removeLastOutboxMessageId(server) // Store the public key storage.setOpenGroupPublicKey(server, publicKey) - // Get capabilities - val capabilities = OpenGroupApi.getCapabilities(server).get() + // Get capabilities & room info + val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get() storage.setServerCapabilities(server, capabilities.capabilities) - // Get room info - val info = OpenGroupApi.getRoomInfo(room, server).get() storage.setUserCount(room, server, info.activeUsers) // Create the group locally if not available already if (threadID < 0) { threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId } - val openGroup = OpenGroup(server, room, info.name, info.infoUpdates, publicKey) + val openGroup = OpenGroup(server = server, room = room, publicKey = publicKey, name = info.name, imageId = info.imageId, canWrite = info.write, infoUpdates = info.infoUpdates) threadDB.setOpenGroupChat(openGroup, threadID) + return info } fun restartPollerForServer(server: String) { @@ -130,12 +129,13 @@ object OpenGroupManager { } } - fun addOpenGroup(urlAsString: String, context: Context) { - val url = HttpUrl.parse(urlAsString) ?: return + fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? { + val url = HttpUrl.parse(urlAsString) ?: return null val server = OpenGroup.getServer(urlAsString) - val room = url.pathSegments().firstOrNull() ?: return - val publicKey = url.queryParameter("public_key") ?: return - add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function + val room = url.pathSegments().firstOrNull() ?: return null + val publicKey = url.queryParameter("public_key") ?: return null + + return add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function } fun updateOpenGroup(openGroup: OpenGroup, context: Context) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index bfa9b14489..7a0c865a42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -99,11 +99,11 @@ class ConversationView : LinearLayout { binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE if (isTyping) { - binding.typingIndicatorView.startAnimation() + binding.typingIndicatorView.root.startAnimation() } else { - binding.typingIndicatorView.stopAnimation() + binding.typingIndicatorView.root.stopAnimation() } - binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE + binding.typingIndicatorView.root.visibility = if (isTyping) View.VISIBLE else View.GONE binding.statusIndicatorImageView.visibility = View.VISIBLE when { !thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index b21eb6ff13..f1a9c8ed94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -102,7 +102,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), when (model) { is GlobalSearchAdapter.Model.Message -> { val threadId = model.messageResult.threadId - val timestamp = model.messageResult.receivedTimestampMs + val timestamp = model.messageResult.sentTimestampMs val author = model.messageResult.messageRecipient.address val intent = Intent(this, ConversationActivityV2::class.java) @@ -202,7 +202,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), OpenGroupManager.startPolling() JobQueue.shared.resumePendingJobs() } - // Set up typing observer + withContext(Dispatchers.Main) { updateProfileButton() TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect { @@ -365,6 +365,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), setupMessageRequestsBanner() updateEmptyState() } + + ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds -> + homeAdapter.typingThreadIDs = (threadIds ?: setOf()) + } } private fun updateEmptyState() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 3efa841b54..0effc43fb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -63,6 +63,8 @@ class HomeAdapter( lateinit var glide: GlideRequests var typingThreadIDs = setOf() set(value) { + if (field == value) { return } + field = value // TODO: replace this with a diffed update or a partial change set with payloads notifyDataSetChanged() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt index fcaf565e0d..1baec20854 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt @@ -22,22 +22,28 @@ class HomeDiffUtil( val newItem = new[newItemPosition] // return early to save getDisplayBody or expensive calls - val sameCount = oldItem.count == newItem.count - if (!sameCount) return false - val sameUnreads = oldItem.unreadCount == newItem.unreadCount - if (!sameUnreads) return false - val samePinned = oldItem.isPinned == newItem.isPinned - if (!samePinned) return false - val sameRecipientHash = oldItem.recipientHash == newItem.recipientHash - if (!sameRecipientHash) return false - val sameSnippet = oldItem.getDisplayBody(context) == newItem.getDisplayBody(context) - if (!sameSnippet) return false - val sameSendStatus = oldItem.isFailed == newItem.isFailed && oldItem.isDelivered == newItem.isDelivered - && oldItem.isSent == newItem.isSent && oldItem.isPending == newItem.isPending - if (!sameSendStatus) return false + var isSameItem = true - // all same - return true + if (isSameItem) { isSameItem = (oldItem.count == newItem.count) } + if (isSameItem) { isSameItem = (oldItem.unreadCount == newItem.unreadCount) } + if (isSameItem) { isSameItem = (oldItem.isPinned == newItem.isPinned) } + + // Note: For some reason the 'hashCode' value can change after initialisation so we can't cache it + if (isSameItem) { isSameItem = (oldItem.recipient.hashCode() == newItem.recipient.hashCode()) } + + // Note: Two instances of 'SpannableString' may not equate even though their content matches + if (isSameItem) { isSameItem = (oldItem.getDisplayBody(context).toString() == newItem.getDisplayBody(context).toString()) } + + if (isSameItem) { + isSameItem = ( + oldItem.isFailed == newItem.isFailed && + oldItem.isDelivered == newItem.isDelivered && + oldItem.isSent == newItem.isSent && + oldItem.isPending == newItem.isPending + ) + } + + return isSameItem } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt index 7603d39224..2c64ded866 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -134,7 +134,7 @@ fun ContentView.bindModel(query: String?, model: Message) { // if (hasUnreads) { // 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) val textSpannable = SpannableStringBuilder() if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java index 62f2ee6b27..dc1d2afcf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/AlarmManagerScheduler.java @@ -6,17 +6,19 @@ import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; + import androidx.annotation.NonNull; import com.annimon.stream.Stream; -import org.thoughtcrime.securesms.ApplicationContext; -import network.loki.messenger.BuildConfig; import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.ApplicationContext; import java.util.List; import java.util.UUID; +import network.loki.messenger.BuildConfig; + /** * Schedules tasks using the {@link AlarmManager}. * @@ -51,7 +53,7 @@ public class AlarmManagerScheduler implements Scheduler { Intent intent = new Intent(context, RetryReceiver.class); 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."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt index fac0a402e2..89a841dc0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt @@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.util.forceShowIcon class MessageRequestsAdapter( context: Context, @@ -64,7 +63,7 @@ class MessageRequestsAdapter( item.iconTintList = ColorStateList.valueOf(context.getColor(R.color.destructive)) item.title = s } - popupMenu.forceShowIcon() + popupMenu.setForceShowIcon(true) popupMenu.show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java index 02ccf85518..6db38e6bc5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -17,17 +17,19 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.annimon.stream.Stream; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.thoughtcrime.securesms.util.MediaUtil; import org.session.libsignal.utilities.guava.Optional; +import org.thoughtcrime.securesms.util.MediaUtil; import java.util.LinkedList; import java.util.List; +import java.util.Objects; public class SlideDeck { @@ -138,4 +140,17 @@ public class SlideDeck { 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); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index 48a5725521..5a0438e15d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -44,7 +44,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor } override fun doWork(): Result { - if (TextSecurePreferences.getLocalNumber(context) == null) { + if (TextSecurePreferences.getLocalNumber(context) == null || !TextSecurePreferences.hasSeenWelcomeScreen(context)) { Log.v(TAG, "User not registered yet.") return Result.failure() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 55a4fcd293..7284629033 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -40,7 +40,6 @@ import com.annimon.stream.Optional; import com.annimon.stream.Stream; 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.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.utilities.SessionId; @@ -453,8 +452,7 @@ public class DefaultMessageNotifier implements MessageNotifier { NotificationState notificationState = new NotificationState(); MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor); ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase(); - LokiThreadDatabase lokiThreadDatabase= DatabaseComponent.get(context).lokiThreadDatabase(); - KeyPair edKeyPair = MessagingModuleConfiguration.getShared().getGetUserED25519KeyPair().invoke(); + MessageRecord record; Map cache = new HashMap(); @@ -575,7 +573,7 @@ public class DefaultMessageNotifier implements MessageNotifier { Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION); 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); alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent); @@ -584,7 +582,7 @@ public class DefaultMessageNotifier implements MessageNotifier { @Override public void clearReminder(Context context) { 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.cancel(pendingIntent); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/FailedNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/FailedNotificationBuilder.java index 1ffd74be63..dc0e52abc6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/FailedNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/FailedNotificationBuilder.java @@ -5,9 +5,10 @@ import android.content.Context; import android.content.Intent; 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.recipients.Recipient; + +import network.loki.messenger.R; public class FailedNotificationBuilder extends AbstractNotificationBuilder { @@ -20,7 +21,7 @@ public class FailedNotificationBuilder extends AbstractNotificationBuilder { setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed)); setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_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); setAlarms(null, Recipient.VibrateState.DEFAULT); setChannelId(NotificationChannels.FAILURES); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java index 81332e87d9..a4871f0fc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java @@ -34,7 +34,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu setColor(context.getResources().getColor(R.color.textsecure_primary)); setSmallIcon(R.drawable.ic_notification); 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); setGroupSummary(true); @@ -52,8 +52,8 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) { String displayName = recipient.toShortString(); - if (threadRecipient.isOpenGroupRecipient()) { - displayName = getOpenGroupDisplayName(recipient); + if (threadRecipient.isGroupRecipient()) { + displayName = getGroupDisplayName(recipient, threadRecipient.isOpenGroupRecipient()); } if (privacy.isDisplayContact()) { setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName)); @@ -78,8 +78,8 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) { String displayName = sender.toShortString(); - if (threadRecipient.isOpenGroupRecipient()) { - displayName = getOpenGroupDisplayName(sender); + if (threadRecipient.isGroupRecipient()) { + displayName = getGroupDisplayName(sender, threadRecipient.isOpenGroupRecipient()); } if (privacy.isDisplayMessage()) { SpannableStringBuilder builder = new SpannableStringBuilder(); @@ -113,14 +113,15 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu } /** - * @param recipient the * individual * recipient for which to get the open group display name. + * @param recipient the * individual * recipient for which to get the display name. + * @param openGroupRecipient whether in an open group context */ - private String getOpenGroupDisplayName(Recipient recipient) { + private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) { SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase(); String sessionID = recipient.getAddress().serialize(); Contact contact = contactDB.getContactWithSessionID(sessionID); if (contact == null) { return sessionID; } - String displayName = contact.displayName(Contact.ContactContext.OPEN_GROUP); + String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR); if (displayName == null) { return sessionID; } return displayName; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java index 991989e8da..0d57751171 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java @@ -4,14 +4,15 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Build; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.TaskStackBuilder; - +import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import org.thoughtcrime.securesms.mms.SlideDeck; -import org.session.libsession.utilities.recipients.Recipient; public class NotificationItem { @@ -75,9 +76,14 @@ public class NotificationItem { intent.putExtra(ConversationActivityV2.THREAD_ID, threadId); 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) .addNextIntentWithParentStack(intent) - .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + .getPendingIntent(0, intentFlags); } public long getId() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java index fe934e229f..108aa12c51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationState.java @@ -4,12 +4,14 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Build; + import androidx.annotation.NonNull; 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.VibrateState; +import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2; import java.util.LinkedHashSet; @@ -114,7 +116,12 @@ public class NotificationState { intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray); 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) { @@ -127,7 +134,12 @@ public class NotificationState { intent.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod); 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) { @@ -141,7 +153,12 @@ public class NotificationState { intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]); 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) { @@ -160,7 +177,12 @@ public class NotificationState { intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId); 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) { @@ -171,7 +193,12 @@ public class NotificationState { intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]); 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) { @@ -190,7 +217,12 @@ public class NotificationState { intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms); 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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java index 1d19c2c8e3..935d575c56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java @@ -4,12 +4,13 @@ package org.thoughtcrime.securesms.notifications; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; + 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.TextSecurePreferences; +import org.session.libsession.utilities.recipients.Recipient; +import org.thoughtcrime.securesms.home.HomeActivity; 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)); 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); setAlarms(null, Recipient.VibrateState.DEFAULT); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 7925b8556a..da0896d05a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -117,15 +117,15 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil setNumber(messageCount); } - public void setPrimaryMessageBody(@NonNull Recipient threadRecipients, + public void setPrimaryMessageBody(@NonNull Recipient threadRecipient, @NonNull Recipient individualRecipient, @NonNull CharSequence message, @Nullable SlideDeck slideDeck) { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); - if (privacy.isDisplayContact() && threadRecipients.isOpenGroupRecipient()) { - String displayName = getOpenGroupDisplayName(individualRecipient); + if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) { + String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient()); stringBuilder.append(Util.getBoldedString(displayName + ": ")); } @@ -214,8 +214,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); - if (privacy.isDisplayContact() && threadRecipient.isOpenGroupRecipient()) { - String displayName = getOpenGroupDisplayName(individualRecipient); + if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) { + String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient()); stringBuilder.append(Util.getBoldedString(displayName + ": ")); } @@ -334,14 +334,15 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil } /** - * @param recipient the * individual * recipient for which to get the open group display name. + * @param recipient the * individual * recipient for which to get the display name. + * @param openGroupRecipient whether in an open group context */ - private String getOpenGroupDisplayName(Recipient recipient) { + private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) { SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase(); String sessionID = recipient.getAddress().serialize(); Contact contact = contactDB.getContactWithSessionID(sessionID); if (contact == null) { return sessionID; } - String displayName = contact.displayName(Contact.ContactContext.OPEN_GROUP); + String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR); if (displayName == null) { return sessionID; } return displayName; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index ee1631a00d..31117ae94d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -22,8 +22,10 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityLinkDeviceBinding import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding +import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec +import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.Log @@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { private lateinit var binding: ActivityLinkDeviceBinding + internal val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage private val adapter = LinkDeviceActivityAdapter(this) private var restoreJob: Job? = null @@ -99,6 +103,11 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel if (restoreJob?.isActive == true) return restoreJob = lifecycleScope.launch { + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + // RestoreActivity handles seed this way val keyPairGenerationResult = KeyPairUtilities.generate(seed) val x25519KeyPair = keyPairGenerationResult.x25519KeyPair diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index 92583d89e5..9cf9c3d049 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -52,7 +52,7 @@ class PNModeActivity : BaseActionBarActivity() { toggleFCM() } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_pn_mode, menu) return true } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt index 6a1c785ad6..5531fea491 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt @@ -13,8 +13,10 @@ import android.view.View import android.widget.Toast import network.loki.messenger.R import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding +import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec +import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey @@ -26,6 +28,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { private lateinit var binding: ActivityRecoveryPhraseRestoreBinding + internal val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -64,6 +68,11 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { private fun restore() { val mnemonic = binding.mnemonicEditText.text.toString() try { + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + val loadFileContents: (String) -> String = { fileName -> MnemonicUtilities.loadFileContents(this, fileName) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt index 0105fbedf0..b6fdaf9cf9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt @@ -18,8 +18,10 @@ import android.widget.Toast import com.goterl.lazysodium.utils.KeyPair import network.loki.messenger.R import network.loki.messenger.databinding.ActivityRegisterBinding +import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.ecc.ECKeyPair +import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.BaseActionBarActivity @@ -29,6 +31,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class RegisterActivity : BaseActionBarActivity() { private lateinit var binding: ActivityRegisterBinding + internal val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage private var seed: ByteArray? = null private var ed25519KeyPair: KeyPair? = null private var x25519KeyPair: ECKeyPair? = null @@ -109,6 +113,11 @@ class RegisterActivity : BaseActionBarActivity() { // region Interaction private fun register() { + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!) val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey val registrationID = KeyHelper.generateRegistrationId(false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt index 90ffbd4b13..f6efd041cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt @@ -41,8 +41,7 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() { addPreferencesFromResource(R.xml.preferences_help) } - override fun onPreferenceTreeClick(preference: Preference?): Boolean { - preference ?: return false + override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { EXPORT_LOGS -> { shareLogs() diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 00f5d72c7b..2d67894011 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -35,6 +35,7 @@ interface ConversationRepository { fun maybeGetRecipientForThreadId(threadId: Long): Recipient? fun saveDraft(threadId: Long, text: String) fun getDraft(threadId: Long): String? + fun clearDrafts(threadId: Long) fun inviteContacts(threadId: Long, contacts: List) fun setBlocked(recipient: Recipient, blocked: Boolean) fun deleteLocally(recipient: Recipient, message: MessageRecord) @@ -98,10 +99,13 @@ class DefaultConversationRepository @Inject constructor( override fun getDraft(threadId: Long): String? { val drafts = draftDb.getDrafts(threadId) - draftDb.clearDrafts(threadId) return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value } + override fun clearDrafts(threadId: Long) { + draftDb.clearDrafts(threadId) + } + override fun inviteContacts(threadId: Long, contacts: List) { val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return for (contact in contacts) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java index a33f4dd11c..ddfe85515c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java @@ -301,10 +301,10 @@ public class SearchRepository { Recipient conversationRecipient = Recipient.from(context, conversationAddress, false); Recipient messageRecipient = Recipient.from(context, messageAddress, false); 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)); - return new MessageResult(conversationRecipient, messageRecipient, body, threadId, receivedMs); + return new MessageResult(conversationRecipient, messageRecipient, body, threadId, sentMs); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java b/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java index 4523ab364b..58e3f1a69a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/model/MessageResult.java @@ -13,18 +13,18 @@ public class MessageResult { public final Recipient messageRecipient; public final String bodySnippet; public final long threadId; - public final long receivedTimestampMs; + public final long sentTimestampMs; public MessageResult(@NonNull Recipient conversationRecipient, @NonNull Recipient messageRecipient, @NonNull String bodySnippet, long threadId, - long receivedTimestampMs) + long sentTimestampMs) { this.conversationRecipient = conversationRecipient; this.messageRecipient = messageRecipient; this.bodySnippet = bodySnippet; this.threadId = threadId; - this.receivedTimestampMs = receivedTimestampMs; + this.sentTimestampMs = sentTimestampMs; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java b/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java index 13701300b5..0516dc2856 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/DirectShareService.java @@ -6,14 +6,12 @@ import android.content.IntentFilter; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.drawable.Icon; -import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTargetService; import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; @@ -28,7 +26,6 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; -@RequiresApi(api = Build.VERSION_CODES.M) public class DirectShareService extends ChooserTargetService { private static final String TAG = DirectShareService.class.getSimpleName(); @@ -40,53 +37,50 @@ public class DirectShareService extends ChooserTargetService { List results = new LinkedList<>(); ComponentName componentName = new ComponentName(this, ShareActivity.class); ThreadDatabase threadDatabase = DatabaseComponent.get(this).threadDatabase(); - Cursor cursor = threadDatabase.getDirectShareList(); - try { - ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor); - ThreadRecord record; + try (Cursor cursor = threadDatabase.getDirectShareList()) { + ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor); + ThreadRecord record; - while ((record = reader.getNext()) != null && results.size() < 10) { - Recipient recipient = Recipient.from(this, record.getRecipient().getAddress(), false); - String name = recipient.toShortString(); + while ((record = reader.getNext()) != null && results.size() < 10) { + Recipient recipient = Recipient.from(this, record.getRecipient().getAddress(), false); + 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(); - 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; } - - return results; - } finally { - if (cursor != null) cursor.close(); - } } private Bitmap getFallbackDrawable(@NonNull Recipient recipient) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpirationListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpirationListener.java index 4a83707ddd..a0ef945c9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpirationListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpirationListener.java @@ -17,7 +17,7 @@ public class ExpirationListener extends BroadcastReceiver { public static void setAlarm(Context context, long waitTimeMillis) { 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.cancel(pendingIntent); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java index 0581883c5e..52a259d5be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -6,6 +6,7 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.IBinder; + import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,9 +14,9 @@ import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import org.session.libsignal.utilities.Log; +import org.session.libsignal.utilities.guava.Preconditions; import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.session.libsignal.utilities.guava.Preconditions; import network.loki.messenger.R; @@ -87,10 +88,10 @@ public class GenericForegroundService extends Service { } 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) .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()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java index 9e79b93d60..402c0f6521 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -29,18 +29,19 @@ import android.os.AsyncTask; import android.os.Binder; import android.os.IBinder; import android.os.SystemClock; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.DummyActivity; -import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.session.libsession.utilities.ServiceUtil; -import org.session.libsession.utilities.TextSecurePreferences; import java.util.concurrent.TimeUnit; @@ -255,18 +256,18 @@ public class KeyCachingService extends Service { private PendingIntent buildLockIntent() { Intent intent = new Intent(this, KeyCachingService.class); intent.setAction(PASSPHRASE_EXPIRED_EVENT); - return PendingIntent.getService(getApplicationContext(), 0, intent, 0); + return PendingIntent.getService(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE); } private PendingIntent buildLaunchIntent() { Intent intent = new Intent(this, HomeActivity.class); 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) { 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java index 4091d953af..f24906c5e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/PersistentAlarmManagerListener.java @@ -6,6 +6,7 @@ import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; + import org.session.libsignal.utilities.Log; public abstract class PersistentAlarmManagerListener extends BroadcastReceiver { @@ -21,7 +22,7 @@ public abstract class PersistentAlarmManagerListener extends BroadcastReceiver { long scheduledTime = getNextScheduledExecutionTime(context); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 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) { scheduledTime = onAlarm(context, scheduledTime); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkReadyListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkReadyListener.java index 323617d81d..eea6ba00f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkReadyListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/UpdateApkReadyListener.java @@ -12,21 +12,22 @@ import android.net.Uri; import androidx.annotation.Nullable; 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.libsignal.utilities.Hex; import org.session.libsession.utilities.ServiceUtil; 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.FileInputStream; import java.io.IOException; import java.security.MessageDigest; +import network.loki.messenger.R; + public class UpdateApkReadyListener extends BroadcastReceiver { 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.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) .setOngoing(true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index 079d7a21ad..d09933ab8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -7,11 +7,15 @@ import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.IntentFilter +import android.content.pm.PackageManager import android.media.AudioManager +import android.os.Build import android.os.IBinder import android.os.ResultReceiver import android.telephony.PhoneStateListener +import android.telephony.PhoneStateListener.LISTEN_NONE import android.telephony.TelephonyManager +import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint @@ -27,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_RINGING import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING -import org.thoughtcrime.securesms.webrtc.AudioManagerCommand -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.* import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.data.Event import org.thoughtcrime.securesms.webrtc.locks.LockManager -import org.webrtc.DataChannel -import org.webrtc.IceCandidate -import org.webrtc.MediaStream -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 org.webrtc.* +import org.webrtc.PeerConnection.IceConnectionState.* +import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.Executors import java.util.concurrent.ScheduledFuture @@ -59,7 +46,7 @@ import javax.inject.Inject import org.thoughtcrime.securesms.webrtc.data.State as CallState @AndroidEntryPoint -class WebRtcCallService: Service(), CallManager.WebRtcListener { +class WebRtcCallService : Service(), CallManager.WebRtcListener { companion object { @@ -107,62 +94,82 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { private const val RECONNECT_SECONDS = 5L 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) .putExtra(EXTRA_MUTE, !enabled) 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) - .setAction(ACTION_ANSWER_CALL) + .setAction(ACTION_ANSWER_CALL) - fun microphoneIntent(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java) - .setAction(ACTION_SET_MUTE_AUDIO) - .putExtra(EXTRA_MUTE, !enabled) + fun microphoneIntent(context: Context, enabled: Boolean) = + Intent(context, WebRtcCallService::class.java) + .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) .putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address) - fun incomingCall(context: Context, address: Address, sdp: String, callId: UUID, callTime: Long) = - 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 incomingCall( + context: Context, + address: Address, + sdp: String, + callId: UUID, + callTime: Long + ) = + 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) = - Intent(context, WebRtcCallService::class.java) - .setAction(ACTION_RESPONSE_MESSAGE) - .putExtra(EXTRA_RECIPIENT_ADDRESS, address) - .putExtra(EXTRA_CALL_ID, callId) - .putExtra(EXTRA_REMOTE_DESCRIPTION, sdp) + Intent(context, WebRtcCallService::class.java) + .setAction(ACTION_RESPONSE_MESSAGE) + .putExtra(EXTRA_RECIPIENT_ADDRESS, address) + .putExtra(EXTRA_CALL_ID, callId) + .putExtra(EXTRA_REMOTE_DESCRIPTION, sdp) fun preOffer(context: Context, address: Address, callId: UUID, callTime: Long) = - Intent(context, WebRtcCallService::class.java) - .setAction(ACTION_PRE_OFFER) - .putExtra(EXTRA_RECIPIENT_ADDRESS, address) - .putExtra(EXTRA_CALL_ID, callId) - .putExtra(EXTRA_TIMESTAMP, callTime) + Intent(context, WebRtcCallService::class.java) + .setAction(ACTION_PRE_OFFER) + .putExtra(EXTRA_RECIPIENT_ADDRESS, address) + .putExtra(EXTRA_CALL_ID, callId) + .putExtra(EXTRA_TIMESTAMP, callTime) - fun iceCandidates(context: Context, address: Address, iceCandidates: List, callId: UUID) = - Intent(context, WebRtcCallService::class.java) - .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 iceCandidates( + context: Context, + address: Address, + iceCandidates: List, + callId: UUID + ) = + Intent(context, WebRtcCallService::class.java) + .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) .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) { val intent = Intent(context, WebRtcCallService::class.java) @@ -173,7 +180,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { fun broadcastWantsToAnswer(context: Context, wantsToAnswer: Boolean) { val intent = Intent(ACTION_WANTS_TO_ANSWER) - .putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer) + .putExtra(EXTRA_WANTS_TO_ANSWER, wantsToAnswer) LocalBroadcastManager.getInstance(context).sendBroadcast(intent) } @@ -181,13 +188,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { @JvmStatic fun isCallActive(context: Context, resultReceiver: ResultReceiver) { val intent = Intent(context, WebRtcCallService::class.java) - .setAction(ACTION_IS_IN_CALL_QUERY) - .putExtra(EXTRA_RESULT_RECEIVER, resultReceiver) + .setAction(ACTION_IS_IN_CALL_QUERY) + .putExtra(EXTRA_RESULT_RECEIVER, resultReceiver) context.startService(intent) } } - @Inject lateinit var callManager: CallManager + @Inject + lateinit var callManager: CallManager private var wantsToAnswer = false private var currentTimeouts = 0 @@ -198,8 +206,17 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { private val lockManager by lazy { LockManager(this) } private val serviceExecutor = Executors.newSingleThreadExecutor() private val timeoutExecutor = Executors.newScheduledThreadPool(1) - private val hangupOnCallAnswered = HangUpRtcOnPstnCallAnsweredListener { - startService(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 @@ -257,7 +274,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { val action = intent.action Log.i("Loki", "Handling ${intent.action}") 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_INCOMING_RING && isBusy(intent) -> handleBusyCall(intent) action == ACTION_INCOMING_RING && isPreOffer() -> handleIncomingRing(intent) @@ -271,7 +290,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent) action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(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_ICE_MESSAGE -> handleRemoteIceCandidate(intent) action == ACTION_ICE_CONNECTED -> handleIceConnected(intent) @@ -292,8 +313,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { registerIncomingPstnCallReceiver() registerWiredHeadsetStateReceiver() registerWantsToAnswerReceiver() - getSystemService(TelephonyManager::class.java) - .listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE) + if (checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + 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() networkChangedReceiver = NetworkChangeReceiver(::networkChange) networkChangedReceiver!!.register(this) @@ -317,7 +345,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } } 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() { @@ -338,7 +367,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { private fun handleUpdateAudio(intent: Intent) { val audioCommand = intent.getParcelableExtra(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") return } @@ -418,8 +451,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { callManager.initializeAudioForCall() callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING) setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient) - callManager.insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_OUTGOING) - scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS) + callManager.insertCallMessage( + recipient.address.serialize(), + CallMessageType.CALL_OUTGOING + ) + scheduledTimeout = timeoutExecutor.schedule( + TimeoutRunnable(callId, this), + TIMEOUT_SECONDS, + TimeUnit.SECONDS + ) callManager.setAudioEnabled(true) val expectedState = callManager.currentConnectionState @@ -428,15 +468,21 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { try { val offerFuture = callManager.onOutgoingCall(this) offerFuture.fail { e -> - if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) { - Log.e(TAG,e) + if (isConsistentState( + expectedState, + expectedCallId, + callManager.currentConnectionState, + callManager.callId + ) + ) { + Log.e(TAG, e) callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE) callManager.postConnectionError() terminate() } } } catch (e: Exception) { - Log.e(TAG,e) + Log.e(TAG, e) callManager.postConnectionError() terminate() } @@ -475,7 +521,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { callManager.silenceIncomingRinger() 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.initializeVideo(this) @@ -486,7 +536,13 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { try { val answerFuture = callManager.onIncomingCall(this) answerFuture.fail { e -> - if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) { + if (isConsistentState( + expectedState, + expectedCallId, + callManager.currentConnectionState, + callManager.callId + ) + ) { Log.e(TAG, e) insertMissedCall(recipient, true) callManager.postConnectionError() @@ -496,7 +552,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING) callManager.setAudioEnabled(true) } catch (e: Exception) { - Log.e(TAG,e) + Log.e(TAG, e) callManager.postConnectionError() terminate() } @@ -517,6 +573,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { private fun handleRemoteHangup(intent: Intent) { if (callManager.callId != getCallId(intent)) { Log.e(TAG, "Hangup for non-active call...") + stopForeground(true) return } @@ -554,7 +611,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } val callId = getCallId(intent) 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) { terminate() } @@ -566,14 +627,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { val sdpLineIndexes = intent.getIntArrayExtra(EXTRA_ICE_SDP_LINE_INDEX) ?: return val sdps = intent.getStringArrayExtra(EXTRA_ICE_SDP) ?: return 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 } val iceCandidates = sdpMids.indices.map { index -> IceCandidate( - sdpMids[index], - sdpLineIndexes[index], - sdps[index] + sdpMids[index], + sdpLineIndexes[index], + sdps[index] ) } callManager.handleRemoteIceCandidate(iceCandidates, callId) @@ -596,7 +657,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { private fun handleIsInCallQuery(intent: Intent) { val listener = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER) ?: return 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()) } @@ -615,10 +680,21 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { if (callId == getCallId(intent) && isNetworkAvailable && numTimeouts <= MAX_RECONNECTS) { Log.i("Loki", "Trying to re-connect") 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) { - Log.i("Loki", "Network isn't available, timeouts == $numTimeouts out of $MAX_RECONNECTS") - scheduledReconnect = timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), RECONNECT_SECONDS, TimeUnit.SECONDS) + Log.i( + "Loki", + "Network isn't available, timeouts == $numTimeouts out of $MAX_RECONNECTS" + ) + scheduledReconnect = timeoutExecutor.schedule( + CheckReconnectedRunnable(callId, this), + RECONNECT_SECONDS, + TimeUnit.SECONDS + ) } else { Log.i("Loki", "Network isn't available, timing out") handleLocalHangup(intent) @@ -626,12 +702,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } - private fun handleCheckTimeout(intent: Intent) { val callId = callManager.callId ?: return 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") handleLocalHangup(intent) } @@ -639,8 +718,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { private fun setCallInProgressNotification(type: Int, recipient: Recipient?) { startForeground( - CallNotificationBuilder.WEBRTC_NOTIFICATION, - CallNotificationBuilder.getCallInProgressNotification(this, type, recipient) + CallNotificationBuilder.WEBRTC_NOTIFICATION, + CallNotificationBuilder.getCallInProgressNotification(this, type, recipient) ) if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) { // start an intent for the fullscreen @@ -660,14 +739,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { private fun getRemoteRecipient(intent: Intent): Recipient { val remoteAddress = intent.getParcelableExtra
(EXTRA_RECIPIENT_ADDRESS) - ?: throw AssertionError("No recipient in intent!") + ?: throw AssertionError("No recipient in intent!") 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 - ?: throw AssertionError("No callId in intent!") + ?: throw AssertionError("No callId in intent!") } private fun insertMissedCall(recipient: Recipient, signal: Boolean) { @@ -679,10 +758,13 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } 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() { - Log.d(TAG,"onDestroy()") + Log.d(TAG, "onDestroy()") callManager.unregisterListener(this) callReceiver?.let { receiver -> unregisterReceiver(receiver) @@ -697,6 +779,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { wantsToAnswer = false currentTimeouts = 0 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() } @@ -708,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() { val intent = Intent(context, WebRtcCallService::class.java) .setAction(ACTION_CHECK_RECONNECT) @@ -717,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() { val intent = Intent(context, WebRtcCallService::class.java) .setAction(ACTION_CHECK_RECONNECT_TIMEOUT) @@ -726,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() { val intent = Intent(context, WebRtcCallService::class.java) - .setAction(ACTION_CHECK_TIMEOUT) - .putExtra(EXTRA_CALL_ID, callId) + .setAction(ACTION_CHECK_TIMEOUT) + .putExtra(EXTRA_CALL_ID, callId) context.startService(intent) } } private abstract class FailureListener( - expectedState: CallState, - expectedCallId: UUID?, - getState: () -> Pair): StateAwareListener(expectedState, expectedCallId, getState) { + expectedState: CallState, + expectedCallId: UUID?, + getState: () -> Pair + ) : StateAwareListener(expectedState, expectedCallId, getState) { override fun onSuccessContinue(result: V) {} } private abstract class SuccessOnlyListener( - expectedState: CallState, - expectedCallId: UUID?, - getState: () -> Pair): StateAwareListener(expectedState, expectedCallId, getState) { + expectedState: CallState, + expectedCallId: UUID?, + getState: () -> Pair + ) : StateAwareListener(expectedState, expectedCallId, getState) { override fun onFailureContinue(throwable: Throwable?) { Log.e(TAG, throwable) throw AssertionError(throwable) @@ -753,9 +850,10 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } private abstract class StateAwareListener( - private val expectedState: CallState, - private val expectedCallId: UUID?, - private val getState: ()->Pair): FutureTaskListener { + private val expectedState: CallState, + private val expectedCallId: UUID?, + private val getState: () -> Pair + ) : FutureTaskListener { companion object { private val TAG = Log.tag(StateAwareListener::class.java) @@ -763,7 +861,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { override fun onSuccess(result: V) { 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 { onSuccessContinue(result) } @@ -772,7 +870,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { override fun onFailure(exception: ExecutionException?) { if (!isConsistentState()) { 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 { exception?.let { onFailureContinue(it.cause) @@ -791,10 +889,10 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } private fun isConsistentState( - expectedState: CallState, - expectedCallId: UUID?, - currentState: CallState, - currentCallId: UUID? + expectedState: CallState, + expectedCallId: UUID?, + currentState: CallState, + currentCallId: UUID? ): Boolean { return expectedState == currentState && expectedCallId == currentCallId } @@ -816,17 +914,29 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { val intent = Intent(this, WebRtcCallService::class.java) .setAction(ACTION_ICE_CONNECTED) 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.postConnectionEvent(Event.IceDisconnect) { callManager.postViewModelState(CallViewModel.State.CALL_RECONNECTING) if (callManager.isInitiator()) { 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 { Log.i("Loki", "Starting timeout, awaiting new reconnect") callManager.postConnectionEvent(Event.PrepareForNewOffer) { - scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS) + scheduledTimeout = timeoutExecutor.schedule( + TimeoutRunnable(callId, this), + TIMEOUT_SECONDS, + TimeUnit.SECONDS + ) } } } @@ -854,7 +964,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { override fun onDataChannel(p0: DataChannel?) {} override fun onRenegotiationNeeded() { - Log.w(TAG,"onRenegotiationNeeded was called!") + Log.w(TAG, "onRenegotiationNeeded was called!") } override fun onAddTrack(p0: RtpReceiver?, p1: Array?) {} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java index c1d6e53690..a18ad8211f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java @@ -1,20 +1,21 @@ package org.thoughtcrime.securesms.sskenvironment; import android.annotation.SuppressLint; +import android.content.Context; + +import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import android.content.Context; -import androidx.annotation.NonNull; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import org.jetbrains.annotations.NotNull; import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; +import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import java.util.ArrayList; @@ -198,12 +199,12 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr if (device != typist.device) return false; if (threadId != typist.threadId) return false; - return author.equals(typist.author); + return author.getAddress().equals(typist.author.getAddress()); } @Override public int hashCode() { - int result = author.hashCode(); + int result = author.getAddress().hashCode(); result = 31 * result + device; result = 31 * result + (int) (threadId ^ (threadId >>> 32)); return result; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt index eaaf06f456..074278cb92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.BackupFileRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.service.LocalBackupListener import java.io.IOException import java.security.MessageDigest import java.security.NoSuchAlgorithmException @@ -74,44 +73,6 @@ object BackupUtil { 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 fun getLastBackupTimeString(context: Context, locale: Locale): String { val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime() diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java index e328e34e0b..bd3d65b9d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java @@ -4,8 +4,6 @@ import android.database.Cursor; import androidx.annotation.NonNull; -import java.util.Optional; - public final class CursorUtil { @@ -19,71 +17,8 @@ public final class CursorUtil { 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) { 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 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 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 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 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(); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt new file mode 100644 index 0000000000..4d76e6aad7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MockDataGenerator.kt @@ -0,0 +1,425 @@ +package org.thoughtcrime.securesms.util + +import android.content.Context +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.messages.signal.IncomingTextMessage +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage +import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.crypto.ecc.Curve +import org.session.libsignal.messages.SignalServiceGroup +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.guava.Optional +import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.groups.GroupManager +import java.security.SecureRandom +import java.util.* +import kotlin.random.asKotlinRandom + +object MockDataGenerator { + private var printProgress = true + private var hasStartedGenerationThisRun = false + + // FIXME: Update this to run in a transaction instead of individual db writes (should drastically speed it up) + fun generateMockData(context: Context) { + // Don't re-generate the mock data if it already exists + val mockDataExistsRecipient = Recipient.from(context, Address.fromSerialized("MockDatabaseThread"), false) + val storage = MessagingModuleConfiguration.shared.storage + val threadDb = DatabaseComponent.get(context).threadDatabase() + val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase() + val contactDb = DatabaseComponent.get(context).sessionContactDatabase() + val recipientDb = DatabaseComponent.get(context).recipientDatabase() + val smsDb = DatabaseComponent.get(context).smsDatabase() + + if (hasStartedGenerationThisRun || threadDb.getThreadIdIfExistsFor(mockDataExistsRecipient) != -1L) { + hasStartedGenerationThisRun = true + return + } + + /// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will + /// also take a long time): + /// Generating the threads & content - ~3m per 100 + val dmThreadCount: Int = 1000 + val closedGroupThreadCount: Int = 50 + val openGroupThreadCount: Int = 20 + val messageRangePerThread: List = listOf(0..500) + val dmRandomSeed: String = "1111" + val cgRandomSeed: String = "2222" + val ogRandomSeed: String = "3333" + val chunkSize: Int = 1000 // Chunk up the thread writing to prevent memory issues + val stringContent: List = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { it.toString() } + val wordContent: List = listOf("alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat") + val timestampNow: Long = System.currentTimeMillis() + val userSessionId: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! + val logProgress: ((String, String) -> Unit) = logProgress@{ title, event -> + if (!printProgress) { return@logProgress } + + Log.i("[MockDataGenerator]", "${System.currentTimeMillis()} $title - $event") + } + + hasStartedGenerationThisRun = true + + // FIXME: Make sure this data doesn't go off device somehow? + logProgress("", "Start") + + // First create the thread used to indicate that the mock data has been generated + threadDb.getOrCreateThreadIdFor(mockDataExistsRecipient) + + // -- DM Thread + val dmThreadRandomGenerator: SecureRandom = SecureRandom(dmRandomSeed.toByteArray()) + var dmThreadIndex: Int = 0 + logProgress("DM Threads", "Start Generating $dmThreadCount threads") + + while (dmThreadIndex < dmThreadCount) { + val remainingThreads: Int = (dmThreadCount - dmThreadIndex) + + (0 until Math.min(chunkSize, remainingThreads)).forEach { index -> + val threadIndex: Int = (dmThreadIndex + index) + + logProgress("DM Thread $threadIndex", "Start") + + val dataBytes = (0 until 16).map { dmThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } + val randomSessionId: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey + val isMessageRequest: Boolean = dmThreadRandomGenerator.nextBoolean() + val contactNameLength: Int = (5 + dmThreadRandomGenerator.nextInt(15)) + + val numMessages: Int = ( + messageRangePerThread[threadIndex % messageRangePerThread.count()].first + + dmThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last()) + ) + + // Generate the thread + val recipient = Recipient.from(context, Address.fromSerialized(randomSessionId), false) + val contact = Contact(randomSessionId) + val threadId = threadDb.getOrCreateThreadIdFor(recipient) + + // Generate the contact + val contactIsApproved: Boolean = (!isMessageRequest || dmThreadRandomGenerator.nextBoolean()) + contactDb.setContact(contact) + contactDb.setContactIsTrusted(contact, true, threadId) + recipientDb.setApproved(recipient, contactIsApproved) + recipientDb.setApprovedMe(recipient, (!isMessageRequest && (dmThreadRandomGenerator.nextInt(10) < 8))) // 80% approved the current user + + contact.name = (0 until dmThreadRandomGenerator.nextInt(contactNameLength)) + .map { stringContent.random(dmThreadRandomGenerator.asKotlinRandom()) } + .joinToString() + recipientDb.setProfileName(recipient, contact.name) + contactDb.setContact(contact) + + // Generate the message history (Note: Unapproved message requests will only include incoming messages) + logProgress("DM Thread $threadIndex", "Generate $numMessages Messages") + (0 until numMessages).forEach { index -> + val isIncoming: Boolean = ( + dmThreadRandomGenerator.nextBoolean() && + (!isMessageRequest || contactIsApproved) + ) + val messageWords: Int = (1 + dmThreadRandomGenerator.nextInt(19)) + + if (isIncoming) { + smsDb.insertMessageInbox( + IncomingTextMessage( + recipient.address, + 1, + (timestampNow - (index * 5000)), + (0 until messageWords) + .map { wordContent.random(dmThreadRandomGenerator.asKotlinRandom()) } + .joinToString(), + Optional.absent(), + 0, + false, + -1 + ), + (timestampNow - (index * 5000)), + false, + false + ) + } + else { + smsDb.insertMessageOutbox( + threadId, + OutgoingTextMessage( + recipient, + (0 until messageWords) + .map { wordContent.random(dmThreadRandomGenerator.asKotlinRandom()) } + .joinToString(), + 0, + -1, + (timestampNow - (index * 5000)) + ), + (timestampNow - (index * 5000)), + false + ) + } + } + + logProgress("DM Thread $threadIndex", "Done") + } + logProgress("DM Threads", "Done") + + dmThreadIndex += chunkSize + } + logProgress("DM Threads", "Done") + + // -- Closed Group + + val cgThreadRandomGenerator: SecureRandom = SecureRandom(cgRandomSeed.toByteArray()) + var cgThreadIndex: Int = 0 + logProgress("Closed Group Threads", "Start Generating $closedGroupThreadCount threads") + + while (cgThreadIndex < closedGroupThreadCount) { + val remainingThreads: Int = (closedGroupThreadCount - cgThreadIndex) + + (0 until Math.min(chunkSize, remainingThreads)).forEach { index -> + val threadIndex: Int = (cgThreadIndex + index) + + logProgress("Closed Group Thread $threadIndex", "Start") + + val dataBytes = (0 until 16).map { cgThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } + val randomGroupPublicKey: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey + val groupNameLength: Int = (5 + cgThreadRandomGenerator.nextInt(15)) + val groupName: String = (0 until groupNameLength) + .map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) } + .joinToString() + val numGroupMembers: Int = cgThreadRandomGenerator.nextInt (10) + val numMessages: Int = ( + messageRangePerThread[threadIndex % messageRangePerThread.count()].first + + cgThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last()) + ) + + // Generate the Contacts in the group + val members: MutableList = mutableListOf(userSessionId) + logProgress("Closed Group Thread $threadIndex", "Generate $numGroupMembers Contacts") + + (0 until numGroupMembers).forEach { + val contactBytes = (0 until 16).map { cgThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } + val randomSessionId: String = KeyPairUtilities.generate(contactBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey + val contactNameLength: Int = (5 + cgThreadRandomGenerator.nextInt(15)) + + val recipient = Recipient.from(context, Address.fromSerialized(randomSessionId), false) + val contact = Contact(randomSessionId) + contactDb.setContact(contact) + recipientDb.setApproved(recipient, true) + recipientDb.setApprovedMe(recipient, true) + + contact.name = (0 until cgThreadRandomGenerator.nextInt(contactNameLength)) + .map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) } + .joinToString() + recipientDb.setProfileName(recipient, contact.name) + contactDb.setContact(contact) + members.add(randomSessionId) + } + + val groupId = GroupUtil.doubleEncodeGroupID(randomGroupPublicKey) + val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupId)) + val adminUserId = members.random(cgThreadRandomGenerator.asKotlinRandom()) + storage.createGroup( + groupId, + groupName, + members.map { Address.fromSerialized(it) }, + null, + null, + listOf(Address.fromSerialized(adminUserId)), + timestampNow + ) + storage.setProfileSharing(Address.fromSerialized(groupId), true) + storage.addClosedGroupPublicKey(randomGroupPublicKey) + + // Add the group to the user's set of public keys to poll for and store the key pair + val encryptionKeyPair = Curve.generateKeyPair() + storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, randomGroupPublicKey) + storage.setExpirationTimer(groupId, 0) + + // Add the group created message + if (userSessionId == adminUserId) { + storage.insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, groupName, members, listOf(adminUserId), threadId, (timestampNow - (numMessages * 5000))) + } + else { + storage.insertIncomingInfoMessage(context, adminUserId, groupId, SignalServiceGroup.Type.CREATION, groupName, members, listOf(adminUserId), (timestampNow - (numMessages * 5000))) + } + + // Generate the message history (Note: Unapproved message requests will only include incoming messages) + logProgress("Closed Group Thread $threadIndex", "Generate $numMessages Messages") + + (0 until numGroupMembers).forEach { + val messageWords: Int = (1 + cgThreadRandomGenerator.nextInt(19)) + val senderId: String = members.random(cgThreadRandomGenerator.asKotlinRandom()) + + if (senderId != userSessionId) { + smsDb.insertMessageInbox( + IncomingTextMessage( + Address.fromSerialized(senderId), + 1, + (timestampNow - (index * 5000)), + (0 until messageWords) + .map { wordContent.random(cgThreadRandomGenerator.asKotlinRandom()) } + .joinToString(), + Optional.absent(), + 0, + false, + -1 + ), + (timestampNow - (index * 5000)), + false, + false + ) + } + else { + smsDb.insertMessageOutbox( + threadId, + OutgoingTextMessage( + threadDb.getRecipientForThreadId(threadId), + (0 until messageWords) + .map { wordContent.random(cgThreadRandomGenerator.asKotlinRandom()) } + .joinToString(), + 0, + -1, + (timestampNow - (index * 5000)) + ), + (timestampNow - (index * 5000)), + false + ) + } + } + + logProgress("Closed Group Thread $threadIndex", "Done") + } + + cgThreadIndex += chunkSize + } + logProgress("Closed Group Threads", "Done") + + // --Open Group + + val ogThreadRandomGenerator: SecureRandom = SecureRandom(cgRandomSeed.toByteArray()) + var ogThreadIndex: Int = 0 + logProgress("Open Group Threads", "Start Generating $openGroupThreadCount threads") + + while (ogThreadIndex < openGroupThreadCount) { + val remainingThreads: Int = (openGroupThreadCount - ogThreadIndex) + + (0 until Math.min(chunkSize, remainingThreads)).forEach { index -> + val threadIndex: Int = (ogThreadIndex + index) + + logProgress("Open Group Thread $threadIndex", "Start") + + val dataBytes = (0 until 32).map { ogThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } + val randomGroupPublicKey: String = KeyPairUtilities.generate(dataBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey + val serverNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15)) + val roomNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15)) + val roomDescriptionLength: Int = (10 + ogThreadRandomGenerator.nextInt(40)) + val serverName: String = (0 until serverNameLength) + .map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) } + .joinToString() + val roomName: String = (0 until roomNameLength) + .map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) } + .joinToString() + val roomDescription: String = (0 until roomDescriptionLength) + .map { stringContent.random(ogThreadRandomGenerator.asKotlinRandom()) } + .joinToString() + val numGroupMembers: Int = ogThreadRandomGenerator.nextInt(250) + val numMessages: Int = ( + messageRangePerThread[threadIndex % messageRangePerThread.count()].first + + ogThreadRandomGenerator.nextInt(messageRangePerThread[threadIndex % messageRangePerThread.count()].last()) + ) + + // Generate the Contacts in the group + val members: MutableList = mutableListOf(userSessionId) + logProgress("Open Group Thread $threadIndex", "Generate $numGroupMembers Contacts") + + (0 until numGroupMembers).forEach { + val contactBytes = (0 until 16).map { ogThreadRandomGenerator.nextInt(UByte.MAX_VALUE.toInt()).toByte() } + val randomSessionId: String = KeyPairUtilities.generate(contactBytes.toByteArray()).x25519KeyPair.hexEncodedPublicKey + val contactNameLength: Int = (5 + ogThreadRandomGenerator.nextInt(15)) + + val recipient = Recipient.from(context, Address.fromSerialized(randomSessionId), false) + val contact = Contact(randomSessionId) + contactDb.setContact(contact) + recipientDb.setApproved(recipient, true) + recipientDb.setApprovedMe(recipient, true) + + contact.name = (0 until ogThreadRandomGenerator.nextInt(contactNameLength)) + .map { stringContent.random(cgThreadRandomGenerator.asKotlinRandom()) } + .joinToString() + recipientDb.setProfileName(recipient, contact.name) + contactDb.setContact(contact) + members.add(randomSessionId) + } + + // Create the open group model and the thread + val openGroupId = "$serverName.$roomName" + val threadId = GroupManager.createOpenGroup(openGroupId, context, null, roomName).threadId + val hasBlinding: Boolean = ogThreadRandomGenerator.nextBoolean() + + // Generate the capabilities and other data + storage.setOpenGroupPublicKey(serverName, randomGroupPublicKey) + storage.setServerCapabilities( + serverName, + ( + listOf(OpenGroupApi.Capability.SOGS.name.lowercase()) + + if (hasBlinding) { listOf(OpenGroupApi.Capability.BLIND.name.lowercase()) } else { emptyList() } + ) + ) + storage.setUserCount(roomName, serverName, numGroupMembers) + lokiThreadDB.setOpenGroupChat(OpenGroup(server = serverName, room = roomName, publicKey = randomGroupPublicKey, name = roomName, imageId = null, infoUpdates = 0), threadId) + + // Generate the message history (Note: Unapproved message requests will only include incoming messages) + logProgress("Open Group Thread $threadIndex", "Generate $numMessages Messages") + + (0 until numMessages).forEach { index -> + val messageWords: Int = (1 + ogThreadRandomGenerator.nextInt(19)) + val senderId: String = members.random(ogThreadRandomGenerator.asKotlinRandom()) + + if (senderId != userSessionId) { + smsDb.insertMessageInbox( + IncomingTextMessage( + Address.fromSerialized(senderId), + 1, + (timestampNow - (index * 5000)), + (0 until messageWords) + .map { wordContent.random(ogThreadRandomGenerator.asKotlinRandom()) } + .joinToString(), + Optional.absent(), + 0, + false, + -1 + ), + (timestampNow - (index * 5000)), + false, + false + ) + } else { + smsDb.insertMessageOutbox( + threadId, + OutgoingTextMessage( + threadDb.getRecipientForThreadId(threadId), + (0 until messageWords) + .map { wordContent.random(ogThreadRandomGenerator.asKotlinRandom()) } + .joinToString(), + 0, + -1, + (timestampNow - (index * 5000)) + ), + (timestampNow - (index * 5000)), + false + ) + } + } + + logProgress("Open Group Thread $threadIndex", "Done") + } + + ogThreadIndex += chunkSize + } + + logProgress("Open Group Threads", "Done") + logProgress("", "Complete") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/PopupMenuUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/PopupMenuUtil.kt deleted file mode 100644 index a1105cfff3..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/PopupMenuUtil.kt +++ /dev/null @@ -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.") - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index 006da2b63e..b7a9b6fd65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -1,7 +1,9 @@ package org.thoughtcrime.securesms.webrtc import android.content.Context +import android.content.pm.PackageManager import android.telephony.TelephonyManager +import androidx.core.content.ContextCompat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.serialization.json.Json @@ -176,8 +178,22 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va _callStateEvents.value = newState } - fun isBusy(context: Context, callId: UUID) = callId != this.callId && (currentConnectionState != CallState.Idle - || context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE) + fun isBusy(context: Context, callId: UUID): Boolean { + // Make sure we have the permission before accessing the callState + if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + return ( + callId != this.callId && ( + currentConnectionState != CallState.Idle || + context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE + ) + ) + } + + return ( + callId != this.callId && + currentConnectionState != CallState.Idle + ) + } fun isPreOffer() = currentConnectionState == CallState.RemotePreOffer diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt index a66dd591f6..bb41c7c971 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt @@ -12,6 +12,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.utilities.WebRtcUtils +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient @@ -29,6 +30,10 @@ import org.webrtc.IceCandidate class CallMessageProcessor(private val context: Context, private val textSecurePreferences: TextSecurePreferences, lifecycle: Lifecycle, private val storage: StorageProtocol) { + companion object { + private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L + } + init { lifecycle.coroutineScope.launch(IO) { while (isActive) { @@ -53,6 +58,13 @@ class CallMessageProcessor(private val context: Context, private val textSecureP } continue } + + val isVeryExpired = (nextMessage.sentTimestamp?:0) + VERY_EXPIRED_TIME < SnodeAPI.nowWithOffset + if (isVeryExpired) { + Log.e("Loki", "Dropping very expired call message") + continue + } + when (nextMessage.type) { OFFER -> incomingCall(nextMessage) ANSWER -> incomingAnswer(nextMessage) @@ -120,7 +132,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, callTime = callMessage.sentTimestamp!! ) - ContextCompat.startForegroundService(context, incomingIntent) + context.startService(incomingIntent) } private fun incomingCall(callMessage: CallMessage) { @@ -134,8 +146,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP callId = callId, callTime = callMessage.sentTimestamp!! ) - ContextCompat.startForegroundService(context, incomingIntent) - + context.startService(incomingIntent) } private fun CallMessage.iceCandidates(): List { diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt index 955356c7d1..09db0022d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallServiceReceivers.kt @@ -3,8 +3,11 @@ package org.thoughtcrime.securesms.webrtc import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Build import android.telephony.PhoneStateListener +import android.telephony.TelephonyCallback import android.telephony.TelephonyManager +import androidx.annotation.RequiresApi import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.service.WebRtcCallService 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() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_SCREEN_OFF == intent.action) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt index 2b4d34807c..67514c58be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.IntentFilter import android.media.AudioManager import android.media.SoundPool -import android.os.Build import android.os.HandlerThread import network.loki.messenger.R import org.session.libsignal.utilities.Log @@ -108,7 +107,7 @@ class SignalAudioManager(private val context: Context, updateAudioDeviceState() 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt index 84a36ee821..0a80cacef8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt @@ -2,14 +2,15 @@ package org.thoughtcrime.securesms.webrtc.audio import android.Manifest import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothHeadset import android.bluetooth.BluetoothProfile import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.media.AudioManager +import androidx.core.app.ActivityCompat import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import java.util.concurrent.TimeUnit @@ -80,7 +81,6 @@ class SignalBluetoothManager( bluetoothReceiver = BluetoothHeadsetBroadcastReceiver() 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") state = State.UNAVAILABLE } @@ -161,7 +161,8 @@ class SignalBluetoothManager( 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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java index a7fac62bbc..59c05af91b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/locks/LockManager.java @@ -49,7 +49,7 @@ public class LockManager { partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:partial"); 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"); fullLock.setReferenceCounted(false); diff --git a/app/src/main/res/drawable-hdpi/ic_delivery_status_failed.png b/app/src/main/res/drawable-hdpi/ic_delivery_status_failed.png new file mode 100644 index 0000000000..c5de641a60 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_delivery_status_failed.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_delivery_status_read.png b/app/src/main/res/drawable-hdpi/ic_delivery_status_read.png index 307190cae6..bc55b7dfee 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_delivery_status_read.png and b/app/src/main/res/drawable-hdpi/ic_delivery_status_read.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_delivery_status_sending.png b/app/src/main/res/drawable-hdpi/ic_delivery_status_sending.png index fbdcef3583..1b8991d98b 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_delivery_status_sending.png and b/app/src/main/res/drawable-hdpi/ic_delivery_status_sending.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_delivery_status_sent.png b/app/src/main/res/drawable-hdpi/ic_delivery_status_sent.png index 72b685aee4..96a7b6340c 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_delivery_status_sent.png and b/app/src/main/res/drawable-hdpi/ic_delivery_status_sent.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_delivery_status_failed.png b/app/src/main/res/drawable-mdpi/ic_delivery_status_failed.png new file mode 100644 index 0000000000..b13352d079 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_delivery_status_failed.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_delivery_status_read.png b/app/src/main/res/drawable-mdpi/ic_delivery_status_read.png index eee83ef591..072dac7b30 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_delivery_status_read.png and b/app/src/main/res/drawable-mdpi/ic_delivery_status_read.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_delivery_status_sending.png b/app/src/main/res/drawable-mdpi/ic_delivery_status_sending.png index b34ea32b8b..f9b7fe3b79 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_delivery_status_sending.png and b/app/src/main/res/drawable-mdpi/ic_delivery_status_sending.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_delivery_status_sent.png b/app/src/main/res/drawable-mdpi/ic_delivery_status_sent.png index ff6bf0fac5..26fceea79c 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_delivery_status_sent.png and b/app/src/main/res/drawable-mdpi/ic_delivery_status_sent.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delivery_status_failed.png b/app/src/main/res/drawable-xhdpi/ic_delivery_status_failed.png new file mode 100644 index 0000000000..79aaa03f2b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_delivery_status_failed.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delivery_status_read.png b/app/src/main/res/drawable-xhdpi/ic_delivery_status_read.png index 79c4857f6c..af79508abf 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_delivery_status_read.png and b/app/src/main/res/drawable-xhdpi/ic_delivery_status_read.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delivery_status_sending.png b/app/src/main/res/drawable-xhdpi/ic_delivery_status_sending.png index aca7fe7ef3..74b8694dd4 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_delivery_status_sending.png and b/app/src/main/res/drawable-xhdpi/ic_delivery_status_sending.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delivery_status_sent.png b/app/src/main/res/drawable-xhdpi/ic_delivery_status_sent.png index 811a54373d..094d8b34ce 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_delivery_status_sent.png and b/app/src/main/res/drawable-xhdpi/ic_delivery_status_sent.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delivery_status_failed.png b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_failed.png new file mode 100644 index 0000000000..4ed4e2d8b1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_failed.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delivery_status_read.png b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_read.png index 474a570724..69376c9a20 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_delivery_status_read.png and b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_read.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sending.png b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sending.png index 4a81c629d8..3dce4a05ea 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sending.png and b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sending.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sent.png b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sent.png index f679aaf244..302afb8370 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sent.png and b/app/src/main/res/drawable-xxhdpi/ic_delivery_status_sent.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_failed.png b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_failed.png new file mode 100644 index 0000000000..6780212b8b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_failed.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_read.png b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_read.png index c138886aa3..d0b16705c0 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_read.png and b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_read.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sending.png b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sending.png index 461c2ea636..411dc9d502 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sending.png and b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sending.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sent.png b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sent.png index 47bd9acd33..657d454f6e 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sent.png and b/app/src/main/res/drawable-xxxhdpi/ic_delivery_status_sent.png differ diff --git a/app/src/main/res/drawable/ic_filled_circle_check.xml b/app/src/main/res/drawable/ic_filled_circle_check.xml index 06c3466fd0..99589252b1 100644 --- a/app/src/main/res/drawable/ic_filled_circle_check.xml +++ b/app/src/main/res/drawable/ic_filled_circle_check.xml @@ -8,6 +8,6 @@ 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"/> diff --git a/app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml b/app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml new file mode 100644 index 0000000000..5f21434d45 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_message_detail.xml b/app/src/main/res/layout/activity_message_detail.xml index c5a7f12bf1..49c1af54e3 100644 --- a/app/src/main/res/layout/activity_message_detail.xml +++ b/app/src/main/res/layout/activity_message_detail.xml @@ -69,6 +69,7 @@ - - - - - - + android:layout_height="match_parent" /> @@ -35,4 +28,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/album_thumbnail_1.xml b/app/src/main/res/layout/album_thumbnail_1.xml index cf0f5d4892..cee81ba3e3 100644 --- a/app/src/main/res/layout/album_thumbnail_1.xml +++ b/app/src/main/res/layout/album_thumbnail_1.xml @@ -6,7 +6,7 @@ android:layout_width="@dimen/media_bubble_default_dimens" android:layout_height="@dimen/media_bubble_default_dimens"> - - - - - - - @@ -20,4 +21,4 @@ android:layout_gravity="center" android:layout="@layout/transfer_controls_stub" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/contact_selection_list_fragment.xml b/app/src/main/res/layout/contact_selection_list_fragment.xml index 7a297bab2c..48221f632f 100644 --- a/app/src/main/res/layout/contact_selection_list_fragment.xml +++ b/app/src/main/res/layout/contact_selection_list_fragment.xml @@ -18,35 +18,13 @@ - - - - - - - - - - - + android:clipToPadding="false" + android:scrollbars="vertical" + tools:listitem="@layout/view_user"/> diff --git a/app/src/main/res/layout/link_preview.xml b/app/src/main/res/layout/link_preview.xml deleted file mode 100644 index f76ad1010c..0000000000 --- a/app/src/main/res/layout/link_preview.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/media_overview_gallery_item.xml b/app/src/main/res/layout/media_overview_gallery_item.xml index 6072611758..a4c3f324af 100644 --- a/app/src/main/res/layout/media_overview_gallery_item.xml +++ b/app/src/main/res/layout/media_overview_gallery_item.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent" android:padding="2dp"> - - - + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/transparent_black_6"> - + diff --git a/app/src/main/res/layout/view_conversation.xml b/app/src/main/res/layout/view_conversation.xml index 8f26f17c77..04833b6a96 100644 --- a/app/src/main/res/layout/view_conversation.xml +++ b/app/src/main/res/layout/view_conversation.xml @@ -34,64 +34,69 @@ android:layout_gravity="center_vertical" android:orientation="vertical"> - + android:layout_height="wrap_content"> - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/unreadCountIndicator" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constrainedWidth="true" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintHorizontal_bias="0" + android:drawablePadding="4dp" + android:maxLines="1" + android:ellipsize="end" + android:textAlignment="viewStart" + android:textSize="@dimen/medium_font_size" + android:textStyle="bold" + android:textColor="?android:textColorPrimary" + app:drawableTint="?conversation_pinned_icon_color" + tools:drawableRight="@drawable/ic_pin" + tools:text="I'm a very long display name. What are you going to do about it?" /> + + + tools:text="8" + tools:textColor="?android:textColorPrimary" /> - - - - - - - + - + - - - @@ -65,4 +66,4 @@ android:visibility="gone" app:constraint_referenced_ids="image_view_show_less, text_view_show_less"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/view_link_preview.xml b/app/src/main/res/layout/view_link_preview.xml index 096ff5dac9..dd2e133bea 100644 --- a/app/src/main/res/layout/view_link_preview.xml +++ b/app/src/main/res/layout/view_link_preview.xml @@ -1,54 +1,47 @@ - + android:orientation="horizontal" + android:gravity="center"> - + - + - - - - - - - + android:scaleType="centerCrop" /> - + - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_link_preview_draft.xml b/app/src/main/res/layout/view_link_preview_draft.xml index 65e2cf7fd5..bf7cd3ebb3 100644 --- a/app/src/main/res/layout/view_link_preview_draft.xml +++ b/app/src/main/res/layout/view_link_preview_draft.xml @@ -26,7 +26,7 @@ android:src="@drawable/ic_link" app:tint="?android:textColorPrimary" /> - - - - + android:layout_height="match_parent"> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/view_visible_message.xml b/app/src/main/res/layout/view_visible_message.xml index 48c2d1d8e0..c07ca8bd71 100644 --- a/app/src/main/res/layout/view_visible_message.xml +++ b/app/src/main/res/layout/view_visible_message.xml @@ -1,5 +1,6 @@ - - - - @@ -104,32 +100,37 @@ - - + + - - - - + app:layout_constraintTop_toBottomOf="@+id/emojiReactionsView" + tools:tint="@color/classic_dark_1" + android:src="@drawable/ic_delivery_status_sent" /> diff --git a/app/src/main/res/layout/view_visible_message_content.xml b/app/src/main/res/layout/view_visible_message_content.xml index edd5e5cbfa..7b09faf832 100644 --- a/app/src/main/res/layout/view_visible_message_content.xml +++ b/app/src/main/res/layout/view_visible_message_content.xml @@ -1,5 +1,5 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/view_voice_message.xml b/app/src/main/res/layout/view_voice_message.xml index 535c7f2349..9895ad95a4 100644 --- a/app/src/main/res/layout/view_voice_message.xml +++ b/app/src/main/res/layout/view_voice_message.xml @@ -11,7 +11,7 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_alignParentStart="true" - android:background="@color/transparent_black_6" /> + android:background="@color/transparent_black_30" /> - - + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 888b0af633..f0d04162bc 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -164,6 +164,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8bde853c73..d0ee94cf1d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -864,4 +864,10 @@ Join Navigate Back Close Dialog + Database Upgrade Failed + Please contact support to report the error. + Sending + Read + Sent + Failed to send diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c97cb9e740..5f9ee03168 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -154,6 +154,7 @@ @@ -533,6 +537,7 @@ @color/ocean_dark_6 ?colorAccent @color/ocean_dark_0 + @color/ocean_dark_5 @color/ocean_dark_1 @color/ocean_dark_5 @color/ocean_dark_6 @@ -548,6 +553,7 @@ @color/ocean_dark_4 ?colorPrimary @color/ocean_dark_4 + @color/ocean_dark_1 @@ -615,6 +621,7 @@ @color/ocean_light_0 ?colorAccent @color/ocean_light_0 + @color/ocean_light_2 @color/ocean_light_6 @color/ocean_light_1 @color/ocean_light_0 @@ -633,6 +640,7 @@ ?colorCellBackground @color/ocean_light_5 ?android:textColorSecondary + @color/ocean_light_5 diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt index fa02721558..fdaff22d8f 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt @@ -37,15 +37,9 @@ class ConversationViewModelTest: BaseViewModelTest() { @Before fun setUp() { recipient = mock(Recipient::class.java) - whenever(repository.isOxenHostedOpenGroup(anyLong())).thenReturn(true) whenever(repository.maybeGetRecipientForThreadId(anyLong())).thenReturn(recipient) } - @Test - fun `should emit group type on init`() = runBlockingTest { - assertTrue(viewModel.uiState.first().isOxenHostedOpenGroup) - } - @Test fun `should save draft message`() { val draft = "Hi there" diff --git a/build.gradle b/build.gradle index a318cea58d..7e7e14f00f 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ buildscript { mavenCentral() } 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 "com.google.gms:google-services:4.3.10" + classpath "com.google.gms:google-services:$googleServicesVersion" classpath files('libs/gradle-witness.jar') } } @@ -51,6 +51,7 @@ allprojects { project.ext { androidMinimumSdkVersion = 23 - androidCompileSdkVersion = 30 + androidTargetSdkVersion = 31 + androidCompileSdkVersion = 32 } } \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 2bce14c43b..a039f0df97 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -6,5 +6,5 @@ repositories { } dependencies { - implementation 'com.android.tools.build:apksig:4.0.1' + implementation 'com.android.tools.build:apksig:4.0.2' } diff --git a/gradle.properties b/gradle.properties index d8668cfa1d..fa51fdbca0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,13 @@ android.useAndroidX=true android.enableJetifier=true -org.gradle.jvmargs=-Xmx4g +org.gradle.jvmargs=-Xmx8g -kotlinVersion=1.6.0 -coroutinesVersion=1.6.0 -kotlinxJsonVersion=1.3.0 -lifecycleVersion=2.3.1 +gradlePluginVersion=7.3.1 +googleServicesVersion=4.3.12 +kotlinVersion=1.6.21 +coroutinesVersion=1.6.4 +kotlinxJsonVersion=1.3.3 +lifecycleVersion=2.5.1 daggerVersion=2.40.1 glideVersion=4.11.0 kovenantVersion=3.3.0 @@ -13,4 +15,12 @@ curve25519Version=0.6.0 protobufVersion=2.5.0 okhttpVersion=3.12.1 jacksonDatabindVersion=2.9.8 -mockitoKotlinVersion=4.0.0 \ No newline at end of file +appcompatVersion=1.5.1 +materialVersion=1.7.0 +preferenceVersion=1.2.0 +coreVersion=1.8.0 + +junitVersion=4.13.2 +mockitoKotlinVersion=4.0.0 +testCoreVersion=1.4.0 +pagingVersion=3.0.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ea8f956162..cd825d0848 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Dec 30 07:09:53 SAST 2021 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 zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/libsession/build.gradle b/libsession/build.gradle index 0515b3562c..dd8959958e 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -19,17 +19,16 @@ android { dependencies { implementation project(":libsignal") implementation project(":liblazysodium") -// implementation 'com.goterl:lazysodium-android:5.0.2@aar' implementation "net.java.dev.jna:jna:5.8.0@aar" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.preference:preference-ktx:1.1.1' - implementation 'com.google.android.material:material:1.2.1' + implementation "androidx.core:core-ktx:$coreVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "androidx.preference:preference-ktx:$preferenceVersion" + implementation "com.google.android.material:material:$materialVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.google.dagger:hilt-android:$daggerVersion" - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation "com.github.bumptech.glide:glide:$glideVersion" implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.annimon:stream:1.1.8' @@ -43,7 +42,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" 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.mockito:mockito-inline:4.0.0" 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-rule: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 "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0" diff --git a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java index 2bcd32ac24..a448b3f7a7 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java +++ b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java @@ -12,7 +12,6 @@ import androidx.annotation.DrawableRes; import com.amulyakhare.textdrawable.TextDrawable; import com.makeramen.roundedimageview.RoundedDrawable; - import org.session.libsession.R; import org.session.libsession.utilities.ThemeUtil; @@ -34,7 +33,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto { Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color); RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId)); - foreground.setScaleType(ImageView.ScaleType.CENTER); + foreground.setScaleType(ImageView.ScaleType.CENTER_INSIDE); if (inverted) { foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index eb40df6e09..9adf6b9327 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -20,7 +20,9 @@ interface MessageDataProvider { * @return pair of sms or mms table-specific ID and whether it is in SMS table */ fun getMessageID(serverId: Long, threadId: Long): Pair? + fun getMessageIDs(serverIDs: List, threadID: Long): Pair, List> fun deleteMessage(messageID: Long, isSms: Boolean) + fun deleteMessages(messageIDs: List, threadId: Long, isSms: Boolean) fun updateMessageAsDeleted(timestamp: Long, author: String) fun getServerHashForMessage(messageID: Long): String? fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index f35e5de0d4..d10f519513 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -12,10 +12,12 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.visible.Attachment +import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.GroupMember import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage @@ -34,9 +36,7 @@ interface StorageProtocol { // General fun getUserPublicKey(): String? fun getUserX25519KeyPair(): ECKeyPair - fun getUserDisplayName(): String? - fun getUserProfileKey(): ByteArray? - fun getUserProfilePictureURL(): String? + fun getUserProfile(): Profile fun setUserProfilePictureURL(newProfilePicture: String) // Signal fun getOrGenerateRegistrationID(): Int @@ -66,7 +66,7 @@ interface StorageProtocol { fun getAllOpenGroups(): Map fun updateOpenGroup(openGroup: OpenGroup) fun getOpenGroup(threadId: Long): OpenGroup? - fun addOpenGroup(urlAsString: String) + fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? fun onOpenGroupAdded(server: String) fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) @@ -80,6 +80,7 @@ interface StorageProtocol { // Open Group Metadata fun updateTitle(groupID: String, newValue: String) fun updateProfilePicture(groupID: String, newValue: ByteArray) + fun hasDownloadedProfilePicture(groupID: String): Boolean fun setUserCount(room: String, server: String, newValue: Int) // Last Message Server ID @@ -108,6 +109,7 @@ interface StorageProtocol { fun markAsSent(timestamp: Long, author: String) fun markUnidentified(timestamp: Long, author: String) fun setErrorMessage(timestamp: Long, author: String, error: Exception) + fun clearErrorMessage(messageID: Long) fun setMessageServerHash(messageID: Long, serverHash: String) // Closed Groups diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index b2ca605d2b..01fae1f503 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -77,7 +77,11 @@ object FileServerApi { OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map { it.body ?: throw Error.ParsingFailed }.fail { e -> - Log.e("Loki", "File server request failed.", e) + when (e) { + // No need for the stack trace for HTTP errors + is HTTP.HTTPRequestFailedException -> Log.e("Loki", "File server request failed due to error: ${e.message}") + else -> Log.e("Loki", "File server request failed", e) + } } } else { Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) @@ -96,7 +100,10 @@ object FileServerApi { ) return send(request).map { response -> 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 } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt index aa37e0f0a8..c679724b9f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt @@ -41,15 +41,10 @@ class BackgroundGroupAddJob(val joinUrl: String): Job { } // get image storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey) - val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(openGroup.room, openGroup.server, false).get() - storage.setServerCapabilities(openGroup.server, capabilities.capabilities) - val imageId = info.imageId - storage.addOpenGroup(openGroup.joinUrl()) + val info = storage.addOpenGroup(openGroup.joinUrl()) + val imageId = info?.imageId if (imageId != null) { - val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(openGroup.server, openGroup.room, imageId).get() - val groupId = GroupUtil.getEncodedOpenGroupID("${openGroup.server}.${openGroup.room}".toByteArray()) - storage.updateProfilePicture(groupId, bytes) - storage.updateTimestampUpdated(groupId, System.currentTimeMillis()) + JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.room, openGroup.server)) } Log.d(KEY, "onOpenGroupAdded(${openGroup.server})") storage.onOpenGroupAdded(openGroup.server) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 07c104cfda..38a193b8d2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -94,12 +94,23 @@ class BatchMessageReceiveJob( threadMap[threadID]!! += parsedParams } } catch (e: Exception) { - Log.e(TAG, "Couldn't receive message.", e) - if (e is MessageReceiver.Error && !e.isRetryable) { - Log.e(TAG, "Message failed permanently",e) - } else { - Log.e(TAG, "Message failed",e) - failures += messageParameters + when (e) { + is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> { + Log.i(TAG, "Couldn't receive message, failed with error: ${e.message}") + } + is MessageReceiver.Error -> { + if (!e.isRetryable) { + Log.e(TAG, "Couldn't receive message, failed permanently", e) + } + else { + Log.e(TAG, "Couldn't receive message, failed", e) + failures += messageParameters + } + } + else -> { + Log.e(TAG, "Couldn't receive message, failed", e) + failures += messageParameters + } } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt index 38e8831fba..02f792117c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt @@ -14,10 +14,9 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job { override fun execute() { val storage = MessagingModuleConfiguration.shared.storage + val imageId = storage.getOpenGroup(room, server)?.imageId ?: return try { - val info = OpenGroupApi.getRoomInfo(room, server).get() - val imageId = info.imageId ?: return - val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, info.token, imageId).get() + val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get() val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) storage.updateProfilePicture(groupId, bytes) storage.updateTimestampUpdated(groupId, System.currentTimeMillis()) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 215d20834a..8e46f275f2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -26,7 +26,7 @@ class JobQueue : JobDelegate { private val jobTimestampMap = ConcurrentHashMap() private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() - private val openGroupDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher() + private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob() private val queue = Channel(UNLIMITED) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index cdd9e0a3ac..8ce1adf481 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.OnionRequestAPI +import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log class MessageSendJob(val message: Message, val destination: Destination) : Job { @@ -67,14 +68,25 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { val promise = MessageSender.send(this.message, this.destination).success { this.handleSuccess() }.fail { exception -> - Log.e(TAG, "Couldn't send message due to error: $exception.") - if (exception is MessageSender.Error) { - if (!exception.isRetryable) { this.handlePermanentFailure(exception) } + var logStacktrace = true + + when (exception) { + // No need for the stack trace for HTTP errors + is HTTP.HTTPRequestFailedException -> { + logStacktrace = false + + if (exception.statusCode == 429) { this.handlePermanentFailure(exception) } + else { this.handleFailure(exception) } + } + is MessageSender.Error -> { + if (!exception.isRetryable) { this.handlePermanentFailure(exception) } + else { this.handleFailure(exception) } + } + else -> this.handleFailure(exception) } - if (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 429) { - this.handlePermanentFailure(exception) - } - this.handleFailure(exception) + + if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) } + else { Log.e(TAG, "Couldn't send message due to error: ${exception.message}") } } try { promise.get() diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt index c4180c0025..1fb2d0df22 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt @@ -23,14 +23,27 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider val numberToDelete = messageServerIds.size Log.d(TAG, "Deleting $numberToDelete messages") - var numberDeleted = 0 - messageServerIds.forEach { serverId -> - val (messageId, isSms) = dataProvider.getMessageID(serverId, threadId) ?: return@forEach - dataProvider.deleteMessage(messageId, isSms) - numberDeleted++ + + // FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded) + try { + val messageIds = dataProvider.getMessageIDs(messageServerIds.toList(), threadId) + + // Delete the SMS messages + if (messageIds.first.isNotEmpty()) { + dataProvider.deleteMessages(messageIds.first, threadId, true) + } + + // Delete the MMS messages + if (messageIds.second.isNotEmpty()) { + dataProvider.deleteMessages(messageIds.second, threadId, false) + } + + Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully") + delegate?.handleJobSucceeded(this) + } + catch (e: Exception) { + delegate?.handleJobFailed(this, e) } - Log.d(TAG, "Deleted $numberDeleted messages successfully") - delegate?.handleJobSucceeded(this) } override fun serialize(): Data = Data.Builder() diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt index f5a65e4ca4..614a6eb811 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt @@ -1,15 +1,22 @@ package org.session.libsession.messaging.messages.control +import com.google.protobuf.ByteString +import org.session.libsession.messaging.messages.visible.Profile import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.Log -class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() { +class MessageRequestResponse(val isApproved: Boolean, var profile: Profile? = null) : ControlMessage() { override val isSelfSendValid: Boolean = true override fun toProto(): SignalServiceProtos.Content? { + val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder() + profile?.displayName?.let { profileProto.displayName = it } + profile?.profilePictureURL?.let { profileProto.profilePicture = it } val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder() .setIsApproved(isApproved) + .setProfile(profileProto.build()) + profile?.profileKey?.let { messageRequestResponseProto.profileKey = ByteString.copyFrom(it) } return try { SignalServiceProtos.Content.newBuilder() .setMessageRequestResponse(messageRequestResponseProto.build()) @@ -26,7 +33,13 @@ class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() { fun fromProto(proto: SignalServiceProtos.Content): MessageRequestResponse? { val messageRequestResponseProto = if (proto.hasMessageRequestResponse()) proto.messageRequestResponse else return null val isApproved = messageRequestResponseProto.isApproved - return MessageRequestResponse(isApproved) + val profileProto = messageRequestResponseProto.profile + val profile = Profile().apply { + displayName = profileProto.displayName + profileKey = if (messageRequestResponseProto.hasProfileKey()) messageRequestResponseProto.profileKey.toByteArray() else null + profilePictureURL = profileProto.profilePicture + } + return MessageRequestResponse(isApproved, profile) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index cf792e6a84..ce6b61524c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -25,7 +25,7 @@ class Profile() { } } - internal constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() { + constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() { this.displayName = displayName this.profileKey = profileKey this.profilePictureURL = profilePictureURL diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt index 9efeaf15d0..6a2a771e08 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt @@ -11,16 +11,19 @@ data class OpenGroup( val id: String, val name: String, val publicKey: String, + val imageId: String?, val infoUpdates: Int, + val canWrite: Boolean, ) { - - constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String) : this( + constructor(server: String, room: String, publicKey: String, name: String, imageId: String?, canWrite: Boolean, infoUpdates: Int) : this( server = server, room = room, id = "$server.$room", name = name, publicKey = publicKey, + imageId = imageId, infoUpdates = infoUpdates, + canWrite = canWrite ) companion object { @@ -29,13 +32,14 @@ data class OpenGroup( return try { val json = JsonUtil.fromJson(jsonAsString) if (!json.has("room")) return null - val room = json.get("room").asText().toLowerCase(Locale.US) - val server = json.get("server").asText().toLowerCase(Locale.US) + val room = json.get("room").asText().lowercase(Locale.US) + val server = json.get("server").asText().lowercase(Locale.US) val displayName = json.get("displayName").asText() val publicKey = json.get("publicKey").asText() + val imageId = json.get("imageId")?.asText() + val canWrite = json.get("canWrite")?.asText()?.toBoolean() ?: true val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0 - val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList() - OpenGroup(server, room, displayName, infoUpdates, publicKey) + OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, canWrite = canWrite, infoUpdates = infoUpdates) } catch (e: Exception) { Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e); null @@ -53,12 +57,14 @@ data class OpenGroup( } } - fun toJson(): Map = mapOf( + fun toJson(): Map = mapOf( "room" to room, "server" to server, - "displayName" to name, "publicKey" to publicKey, + "displayName" to name, + "imageId" to imageId, "infoUpdates" to infoUpdates.toString(), + "canWrite" to canWrite.toString() ) val joinURL: String get() = "$server/$room?public_key=$publicKey" diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index daa735aa4c..46eff4b03b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -91,7 +91,7 @@ object OpenGroupApi { val created: Long = 0, val activeUsers: Int = 0, val activeUsersCutoff: Int = 0, - val imageId: Long? = null, + val imageId: String? = null, val pinnedMessages: List = emptyList(), val admin: Boolean = false, val globalAdmin: Boolean = false, @@ -148,7 +148,7 @@ object OpenGroupApi { ) enum class Capability { - BLIND, REACTIONS + SOGS, BLIND, REACTIONS } @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) @@ -337,7 +337,7 @@ object OpenGroupApi { .plus(request.verb.rawValue.toByteArray()) .plus("/${request.endpoint.value}".toByteArray()) .plus(bodyHash) - if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) { + if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) { SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair -> pubKey = SessionId( IdPrefix.BLINDED, @@ -383,7 +383,11 @@ object OpenGroupApi { } return if (request.useOnionRouting) { OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e -> - Log.e("SOGS", "Failed onion request", e) + when (e) { + // No need for the stack trace for HTTP errors + is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}") + else -> Log.e("SOGS", "Failed onion request", e) + } } } else { Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) @@ -395,13 +399,13 @@ object OpenGroupApi { fun downloadOpenGroupProfilePicture( server: String, roomID: String, - imageId: Long + imageId: String ): Promise { val request = Request( verb = GET, room = roomID, server = server, - endpoint = Endpoint.RoomFileIndividual(roomID, imageId.toString()) + endpoint = Endpoint.RoomFileIndividual(roomID, imageId) ) return getResponseBody(request) } @@ -794,16 +798,14 @@ object OpenGroupApi { private fun sequentialBatch( server: String, - requests: MutableList>, - authRequired: Boolean = true + requests: MutableList> ): Promise>, Exception> { val request = Request( verb = POST, room = null, server = server, endpoint = Endpoint.Sequence, - parameters = requests.map { it.request }, - isAuthRequired = authRequired + parameters = requests.map { it.request } ) return getBatchResponseJson(request, requests) } @@ -912,8 +914,7 @@ object OpenGroupApi { fun getCapabilitiesAndRoomInfo( room: String, - server: String, - authRequired: Boolean = true + server: String ): Promise, Exception> { val requests = mutableListOf>( BatchRequestInfo( @@ -933,7 +934,7 @@ object OpenGroupApi { responseType = object : TypeReference(){} ) ) - return sequentialBatch(server, requests, authRequired).map { + return sequentialBatch(server, requests).map { val capabilities = it.firstOrNull()?.body as? Capabilities ?: throw Error.ParsingFailed val roomInfo = it.lastOrNull()?.body as? RoomInfo ?: throw Error.ParsingFailed capabilities to roomInfo diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index d6a4618d96..3cc23da1e7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -12,9 +12,9 @@ import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.LinkPreview -import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.Quote import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi @@ -32,12 +32,7 @@ import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.SSKEnvironment import org.session.libsignal.crypto.PushTransportDetails import org.session.libsignal.protos.SignalServiceProtos -import org.session.libsignal.utilities.Base64 -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 org.session.libsignal.utilities.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment @@ -118,14 +113,10 @@ object MessageSender { } // Attach the user's profile if needed if (message is VisibleMessage) { - val displayName = storage.getUserDisplayName()!! - val profileKey = storage.getUserProfileKey() - val profilePictureUrl = storage.getUserProfilePictureURL() - if (profileKey != null && profilePictureUrl != null) { - message.profile = Profile(displayName, profileKey, profilePictureUrl) - } else { - message.profile = Profile(displayName) - } + message.profile = storage.getUserProfile() + } + if (message is MessageRequestResponse) { + message.profile = storage.getUserProfile() } // Convert it to protobuf val proto = message.toProto() ?: throw Error.ProtoConversionFailed @@ -257,14 +248,7 @@ object MessageSender { try { // Attach the user's profile if needed if (message is VisibleMessage) { - val displayName = storage.getUserDisplayName()!! - val profileKey = storage.getUserProfileKey() - val profilePictureUrl = storage.getUserProfilePictureURL() - if (profileKey != null && profilePictureUrl != null) { - message.profile = Profile(displayName, profileKey, profilePictureUrl) - } else { - message.profile = Profile(displayName) - } + message.profile = storage.getUserProfile() } when (destination) { is Destination.OpenGroup -> { @@ -337,6 +321,8 @@ object MessageSender { message.serverHash?.let { storage.setMessageServerHash(messageID, it) } + // in case any errors from previous sends + storage.clearErrorMessage(messageID) // Track the open group server message ID if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) { val server: String diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 86b856d952..f2fdc6703b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -307,6 +307,8 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, return@mapNotNull attachment } } + // Cancel any typing indicators if needed + cancelTypingIndicatorsIfNeeded(message.sender!!) // Parse reaction if needed val threadIsGroup = threadRecipient?.isGroupRecipient == true message.reaction?.let { reaction -> @@ -332,8 +334,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, } return messageID } - // Cancel any typing indicators if needed - cancelTypingIndicatorsIfNeeded(message.sender!!) return null } @@ -423,7 +423,7 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) { val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false) - if (!recipient.isApproved) return + if (!recipient.isApproved && !recipient.isLocalNumber) return val groupPublicKey = kind.publicKey.toByteArray().toHexString() val members = kind.members.map { it.toByteArray().toHexString() } val admins = kind.admins.map { it.toByteArray().toHexString() } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java index 8aa8102a60..b2b7cfc7d4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java @@ -13,6 +13,7 @@ import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.guava.Optional; import java.io.IOException; +import java.util.Objects; public class LinkPreview { @@ -75,4 +76,17 @@ public class LinkPreview { public static LinkPreview deserialize(@NonNull String serialized) throws IOException { 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); + } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 145155e97c..353b4e1bf5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -59,7 +59,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S fun poll(isPostCapabilitiesRetry: Boolean = false): Promise { val storage = MessagingModuleConfiguration.shared.storage val rooms = storage.getAllOpenGroups().values.filter { it.server == server }.map { it.room } - rooms.forEach { downloadGroupAvatarIfNeeded(it) } + return OpenGroupApi.poll(rooms, server).successBackground { responses -> responses.filterNot { it.body == null }.forEach { response -> when (response.endpoint) { @@ -117,15 +117,18 @@ class OpenGroupPoller(private val server: String, private val executorService: S ) { val storage = MessagingModuleConfiguration.shared.storage val groupId = "$server.$roomToken" + val dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray()) val existingOpenGroup = storage.getOpenGroup(roomToken, server) val publicKey = existingOpenGroup?.publicKey ?: return val openGroup = OpenGroup( server = server, room = pollInfo.token, - name = pollInfo.details?.name ?: "", - infoUpdates = pollInfo.details?.infoUpdates ?: 0, + name = if (pollInfo.details != null) { pollInfo.details.name } else { existingOpenGroup.name }, publicKey = publicKey, + imageId = if (pollInfo.details != null) { pollInfo.details.imageId } else { existingOpenGroup.imageId }, + canWrite = pollInfo.write, + infoUpdates = if (pollInfo.details != null) { pollInfo.details.infoUpdates } else { existingOpenGroup.infoUpdates } ) // - Open Group changes storage.updateOpenGroup(openGroup) @@ -155,6 +158,22 @@ class OpenGroupPoller(private val server: String, private val executorService: S GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN) }) } + + if ( + ( + pollInfo.details != null && + pollInfo.details.imageId != null && ( + pollInfo.details.imageId != existingOpenGroup.imageId || + !storage.hasDownloadedProfilePicture(dbGroupId) + ) + ) || ( + pollInfo.details == null && + existingOpenGroup.imageId != null && + !storage.hasDownloadedProfilePicture(dbGroupId) + ) + ) { + JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server)) + } } private fun handleMessages( @@ -284,16 +303,4 @@ class OpenGroupPoller(private val server: String, private val executorService: S JobQueue.shared.add(deleteJob) } } - - private fun downloadGroupAvatarIfNeeded(room: String) { - val storage = MessagingModuleConfiguration.shared.storage - if (storage.getGroupAvatarDownloadJob(server, room) != null) return - val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) - storage.getGroup(groupId)?.let { - if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) { - JobQueue.shared.add(GroupAvatarDownloadJob(room, server)) - } - } - } - } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index f93a7b243e..bcce887a5a 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -78,8 +78,8 @@ object OnionRequestAPI { // endregion class HTTPRequestFailedBlindingRequiredException(statusCode: Int, json: Map<*, *>, destination: String): HTTPRequestFailedAtDestinationException(statusCode, json, destination) - open class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>, val destination: String) - : Exception("HTTP request failed at destination ($destination) with status code $statusCode.") + open class HTTPRequestFailedAtDestinationException(statusCode: Int, json: Map<*, *>, val destination: String) + : HTTP.HTTPRequestFailedException(statusCode, json, "HTTP request failed at destination ($destination) with status code $statusCode.") class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.") private data class OnionBuildingResult( diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 9a17958952..8077594643 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -56,6 +56,10 @@ object SnodeAPI { * user's clock is incorrect. */ internal var clockOffset = 0L + + val nowWithOffset + get() = System.currentTimeMillis() + clockOffset + internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue -> if (newValue > oldValue) { Log.d("Loki", "Setting new fork info new: $newValue, old: $oldValue") diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index 0a61d1ede0..b850baa253 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -2,6 +2,7 @@ package org.session.libsession.utilities import okhttp3.HttpUrl import org.session.libsession.messaging.file_server.FileServerApi +import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log import java.io.* @@ -40,7 +41,11 @@ object DownloadUtilities { outputStream.write(it) } } catch (e: Exception) { - Log.e("Loki", "Couldn't download attachment.", e) + when (e) { + // No need for the stack trace for HTTP errors + is HTTP.HTTPRequestFailedException -> Log.e("Loki", "Couldn't download attachment due to error: ${e.message}") + else -> Log.e("Loki", "Couldn't download attachment", e) + } throw e } } diff --git a/libsignal/build.gradle b/libsignal/build.gradle index 681fdfa12d..1ea5f2de04 100644 --- a/libsignal/build.gradle +++ b/libsignal/build.gradle @@ -15,16 +15,16 @@ android { } dependencies { - implementation "androidx.annotation:annotation:1.2.0" + implementation "androidx.annotation:annotation:1.5.0" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$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" - testImplementation "junit:junit:3.8.2" + testImplementation "junit:junit:$junitVersion" testImplementation "org.assertj:assertj-core:1.7.1" testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0" } diff --git a/libsignal/protobuf/SignalService.proto b/libsignal/protobuf/SignalService.proto index e1c1c856d9..50c5218334 100644 --- a/libsignal/protobuf/SignalService.proto +++ b/libsignal/protobuf/SignalService.proto @@ -233,7 +233,9 @@ message ConfigurationMessage { message MessageRequestResponse { // @required - required bool isApproved = 1; // Whether the request was approved + required bool isApproved = 1; + optional bytes profileKey = 2; + optional DataMessage.LokiProfile profile = 3; } message ReceiptMessage { diff --git a/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt b/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt index a1866bf21e..18880f5538 100644 --- a/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt +++ b/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt @@ -16,8 +16,10 @@ interface LokiAPIDatabaseProtocol { fun setSwarm(publicKey: String, newValue: Set) fun getLastMessageHashValue(snode: Snode, publicKey: String, namespace: Int): String? fun setLastMessageHashValue(snode: Snode, publicKey: String, newValue: String, namespace: Int) + fun clearAllLastMessageHashes() fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set? fun setReceivedMessageHashValues(publicKey: String, newValue: Set, namespace: Int) + fun clearReceivedMessageHashValues() fun getAuthToken(server: String): String? fun setAuthToken(server: String, newValue: String?) fun setUserCount(room: String, server: String, newValue: Int) diff --git a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java index ead1b6255e..7c44087f83 100644 --- a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java +++ b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java @@ -21504,6 +21504,30 @@ public final class SignalServiceProtos { * */ boolean getIsApproved(); + + // optional bytes profileKey = 2; + /** + * optional bytes profileKey = 2; + */ + boolean hasProfileKey(); + /** + * optional bytes profileKey = 2; + */ + com.google.protobuf.ByteString getProfileKey(); + + // optional .signalservice.DataMessage.LokiProfile profile = 3; + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + boolean hasProfile(); + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile getProfile(); + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfileOrBuilder getProfileOrBuilder(); } /** * Protobuf type {@code signalservice.MessageRequestResponse} @@ -21561,6 +21585,24 @@ public final class SignalServiceProtos { isApproved_ = input.readBool(); break; } + case 18: { + bitField0_ |= 0x00000002; + profileKey_ = input.readBytes(); + break; + } + case 26: { + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder subBuilder = null; + if (((bitField0_ & 0x00000004) == 0x00000004)) { + subBuilder = profile_.toBuilder(); + } + profile_ = input.readMessage(org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.PARSER, extensionRegistry); + if (subBuilder != null) { + subBuilder.mergeFrom(profile_); + profile_ = subBuilder.buildPartial(); + } + bitField0_ |= 0x00000004; + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -21625,8 +21667,48 @@ public final class SignalServiceProtos { return isApproved_; } + // optional bytes profileKey = 2; + public static final int PROFILEKEY_FIELD_NUMBER = 2; + private com.google.protobuf.ByteString profileKey_; + /** + * optional bytes profileKey = 2; + */ + public boolean hasProfileKey() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * optional bytes profileKey = 2; + */ + public com.google.protobuf.ByteString getProfileKey() { + return profileKey_; + } + + // optional .signalservice.DataMessage.LokiProfile profile = 3; + public static final int PROFILE_FIELD_NUMBER = 3; + private org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile profile_; + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public boolean hasProfile() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile getProfile() { + return profile_; + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfileOrBuilder getProfileOrBuilder() { + return profile_; + } + private void initFields() { isApproved_ = false; + profileKey_ = com.google.protobuf.ByteString.EMPTY; + profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance(); } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -21647,6 +21729,12 @@ public final class SignalServiceProtos { if (((bitField0_ & 0x00000001) == 0x00000001)) { output.writeBool(1, isApproved_); } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeBytes(2, profileKey_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeMessage(3, profile_); + } getUnknownFields().writeTo(output); } @@ -21660,6 +21748,14 @@ public final class SignalServiceProtos { size += com.google.protobuf.CodedOutputStream .computeBoolSize(1, isApproved_); } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, profileKey_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, profile_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -21768,6 +21864,7 @@ public final class SignalServiceProtos { } private void maybeForceBuilderInitialization() { if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + getProfileFieldBuilder(); } } private static Builder create() { @@ -21778,6 +21875,14 @@ public final class SignalServiceProtos { super.clear(); isApproved_ = false; bitField0_ = (bitField0_ & ~0x00000001); + profileKey_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000002); + if (profileBuilder_ == null) { + profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance(); + } else { + profileBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000004); return this; } @@ -21810,6 +21915,18 @@ public final class SignalServiceProtos { to_bitField0_ |= 0x00000001; } result.isApproved_ = isApproved_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.profileKey_ = profileKey_; + if (((from_bitField0_ & 0x00000004) == 0x00000004)) { + to_bitField0_ |= 0x00000004; + } + if (profileBuilder_ == null) { + result.profile_ = profile_; + } else { + result.profile_ = profileBuilder_.build(); + } result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -21829,6 +21946,12 @@ public final class SignalServiceProtos { if (other.hasIsApproved()) { setIsApproved(other.getIsApproved()); } + if (other.hasProfileKey()) { + setProfileKey(other.getProfileKey()); + } + if (other.hasProfile()) { + mergeProfile(other.getProfile()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -21909,6 +22032,159 @@ public final class SignalServiceProtos { return this; } + // optional bytes profileKey = 2; + private com.google.protobuf.ByteString profileKey_ = com.google.protobuf.ByteString.EMPTY; + /** + * optional bytes profileKey = 2; + */ + public boolean hasProfileKey() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * optional bytes profileKey = 2; + */ + public com.google.protobuf.ByteString getProfileKey() { + return profileKey_; + } + /** + * optional bytes profileKey = 2; + */ + public Builder setProfileKey(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + profileKey_ = value; + onChanged(); + return this; + } + /** + * optional bytes profileKey = 2; + */ + public Builder clearProfileKey() { + bitField0_ = (bitField0_ & ~0x00000002); + profileKey_ = getDefaultInstance().getProfileKey(); + onChanged(); + return this; + } + + // optional .signalservice.DataMessage.LokiProfile profile = 3; + private org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile, org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder, org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfileOrBuilder> profileBuilder_; + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public boolean hasProfile() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile getProfile() { + if (profileBuilder_ == null) { + return profile_; + } else { + return profileBuilder_.getMessage(); + } + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public Builder setProfile(org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile value) { + if (profileBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + profile_ = value; + onChanged(); + } else { + profileBuilder_.setMessage(value); + } + bitField0_ |= 0x00000004; + return this; + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public Builder setProfile( + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder builderForValue) { + if (profileBuilder_ == null) { + profile_ = builderForValue.build(); + onChanged(); + } else { + profileBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000004; + return this; + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public Builder mergeProfile(org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile value) { + if (profileBuilder_ == null) { + if (((bitField0_ & 0x00000004) == 0x00000004) && + profile_ != org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance()) { + profile_ = + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.newBuilder(profile_).mergeFrom(value).buildPartial(); + } else { + profile_ = value; + } + onChanged(); + } else { + profileBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000004; + return this; + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public Builder clearProfile() { + if (profileBuilder_ == null) { + profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance(); + onChanged(); + } else { + profileBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000004); + return this; + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder getProfileBuilder() { + bitField0_ |= 0x00000004; + onChanged(); + return getProfileFieldBuilder().getBuilder(); + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfileOrBuilder getProfileOrBuilder() { + if (profileBuilder_ != null) { + return profileBuilder_.getMessageOrBuilder(); + } else { + return profile_; + } + } + /** + * optional .signalservice.DataMessage.LokiProfile profile = 3; + */ + private com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile, org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder, org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfileOrBuilder> + getProfileFieldBuilder() { + if (profileBuilder_ == null) { + profileBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile, org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder, org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfileOrBuilder>( + profile_, + getParentForChildren(), + isClean()); + profile_ = null; + } + return profileBuilder_; + } + // @@protoc_insertion_point(builder_scope:signalservice.MessageRequestResponse) } @@ -25921,24 +26197,26 @@ public final class SignalServiceProtos { "\001 \002(\014\022\014\n\004name\030\002 \002(\t\022\026\n\016profilePicture\030\003 ", "\001(\t\022\022\n\nprofileKey\030\004 \001(\014\022\022\n\nisApproved\030\005 " + "\001(\010\022\021\n\tisBlocked\030\006 \001(\010\022\024\n\014didApproveMe\030\007" + - " \001(\010\",\n\026MessageRequestResponse\022\022\n\nisAppr" + - "oved\030\001 \002(\010\"u\n\016ReceiptMessage\0220\n\004type\030\001 \002" + - "(\0162\".signalservice.ReceiptMessage.Type\022\021" + - "\n\ttimestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022" + - "\010\n\004READ\020\001\"\354\001\n\021AttachmentPointer\022\n\n\002id\030\001 " + - "\002(\006\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n" + - "\004size\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest" + - "\030\006 \001(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022", - "\r\n\005width\030\t \001(\r\022\016\n\006height\030\n \001(\r\022\017\n\007captio" + - "n\030\013 \001(\t\022\013\n\003url\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_M" + - "ESSAGE\020\001\"\365\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022.\n" + - "\004type\030\002 \001(\0162 .signalservice.GroupContext" + - ".Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\0220\n\006" + - "avatar\030\005 \001(\0132 .signalservice.AttachmentP" + - "ointer\022\016\n\006admins\030\006 \003(\t\"H\n\004Type\022\013\n\007UNKNOW" + - "N\020\000\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020" + - "\n\014REQUEST_INFO\020\004B3\n\034org.session.libsigna" + - "l.protosB\023SignalServiceProtos" + " \001(\010\"y\n\026MessageRequestResponse\022\022\n\nisAppr" + + "oved\030\001 \002(\010\022\022\n\nprofileKey\030\002 \001(\014\0227\n\007profil" + + "e\030\003 \001(\0132&.signalservice.DataMessage.Loki" + + "Profile\"u\n\016ReceiptMessage\0220\n\004type\030\001 \002(\0162" + + "\".signalservice.ReceiptMessage.Type\022\021\n\tt" + + "imestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022\010\n\004" + + "READ\020\001\"\354\001\n\021AttachmentPointer\022\n\n\002id\030\001 \002(\006" + + "\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004si", + "ze\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest\030\006 " + + "\001(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n\005" + + "width\030\t \001(\r\022\016\n\006height\030\n \001(\r\022\017\n\007caption\030\013" + + " \001(\t\022\013\n\003url\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_MESS" + + "AGE\020\001\"\365\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022.\n\004ty" + + "pe\030\002 \001(\0162 .signalservice.GroupContext.Ty" + + "pe\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\0220\n\006ava" + + "tar\030\005 \001(\0132 .signalservice.AttachmentPoin" + + "ter\022\016\n\006admins\030\006 \003(\t\"H\n\004Type\022\013\n\007UNKNOWN\020\000" + + "\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020\n\014R", + "EQUEST_INFO\020\004B3\n\034org.session.libsignal.p" + + "rotosB\023SignalServiceProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -26064,7 +26342,7 @@ public final class SignalServiceProtos { internal_static_signalservice_MessageRequestResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_MessageRequestResponse_descriptor, - new java.lang.String[] { "IsApproved", }); + new java.lang.String[] { "IsApproved", "ProfileKey", "Profile", }); internal_static_signalservice_ReceiptMessage_descriptor = getDescriptor().getMessageTypes().get(10); internal_static_signalservice_ReceiptMessage_fieldAccessorTable = new diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt index aea1fce2d9..5eac7cecd4 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt @@ -12,6 +12,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager object HTTP { + var isConnectedToNetwork: (() -> Boolean) = { false } private val seedNodeConnection by lazy { OkHttpClient().newBuilder() @@ -64,8 +65,12 @@ object HTTP { private const val timeout: Long = 120 - class HTTPRequestFailedException(val statusCode: Int, val json: Map<*, *>?) - : kotlin.Exception("HTTP request failed with status code $statusCode.") + open class HTTPRequestFailedException( + val statusCode: Int, + val json: Map<*, *>?, + message: String = "HTTP request failed with status code $statusCode" + ) : kotlin.Exception(message) + class HTTPNoNetworkException : HTTPRequestFailedException(0, null, "No network connection") enum class Verb(val rawValue: String) { GET("GET"), PUT("PUT"), POST("POST"), DELETE("DELETE") @@ -120,8 +125,11 @@ object HTTP { response = connection.newCall(request.build()).execute() } catch (exception: Exception) { Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.") + + if (!isConnectedToNetwork()) { throw HTTPNoNetworkException() } + // Override the actual error so that we can correctly catch failed requests in OnionRequestAPI - throw HTTPRequestFailedException(0, null) + throw HTTPRequestFailedException(0, null, "HTTP request failed due to: ${exception.message}") } return when (val statusCode = response.code()) { 200 -> {