diff --git a/app/build.gradle b/app/build.gradle index 49ecd7a8f5..8cc1f4146b 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' @@ -119,7 +120,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 +128,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 +142,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 +152,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 = 321 +def canonicalVersionName = "1.16.3" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, @@ -169,13 +170,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 +206,7 @@ android { versionName canonicalVersionName minSdkVersion androidMinimumSdkVersion - targetSdkVersion androidCompileSdkVersion + targetSdkVersion androidTargetSdkVersion multiDexEnabled = true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 16a4418724..0ba2c62d0a 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" /> + @@ -177,6 +177,7 @@ android:theme="@style/Theme.Session.DayNight.NoActionBar" /> @@ -401,42 +403,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..8fe65767b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -481,6 +481,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 +498,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) { 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/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 d22fcfd3a8..0c551c1c7b 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,21 +3,13 @@ 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 @@ -66,12 +58,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.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.Recipient.DisappearingState @@ -134,14 +122,7 @@ 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 @@ -633,7 +614,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe ) } viewModel.recipient?.let { maybeUpdateToolbar(it) } - super.onPrepareOptionsMenu(menu) return true } @@ -1760,6 +1740,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe endActionMode() } + override fun destroyActionMode() { + this.actionMode = null + } + private fun sendScreenshotNotification() { val recipient = viewModel.recipient ?: return if (recipient.isGroupRecipient) return @@ -1805,7 +1789,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() } } } @@ -1871,7 +1855,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/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 4b5ee8458c..b863823dcf 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 @@ -186,7 +186,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/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/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 88045f8e47..c6fc57c289 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 @@ -44,7 +44,7 @@ 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 { @@ -86,6 +86,14 @@ class VisibleMessageContentView : LinearLayout { 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.isVisible = false + binding.untrustedView.root.isVisible = false + binding.voiceMessageView.root.isVisible = false + binding.documentView.root.isVisible = false + binding.albumThumbnailView.isVisible = false + binding.openGroupInvitationView.root.isVisible = false return } else { binding.deletedMessageView.root.isVisible = false 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/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt index 553809e9fe..5b4d27ad39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt @@ -136,6 +136,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 { 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..b3afeac477 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -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/SearchDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java index 37efc9a438..6bce73e227 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java @@ -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/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index bb7ab440d4..1b3181a6f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -17,13 +17,7 @@ import org.session.libsession.messaging.messages.ExpirationConfiguration 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.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage @@ -38,12 +32,8 @@ import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.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.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer @@ -439,6 +429,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } + override fun clearErrorMessage(messageID: Long) { + val db = DatabaseComponent.get(context).lokiMessageDatabase() + db.clearErrorMessage(messageID) + } + override fun setMessageServerHash(messageID: Long, serverHash: String) { DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(messageID, serverHash) } 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..a0c701fc99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -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/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/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index b21eb6ff13..45f3b4a63f 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) 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/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..4b6499b999 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); 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/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/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/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 0f10a93b0b..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,10 +7,13 @@ 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 @@ -28,30 +31,13 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_IN import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_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 @@ -60,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 { @@ -108,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) @@ -174,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) } @@ -182,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 @@ -199,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 { - ContextCompat.startForegroundService(this, hangupIntent(this)) + + private val hangupOnCallAnswered by lazy { + HangUpRtcOnPstnCallAnsweredListener { + ContextCompat.startForegroundService(this, hangupIntent(this)) + } + } + + private val hangupTelephonyCallback by lazy { + HangUpRtcTelephonyCallback { + ContextCompat.startForegroundService(this, hangupIntent(this)) + } } private var networkChangedReceiver: NetworkChangeReceiver? = null @@ -258,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) @@ -272,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) @@ -293,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) @@ -318,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() { @@ -339,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 } @@ -419,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 @@ -429,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() } @@ -476,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) @@ -487,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() @@ -497,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() } @@ -518,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 } @@ -555,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() } @@ -567,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) @@ -597,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()) } @@ -616,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) @@ -627,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) } @@ -640,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 @@ -661,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) { @@ -680,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) @@ -698,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() } @@ -709,7 +800,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } } - private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context): Runnable { + private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context) : + Runnable { override fun run() { val intent = Intent(context, WebRtcCallService::class.java) .setAction(ACTION_CHECK_RECONNECT) @@ -718,7 +810,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } } - private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context): Runnable { + private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context) : + Runnable { override fun run() { val intent = Intent(context, WebRtcCallService::class.java) .setAction(ACTION_CHECK_RECONNECT_TIMEOUT) @@ -727,26 +820,29 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener { } } - private class TimeoutRunnable(private val callId: UUID, private val context: Context): Runnable { + private class TimeoutRunnable(private val callId: UUID, private val context: Context) : + Runnable { override fun run() { 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) @@ -754,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) @@ -764,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) } @@ -773,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) @@ -792,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 } @@ -817,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 + ) } } } @@ -855,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/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/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/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/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/mediasend_fragment.xml b/app/src/main/res/layout/mediasend_fragment.xml index 4c6115b539..4ef993d529 100644 --- a/app/src/main/res/layout/mediasend_fragment.xml +++ b/app/src/main/res/layout/mediasend_fragment.xml @@ -95,7 +95,6 @@ + android:background="@color/transparent_black_30" /> - - + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ea9ddb64f2..a388051b8f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -158,6 +158,7 @@