diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
index 38d61bbf92..ca849ec12b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
@@ -119,6 +119,7 @@ import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.BatterySnapshotTracker;
+import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DeviceProperties;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Environment;
@@ -127,6 +128,7 @@ import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.SqlCipherLogTarget;
+import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
@@ -438,6 +440,11 @@ public class ApplicationContext extends Application implements AppForegroundObse
context -> {
context.startActivity(AppSettingsActivity.proxy(context));
return Unit.INSTANCE;
+ },
+ (context, subject) -> {
+ String body = SupportEmailUtil.generateSupportEmailBody(context, subject, null, null);
+ CommunicationActions.openEmail(context, SupportEmailUtil.getSupportEmailAddress(context), subject, body);
+ return Unit.INSTANCE;
}
)
);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java
index 7a5daab42a..8387d606a1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SupportEmailUtil.java
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
-import android.content.res.Resources;
import android.os.Build;
import androidx.annotation.NonNull;
@@ -48,18 +47,28 @@ public final class SupportEmailUtil {
@Nullable String suffix)
{
filterSuffix = Util.emptyIfNull(filterSuffix);
- prefix = Util.emptyIfNull(prefix);
- suffix = Util.emptyIfNull(suffix);
- return String.format("%s\n%s\n%s", prefix, buildSystemInfo(context, filter, filterSuffix), suffix);
+ return generateSupportEmailBody(context, ResourceUtil.getEnglishResources(context).getString(filter) + filterSuffix, prefix, suffix);
}
- private static @NonNull String buildSystemInfo(@NonNull Context context, @StringRes int filter, @NonNull String filterSuffix) {
- Resources englishResources = ResourceUtil.getEnglishResources(context);
+ /**
+ * Generates a support email body with system info near the top, using the given already-resolved filter text.
+ */
+ public static @NonNull String generateSupportEmailBody(@NonNull Context context,
+ @NonNull String filter,
+ @Nullable String prefix,
+ @Nullable String suffix)
+ {
+ prefix = Util.emptyIfNull(prefix);
+ suffix = Util.emptyIfNull(suffix);
+ return String.format("%s\n%s\n%s", prefix, buildSystemInfo(context, filter), suffix);
+ }
+
+ private static @NonNull String buildSystemInfo(@NonNull Context context, @NonNull String filter) {
return "--- " + context.getString(R.string.HelpFragment__support_info) + " ---" +
"\n" +
- context.getString(R.string.SupportEmailUtil_filter) + " " + englishResources.getString(filter) + filterSuffix +
+ context.getString(R.string.SupportEmailUtil_filter) + " " + filter +
"\n" +
context.getString(R.string.SupportEmailUtil_device_info) + " " + getDeviceInfo() +
"\n" +
diff --git a/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt b/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt
index 3e17a06365..5becd20654 100644
--- a/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt
+++ b/demo/registration/src/main/java/org/signal/registration/sample/RegistrationApplication.kt
@@ -69,6 +69,12 @@ class RegistrationApplication : Application() {
.setPositiveButton(android.R.string.ok, null)
.show()
},
+ contactSupportCallback = { context, subject ->
+ MaterialAlertDialogBuilder(context)
+ .setMessage("Contact support not supported in the demo. Subject: $subject")
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ },
isLinkAndSyncAvailable = true
)
)
diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt
index 46248a4517..37ba24ca6d 100644
--- a/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt
+++ b/feature/registration/src/main/java/org/signal/registration/RegistrationDependencies.kt
@@ -16,6 +16,8 @@ import org.signal.registration.util.SensitiveLog
* the actual app would just pass null.
* @param debugLogCallback Callback to launch the debug log viewer. The actual app provides the real implementation.
* @param proxyConfigCallback Callback to launch the proxy configuration settings. The actual app provides the real implementation.
+ * @param contactSupportCallback Callback to let the user contact support, using the provided email subject. The actual app provides the real
+ * implementation.
*/
class RegistrationDependencies(
val networkController: NetworkController,
@@ -23,7 +25,8 @@ class RegistrationDependencies(
val isLinkAndSyncAvailable: Boolean,
val sensitiveLogger: Log.Logger?,
val debugLogCallback: ((Context) -> Unit)?,
- val proxyConfigCallback: ((Context) -> Unit)?
+ val proxyConfigCallback: ((Context) -> Unit)?,
+ val contactSupportCallback: ((Context, subject: String) -> Unit)?
) {
companion object {
lateinit var dependencies: RegistrationDependencies
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt
index 0be254815a..d7e071f3d1 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt
@@ -70,8 +70,7 @@ class PinEntryForRegistrationLockViewModel(
}
is PinEntryScreenEvents.CreateNewPin,
is PinEntryScreenEvents.ContactSupport -> Unit
- is PinEntryScreenEvents.ToggleKeyboard,
- is PinEntryScreenEvents.NeedHelp -> {
+ is PinEntryScreenEvents.ToggleKeyboard -> {
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt
index 2f7e5bdce7..b29a871995 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSmsBypassViewModel.kt
@@ -77,8 +77,7 @@ class PinEntryForSmsBypassViewModel(
}
is PinEntryScreenEvents.CreateNewPin,
is PinEntryScreenEvents.ContactSupport -> Unit
- is PinEntryScreenEvents.ToggleKeyboard,
- is PinEntryScreenEvents.NeedHelp -> {
+ is PinEntryScreenEvents.ToggleKeyboard -> {
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt
index 94f13f1d55..a090e65108 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt
@@ -80,8 +80,7 @@ class PinEntryForSvrRestoreViewModel(
Log.i(TAG, "[ContactSupport] User opted to contact support after no data was found.")
stateEmitter(state.copy(showNoDataToRestoreDialog = false))
}
- is PinEntryScreenEvents.ToggleKeyboard,
- is PinEntryScreenEvents.NeedHelp -> {
+ is PinEntryScreenEvents.ToggleKeyboard -> {
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt
index ffb7b97438..f49ea99663 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt
@@ -39,6 +39,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -52,6 +53,7 @@ import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.registration.R
+import org.signal.registration.RegistrationDependencies
import org.signal.registration.screens.PinVisualTransformation
import org.signal.registration.screens.RegistrationScaffold
import org.signal.registration.screens.TwoPaneRegistrationScaffold
@@ -67,10 +69,13 @@ fun PinEntryScreen(
onEvent: (PinEntryScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
+ val context = LocalContext.current
var pin by rememberSaveable { mutableStateOf("") }
var showSkipDialog by rememberSaveable { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val canSubmitPin = pin.isNotEmpty()
+ val supportEmailSubject = stringResource(R.string.PinEntryScreen__contact_support_email_subject)
+ val onContactSupport: () -> Unit = { RegistrationDependencies.get().contactSupportCallback?.invoke(context, supportEmailSubject) }
when (val params = RegistrationScaffold.rememberLayoutParams()) {
is RegistrationScaffold.Params.OnePane -> OnePaneLayout(
@@ -81,6 +86,7 @@ fun PinEntryScreen(
focusRequester = focusRequester,
onPinChanged = { pin = it },
onSkip = { showSkipDialog = true },
+ onContactSupport = onContactSupport,
onEvent = onEvent,
modifier = modifier
)
@@ -93,6 +99,7 @@ fun PinEntryScreen(
focusRequester = focusRequester,
onPinChanged = { pin = it },
onSkip = { showSkipDialog = true },
+ onContactSupport = onContactSupport,
onEvent = onEvent,
modifier = modifier
)
@@ -119,7 +126,10 @@ fun PinEntryScreen(
confirm = stringResource(R.string.PinEntryScreen__create_new_pin),
dismiss = stringResource(R.string.PinEntryScreen__contact_support),
onConfirm = { onEvent(PinEntryScreenEvents.CreateNewPin) },
- onDeny = { onEvent(PinEntryScreenEvents.ContactSupport) },
+ onDeny = {
+ onContactSupport()
+ onEvent(PinEntryScreenEvents.ContactSupport)
+ },
onDismissRequest = { onEvent(PinEntryScreenEvents.ContactSupport) },
properties = DialogProperties(
dismissOnBackPress = false,
@@ -143,6 +153,7 @@ private fun OnePaneLayout(
focusRequester: FocusRequester,
onPinChanged: (String) -> Unit,
onSkip: () -> Unit,
+ onContactSupport: () -> Unit,
onEvent: (PinEntryScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
@@ -174,7 +185,7 @@ private fun OnePaneLayout(
focusRequester = focusRequester,
onPinChanged = onPinChanged,
onSubmit = { onEvent(PinEntryScreenEvents.PinEntered(pin)) },
- onNeedsHelp = { onEvent(PinEntryScreenEvents.NeedHelp) },
+ onNeedsHelp = onContactSupport,
modifier = Modifier.fillMaxWidth()
)
@@ -214,6 +225,7 @@ private fun TwoPaneLayout(
focusRequester: FocusRequester,
onPinChanged: (String) -> Unit,
onSkip: () -> Unit,
+ onContactSupport: () -> Unit,
onEvent: (PinEntryScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
@@ -255,7 +267,7 @@ private fun TwoPaneLayout(
focusRequester = focusRequester,
onPinChanged = onPinChanged,
onSubmit = { onEvent(PinEntryScreenEvents.PinEntered(pin)) },
- onNeedsHelp = { onEvent(PinEntryScreenEvents.NeedHelp) },
+ onNeedsHelp = onContactSupport,
modifier = Modifier.fillMaxWidth()
)
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt
index 6957617ba1..850ad8a529 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreenEvents.kt
@@ -10,7 +10,6 @@ sealed class PinEntryScreenEvents {
override fun toString(): String = "PinEntered(pin=${pin.length} chars)"
}
data object ToggleKeyboard : PinEntryScreenEvents()
- data object NeedHelp : PinEntryScreenEvents()
data object Skip : PinEntryScreenEvents()
data object CreateNewPin : PinEntryScreenEvents()
data object ContactSupport : PinEntryScreenEvents()
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt
index 186c65833c..4e6f75b1cd 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt
@@ -46,6 +46,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.registration.R
+import org.signal.registration.RegistrationDependencies
import org.signal.registration.screens.OnePaneRegistrationScaffold
import org.signal.registration.screens.RegistrationScaffold
import org.signal.registration.screens.TwoPaneRegistrationScaffold
@@ -296,6 +297,9 @@ private fun RestoreStateDialogs(
state: RemoteBackupRestoreState,
onEvent: (RemoteBackupRestoreScreenEvents) -> Unit
) {
+ val context = LocalContext.current
+ val contactSupportEmailSubject = stringResource(R.string.RemoteRestoreScreen__contact_support_email_subject)
+
when (state.restoreState) {
RemoteBackupRestoreState.RestoreState.None -> Unit
RemoteBackupRestoreState.RestoreState.InProgress -> {
@@ -330,7 +334,10 @@ private fun RestoreStateDialogs(
body = stringResource(R.string.RemoteRestoreScreen__your_backup_is_not_recoverable),
confirm = stringResource(R.string.RemoteRestoreScreen__contact_support),
dismiss = stringResource(android.R.string.ok),
- onConfirm = { onEvent(RemoteBackupRestoreScreenEvents.DismissError) },
+ onConfirm = {
+ RegistrationDependencies.get().contactSupportCallback?.invoke(context, contactSupportEmailSubject)
+ onEvent(RemoteBackupRestoreScreenEvents.DismissError)
+ },
onDismiss = { onEvent(RemoteBackupRestoreScreenEvents.DismissError) }
)
}
diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml
index e0a5383497..0f4c508c03 100644
--- a/feature/registration/src/main/res/values/strings.xml
+++ b/feature/registration/src/main/res/values/strings.xml
@@ -270,6 +270,8 @@
An error occurred while restoring your backup. Your backup is not recoverable. Please contact support for help.
Contact support
+
+ Signal Android Backup restore permanent failure
Downloading backup…
@@ -379,6 +381,8 @@
No data could be found to restore your account. Please create a new PIN.
Contact support
+
+ Signal Registration - Need Help with PIN for Android
Allow Notifications