Update target API to 33

This commit is contained in:
Alex Hart
2023-08-29 16:48:46 -03:00
committed by Nicholas Tinsley
parent b9449a798b
commit a3e36d2453
38 changed files with 1236 additions and 203 deletions

View File

@@ -0,0 +1,303 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.fragments
import android.os.Build
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.Buttons
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
import org.thoughtcrime.securesms.util.BackupUtil
/**
* Fragment displayed during registration which allows a user to read through
* what permissions are granted to Signal and why, and a means to either skip
* granting those permissions or continue to grant via system dialogs.
*/
class GrantPermissionsFragment : ComposeFragment() {
private val args by navArgs<GrantPermissionsFragmentArgs>()
private val viewModel by activityViewModels<RegistrationViewModel>()
private val isSearchingForBackup = mutableStateOf(false)
@Composable
override fun FragmentContent() {
val isSearchingForBackup by this.isSearchingForBackup
GrantPermissionsScreen(
deviceBuildVersion = Build.VERSION.SDK_INT,
isSearchingForBackup = isSearchingForBackup,
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
onNextClicked = this::onNextClicked,
onNotNowClicked = this::onNotNowClicked
)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
private fun onNextClicked() {
when (args.welcomeAction) {
WelcomeAction.CONTINUE -> {
WelcomeFragment.continueClicked(
this,
viewModel,
{ isSearchingForBackup.value = true },
{ isSearchingForBackup.value = false },
GrantPermissionsFragmentDirections.actionSkipRestore(),
GrantPermissionsFragmentDirections.actionRestore()
)
}
WelcomeAction.RESTORE_BACKUP -> {
WelcomeFragment.restoreFromBackupClicked(
this,
viewModel,
GrantPermissionsFragmentDirections.actionTransferOrRestore()
)
}
}
}
private fun onNotNowClicked() {
when (args.welcomeAction) {
WelcomeAction.CONTINUE -> {
WelcomeFragment.gatherInformationAndContinue(
this,
viewModel,
{ isSearchingForBackup.value = true },
{ isSearchingForBackup.value = false },
GrantPermissionsFragmentDirections.actionSkipRestore(),
GrantPermissionsFragmentDirections.actionRestore()
)
}
WelcomeAction.RESTORE_BACKUP -> {
WelcomeFragment.gatherInformationAndChooseBackup(
this,
viewModel,
GrantPermissionsFragmentDirections.actionTransferOrRestore()
)
}
}
}
/**
* Which welcome action the user selected which prompted this
* screen.
*/
enum class WelcomeAction {
CONTINUE,
RESTORE_BACKUP
}
}
@Preview
@Composable
fun GrantPermissionsScreenPreview() {
SignalTheme(isDarkMode = false) {
GrantPermissionsScreen(
deviceBuildVersion = 33,
isBackupSelectionRequired = true,
isSearchingForBackup = true,
{},
{}
)
}
}
@Composable
fun GrantPermissionsScreen(
deviceBuildVersion: Int,
isBackupSelectionRequired: Boolean,
isSearchingForBackup: Boolean,
onNextClicked: () -> Unit,
onNotNowClicked: () -> Unit
) {
Surface {
Column(
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 40.dp, bottom = 24.dp)
) {
LazyColumn(
modifier = Modifier.weight(1f)
) {
item {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__allow_permissions),
style = MaterialTheme.typography.headlineMedium
)
}
item {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__to_help_you_message_people_you_know),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 12.dp, bottom = 41.dp)
)
}
if (deviceBuildVersion >= 33) {
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
)
}
}
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_contact),
title = stringResource(id = R.string.GrantPermissionsFragment__contacts),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__find_people_you_know)
)
}
if (deviceBuildVersion < 29 || !isBackupSelectionRequired) {
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_file),
title = stringResource(id = R.string.GrantPermissionsFragment__storage),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__send_photos_videos_and_files)
)
}
}
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_phone),
title = stringResource(id = R.string.GrantPermissionsFragment__phone_calls),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__make_registering_easier)
)
}
}
Row {
TextButton(onClick = onNotNowClicked) {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__not_now)
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSearchingForBackup) {
Box {
NextButton(
isSearchingForBackup = true,
onNextClicked = onNextClicked
)
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
} else {
NextButton(
isSearchingForBackup = false,
onNextClicked = onNextClicked
)
}
}
}
}
}
@Preview
@Composable
fun PermissionRowPreview() {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
)
}
@Composable
fun PermissionRow(
imageVector: ImageVector,
title: String,
subtitle: String
) {
Row(modifier = Modifier.padding(bottom = 32.dp)) {
Image(
imageVector = imageVector,
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.size(16.dp))
Column {
Text(
text = title,
style = MaterialTheme.typography.titleSmall
)
Text(
text = subtitle,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.size(32.dp))
}
}
@Composable
fun NextButton(
isSearchingForBackup: Boolean,
onNextClicked: () -> Unit
) {
val alpha = if (isSearchingForBackup) {
0f
} else {
1f
}
Buttons.LargeTonal(
onClick = onNextClicked,
enabled = !isSearchingForBackup,
modifier = Modifier.alpha(alpha)
) {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__next)
)
}
}

