mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Update target API to 33
This commit is contained in:
committed by
Nicholas Tinsley
parent
b9449a798b
commit
a3e36d2453
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -238,7 +239,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openGallery() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(*PermissionCompat.forImages())
|
||||
.ifNecessary()
|
||||
.onAllGranted {
|
||||
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext())
|
||||
|
||||
@@ -31,8 +31,11 @@ import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||
import org.thoughtcrime.securesms.components.settings.RadioListPreference
|
||||
import org.thoughtcrime.securesms.components.settings.RadioListPreferenceViewHolder
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.Banner
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.TurnOnNotificationsBottomSheet
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.RingtoneUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
@@ -62,6 +65,11 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
|
||||
private lateinit var viewModel: NotificationsSettingsViewModel
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == MESSAGE_SOUND_SELECT && resultCode == Activity.RESULT_OK && data != null) {
|
||||
val uri: Uri? = data.getParcelableExtraCompat(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java)
|
||||
@@ -78,6 +86,8 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item)
|
||||
)
|
||||
|
||||
Banner.register(adapter)
|
||||
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val factory = NotificationsSettingsViewModel.Factory(sharedPreferences)
|
||||
|
||||
@@ -90,10 +100,23 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
|
||||
private fun getConfiguration(state: NotificationsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
if (!state.messageNotificationsState.canEnableNotifications) {
|
||||
customPref(
|
||||
Banner.Model(
|
||||
textId = R.string.NotificationSettingsFragment__to_enable_notifications,
|
||||
actionId = R.string.NotificationSettingsFragment__turn_on,
|
||||
onClick = {
|
||||
TurnOnNotificationsBottomSheet().show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
sectionHeaderPref(R.string.NotificationsSettingsFragment__messages)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__notifications),
|
||||
isEnabled = state.messageNotificationsState.canEnableNotifications,
|
||||
isChecked = state.messageNotificationsState.notificationsEnabled,
|
||||
onClick = {
|
||||
viewModel.setMessageNotificationsEnabled(!state.messageNotificationsState.notificationsEnabled)
|
||||
@@ -223,6 +246,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__notifications),
|
||||
isEnabled = state.callNotificationsState.canEnableNotifications,
|
||||
isChecked = state.callNotificationsState.notificationsEnabled,
|
||||
onClick = {
|
||||
viewModel.setCallNotificationsEnabled(!state.callNotificationsState.notificationsEnabled)
|
||||
|
||||
@@ -10,6 +10,7 @@ data class NotificationsSettingsState(
|
||||
|
||||
data class MessageNotificationsState(
|
||||
val notificationsEnabled: Boolean,
|
||||
val canEnableNotifications: Boolean,
|
||||
val sound: Uri,
|
||||
val vibrateEnabled: Boolean,
|
||||
val ledColor: String,
|
||||
@@ -23,6 +24,7 @@ data class MessageNotificationsState(
|
||||
|
||||
data class CallNotificationsState(
|
||||
val notificationsEnabled: Boolean,
|
||||
val canEnableNotifications: Boolean,
|
||||
val ringtone: Uri,
|
||||
val vibrateEnabled: Boolean
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.notifications
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
@@ -26,78 +27,83 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
|
||||
|
||||
val state: LiveData<NotificationsSettingsState> = store.stateLiveData
|
||||
|
||||
fun refresh() {
|
||||
store.update { getState() }
|
||||
}
|
||||
|
||||
fun setMessageNotificationsEnabled(enabled: Boolean) {
|
||||
SignalStore.settings().isMessageNotificationsEnabled = enabled
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageNotificationsSound(sound: Uri?) {
|
||||
val messageSound = sound ?: Uri.EMPTY
|
||||
SignalStore.settings().messageNotificationSound = messageSound
|
||||
NotificationChannels.getInstance().updateMessageRingtone(messageSound)
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageNotificationVibration(enabled: Boolean) {
|
||||
SignalStore.settings().isMessageVibrateEnabled = enabled
|
||||
NotificationChannels.getInstance().updateMessageVibrate(enabled)
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageNotificationLedColor(color: String) {
|
||||
SignalStore.settings().messageLedColor = color
|
||||
NotificationChannels.getInstance().updateMessagesLedColor(color)
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageNotificationLedBlink(blink: String) {
|
||||
SignalStore.settings().messageLedBlinkPattern = blink
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageNotificationInChatSoundsEnabled(enabled: Boolean) {
|
||||
SignalStore.settings().isMessageNotificationsInChatSoundsEnabled = enabled
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageRepeatAlerts(repeats: Int) {
|
||||
SignalStore.settings().messageNotificationsRepeatAlerts = repeats
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageNotificationPrivacy(preference: String) {
|
||||
SignalStore.settings().messageNotificationsPrivacy = NotificationPrivacyPreference(preference)
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setMessageNotificationPriority(priority: Int) {
|
||||
sharedPreferences.edit().putString(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF, priority.toString()).apply()
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setCallNotificationsEnabled(enabled: Boolean) {
|
||||
SignalStore.settings().isCallNotificationsEnabled = enabled
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setCallRingtone(ringtone: Uri?) {
|
||||
SignalStore.settings().callRingtone = ringtone ?: Uri.EMPTY
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setCallVibrateEnabled(enabled: Boolean) {
|
||||
SignalStore.settings().isCallVibrateEnabled = enabled
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setNotifyWhenContactJoinsSignal(enabled: Boolean) {
|
||||
SignalStore.settings().isNotifyWhenContactJoinsSignal = enabled
|
||||
store.update { getState() }
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun getState(): NotificationsSettingsState = NotificationsSettingsState(
|
||||
messageNotificationsState = MessageNotificationsState(
|
||||
notificationsEnabled = SignalStore.settings().isMessageNotificationsEnabled,
|
||||
notificationsEnabled = SignalStore.settings().isMessageNotificationsEnabled && canEnableNotifications(),
|
||||
canEnableNotifications = canEnableNotifications(),
|
||||
sound = SignalStore.settings().messageNotificationSound,
|
||||
vibrateEnabled = SignalStore.settings().isMessageVibrateEnabled,
|
||||
ledColor = SignalStore.settings().messageLedColor,
|
||||
@@ -109,13 +115,24 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
|
||||
troubleshootNotifications = SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations() && SlowNotificationHeuristics.isHavingDelayedNotifications()
|
||||
),
|
||||
callNotificationsState = CallNotificationsState(
|
||||
notificationsEnabled = SignalStore.settings().isCallNotificationsEnabled,
|
||||
notificationsEnabled = SignalStore.settings().isCallNotificationsEnabled && canEnableNotifications(),
|
||||
canEnableNotifications = canEnableNotifications(),
|
||||
ringtone = SignalStore.settings().callRingtone,
|
||||
vibrateEnabled = SignalStore.settings().isCallVibrateEnabled
|
||||
),
|
||||
notifyWhenContactJoinsSignal = SignalStore.settings().isNotifyWhenContactJoinsSignal
|
||||
)
|
||||
|
||||
private fun canEnableNotifications(): Boolean {
|
||||
val areNotificationsDisabledBySystem = Build.VERSION.SDK_INT >= 26 && (
|
||||
!NotificationChannels.getInstance().isMessageChannelEnabled ||
|
||||
!NotificationChannels.getInstance().isMessagesChannelGroupEnabled ||
|
||||
!NotificationChannels.getInstance().areNotificationsEnabled()
|
||||
)
|
||||
|
||||
return !areNotificationsDisabledBySystem
|
||||
}
|
||||
|
||||
class Factory(private val sharedPreferences: SharedPreferences) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(NotificationsSettingsViewModel(sharedPreferences)))
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.models
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.thoughtcrime.securesms.databinding.DslBannerBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
/**
|
||||
* Displays a banner to notify the user of certain state or action that needs to be taken.
|
||||
*/
|
||||
object Banner {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, DslBannerBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(
|
||||
@StringRes val textId: Int,
|
||||
@StringRes val actionId: Int,
|
||||
val onClick: () -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return textId == newItem.textId && actionId == newItem.actionId
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(binding: DslBannerBinding) : BindingViewHolder<Model, DslBannerBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
binding.bannerText.setText(model.textId)
|
||||
binding.bannerAction.setText(model.actionId)
|
||||
binding.bannerAction.setOnClickListener { model.onClick() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.maps.PlacePickerActivity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
@@ -72,7 +73,7 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat
|
||||
fun launchGallery(recipientId: RecipientId, text: CharSequence?, isReply: Boolean) {
|
||||
Permissions
|
||||
.with(fragment)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(*PermissionCompat.forImagesAndVideos())
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted { mediaGalleryLauncher.launch(MediaSelectionInput(emptyList(), recipientId, text, isReply)) }
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.keyboard
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
@@ -23,6 +22,7 @@ import org.thoughtcrime.securesms.conversation.AttachmentKeyboardButton
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.function.Predicate
|
||||
@@ -93,7 +93,7 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_
|
||||
|
||||
override fun onAttachmentPermissionsRequested() {
|
||||
Permissions.with(requireParentFragment())
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(*PermissionCompat.forImagesAndVideos())
|
||||
.onAllGranted { viewModel.refreshRecentMedia() }
|
||||
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.execute()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.devicetransfer;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.location.LocationManager;
|
||||
@@ -16,7 +15,6 @@ import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
@@ -100,7 +98,7 @@ public abstract class DeviceTransferSetupFragment extends LoggingFragment {
|
||||
case INITIAL:
|
||||
status.setText("");
|
||||
case PERMISSIONS_CHECK:
|
||||
requestLocationPermission();
|
||||
requestRequiredPermission();
|
||||
break;
|
||||
case PERMISSIONS_DENIED:
|
||||
error.setText(getErrorTextForStep(step));
|
||||
@@ -280,9 +278,9 @@ public abstract class DeviceTransferSetupFragment extends LoggingFragment {
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void requestLocationPermission() {
|
||||
private void requestRequiredPermission() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
.request(WifiDirect.requiredPermission())
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)), false, R.drawable.ic_location_on_white_24dp)
|
||||
.withPermanentDenialDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)))
|
||||
|
||||
@@ -78,7 +78,7 @@ public final class DeviceTransferSetupViewModel extends ViewModel {
|
||||
|
||||
public void onWifiDirectUnavailable(WifiDirect.AvailableStatus availability) {
|
||||
Log.i(TAG, "Wifi Direct unavailable: " + availability);
|
||||
if (availability == WifiDirect.AvailableStatus.FINE_LOCATION_PERMISSION_NOT_GRANTED) {
|
||||
if (availability == WifiDirect.AvailableStatus.REQUIRED_PERMISSION_NOT_GRANTED) {
|
||||
store.update(s -> s.updateStep(SetupStep.PERMISSIONS_CHECK));
|
||||
} else {
|
||||
store.update(s -> s.updateStep(SetupStep.WIFI_DIRECT_UNAVAILABLE));
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.NavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -46,7 +47,7 @@ class MediaSelectionNavigator(
|
||||
onGranted: () -> Unit
|
||||
) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(*PermissionCompat.forImagesAndVideos())
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.AttachmentKeyboard_Signal_needs_permission_to_show_your_photos_and_videos))
|
||||
.onAllGranted(onGranted)
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrMigrationActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.notifications.TurnOnNotificationsBottomSheet;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -232,17 +233,8 @@ public final class Megaphones {
|
||||
.setBody(R.string.NotificationsMegaphone_never_miss_a_message)
|
||||
.setImage(R.drawable.megaphone_notifications_64)
|
||||
.setActionButton(R.string.NotificationsMegaphone_turn_on, (megaphone, controller) -> {
|
||||
if (Build.VERSION.SDK_INT >= 26 && !NotificationChannels.getInstance().isMessageChannelEnabled()) {
|
||||
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getInstance().getMessagesChannel());
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
|
||||
controller.onMegaphoneNavigationRequested(intent);
|
||||
} else if (Build.VERSION.SDK_INT >= 26 &&
|
||||
(!NotificationChannels.getInstance().areNotificationsEnabled() || !NotificationChannels.getInstance().isMessagesChannelGroupEnabled()))
|
||||
{
|
||||
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
|
||||
controller.onMegaphoneNavigationRequested(intent);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
controller.onMegaphoneDialogFragmentRequested(new TurnOnNotificationsBottomSheet());
|
||||
} else {
|
||||
controller.onMegaphoneNavigationRequested(AppSettingsActivity.notifications(context));
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ import org.thoughtcrime.securesms.payments.create.CreatePaymentFragmentArgs;
|
||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
||||
import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog;
|
||||
import org.thoughtcrime.securesms.payments.preferences.model.PayeeParcelable;
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
|
||||
@@ -407,7 +408,7 @@ public class AttachmentManager {
|
||||
|
||||
public static void selectGallery(Fragment fragment, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull MessageSendType messageSendType, boolean hasQuote) {
|
||||
Permissions.with(fragment)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(PermissionCompat.forImagesAndVideos())
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> fragment.startActivityForResult(MediaSelectionActivity.gallery(fragment.requireContext(), messageSendType, Collections.emptyList(), recipient.getId(), body, hasQuote), requestCode))
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
|
||||
private const val PLACEHOLDER = "__TOGGLE_PLACEHOLDER__"
|
||||
|
||||
/**
|
||||
* Sheet explaining how to turn on notifications and providing an action to do so.
|
||||
*/
|
||||
class TurnOnNotificationsBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
TurnOnNotificationsSheetContent(this::goToSystemNotificationSettings)
|
||||
}
|
||||
|
||||
private fun goToSystemNotificationSettings() {
|
||||
if (Build.VERSION.SDK_INT >= 26 && !NotificationChannels.getInstance().isMessageChannelEnabled) {
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getInstance().messagesChannel)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
startActivity(intent)
|
||||
} else if (Build.VERSION.SDK_INT >= 26 && (!NotificationChannels.getInstance().areNotificationsEnabled() || !NotificationChannels.getInstance().isMessagesChannelGroupEnabled)) {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
startActivity(AppSettingsActivity.notifications(requireContext()))
|
||||
}
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TurnOnNotificationsSheetContentPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
TurnOnNotificationsSheetContent {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TurnOnNotificationsSheetContent(
|
||||
onGoToSettingsClicked: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
BottomSheets.Handle(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.TurnOnNotificationsBottomSheet__turn_on_notifications),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(bottom = 12.dp, top = 10.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.TurnOnNotificationsBottomSheet__to_receive_notifications),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(bottom = 32.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.TurnOnNotificationsBottomSheet__1_tap_settings_below),
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
)
|
||||
|
||||
val step2String = stringResource(id = R.string.TurnOnNotificationsBottomSheet__2_s_turn_on_notifications, PLACEHOLDER)
|
||||
val (step2Text, step2InlineContent) = remember(step2String) {
|
||||
val parts = step2String.split(PLACEHOLDER)
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append(parts[0])
|
||||
appendInlineContent("toggle")
|
||||
append(parts[1])
|
||||
}
|
||||
|
||||
val inlineContentMap = mapOf(
|
||||
"toggle" to InlineTextContent(Placeholder(36.sp, 22.sp, PlaceholderVerticalAlign.Center)) {
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.illustration_toggle_switch),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
annotatedString to inlineContentMap
|
||||
}
|
||||
|
||||
Text(
|
||||
text = step2Text,
|
||||
inlineContent = step2InlineContent,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onGoToSettingsClicked,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.fillMaxWidth(1f)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.TurnOnNotificationsBottomSheet__settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.permissions
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
|
||||
/**
|
||||
* Compatibility object for requesting specific permissions that have become more
|
||||
* granular as the APIs have evolved.
|
||||
*/
|
||||
object PermissionCompat {
|
||||
@JvmStatic
|
||||
fun forImages(): Array<String> {
|
||||
return if (Build.VERSION.SDK_INT >= 33) {
|
||||
arrayOf(Manifest.permission.READ_MEDIA_IMAGES)
|
||||
} else {
|
||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forVideos(): Array<String> {
|
||||
return if (Build.VERSION.SDK_INT >= 33) {
|
||||
arrayOf(Manifest.permission.READ_MEDIA_VIDEO)
|
||||
} else {
|
||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forImagesAndVideos(): Array<String> {
|
||||
return setOf(*(forImages() + forVideos())).toTypedArray()
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,10 @@ public class Permissions {
|
||||
return new PermissionsBuilder(new FragmentPermissionObject(fragment));
|
||||
}
|
||||
|
||||
public static boolean isRuntimePermissionsRequired() {
|
||||
return Build.VERSION.SDK_INT >= 23;
|
||||
}
|
||||
|
||||
public static class PermissionsBuilder {
|
||||
|
||||
private final PermissionObject permissionObject;
|
||||
@@ -239,13 +243,13 @@ public class Permissions {
|
||||
}
|
||||
|
||||
public static boolean hasAny(@NonNull Context context, String... permissions) {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
|
||||
return !isRuntimePermissionsRequired() ||
|
||||
Stream.of(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
}
|
||||
|
||||
public static boolean hasAll(@NonNull Context context, String... permissions) {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
|
||||
return !isRuntimePermissionsRequired() ||
|
||||
Stream.of(permissions).allMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
|
||||
import java.io.File;
|
||||
@@ -82,10 +83,6 @@ public class StorageUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static File getBackupCacheDirectory(Context context) {
|
||||
return context.getExternalCacheDir();
|
||||
}
|
||||
|
||||
private static File getSignalStorageDir() throws NoExternalStorageException {
|
||||
final File storage = Environment.getExternalStorageDirectory();
|
||||
|
||||
@@ -108,17 +105,13 @@ public class StorageUtil {
|
||||
return storage.canWrite();
|
||||
}
|
||||
|
||||
public static File getLegacyBackupDirectory() throws NoExternalStorageException {
|
||||
return getSignalStorageDir();
|
||||
}
|
||||
|
||||
public static boolean canWriteToMediaStore() {
|
||||
return Build.VERSION.SDK_INT > 28 ||
|
||||
Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
|
||||
public static boolean canReadFromMediaStore() {
|
||||
return Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
return Permissions.hasAll(ApplicationDependencies.getApplication(), PermissionCompat.forImagesAndVideos());
|
||||
}
|
||||
|
||||
public static @NonNull Uri getVideoUri() {
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.navigation.Navigation;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.wallpaper.crop.WallpaperImageSelectionActivity;
|
||||
|
||||
@@ -76,7 +77,7 @@ public class ChatWallpaperSelectionFragment extends Fragment {
|
||||
|
||||
private void askForPermissionIfNeededAndLaunchPhotoSelection() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(PermissionCompat.forImages())
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
startActivityForResult(WallpaperImageSelectionActivity.getIntent(requireContext(), viewModel.getRecipientId()), CHOOSE_WALLPAPER);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.wallpaper.crop;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
@@ -8,7 +7,6 @@ import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
@@ -23,7 +21,6 @@ public final class WallpaperImageSelectionActivity extends AppCompatActivity
|
||||
private static final String EXTRA_RECIPIENT_ID = "RECIPIENT_ID";
|
||||
private static final int CROP = 901;
|
||||
|
||||
@RequiresPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
public static Intent getIntent(@NonNull Context context,
|
||||
@Nullable RecipientId recipientId)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user