diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 4fd99dd6c07..f761cdd4e73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -412,14 +412,9 @@ private void saveToDisk() { Permissions.with(this) .request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) .maxSdkVersion(Build.VERSION_CODES.P) - .withPermanentDenialDialog(Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied) - .put(APP_NAME_KEY, getString(R.string.app_name)) - .format().toString()) + .withPermanentDenialDialog(getPermanentlyDeniedStorageText()) .onAnyDenied(() -> { - String txt = Phrase.from(getApplicationContext(), R.string.permissionsStorageSaveDenied) - .put(APP_NAME_KEY, getString(R.string.app_name)) - .format().toString(); - Toast.makeText(this, txt, Toast.LENGTH_LONG).show(); + Toast.makeText(this, getPermanentlyDeniedStorageText(), Toast.LENGTH_LONG).show(); }) .onAllGranted(() -> { SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this); @@ -436,6 +431,12 @@ private void saveToDisk() { }); } + private String getPermanentlyDeniedStorageText(){ + return Phrase.from(getApplicationContext(), R.string.permissionsStorageDeniedLegacy) + .put(APP_NAME_KEY, getString(R.string.app_name)) + .format().toString(); + } + private void sendMediaSavedNotificationIfNeeded() { if (conversationRecipient.isGroupRecipient()) return; DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt index b45d1e9f8e6..2c03fe25113 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt @@ -26,14 +26,7 @@ fun showMuteDialog( context.getSubbedString(entry.stringRes, TIME_LARGE_KEY to largeTimeUnitString) } }.toTypedArray()) { - // Note: We add the current timestamp to the mute duration to get the un-mute timestamp - // that gets stored in the database via ConversationMenuHelper.mute(). - // Also: This is a kludge, but we ADD one second to the mute duration because otherwise by - // the time the view for how long the conversation is muted for gets set then it's actually - // less than the entire duration - so 1 hour becomes 59 minutes, 1 day becomes 23 hours etc. - // As we really want to see the actual set time (1 hour / 1 day etc.) then we'll bump it by - // 1 second which is neither here nor there in the grand scheme of things. - onMuteDuration(Option.entries[it].getTime() + System.currentTimeMillis() + 1.seconds.inWholeMilliseconds) + onMuteDuration(Option.entries[it].getTime()) } } @@ -44,5 +37,12 @@ private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long SEVEN_DAYS(R.string.notificationsMuteFor, duration = TimeUnit.DAYS.toMillis(7)), FOREVER(R.string.notificationsMute, getTime = { Long.MAX_VALUE } ); - constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { duration } ) + // Note: We add the current timestamp to the mute duration to get the un-mute timestamp + // that gets stored in the database via ConversationMenuHelper.mute(). + // Also: This is a kludge, but we ADD one second to the mute duration because otherwise by + // the time the view for how long the conversation is muted for gets set then it's actually + // less than the entire duration - so 1 hour becomes 59 minutes, 1 day becomes 23 hours etc. + // As we really want to see the actual set time (1 hour / 1 day etc.) then we'll bump it by + // 1 second which is neither here nor there in the grand scheme of things. + constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { duration + System.currentTimeMillis() + 1.seconds.inWholeMilliseconds } ) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt index 61d17e44f0b..ff7f781ebf8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt @@ -125,9 +125,7 @@ class ConversationActionBarView @JvmOverloads constructor( settings += ConversationSetting( recipient.mutedUntil.takeUnless { it == Long.MAX_VALUE } ?.let { - val mutedDuration = (it - System.currentTimeMillis()).milliseconds - val durationString = LocalisedTimeUtil.getDurationWithSingleLargestTimeUnit(context, mutedDuration) - context.getSubbedString(R.string.notificationsMuteFor, TIME_LARGE_KEY to durationString) + context.getString(R.string.notificationsHeaderMute) } ?: context.getString(R.string.notificationsMuted), ConversationSettingType.NOTIFICATION, 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 e687a48752c..5a3a14564a6 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 @@ -2257,8 +2257,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // that we've warned the user just _once_ that any attachments they save can be accessed by other apps. val haveWarned = TextSecurePreferences.getHaveWarnedUserAboutSavingAttachments(this) if (haveWarned) { - // On Android versions below 30 we require the WRITE_EXTERNAL_STORAGE permission to save attachments. - if (Build.VERSION.SDK_INT < 30) { + // On Android versions below 29 we require the WRITE_EXTERNAL_STORAGE permission to save attachments. + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // Save the attachment(s) then bail if we already have permission to do so if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { saveAttachments(message) @@ -2279,7 +2279,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe Permissions.with(this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .maxSdkVersion(Build.VERSION_CODES.P) // P is 28 - .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied) + .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsStorageDeniedLegacy) .put(APP_NAME_KEY, getString(R.string.app_name)) .format().toString()) .onAnyDenied { @@ -2289,7 +2289,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe showSessionDialog { title(R.string.permissionsRequired) - val txt = Phrase.from(applicationContext, R.string.permissionsStorageSaveDenied) + val txt = Phrase.from(applicationContext, R.string.permissionsStorageDeniedLegacy) .put(APP_NAME_KEY, getString(R.string.app_name)) .format().toString() text(txt) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java index c3961223e05..d042a301968 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java @@ -245,51 +245,58 @@ SlideDeck buildSlideDeck() { public static void selectDocument(Activity activity, int requestCode) { Permissions.PermissionsBuilder builder = Permissions.with(activity); + Context c = activity.getApplicationContext(); // The READ_EXTERNAL_STORAGE permission is deprecated (and will AUTO-FAIL if requested!) on // Android 13 and above (API 33 - 'Tiramisu') we must ask for READ_MEDIA_VIDEO/IMAGES/AUDIO instead. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO) .request(Manifest.permission.READ_MEDIA_IMAGES) - .request(Manifest.permission.READ_MEDIA_AUDIO); + .request(Manifest.permission.READ_MEDIA_AUDIO) + .withRationaleDialog( + Phrase.from(c, R.string.permissionsStorageSend) + .put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString() + ) + .withPermanentDenialDialog( + Phrase.from(c, R.string.permissionMusicAudioDenied) + .put(APP_NAME_KEY, c.getString(R.string.app_name)) + .format().toString() + ); } else { - builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE); + builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE) + .withPermanentDenialDialog( + Phrase.from(c, R.string.permissionsStorageDeniedLegacy) + .put(APP_NAME_KEY, c.getString(R.string.app_name)) + .format().toString() + ); } - Context c = activity.getApplicationContext(); - - String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend).put(APP_NAME_KEY, c.getString(R.string.app_name)).format().toString(); - - String storagePermissionDeniedTxt = Phrase.from(c, R.string.permissionsStorageSaveDenied) - .put(APP_NAME_KEY, c.getString(R.string.app_name)) - .format().toString(); - - builder.withPermanentDenialDialog(storagePermissionDeniedTxt) - .withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24) - .onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this. + builder.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode)) // Note: We can use startActivityForResult w/ the ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE intent if we need to modernise this. .execute(); } public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body) { Context c = activity.getApplicationContext(); - String needStoragePermissionTxt = Phrase.from(c, R.string.permissionsStorageSend) - .put(APP_NAME_KEY, c.getString(R.string.app_name)) - .format().toString(); - String cameraPermissionDeniedTxt = Phrase.from(c, R.string.cameraGrantAccessDenied) - .put(APP_NAME_KEY, c.getString(R.string.app_name)) - .format().toString(); Permissions.PermissionsBuilder builder = Permissions.with(activity); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { builder = builder.request(Manifest.permission.READ_MEDIA_VIDEO) - .request(Manifest.permission.READ_MEDIA_IMAGES); + .request(Manifest.permission.READ_MEDIA_IMAGES) + .withPermanentDenialDialog( + Phrase.from(c, R.string.permissionsStorageDenied) + .put(APP_NAME_KEY, c.getString(R.string.app_name)) + .format().toString() + ); } else { - builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE); + builder = builder.request(Manifest.permission.READ_EXTERNAL_STORAGE) + .withPermanentDenialDialog( + Phrase.from(c, R.string.permissionsStorageDeniedLegacy) + .put(APP_NAME_KEY, c.getString(R.string.app_name)) + .format().toString() + ); } - builder.withPermanentDenialDialog(cameraPermissionDeniedTxt) - .withRationaleDialog(needStoragePermissionTxt, R.drawable.ic_baseline_photo_library_24) - .onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) + builder.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body), requestCode)) .execute(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt index 5eb378ef30e..f277d1f40b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.debugmenu import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer @@ -19,6 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import network.loki.messenger.BuildConfig import network.loki.messenger.R @@ -45,7 +48,7 @@ fun DebugMenu( sendCommand: (DebugMenuViewModel.Commands) -> Unit, modifier: Modifier = Modifier, onClose: () -> Unit -){ +) { val snackbarHostState = remember { SnackbarHostState() } Scaffold( @@ -56,7 +59,7 @@ fun DebugMenu( ) { contentPadding -> // display a snackbar when required LaunchedEffect(uiState.snackMessage) { - if(!uiState.snackMessage.isNullOrEmpty()){ + if (!uiState.snackMessage.isNullOrEmpty()) { snackbarHostState.showSnackbar(uiState.snackMessage) } } @@ -102,13 +105,22 @@ fun DebugMenu( .verticalScroll(rememberScrollState()) ) { // Info pane - DebugCell("App Info") { + val clipboardManager = LocalClipboardManager.current + val appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - ${ + BuildConfig.GIT_HASH.take( + 6 + ) + })" + + DebugCell( + modifier = Modifier.clickable { + // clicking the cell copies the version number to the clipboard + clipboardManager.setText(AnnotatedString(appVersion)) + }, + title = "App Info" + ) { Text( - text = "Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - ${ - BuildConfig.GIT_HASH.take( - 6 - ) - })", + text = "Version: $appVersion", style = LocalType.current.base ) } @@ -155,7 +167,7 @@ fun ColumnScope.DebugCell( @Preview @Composable -fun PreviewDebugMenu(){ +fun PreviewDebugMenu() { PreviewTheme { DebugMenu( uiState = DebugMenuViewModel.UIState( 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 dbcc39e7b62..05c2fe658b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -547,6 +547,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } else { showMuteDialog(this) { until -> lifecycleScope.launch(Dispatchers.IO) { + Log.d("", "**** until: $until") recipientDatabase.setMuted(thread.recipient, until) withContext(Dispatchers.Main) { binding.recyclerView.adapter!!.notifyDataSetChanged() diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index f38d4c8613d..bdc90830fad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -73,7 +73,7 @@ public PermissionsBuilder request(String... requestedPermissions) { return this; } - public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) { + public PermissionsBuilder withRationaleDialog(@NonNull String message, @DrawableRes int... headers) { this.rationalDialogHeader = headers; this.rationaleDialogMessage = message; return this; @@ -143,7 +143,7 @@ public void execute() { if (!isInTargetSDKRange || permissionObject.hasAll(requestedPermissions)) { executePreGrantedPermissionsRequest(request); - } else if (rationaleDialogMessage != null && rationalDialogHeader != null) { + } else if (rationaleDialogMessage != null) { executePermissionsRequestWithRationale(request); } else { executePermissionsRequest(request); diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt index 021b67facb2..e1b5d3f03cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.Color import android.util.TypedValue import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout @@ -25,34 +26,44 @@ object RationaleDialog { onNegative: Runnable, @DrawableRes vararg drawables: Int ): AlertDialog { - val view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null) - .apply { clipToOutline = true } - val header = view.findViewById(R.id.header_container) - view.findViewById(R.id.message).text = message + var customView: View? = null + if (!drawables.isEmpty()) { + customView = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null) + .apply { clipToOutline = true } + val header = customView.findViewById(R.id.header_container) - fun addIcon(id: Int) { - ImageView(context).apply { - setImageDrawable(ResourcesCompat.getDrawable(context.resources, id, context.theme)) - layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) - }.also(header::addView) - } + customView.findViewById(R.id.message).text = message - fun addPlus() { - TextView(context).apply { - text = "+" - setTextSize(TypedValue.COMPLEX_UNIT_SP, 40f) - setTextColor(Color.WHITE) - layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { - ViewUtil.dpToPx(context, 20).let { setMargins(it, 0, it, 0) } - } - }.also(header::addView) - } + fun addIcon(id: Int) { + ImageView(context).apply { + setImageDrawable(ResourcesCompat.getDrawable(context.resources, id, context.theme)) + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + }.also(header::addView) + } - drawables.firstOrNull()?.let(::addIcon) - drawables.drop(1).forEach { addPlus(); addIcon(it) } + fun addPlus() { + TextView(context).apply { + text = "+" + setTextSize(TypedValue.COMPLEX_UNIT_SP, 40f) + setTextColor(Color.WHITE) + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + ViewUtil.dpToPx(context, 20).let { setMargins(it, 0, it, 0) } + } + }.also(header::addView) + } + + drawables.firstOrNull()?.let(::addIcon) + drawables.drop(1).forEach { addPlus(); addIcon(it) } + } return context.showSessionDialog { - view(view) + // show the generic title when there are no icons + if(customView != null){ + view(customView) + } else { + title(R.string.permissionsRequired) + text(message) + } button(R.string.theContinue) { onPositive.run() } button(R.string.notNow) { onNegative.run() } } 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 d3c68a814d4..2ae5604c06d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/HelpSettingsActivity.kt @@ -98,10 +98,10 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() { Permissions.with(this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .maxSdkVersion(Build.VERSION_CODES.P) - .withPermanentDenialDialog(requireContext().getSubbedString(R.string.permissionsStorageSaveDenied, APP_NAME_KEY to getString(R.string.app_name))) + .withPermanentDenialDialog(requireContext().getSubbedString(R.string.permissionsStorageDeniedLegacy, APP_NAME_KEY to getString(R.string.app_name))) .onAnyDenied { val c = requireContext() - val txt = c.getSubbedString(R.string.permissionsStorageSaveDenied, APP_NAME_KEY to getString(R.string.app_name)) + val txt = c.getSubbedString(R.string.permissionsStorageDeniedLegacy, APP_NAME_KEY to getString(R.string.app_name)) Toast.makeText(c, txt, Toast.LENGTH_LONG).show() } .onAllGranted { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index b8dada12048..32582571126 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -63,7 +63,6 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.ui.AlertDialog import org.thoughtcrime.securesms.ui.DialogButtonModel