View File

@@ -14,11 +14,11 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.ActivityNavigator;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
@@ -49,29 +49,6 @@ public final class WelcomeFragment extends LoggingFragment {
private static final String TAG = Log.tag(WelcomeFragment.class);
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE };
@RequiresApi(26)
private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
@RequiresApi(26)
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
private CircularProgressMaterialButton continueButton;
private RegistrationViewModel viewModel;
@@ -97,7 +74,7 @@ public final class WelcomeFragment extends LoggingFragment {
return;
}
initializeNumber();
initializeNumber(requireContext(), viewModel);
Log.i(TAG, "Skipping restore because this is a reregistration.");
viewModel.setWelcomeSkippedOnRestore();
@@ -109,10 +86,10 @@ public final class WelcomeFragment extends LoggingFragment {
setDebugLogSubmitMultiTapView(view.findViewById(R.id.title));
continueButton = view.findViewById(R.id.welcome_continue_button);
continueButton.setOnClickListener(this::continueClicked);
continueButton.setOnClickListener(v -> onContinueClicked());
Button restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
restoreFromBackup.setOnClickListener(this::restoreFromBackupClicked);
restoreFromBackup.setOnClickListener(v -> onRestoreFromBackupClicked());
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
welcomeTermsButton.setOnClickListener(v -> onTermsClicked());
@@ -139,70 +116,116 @@ public final class WelcomeFragment extends LoggingFragment {
}
}
private void continueClicked(@NonNull View view) {
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext());
private void onContinueClicked() {
if (Permissions.isRuntimePermissionsRequired()) {
NavHostFragment.findNavController(this)
.navigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.CONTINUE));
} else {
gatherInformationAndContinue(
this,
viewModel,
() -> continueButton.setSpinning(),
() -> continueButton.cancelSpinning(),
WelcomeFragmentDirections.actionSkipRestore(),
WelcomeFragmentDirections.actionRestore()
);
}
}
Permissions.with(this)
.request(getContinuePermissions(isUserSelectionRequired))
private void onRestoreFromBackupClicked() {
if (Permissions.isRuntimePermissionsRequired()) {
NavHostFragment.findNavController(this)
.navigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(GrantPermissionsFragment.WelcomeAction.RESTORE_BACKUP));
} else {
gatherInformationAndChooseBackup(this, viewModel, WelcomeFragmentDirections.actionTransferOrRestore());
}
}
static void continueClicked(@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull Runnable onSearchForBackupStarted,
@NonNull Runnable onSearchForBackupFinished,
@NonNull NavDirections actionSkipRestore,
@NonNull NavDirections actionRestore)
{
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext());
Permissions.with(fragment)
.request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired))
.ifNecessary()
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired))
.onAnyResult(() -> gatherInformationAndContinue(continueButton))
.onAnyResult(() -> gatherInformationAndContinue(fragment,
viewModel,
onSearchForBackupStarted,
onSearchForBackupFinished,
actionSkipRestore,
actionRestore))
.execute();
}
private void restoreFromBackupClicked(@NonNull View view) {
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext());
static void restoreFromBackupClicked(@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull NavDirections actionTransferOrRestore)
{
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext());
Permissions.with(this)
.request(getContinuePermissions(isUserSelectionRequired))
Permissions.with(fragment)
.request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired))
.ifNecessary()
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired))
.onAnyResult(() -> gatherInformationAndChooseBackup(continueButton))
.onAnyResult(() -> gatherInformationAndChooseBackup(fragment, viewModel, actionTransferOrRestore))
.execute();
}
private void gatherInformationAndContinue(@NonNull View view) {
continueButton.setSpinning();
static void gatherInformationAndContinue(
@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull Runnable onSearchForBackupStarted,
@NonNull Runnable onSearchForBackupFinished,
@NonNull NavDirections actionSkipRestore,
@NonNull NavDirections actionRestore
) {
onSearchForBackupStarted.run();
RestoreBackupFragment.searchForBackup(backup -> {
Context context = getContext();
Context context = fragment.getContext();
if (context == null) {
Log.i(TAG, "No context on fragment, must have navigated away.");
return;
}
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true);
TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
initializeNumber();
initializeNumber(fragment.requireContext(), viewModel);
continueButton.cancelSpinning();
onSearchForBackupFinished.run();
if (backup == null) {
Log.i(TAG, "Skipping backup. No backup found, or no permission to look.");
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
WelcomeFragmentDirections.actionSkipRestore());
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
actionSkipRestore);
} else {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
WelcomeFragmentDirections.actionRestore());
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
actionRestore);
}
});
}
private void gatherInformationAndChooseBackup(@NonNull View view) {
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true);
static void gatherInformationAndChooseBackup(@NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull NavDirections actionTransferOrRestore) {
TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
initializeNumber();
initializeNumber(fragment.requireContext(), viewModel);
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
WelcomeFragmentDirections.actionTransferOrRestore());
SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
actionTransferOrRestore);
}
@SuppressLint("MissingPermission")
private void initializeNumber() {
private static void initializeNumber(@NonNull Context context, @NonNull RegistrationViewModel viewModel) {
Optional<Phonenumber.PhoneNumber> localNumber = Optional.empty();
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
localNumber = Util.getDeviceNumber(requireContext());
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
localNumber = Util.getDeviceNumber(context);
} else {
Log.i(TAG, "No phone permission");
}
@@ -215,7 +238,7 @@ public final class WelcomeFragment extends LoggingFragment {
viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
} else {
Log.i(TAG, "No number detected");
Optional<String> simCountryIso = Util.getSimCountryIso(requireContext());
Optional<String> simCountryIso = Util.getSimCountryIso(context);
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
@@ -232,23 +255,4 @@ public final class WelcomeFragment extends LoggingFragment {
!viewModel.isReregister() &&
!SignalStore.settings().isBackupEnabled();
}
@SuppressLint("NewApi")
private static String[] getContinuePermissions(boolean isUserSelectionRequired) {
if (isUserSelectionRequired) {
return PERMISSIONS_API_29;
} else if (Build.VERSION.SDK_INT >= 26) {
return PERMISSIONS_API_26;
} else {
return PERMISSIONS;
}
}
private static @StringRes int getContinueRationale(boolean isUserSelectionRequired) {
return isUserSelectionRequired ? RATIONALE_API_29 : RATIONALE;
}
private static int[] getContinueHeaders(boolean isUserSelectionRequired) {
return isUserSelectionRequired ? HEADERS_API_29 : HEADERS;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.fragments
import android.Manifest
import android.os.Build
/**
* Handles welcome permissions instead of having to do weird giant if statements.
*/
object WelcomePermissions {
private enum class Permissions {
POST_NOTIFICATIONS {
override fun getPermissions(isUserBackupSelectionRequired: Boolean): List<String> {
return if (Build.VERSION.SDK_INT >= 33) {
listOf(Manifest.permission.POST_NOTIFICATIONS)
} else {
emptyList()
}
}
},
CONTACTS {
override fun getPermissions(isUserBackupSelectionRequired: Boolean): List<String> {
return listOf(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
}
},
STORAGE {
override fun getPermissions(isUserBackupSelectionRequired: Boolean): List<String> {
return if (Build.VERSION.SDK_INT < 29 || !isUserBackupSelectionRequired) {
listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
emptyList()
}
}
},
PHONE {
override fun getPermissions(isUserBackupSelectionRequired: Boolean): List<String> {
return listOf(Manifest.permission.READ_PHONE_STATE) +
(if (Build.VERSION.SDK_INT >= 26) listOf(Manifest.permission.READ_PHONE_NUMBERS) else emptyList())
}
};
abstract fun getPermissions(isUserBackupSelectionRequired: Boolean): List<String>
}
@JvmStatic
fun getWelcomePermissions(isUserBackupSelectionRequired: Boolean): Array<String> {
return Permissions.values().map { it.getPermissions(isUserBackupSelectionRequired) }.flatten().toTypedArray()
}
}