mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 00:35:47 +01:00
Mark copied secrets as sensitive and auto-clear the backup passphrase.
Co-authored-by: Greyson Parrelil <greyson@signal.org>
This commit is contained in:
committed by
Cody Henthorne
co-authored by
Greyson Parrelil
parent
c6ff93af98
commit
e90e421fea
@@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.backup;
|
||||
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -39,6 +37,7 @@ public class BackupDialog {
|
||||
|
||||
private static final String TAG = Log.tag(BackupDialog.class);
|
||||
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context,
|
||||
@Nullable Intent backupDirectorySelectionIntent,
|
||||
@Nullable String backupDirectoryDisplayName,
|
||||
@@ -103,7 +102,7 @@ public class BackupDialog {
|
||||
textView.setOnClickListener(v -> checkBox.toggle());
|
||||
|
||||
dialog.findViewById(R.id.number_table).setOnClickListener(v -> {
|
||||
((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", Util.join(password, " ")));
|
||||
Util.copyToClipboardSensitive(context, Util.join(password, " "));
|
||||
Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
|
||||
+3
-4
@@ -65,7 +65,6 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
|
||||
@VisibleForTesting
|
||||
const val TIER = "tier"
|
||||
const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
|
||||
private const val PDF_MIME_TYPE = "application/pdf"
|
||||
|
||||
@@ -192,7 +191,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
onSaveSuccessful = viewModel::onBackupKeySavedToPasswordManager
|
||||
)
|
||||
},
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||
onCopyToClipboardClick = { Util.copyToClipboardSensitive(context, it) },
|
||||
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
|
||||
onSaveStateCleared = viewModel::onBackupKeySaveStateCleared,
|
||||
@@ -212,7 +211,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
|
||||
onNavigationClick = viewModel::goToPreviousStage,
|
||||
mode = remember { MessageBackupsKeyRecordMode.Next(viewModel::goToNextStage) },
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||
onCopyToClipboardClick = { Util.copyToClipboardSensitive(context, it) },
|
||||
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
|
||||
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
|
||||
@@ -337,7 +336,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
}
|
||||
}
|
||||
SignalLoginViewDetailsAction.LaunchSaveAsPdf -> savePdfLauncher.launch(SignalLoginPdfRenderer.suggestedFileName(requireContext()))
|
||||
is SignalLoginViewDetailsAction.CopyTextToClipboard -> Util.copyToClipboard(requireContext(), action.text, CLIPBOARD_TIMEOUT_SECONDS)
|
||||
is SignalLoginViewDetailsAction.CopyTextToClipboard -> Util.copyToClipboardSensitive(requireContext(), action.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -91,8 +91,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
private const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
|
||||
@Stable
|
||||
sealed interface MessageBackupsKeyRecordMode {
|
||||
data class Next(val onNextClick: () -> Unit) : MessageBackupsKeyRecordMode
|
||||
@@ -133,7 +131,7 @@ fun MessageBackupsKeyRecordScreen(
|
||||
keySaveState = keySaveState,
|
||||
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
|
||||
onNavigationClick = { onBackPressedDispatcher?.onBackPressed() },
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||
onCopyToClipboardClick = { Util.copyToClipboardSensitive(context, it) },
|
||||
onRequestSaveToPasswordManager = backupKeyCredentialManagerHandler::onBackupKeySaveRequested,
|
||||
onConfirmSaveToPasswordManager = backupKeyCredentialManagerHandler::onBackupKeySaveConfirmed,
|
||||
onSaveToPasswordManagerComplete = backupKeyCredentialManagerHandler::onBackupKeySaveCompleted,
|
||||
|
||||
-4
@@ -16,10 +16,6 @@ import org.thoughtcrime.securesms.util.viewModel
|
||||
*/
|
||||
class ForgotBackupKeyFragment : ComposeFragment() {
|
||||
|
||||
companion object {
|
||||
const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
}
|
||||
|
||||
private val viewModel: ForgotBackupKeyViewModel by viewModel { ForgotBackupKeyViewModel() }
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ class TotpSetupFragment : ComposeFragment() {
|
||||
when (action) {
|
||||
TotpSetupAction.NavigateBack -> requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
is TotpSetupAction.LaunchTotpApp -> launchTotpApp(action.uri)
|
||||
is TotpSetupAction.CopyKeyToClipboard -> Util.copyToClipboard(requireContext(), action.key)
|
||||
is TotpSetupAction.CopyKeyToClipboard -> Util.copyToClipboardSensitive(requireContext(), action.key)
|
||||
TotpSetupAction.ShowKeyCopied -> toast(AppSettingsR.string.TotpSetupScreen__copied_to_clipboard)
|
||||
TotpSetupAction.ShowNoTotpAppFound -> toast(AppSettingsR.string.TotpSetupScreen__no_authenticator_app_found)
|
||||
TotpSetupAction.NavigateToCodeEntry -> findNavController().safeNavigate(R.id.action_authenticatorSetupFragment_to_authenticatorCodeEntryFragment)
|
||||
|
||||
+1
-5
@@ -28,10 +28,6 @@ import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
*/
|
||||
class SignalLoginViewDetailsFragment : ComposeFragment() {
|
||||
|
||||
companion object {
|
||||
private const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
}
|
||||
|
||||
private val viewModel: SignalLoginViewDetailsViewModel by viewModels()
|
||||
|
||||
private val savePdfLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("application/pdf")) { uri: Uri? ->
|
||||
@@ -73,7 +69,7 @@ class SignalLoginViewDetailsFragment : ComposeFragment() {
|
||||
}
|
||||
}
|
||||
SignalLoginViewDetailsAction.LaunchSaveAsPdf -> savePdfLauncher.launch(SignalLoginPdfRenderer.suggestedFileName(requireContext()))
|
||||
is SignalLoginViewDetailsAction.CopyTextToClipboard -> Util.copyToClipboard(requireContext(), action.text, CLIPBOARD_TIMEOUT_SECONDS)
|
||||
is SignalLoginViewDetailsAction.CopyTextToClipboard -> Util.copyToClipboardSensitive(requireContext(), action.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-18
@@ -1,10 +1,5 @@
|
||||
package org.thoughtcrime.securesms.payments.backup.phrase;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -23,13 +18,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.ClearClipboardAlarmReceiver;
|
||||
import org.signal.core.util.PendingIntentFlags;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.payments.Mnemonic;
|
||||
import org.signal.core.util.ServiceUtil;
|
||||
import org.signal.core.util.Util;
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
@@ -37,13 +29,13 @@ import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PaymentsRecoveryPhraseFragment extends Fragment {
|
||||
|
||||
private static final int SPAN_COUNT = 2;
|
||||
private static final int SPAN_COUNT = 2;
|
||||
private static final int CLIPBOARD_TIMEOUT_SECONDS = 30;
|
||||
|
||||
public PaymentsRecoveryPhraseFragment() {
|
||||
super(R.layout.payments_recovery_phrase_fragment);
|
||||
@@ -102,14 +94,7 @@ public class PaymentsRecoveryPhraseFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void copyWordsToClipboard(List<String> words) {
|
||||
ClipboardManager clipboardManager = ServiceUtil.getClipboardManager(requireContext());
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(getString(R.string.app_name), Util.join(words, " ")));
|
||||
|
||||
AlarmManager alarmManager = ServiceUtil.getAlarmManager(requireContext());
|
||||
Intent alarmIntent = new Intent(requireContext(), ClearClipboardAlarmReceiver.class);
|
||||
PendingIntent pendingAlarmIntent = PendingIntent.getBroadcast(requireContext(), 0, alarmIntent, PendingIntentFlags.mutable());
|
||||
|
||||
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30), pendingAlarmIntent);
|
||||
Util.copyToClipboardSensitive(requireContext(), Util.join(words, " "), CLIPBOARD_TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
private void setUpForConfirmation(@NonNull TextView message,
|
||||
|
||||
@@ -21,12 +21,15 @@ import android.app.ActivityManager;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PersistableBundle;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -64,6 +67,9 @@ public class Util {
|
||||
|
||||
public static final String COPY_LABEL = "text\u00AD";
|
||||
|
||||
/** How long a secret sits on the clipboard before it is cleared, when the caller doesn't specify. */
|
||||
public static final int SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS = 60;
|
||||
|
||||
public static <T> List<T> asList(T... elements) {
|
||||
List<T> result = new LinkedList<>();
|
||||
Collections.addAll(result, elements);
|
||||
@@ -436,9 +442,26 @@ public class Util {
|
||||
((ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText(COPY_LABEL, text));
|
||||
}
|
||||
|
||||
public static void copyToClipboard(@NonNull Context context, @NonNull CharSequence text, int expiresInSeconds) {
|
||||
/**
|
||||
* Copies a secret to the clipboard, clearing it after {@link #SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS}. See
|
||||
* {@link #copyToClipboardSensitive(Context, CharSequence, int)}.
|
||||
*/
|
||||
public static void copyToClipboardSensitive(@NonNull Context context, @NonNull CharSequence text) {
|
||||
copyToClipboardSensitive(context, text, SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a secret (backup passphrase, recovery phrase, key) to the clipboard.
|
||||
*
|
||||
* Flags the clip as sensitive, which stops the system clipboard UI on API 33+ from rendering a preview of the value and stops keyboards
|
||||
* from offering it as a paste suggestion. It also schedules an alarm to clear the clipboard after {@code expiresInSeconds}.
|
||||
*/
|
||||
public static void copyToClipboardSensitive(@NonNull Context context, @NonNull CharSequence text, int expiresInSeconds) {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(getPackageLabel(context), text));
|
||||
ClipData clip = ClipData.newPlainText(getPackageLabel(context), text);
|
||||
|
||||
markSensitive(clip);
|
||||
clipboardManager.setPrimaryClip(clip);
|
||||
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
Intent alarmIntent = new Intent(context, ClearClipboardAlarmReceiver.class);
|
||||
@@ -447,6 +470,19 @@ public class Util {
|
||||
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiresInSeconds), pendingAlarmIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a clip as containing sensitive content. No-op below API 33. Does not prevent other apps from reading the clipboard on any API
|
||||
* level, just affects how the system and keyboards display the clip.
|
||||
*/
|
||||
private static void markSensitive(@NonNull ClipData clip) {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
PersistableBundle extras = new PersistableBundle();
|
||||
extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
|
||||
|
||||
clip.getDescription().setExtras(extras);
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseInt(String integer, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(integer);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.signal.core.util
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.Application
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isGreaterThanOrEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Verifies that secrets copied to the clipboard are flagged sensitive and scheduled for clearing, and -- just as importantly -- that
|
||||
* ordinary text is not.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class UtilTest_copyToClipboard {
|
||||
|
||||
private val context: Context
|
||||
get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
private val clipboardManager: ClipboardManager
|
||||
get() = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
private val alarmManager: AlarmManager
|
||||
get() = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - marks the clip as sensitive`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
val extras = clipboardManager.primaryClip!!.description.extras
|
||||
assertThat(extras).isNotNull()
|
||||
assertThat(extras!!.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - still copies the text`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
assertThat(clipboardManager.primaryClip!!.getItemAt(0).text.toString()).isEqualTo(SECRET)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - schedules a clear alarm`() {
|
||||
val before = System.currentTimeMillis()
|
||||
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
val alarms = shadowOf(alarmManager).scheduledAlarms
|
||||
assertThat(alarms.size).isEqualTo(1)
|
||||
assertThat(alarms[0].type).isEqualTo(AlarmManager.RTC_WAKEUP)
|
||||
assertThat(alarms[0].triggerAtTime).isGreaterThanOrEqualTo(before + TimeUnit.SECONDS.toMillis(60))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [26])
|
||||
fun `copyToClipboardSensitive - below api 33, does not set extras`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
assertThat(clipboardManager.primaryClip!!.description.extras).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [26])
|
||||
fun `copyToClipboardSensitive - below api 33, still schedules a clear alarm`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
assertThat(shadowOf(alarmManager).scheduledAlarms.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - default overload marks the clip sensitive and uses the default timeout`() {
|
||||
val before = System.currentTimeMillis()
|
||||
|
||||
Util.copyToClipboardSensitive(context, SECRET)
|
||||
|
||||
val extras = clipboardManager.primaryClip!!.description.extras
|
||||
assertThat(extras).isNotNull()
|
||||
assertThat(extras!!.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE)).isTrue()
|
||||
|
||||
val alarms = shadowOf(alarmManager).scheduledAlarms
|
||||
assertThat(alarms.size).isEqualTo(1)
|
||||
assertThat(alarms[0].triggerAtTime).isGreaterThanOrEqualTo(before + TimeUnit.SECONDS.toMillis(Util.SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS.toLong()))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboard - does not mark the clip as sensitive`() {
|
||||
Util.copyToClipboard(context, NOT_A_SECRET)
|
||||
|
||||
assertThat(clipboardManager.primaryClip!!.description.extras).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboard - does not schedule a clear alarm`() {
|
||||
Util.copyToClipboard(context, NOT_A_SECRET)
|
||||
|
||||
assertThat(shadowOf(alarmManager).scheduledAlarms).isEmpty()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SECRET = "12345 67890 12345 67890 12345 67890"
|
||||
private const val NOT_A_SECRET = "https://signal.me/#p/+15551234567"
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -411,7 +411,6 @@ private const val AEP_FOR_LOCAL_BACKUP_RESULT = "aep_for_local_backup_result"
|
||||
private const val LOCAL_BACKUP_RESTORE_RESULT = "local_backup_restore_result"
|
||||
private const val PHONE_NUMBER_DISCOVERABILITY_RESULT = "phone_number_discoverability_result"
|
||||
private const val TWO_FACTOR_CODE_RESULT = "two_factor_code_result"
|
||||
private const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
|
||||
/** Opens [url] in a browser, surfacing a toast if the device has none. */
|
||||
private fun openUrl(context: Context, url: String) {
|
||||
@@ -852,7 +851,7 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
|
||||
SignalLoginViewDetailsScreenActions.LaunchSaveAsPdf -> savePdfLauncher.launch(SignalLoginPdfRenderer.suggestedFileName(context))
|
||||
|
||||
is SignalLoginViewDetailsScreenActions.CopyTextToClipboard -> Util.copyToClipboard(context, action.text, CLIPBOARD_TIMEOUT_SECONDS)
|
||||
is SignalLoginViewDetailsScreenActions.CopyTextToClipboard -> Util.copyToClipboardSensitive(context, action.text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,7 +888,7 @@ private fun EntryProviderScope<NavKey>.navigationEntries(
|
||||
when (action) {
|
||||
SignalLoginViewDetailsForManualSaveScreenActions.LaunchSaveAsPdf -> savePdfLauncher.launch(SignalLoginPdfRenderer.suggestedFileName(context))
|
||||
|
||||
is SignalLoginViewDetailsForManualSaveScreenActions.CopyTextToClipboard -> Util.copyToClipboard(context, action.text, CLIPBOARD_TIMEOUT_SECONDS)
|
||||
is SignalLoginViewDetailsForManualSaveScreenActions.CopyTextToClipboard -> Util.copyToClipboardSensitive(context, action.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user