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

@ -596,6 +596,7 @@ dependencies {
testImplementation testLibs.robolectric.shadows.multidex
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
testImplementation (testLibs.bouncycastle.bcpkix.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
testImplementation testLibs.conscrypt.openjdk.uber // Used by robolectric
testImplementation testLibs.hamcrest.hamcrest
testImplementation testLibs.mockk

View file

@ -44,6 +44,8 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
@ -94,6 +96,10 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"

View file

@ -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())

View file

@ -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)

View file

@ -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
)

View file

@ -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)))

View file

@ -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() }
}
}
}

View file

@ -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)) }

View file

@ -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()

View file

@ -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)))

View file

@ -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));

View file

@ -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)

View file

@ -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));
}

View file

@ -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))

View file

@ -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))
}
}
}

View file

@ -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()
}
}

View file

@ -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);
}

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()
}
}

View file

@ -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() {

View file

@ -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);

View file

@ -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)
{

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="22dp"
android:viewportWidth="36"
android:viewportHeight="22">
<path
android:pathData="M11,0L25,0A11,11 0,0 1,36 11L36,11A11,11 0,0 1,25 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#B6C5FA"/>
<path
android:pathData="M25,11m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:fillColor="#1E2438"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="22dp"
android:viewportWidth="36"
android:viewportHeight="22">
<path
android:pathData="M11,0L25,0A11,11 0,0 1,36 11L36,11A11,11 0,0 1,25 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#2C58C3"/>
<path
android:pathData="M25,11m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,24m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
android:fillColor="#fff"/>
<path
android:pathData="M24,27.75c3.59,0 6.5,-3.772 6.5,-8.121C30.5,15.279 27.59,12 24,12s-6.5,3.28 -6.5,7.629c0,4.35 2.91,8.121 6.5,8.121ZM24,44a19.92,19.92 0,0 0,13.139 -4.921c-2.82,-3.513 -7.652,-5.829 -13.14,-5.829 -5.486,0 -10.318,2.316 -13.138,5.829A19.923,19.923 0,0 0,24 44Z"
android:fillColor="#E3E9F5"/>
<path
android:pathData="M24,11c-4.276,0 -7.5,3.871 -7.5,8.629 0,2.39 0.798,4.642 2.12,6.312 1.321,1.67 3.214,2.809 5.38,2.809s4.059,-1.139 5.38,-2.81c1.322,-1.67 2.12,-3.92 2.12,-6.311C31.5,14.87 28.276,11 24,11ZM18.5,19.629C18.5,15.689 21.096,13 24,13s5.5,2.688 5.5,6.629c0,1.958 -0.657,3.768 -1.688,5.07 -1.03,1.304 -2.388,2.051 -3.812,2.051s-2.781,-0.747 -3.812,-2.05c-1.031,-1.303 -1.688,-3.113 -1.688,-5.071Z"
android:fillColor="#7282A5"
android:fillType="evenOdd"/>
<path
android:pathData="M3,24C3,12.402 12.402,3 24,3s21,9.402 21,21 -9.402,21 -21,21S3,35.598 3,24ZM24,5C13.507,5 5,13.507 5,24a18.944,18.944 0,0 0,5.78 13.646c3.085,-3.31 7.884,-5.396 13.22,-5.396s10.135,2.087 13.22,5.396A18.944,18.944 0,0 0,43 24c0,-10.493 -8.507,-19 -19,-19ZM24,43a18.918,18.918 0,0 1,-11.712 -4.038C14.964,36.122 19.19,34.25 24,34.25s9.036,1.872 11.712,4.712A18.918,18.918 0,0 1,24 43Z"
android:fillColor="#7282A5"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M33.4,12c3.36,0 5.04,0 6.324,0.654a6,6 0,0 1,2.622 2.622C43,16.56 43,18.24 43,21.6v8.8c0,3.36 0,5.04 -0.654,6.324a6,6 0,0 1,-2.622 2.622C38.44,40 36.76,40 33.4,40H14.6c-3.36,0 -5.04,0 -6.324,-0.654a6,6 0,0 1,-2.622 -2.622C5,35.44 5,33.76 5,30.4v-8.8c0,-3.36 0,-5.04 0.654,-6.324a6,6 0,0 1,2.622 -2.622C9.56,12 11.24,12 14.6,12h18.8Z"
android:fillColor="#FFEABC"/>
<path
android:pathData="M5,21.6v8.8c0,3.36 0,5.04 0.654,6.324a6,6 0,0 0,2.622 2.622C9.56,40 11.24,40 14.6,40h18.943c3.265,0 4.916,-0.01 6.18,-0.654 0.452,-0.23 0.869,-0.514 1.244,-0.844L7.033,13.498a5.998,5.998 0,0 0,-1.379 1.778C5,16.56 5,18.24 5,21.6Z"
android:fillColor="#FFE3A5"/>
<path
android:pathData="M5.026,18.681c0.051,-1.548 0.199,-2.562 0.628,-3.405a6,6 0,0 1,2.622 -2.622C9.56,12 11.24,12 14.6,12h7.891c-0.073,-0.163 -0.166,-0.35 -0.32,-0.658 -0.495,-0.99 -0.743,-1.485 -1.048,-1.9a6,6 0,0 0,-3.81 -2.354C16.806,7 16.253,7 15.146,7H14c-2.796,0 -4.193,0 -5.296,0.457a6,6 0,0 0,-3.247 3.247C5,11.806 5,13.204 5,16c0,1.082 0,1.954 0.026,2.681Z"
android:fillColor="#E9C576"/>
<path
android:pathData="M4,21.473v-4.94c0,-1.632 0,-2.918 0.084,-3.953 0.086,-1.055 0.264,-1.937 0.67,-2.74A7,7 0,0 1,7.84 6.755c0.802,-0.406 1.684,-0.584 2.74,-0.67C11.615,6 12.9,6 14.533,6h2.551a6.15,6.15 0,0 1,5.274 2.986A4.15,4.15 0,0 0,25.912 11h7.532c1.643,0 2.937,0 3.978,0.085 1.063,0.087 1.95,0.267 2.756,0.678a7,7 0,0 1,3.059 3.06c0.41,0.805 0.591,1.692 0.678,2.755 0.085,1.041 0.085,2.335 0.085,3.978v8.888c0,1.643 0,2.937 -0.085,3.978 -0.087,1.063 -0.267,1.95 -0.678,2.756a7,7 0,0 1,-3.06 3.059c-0.805,0.41 -1.692,0.591 -2.755,0.678 -1.041,0.085 -2.335,0.085 -3.978,0.085L14.556,41c-1.643,0 -2.937,0 -3.978,-0.085 -1.063,-0.087 -1.95,-0.267 -2.756,-0.678a7,7 0,0 1,-3.059 -3.06c-0.41,-0.805 -0.591,-1.692 -0.678,-2.755C4,33.381 4,32.087 4,30.444v-8.97ZM25.912,13L14.6,13c-1.697,0 -2.909,0 -3.86,0.078 -0.938,0.077 -1.533,0.224 -2.01,0.467a5,5 0,0 0,-2.185 2.185c-0.243,0.477 -0.39,1.072 -0.467,2.01 -0.076,0.931 -0.078,2.112 -0.078,3.754L6,30.4c0,1.697 0,2.909 0.078,3.86 0.077,0.938 0.224,1.533 0.467,2.01a5,5 0,0 0,2.185 2.185c0.477,0.243 1.072,0.39 2.01,0.467 0.951,0.077 2.163,0.078 3.86,0.078h18.8c1.697,0 2.909,0 3.86,-0.078 0.938,-0.077 1.533,-0.224 2.01,-0.467a5,5 0,0 0,2.185 -2.185c0.243,-0.477 0.39,-1.072 0.467,-2.01 0.077,-0.951 0.078,-2.163 0.078,-3.86v-8.8c0,-1.697 0,-2.909 -0.078,-3.86 -0.077,-0.938 -0.224,-1.533 -0.467,-2.01a5,5 0,0 0,-2.185 -2.185c-0.477,-0.243 -1.072,-0.39 -2.01,-0.467 -0.951,-0.077 -2.163,-0.078 -3.86,-0.078h-7.488ZM21.377,11c-0.274,-0.3 -0.52,-0.63 -0.735,-0.986A4.15,4.15 0,0 0,17.084 8h-2.507c-1.685,0 -2.89,0 -3.835,0.077 -0.933,0.076 -1.524,0.221 -1.999,0.461a5,5 0,0 0,-2.205 2.205c-0.24,0.475 -0.385,1.066 -0.46,2a15.71,15.71 0,0 0,-0.022 0.301,7 7,0 0,1 1.766,-1.281c0.806,-0.41 1.693,-0.591 2.756,-0.678C11.619,11 12.913,11 14.556,11h6.82Z"
android:fillColor="#9C7A2D"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M17,36.5c0,3.967 3.033,7 7,7s7,-3.033 7,-7"
android:fillColor="#E9C576"/>
<path
android:pathData="M5.25,33.403c0,1.71 1.361,3.097 3.04,3.097h31.42c1.679,0 3.04,-1.386 3.04,-3.097 0,-4.325 -5.557,-1.548 -6.081,-16.516C36.416,9.647 30.717,4.5 24,4.5c-6.717,0 -12.416,5.147 -12.669,12.387 -0.524,14.968 -6.081,12.19 -6.081,16.516Z"
android:fillColor="#FFEABC"/>
<path
android:pathData="M8.29,36.5c-1.679,0 -3.04,-1.386 -3.04,-3.097 0,-1.528 0.694,-2.17 1.615,-3.021 1.53,-1.415 3.687,-3.41 4.323,-11.022A20.908,20.908 0,0 1,24 15c4.823,0 9.267,1.626 12.812,4.36 0.636,7.613 2.793,9.607 4.323,11.022 0.921,0.851 1.615,1.493 1.615,3.021 0,1.71 -1.361,3.097 -3.04,3.097H8.29Z"
android:fillColor="#FFE3A5"/>
<path
android:pathData="M10.707,3.293a1,1 0,0 1,0 1.414c-2.355,2.356 -4.24,5.65 -4.713,9.903a1,1 0,0 1,-1.988 -0.22c0.528,-4.748 2.642,-8.453 5.287,-11.097a1,1 0,0 1,1.414 0ZM37.293,3.293a1,1 0,0 0,0 1.414c2.355,2.356 4.24,5.65 4.713,9.903a1,1 0,0 0,1.988 -0.22c-0.527,-4.748 -2.642,-8.453 -5.287,-11.097a1,1 0,0 0,-1.414 0Z"
android:fillColor="#9C7A2D"/>
<path
android:pathData="M10.332,16.852C10.604,9.064 16.755,3.5 24,3.5c7.245,0 13.396,5.564 13.668,13.352 0.258,7.364 1.747,10.157 3,11.638 0.326,0.383 0.651,0.698 0.977,1.001l0.155,0.144 0.004,0.003c0.27,0.25 0.558,0.516 0.81,0.792 0.67,0.738 1.136,1.608 1.136,2.973 0,2.245 -1.792,4.097 -4.04,4.097h-7.768c-0.471,4.022 -3.763,7 -7.942,7 -4.18,0 -7.471,-2.978 -7.942,-7L8.29,37.5c-2.25,0 -4.041,-1.852 -4.041,-4.097 0,-1.365 0.466,-2.235 1.137,-2.973 0.25,-0.276 0.54,-0.543 0.809,-0.792l0.159,-0.147c0.326,-0.303 0.651,-0.618 0.976,-1.001 1.254,-1.48 2.743,-4.274 3,-11.638ZM24,5.5c-6.189,0 -11.435,4.731 -11.67,11.422 -0.266,7.604 -1.817,10.906 -3.473,12.86a13.45,13.45 0,0 1,-1.14 1.173l-0.184,0.171c-0.268,0.248 -0.48,0.444 -0.667,0.65 -0.387,0.425 -0.616,0.83 -0.616,1.627 0,1.176 0.93,2.097 2.04,2.097h31.42c1.11,0 2.04,-0.921 2.04,-2.097 0,-0.797 -0.23,-1.202 -0.616,-1.627 -0.187,-0.206 -0.399,-0.402 -0.667,-0.65l-0.184,-0.17c-0.344,-0.32 -0.738,-0.699 -1.14,-1.174 -1.656,-1.954 -3.207,-5.256 -3.474,-12.86C35.437,10.232 30.19,5.5 24,5.5ZM24,42.5c-3.072,0 -5.473,-2.093 -5.924,-5h11.848c-0.45,2.907 -2.852,5 -5.924,5Z"
android:fillColor="#9C7A2D"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M15.847,7.103a4.058,4.058 0,0 0,-6.1 -0.414l-0.871,0.87C6.09,10.347 4.7,14.355 5.978,18.083a38.396,38.396 0,0 0,9.208 14.732,38.395 38.395,0 0,0 14.732,9.208c3.728,1.277 7.736,-0.112 10.522,-2.898l0.871,-0.871a4.058,4.058 0,0 0,-0.414 -6.1l-5.706,-4.337a4.058,4.058 0,0 0,-5.325 0.361l-2.484,2.485c-0.793,0.792 -3.683,-0.814 -6.457,-3.587 -2.773,-2.774 -4.379,-5.664 -3.587,-6.457l2.485,-2.484a4.058,4.058 0,0 0,0.361 -5.325l-4.337,-5.706Z"
android:fillColor="#DDE7FF"/>
<path
android:pathData="M20.083,17.848a4.058,4.058 0,0 0,0.101 -5.039l-4.337,-5.706a4.059,4.059 0,0 0,-5.839 -0.654c0.213,5.773 4.503,10.504 10.075,11.399ZM29.524,28.519 L29.866,28.177a4.058,4.058 0,0 1,5.325 -0.361l5.706,4.337a4.06,4.06 0,0 1,1.294 4.789c-0.392,0.038 -0.79,0.058 -1.19,0.058 -5.404,0 -9.973,-3.57 -11.477,-8.481Z"
android:fillColor="#C3CFE9"/>
<path
android:pathData="M15.051,7.708a3.058,3.058 0,0 0,-4.597 -0.312l-0.871,0.87c-2.585,2.586 -3.784,6.209 -2.659,9.492a37.396,37.396 0,0 0,8.97 14.348,37.397 37.397,0 0,0 14.348,8.97c3.283,1.125 6.906,-0.074 9.491,-2.659l0.871,-0.871a3.058,3.058 0,0 0,-0.312 -4.597l-5.706,-4.337a3.058,3.058 0,0 0,-4.013 0.272l-2.484,2.485c-0.557,0.556 -1.32,0.54 -1.85,0.438 -0.572,-0.11 -1.2,-0.378 -1.828,-0.728 -1.27,-0.705 -2.76,-1.864 -4.193,-3.297 -1.433,-1.433 -2.592,-2.922 -3.297,-4.193 -0.35,-0.629 -0.617,-1.256 -0.728,-1.827 -0.103,-0.53 -0.119,-1.294 0.438,-1.85l2.485,-2.485a3.058,3.058 0,0 0,0.272 -4.013l-4.337,-5.706ZM9.04,5.982a5.058,5.058 0,0 1,7.603 0.516l4.337,5.706a5.058,5.058 0,0 1,-0.45 6.637l-2.394,2.394c0.002,0.033 0.008,0.08 0.02,0.146 0.055,0.28 0.213,0.697 0.513,1.237 0.592,1.066 1.623,2.41 2.963,3.75 1.34,1.34 2.684,2.37 3.75,2.963 0.54,0.3 0.957,0.458 1.237,0.513 0.065,0.012 0.113,0.018 0.146,0.02l2.394,-2.394a5.058,5.058 0,0 1,6.637 -0.45l5.706,4.337a5.058,5.058 0,0 1,0.516 7.603l-0.87,0.871c-2.988,2.988 -7.381,4.567 -11.554,3.137a39.395,39.395 0,0 1,-15.115 -9.447,39.395 39.395,0 0,1 -9.447,-15.115c-1.43,-4.173 0.15,-8.566 3.137,-11.553l0.87,-0.871 0.708,0.707 -0.707,-0.707ZM26.828,29.862 L26.817,29.864 26.827,29.862ZM18.138,21.172 L18.136,21.183c0,-0.008 0.002,-0.011 0.002,-0.01Z"
android:fillColor="#6C7B9D"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
app:cardCornerRadius="18dp"
app:cardElevation="0dp"
app:strokeColor="@color/signal_colorOutline_38"
app:strokeWidth="1dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/banner_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16.57dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="10dp"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurface"
app:layout_constraintBottom_toTopOf="@id/banner_action"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Banner text will go here and probably be about something important" />
<com.google.android.material.button.MaterialButton
android:id="@+id/banner_action"
style="@style/Widget.Signal.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banner_text"
tools:text="Action" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -43,6 +43,49 @@
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_welcomeFragment_to_grantPermissionsFragment"
app:destination="@id/grantPermissionsFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/grantPermissionsFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.GrantPermissionsFragment"
android:label="fragment_grant_permissions">
<action
android:id="@+id/action_restore"
app:destination="@id/restoreBackupFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_skip_restore"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_transfer_or_restore"
app:destination="@id/transferOrRestore"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<argument
android:name="welcomeAction"
app:argType="org.thoughtcrime.securesms.registration.fragments.GrantPermissionsFragment$WelcomeAction" />
</fragment>
<fragment

View file

@ -202,4 +202,5 @@
<color name="message_request_bar_background_wallpaper">@color/signal_colorNeutralVariant</color>
<color name="message_request_bar_denyForeground_wallpaper">@color/signal_colorError</color>
<color name="message_request_bar_acceptForeground_wallpaper">@color/signal_colorNeutralInverse</color>
</resources>

View file

@ -2246,6 +2246,18 @@
<string name="TurnOffContactJoinedNotificationsActivity__turn_off_contact_joined_signal">Turn off contact joined Signal notifications? You can enable them again in Signal > Settings > Notifications.</string>
<!-- TurnOnNotificationsBottomSheet -->
<!-- Title for sheet explaining how to turn on app notifications -->
<string name="TurnOnNotificationsBottomSheet__turn_on_notifications">Turn on notifications</string>
<!-- Subtitle for sheet explaining how to turn on app notifications -->
<string name="TurnOnNotificationsBottomSheet__to_receive_notifications">To receive notifications for new messages:</string>
<!-- Sheet step 1 for sheet explaining how to turn on app notifications-->
<string name="TurnOnNotificationsBottomSheet__1_tap_settings_below">1. Tap “Settings” below</string>
<!-- Sheet step 2 with placeholder which will be replaced with an image of a toggle for sheet explaining how to turn on app notifications -->
<string name="TurnOnNotificationsBottomSheet__2_s_turn_on_notifications">2. %1$s Turn on notifications</string>
<!-- Label for button at the bottom of the sheet which opens system app notification settings for sheet explaining how to turn on app notifications -->
<string name="TurnOnNotificationsBottomSheet__settings">Settings</string>
<!-- Notification Channels -->
<string name="NotificationChannel_channel_messages">Messages</string>
<string name="NotificationChannel_calls">Calls</string>
@ -3117,6 +3129,32 @@
<!-- Alert dialog button to update now -->
<string name="PaymentsHomeFragment__update_now">Update now</string>
<!-- GrantPermissionsFragment -->
<!-- Displayed as a text-only action button at the bottom start of the screen -->
<string name="GrantPermissionsFragment__not_now">Not now</string>
<!-- Displayed as an action button at the bottom end of the screen -->
<string name="GrantPermissionsFragment__next">Next</string>
<!-- Displayed as a title at the top of the screen -->
<string name="GrantPermissionsFragment__allow_permissions">Allow permissions</string>
<!-- Displayed as a subtitle at the top of the screen -->
<string name="GrantPermissionsFragment__to_help_you_message_people_you_know">To help you message people you know, Signal will request these permissions. </string>
<!-- Notifications permission row title -->
<string name="GrantPermissionsFragment__notifications">Notifications</string>
<!-- Notifications permission row description -->
<string name="GrantPermissionsFragment__get_notified_when">Get notified when new messages arrive.</string>
<!-- Contacts permission row title -->
<string name="GrantPermissionsFragment__contacts">Contacts</string>
<!-- Contacts permission row description -->
<string name="GrantPermissionsFragment__find_people_you_know">Find people you know. Your contacts are encrypted and not visible to the Signal service.</string>
<!-- Phone calls permission row title -->
<string name="GrantPermissionsFragment__phone_calls">Phone calls</string>
<!-- Phone calls permission row description -->
<string name="GrantPermissionsFragment__make_registering_easier">Make registering easier and enable additional calling features.</string>
<!-- Storage permission row title -->
<string name="GrantPermissionsFragment__storage">Storage</string>
<!-- Storage permission row description -->
<string name="GrantPermissionsFragment__send_photos_videos_and_files">Send photos, videos and files from your device.</string>
<!-- PaymentsSecuritySetupFragment -->
<!-- Toolbar title -->
<string name="PaymentsSecuritySetupFragment__security_setup">Security setup</string>
@ -4991,6 +5029,10 @@
<!-- Displayed in a toast when we fail to open the ringtone picker -->
<string name="NotificationSettingsFragment__failed_to_open_picker">Failed to open picker.</string>
<!-- Banner title when notification permission is disabled -->
<string name="NotificationSettingsFragment__to_enable_notifications">To enable notifications, Signal needs permission to display them.</string>
<!-- Banner action when notification permission is disabled -->
<string name="NotificationSettingsFragment__turn_on">Turn on</string>
<!-- Description shown for the Signal Release Notes channel -->
<string name="ReleaseNotes__signal_release_notes_and_news">Signal Release Notes &amp; News</string>

View file

@ -0,0 +1,97 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.fragments
import android.Manifest
import android.app.Application
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@Ignore("Causing OOM errors.")
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class WelcomePermissionsTest {
@Test
@Config(sdk = [33])
fun givenApi33_whenIGetWelcomePermissions_thenIExpectPostNotifications() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertTrue(Manifest.permission.POST_NOTIFICATIONS in result)
}
@Test
@Config(sdk = [23, 26, 29])
fun givenApiUnder33_whenIGetWelcomePermissions_thenIExpectNoPostNotifications() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertFalse(Manifest.permission.POST_NOTIFICATIONS in result)
}
@Test
@Config(sdk = [23, 26, 29, 33])
fun givenAnyApi_whenIGetWelcomePermissions_thenIExpectContacts() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertTrue(Manifest.permission.WRITE_CONTACTS in result)
assertTrue(Manifest.permission.READ_CONTACTS in result)
}
@Test
@Config(sdk = [23, 26, 29, 33])
fun givenAnyApi_whenIGetWelcomePermissions_thenIExpectReadPhoneState() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertTrue(Manifest.permission.READ_PHONE_STATE in result)
}
@Test
@Config(sdk = [26, 29, 33])
fun givenApi26Plus_whenIGetWelcomePermissions_thenIExpectReadPhoneNumbers() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertTrue(Manifest.permission.READ_PHONE_NUMBERS in result)
}
@Test
@Config(sdk = [23])
fun givenApiUnder26_whenIGetWelcomePermissions_thenIExpectNoReadPhoneNumbers() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertFalse(Manifest.permission.READ_PHONE_NUMBERS in result)
}
@Test
@Config(sdk = [23, 26])
fun givenApiUnder29_whenIGetWelcomePermissions_thenIExpectPhoneStorage() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertTrue(Manifest.permission.WRITE_EXTERNAL_STORAGE in result)
assertTrue(Manifest.permission.READ_EXTERNAL_STORAGE in result)
}
@Test
@Config(sdk = [29, 33])
fun givenApi29Plus_whenIGetWelcomePermissionsAndSelectionNotRequired_thenIExpectPhoneStorage() {
val result = WelcomePermissions.getWelcomePermissions(false)
assertTrue(Manifest.permission.WRITE_EXTERNAL_STORAGE in result)
assertTrue(Manifest.permission.READ_EXTERNAL_STORAGE in result)
}
@Test
@Config(sdk = [29, 33])
fun givenApi29Plus_whenIGetWelcomePermissionsAndSelectionRequired_thenIExpectNoPhoneStorage() {
val result = WelcomePermissions.getWelcomePermissions(true)
assertFalse(Manifest.permission.WRITE_EXTERNAL_STORAGE in result)
assertFalse(Manifest.permission.READ_EXTERNAL_STORAGE in result)
}
}

View file

@ -1,6 +1,6 @@
val signalBuildToolsVersion by extra("34.0.0")
val signalCompileSdkVersion by extra("android-34")
val signalTargetSdkVersion by extra(31)
val signalTargetSdkVersion by extra(33)
val signalMinSdkVersion by extra(21)
val signalJavaVersion by extra(JavaVersion.VERSION_17)
val signalKotlinJvmTarget by extra("17")

View file

@ -172,7 +172,7 @@ dependencyResolutionManagement {
testLibs {
version('androidx-test', '1.4.0')
version('androidx-test-ext-junit', '1.1.1')
version('robolectric', '4.8.1')
version('robolectric', '4.10.3')
library('junit-junit', 'junit:junit:4.13.2')
library('androidx-test-core', 'androidx.test', 'core').versionRef('androidx-test')

View file

@ -65,6 +65,14 @@ public final class WifiDirect {
private WifiP2pDnsSdServiceRequest serviceRequest;
private final HandlerThread wifiDirectCallbacksHandler;
public static @NonNull String requiredPermission() {
if (Build.VERSION.SDK_INT >= 33) {
return Manifest.permission.NEARBY_WIFI_DEVICES;
} else {
return Manifest.permission.ACCESS_FINE_LOCATION;
}
}
/**
* Determine the ability to use WiFi Direct by checking if the device supports WiFi Direct
* and the appropriate permissions have been granted.
@ -81,9 +89,12 @@ public final class WifiDirect {
return AvailableStatus.WIFI_MANAGER_NOT_AVAILABLE;
}
if (Build.VERSION.SDK_INT >= 23 && context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= 33 && context.checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "Nearby Wifi permission required");
return AvailableStatus.REQUIRED_PERMISSION_NOT_GRANTED;
} else if (Build.VERSION.SDK_INT >= 23 && context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "Fine location permission required");
return AvailableStatus.FINE_LOCATION_PERMISSION_NOT_GRANTED;
return AvailableStatus.REQUIRED_PERMISSION_NOT_GRANTED;
}
return Build.VERSION.SDK_INT <= 23 || wifiManager.isP2pSupported() ? AvailableStatus.AVAILABLE
@ -464,7 +475,7 @@ public final class WifiDirect {
public enum AvailableStatus {
FEATURE_NOT_AVAILABLE,
WIFI_MANAGER_NOT_AVAILABLE,
FINE_LOCATION_PERMISSION_NOT_GRANTED,
REQUIRED_PERMISSION_NOT_GRANTED,
WIFI_DIRECT_NOT_AVAILABLE,
AVAILABLE
}

View file

@ -1538,6 +1538,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="c0754928effe1968c3a9a7b55d1dfc7ceb1e1e7c9f3f09f98afd42431f712492" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="annotation" version="1.0.1">
<artifact name="annotation-1.0.1.aar">
<sha256 value="c0754928effe1968c3a9a7b55d1dfc7ceb1e1e7c9f3f09f98afd42431f712492" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="core" version="1.4.0">
<artifact name="core-1.4.0.aar">
<sha256 value="671284e62e393f16ceae1a99a3a9a07bf1aacda29f8fe7b6b884355ef34c09cf" origin="Generated by Gradle"/>
@ -1558,6 +1563,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="10b1723c436beecb5884c69f8473504bc59611f9463ae549c48b3cf8e73b09c0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="monitor" version="1.6.1">
<artifact name="monitor-1.6.1.aar">
<sha256 value="2985ce8556989baf7c84342e7f687713c037a39a922e614d1a3ddf1ca3777079" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="orchestrator" version="1.4.1">
<artifact name="orchestrator-1.4.1.apk">
<sha256 value="0c7a02b619e4a97ee8ab55983ab19c6559d02aaff55710e6d2c1b8581832d4c1" origin="Generated by Gradle"/>
@ -1588,6 +1598,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8845d93979f09fffcc974c0be7d6b6ce4cf4275a4e3ba26bf0f83402e7f0cca5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test.espresso" name="espresso-idling-resource" version="3.5.1">
<artifact name="espresso-idling-resource-3.5.1.aar">
<sha256 value="84fb8e2f5eda937771bee28582f5d2cfa61b0e9438d02041ca61b81e3dac3c87" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test.ext" name="junit" version="1.1.1">
<artifact name="junit-1.1.1.aar">
<sha256 value="449df418d2916a0f86fe7dafb1edb09480fafb6e995d5c751c7d0d1970d4ae72" origin="Generated by Gradle"/>
@ -2606,6 +2621,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="5ce71656118618731e34a5d4c61aa3a031be23446dc7de8b5a5e77b66ebcd6ef" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.auto.value" name="auto-value-annotations" version="1.10.1">
<artifact name="auto-value-annotations-1.10.1.jar">
<sha256 value="a4fe0a211925e938a8510d741763ee1171a11bf931f5891ef4d4ee84fca72be2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.auto.value" name="auto-value-annotations" version="1.6.2">
<artifact name="auto-value-annotations-1.6.2.jar">
<sha256 value="b48b04ddba40e8ac33bf036f06fc43995fc5084bd94bdaace807ce27d3bea3fb" origin="Generated by Gradle"/>
@ -2970,6 +2990,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="2b4d8d4e098e86aa5f905ec81c46751d218b16afd3f7fc02b64f80dd20fffa20" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.ibm.icu" name="icu4j" version="72.1">
<artifact name="icu4j-72.1.jar">
<sha256 value="3df572b240a68d13b5cd778ad2393e885d26411434cd8f098ac5987ea2e64ce3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.jakewharton.android.repackaged" name="dalvik-dx" version="9.0.0_r3">
<artifact name="dalvik-dx-9.0.0_r3.jar">
<sha256 value="b29c1c21e52ed6238cd3fed39d880a17ecf2360118604548cea8821be6801e1c" origin="Generated by Gradle"/>
@ -4229,6 +4254,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8f3c20e3e2d565d26f33e8d4857a37d0d7f8ac39b62a7026496fcab1bdac30d4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bouncycastle" name="bcprov-jdk18on" version="1.72">
<artifact name="bcprov-jdk18on-1.72.jar">
<sha256 value="39287f2208a753db419f5ca529d6c80f094614aa74d790331126b3c9c6b85fda" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.bouncycastle" name="bcutil-jdk15on" version="1.70">
<artifact name="bcutil-jdk15on-1.70.jar">
<sha256 value="52dc5551b0257666526c5095424567fed7dc7b00d2b1ba7bd52298411112b1d0" origin="Generated by Gradle"/>
@ -5051,6 +5081,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="b9d4fe4d71938df38839f0eca42aaaa64cf8b313d678da036f0cb3ca199b47f5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm" version="9.5">
<artifact name="asm-9.5.jar">
<sha256 value="b62e84b5980729751b0458c534cf1366f727542bb8d158621335682a460f0353" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-analysis" version="9.1">
<artifact name="asm-analysis-9.1.jar">
<sha256 value="81a88041b1b8beda5a8a99646098046c48709538270c49def68abff25ac3be34" origin="Generated by Gradle"/>
@ -5061,11 +5096,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="878fbe521731c072d14d2d65b983b1beae6ad06fda0007b6a8bae81f73f433c4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-analysis" version="9.5">
<artifact name="asm-analysis-9.5.jar">
<sha256 value="39f1cf1791335701c3b02cae7b2bc21057ec9a55b2240789cb6d552b2b2c62fa" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-commons" version="9.2">
<artifact name="asm-commons-9.2.jar">
<sha256 value="be4ce53138a238bb522cd781cf91f3ba5ce2f6ca93ec62d46a162a127225e0a6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-commons" version="9.5">
<artifact name="asm-commons-9.5.jar">
<sha256 value="72eee9fbafb9de8d9463f20dd584a48ceeb7e5152ad4c987bfbe17dd4811c9ae" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-tree" version="9.1">
<artifact name="asm-tree-9.1.jar">
<sha256 value="fd00afa49e9595d7646205b09cecb4a776a8ff0ba06f2d59b8f7bf9c704b4a73" origin="Generated by Gradle"/>
@ -5076,118 +5121,127 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="aabf9bd23091a4ebfc109c1f3ee7cf3e4b89f6ba2d3f51c5243f16b3cffae011" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-tree" version="9.5">
<artifact name="asm-tree-9.5.jar">
<sha256 value="3c33a648191079aeaeaeb7c19a49b153952f9e40fe86fbac5205554ddd9acd94" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-util" version="9.2">
<artifact name="asm-util-9.2.jar">
<sha256 value="ff5b3cd331ae8a9a804768280da98f50f424fef23dd3c788bb320e08c94ee598" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.ow2.asm" name="asm-util" version="9.5">
<artifact name="asm-util-9.5.jar">
<sha256 value="c467f1bb3c08888f47243e2d475209b34a772d627e44fca06752e18bb038bd74" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.reactivestreams" name="reactive-streams" version="1.0.3">
<artifact name="reactive-streams-1.0.3.jar">
<sha256 value="1dee0481072d19c929b623e155e14d2f6085dc011529a0a0dbefc84cf571d865" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="annotations" version="4.8.1">
<artifact name="annotations-4.8.1.jar">
<sha256 value="d3924abb4c9f7c20df582e4553ac13203bb40b7c367bf5bf776a9acbb252e181" origin="Generated by Gradle"/>
<component group="org.robolectric" name="annotations" version="4.10.3">
<artifact name="annotations-4.10.3.jar">
<sha256 value="f3d6b921b7bf9d541577414c3b3124293eb09ced71f939e0c325c8d8abad0b6f" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotations-4.8.1.module">
<sha256 value="a5e9f2ee5794c9ac22921e12646869025540924abfeacc5f39152dcee5b4a628" origin="Generated by Gradle"/>
<artifact name="annotations-4.10.3.module">
<sha256 value="a8a2a9b49333cba29c6598fdd18f111c562e5c2c37a7e92826ae0093e812bb7d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="junit" version="4.8.1">
<artifact name="junit-4.8.1.jar">
<sha256 value="1475e1a271dce425d95ef53e837c07542023d718b0a320a9af6b879fa26c49ac" origin="Generated by Gradle"/>
<component group="org.robolectric" name="junit" version="4.10.3">
<artifact name="junit-4.10.3.jar">
<sha256 value="815f0bae88eb198889e1878ef65b904c4ec59131be2458829bcc942bd7b9f6da" origin="Generated by Gradle"/>
</artifact>
<artifact name="junit-4.8.1.module">
<sha256 value="e4b95ac9e273333dfa3938e50790fc5d3ba8bfe78f38ad61376b1baf6fa50b70" origin="Generated by Gradle"/>
<artifact name="junit-4.10.3.module">
<sha256 value="fc951c7170ebe9b8da140bffdb3822c158ef4ace7b21d1e81d8752cd7ed92cda" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="nativeruntime" version="4.8.1">
<artifact name="nativeruntime-4.8.1.jar">
<sha256 value="62e3e2de0e205a5a9a017f71de661d9529ac1a1f6bf4014609493a0ec95178f8" origin="Generated by Gradle"/>
<component group="org.robolectric" name="nativeruntime" version="4.10.3">
<artifact name="nativeruntime-4.10.3.jar">
<sha256 value="71fd2d1e8e78f2d70cc4879f5aa6910bf05a68274d3ca87179fb6f9447db5fb9" origin="Generated by Gradle"/>
</artifact>
<artifact name="nativeruntime-4.8.1.module">
<sha256 value="796f67b8d2a5a93dd9917034089a8dfae1eb96c59e61223ac319a298475022c8" origin="Generated by Gradle"/>
<artifact name="nativeruntime-4.10.3.module">
<sha256 value="989c43aacf186fc92edd59ee7be4d562a658130b7d3bfba3df9a5115a14f105e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="pluginapi" version="4.8.1">
<artifact name="pluginapi-4.8.1.jar">
<sha256 value="bea89e22d02b946563cf320312bb3eb479f8f31aa9cf96f65ce35c2b80cd02bc" origin="Generated by Gradle"/>
</artifact>
<artifact name="pluginapi-4.8.1.module">
<sha256 value="d2580abc08a992fe0256e209305dacdcc8e523d2c8d2ff0ddc7b25eaa7a39ac3" origin="Generated by Gradle"/>
<component group="org.robolectric" name="nativeruntime-dist-compat" version="1.0.1">
<artifact name="nativeruntime-dist-compat-1.0.1.jar">
<sha256 value="2dd7aae2332b8f57932e1ef78fb8d973aac1da631ec9fb471752280df50d140c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="plugins-maven-dependency-resolver" version="4.8.1">
<artifact name="plugins-maven-dependency-resolver-4.8.1.jar">
<sha256 value="dc87b3d6c55f5a6691246038ef4ba53311f406c784c8d9469d1f1112f885f72d" origin="Generated by Gradle"/>
<component group="org.robolectric" name="pluginapi" version="4.10.3">
<artifact name="pluginapi-4.10.3.jar">
<sha256 value="56be2717854add52e3437bb3be1b898dfea8ce8c6fcd26c4d0de68bf605274b0" origin="Generated by Gradle"/>
</artifact>
<artifact name="plugins-maven-dependency-resolver-4.8.1.module">
<sha256 value="e47aca957baeb036487497d52242c2fc5300435e6d31a106773baa9fae85b302" origin="Generated by Gradle"/>
<artifact name="pluginapi-4.10.3.module">
<sha256 value="2581c1cdc15888ce414f719763a8c784035189833f28debe049f9d686d1aeb0e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="resources" version="4.8.1">
<artifact name="resources-4.8.1.jar">
<sha256 value="fd15dedf3721e46412c23c238a0b905166fbfba65628db546b1c121546d4712a" origin="Generated by Gradle"/>
</artifact>
<artifact name="resources-4.8.1.module">
<sha256 value="e8625ecc2f8c4476bc0a67bdf41824fd8a4dd871246d2eaa06a5cf01ea8d89c2" origin="Generated by Gradle"/>
<component group="org.robolectric" name="plugins-maven-dependency-resolver" version="4.10.3">
<artifact name="plugins-maven-dependency-resolver-4.10.3.jar">
<sha256 value="54618c67214824dd5ebd72c5ed9c56fb62b776902455d0b0efc0e0940d8ebcf6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="robolectric" version="4.8.1">
<artifact name="robolectric-4.8.1.jar">
<sha256 value="c17a5358ca9f1adb5d182bda0cf276efa01a0cc1a7ba456325ecffd6e9c70fef" origin="Generated by Gradle"/>
<component group="org.robolectric" name="resources" version="4.10.3">
<artifact name="resources-4.10.3.jar">
<sha256 value="8decd0518e147c1038d38f6d33632e3310886194d7a8afeeb62849495f36e5f7" origin="Generated by Gradle"/>
</artifact>
<artifact name="robolectric-4.8.1.module">
<sha256 value="34b34fdcf60982097fcad301036e379378a1c3d2cced824318fa43ed0b2fbcc3" origin="Generated by Gradle"/>
<artifact name="resources-4.10.3.module">
<sha256 value="723823f373052ed4cb4c660039880f21ca88dc731260333bb8c7f01eb95f7039" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="sandbox" version="4.8.1">
<artifact name="sandbox-4.8.1.jar">
<sha256 value="ec132754fbd4a9b0c6658a2d3b12a2c6c2f2f56dfbd4ab1bd429441853697a41" origin="Generated by Gradle"/>
<component group="org.robolectric" name="robolectric" version="4.10.3">
<artifact name="robolectric-4.10.3.jar">
<sha256 value="e61c4733bd64f57ba9884bf232b293fdd19b233608dd3481cd0e3c99f0f7c0fc" origin="Generated by Gradle"/>
</artifact>
<artifact name="sandbox-4.8.1.module">
<sha256 value="09f45b4a6240134d8859245017b8d18f654dc1e0e56605b4840cf49dbd01f8ee" origin="Generated by Gradle"/>
<artifact name="robolectric-4.10.3.module">
<sha256 value="f4491fc2ede8245609502e1bf6f41c0eaad532eec16c568e63091583fb30ece0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadowapi" version="4.8.1">
<artifact name="shadowapi-4.8.1.jar">
<sha256 value="48ea81a61603d316f971a1c59f74b66d5914a8a5f96f657c8a57a3a5104455fb" origin="Generated by Gradle"/>
<component group="org.robolectric" name="sandbox" version="4.10.3">
<artifact name="sandbox-4.10.3.jar">
<sha256 value="59611ce3f110f21d464003a7a812dc8155f4132173cb13cf4e246da496cf17d0" origin="Generated by Gradle"/>
</artifact>
<artifact name="shadowapi-4.8.1.module">
<sha256 value="ecc1e0e90857cb1dcde67b0ca27cf3cbc7be84a09c4a6dd107b45b40588b7b9c" origin="Generated by Gradle"/>
<artifact name="sandbox-4.10.3.module">
<sha256 value="3a2c82c681484a66288016006ec30dca02c48cc7a9855ca5ece8538fbf062c75" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadows-framework" version="4.8.1">
<artifact name="shadows-framework-4.8.1.jar">
<sha256 value="5fda3468ab58877ade2fab5c0a954a947a7b89d67c4e9eeda939eb8a91e1bf34" origin="Generated by Gradle"/>
<component group="org.robolectric" name="shadowapi" version="4.10.3">
<artifact name="shadowapi-4.10.3.jar">
<sha256 value="1ba648a76968f1bb9f4fc64321af70c4eeed94b2a3fa1b2a848a7706ec25c75a" origin="Generated by Gradle"/>
</artifact>
<artifact name="shadows-framework-4.8.1.module">
<sha256 value="13e1f7e83dec909ef0f08130b4433e47a15821ccb8af1dad51fd344103ba7cfc" origin="Generated by Gradle"/>
<artifact name="shadowapi-4.10.3.module">
<sha256 value="09790eeb56f1a2ffa997fb3373d0b62ca763487e9540d7ae83198ab999f793ee" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="shadows-multidex" version="4.8.1">
<artifact name="shadows-multidex-4.8.1.jar">
<sha256 value="010216f7e8b05598e2b15c1091afc93e8e23f4258e87413ef28f38831afc352b" origin="Generated by Gradle"/>
<component group="org.robolectric" name="shadows-framework" version="4.10.3">
<artifact name="shadows-framework-4.10.3.jar">
<sha256 value="106f6a19abc9d5786a18461a2554afbf782a30e799f867d7f1a9f26bcbb873a7" origin="Generated by Gradle"/>
</artifact>
<artifact name="shadows-multidex-4.8.1.module">
<sha256 value="fbc182dca3e86887e055dff179eaaf68caf04f9c7532b29cdaa85b52b851be5a" origin="Generated by Gradle"/>
<artifact name="shadows-framework-4.10.3.module">
<sha256 value="126f485b5f1570021ab79a2b819bf44a2f64405bd248f7e819236f13728b9ce7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="utils" version="4.8.1">
<artifact name="utils-4.8.1.jar">
<sha256 value="c40afc0a140aada6830b3c2f7a331ad96d8e2f2a905458b6624b8fb12f7552d4" origin="Generated by Gradle"/>
<component group="org.robolectric" name="shadows-multidex" version="4.10.3">
<artifact name="shadows-multidex-4.10.3.jar">
<sha256 value="34d1da27044528c07ca43c6334af3890868f2570d48114459c25ec4331ea6966" origin="Generated by Gradle"/>
</artifact>
<artifact name="utils-4.8.1.module">
<sha256 value="816c8a178a9346c9e7a9ecf3081f81df42ecb5e7b77ded0e36c6944635079aba" origin="Generated by Gradle"/>
<artifact name="shadows-multidex-4.10.3.module">
<sha256 value="ad1276a3820bbe37672624ee412b14bf35660604fb4802ac9b2ce8de494b72ff" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.robolectric" name="utils-reflector" version="4.8.1">
<artifact name="utils-reflector-4.8.1.jar">
<sha256 value="d17357e5254e1e14ee258fc1604bbe011aa425fe5e7c768a04fd043476e41267" origin="Generated by Gradle"/>
<component group="org.robolectric" name="utils" version="4.10.3">
<artifact name="utils-4.10.3.jar">
<sha256 value="0081b1a65c2c6d7cf56a56f6b4ed85b35a91f5a9f40a4b81c6771b497265518e" origin="Generated by Gradle"/>
</artifact>
<artifact name="utils-reflector-4.8.1.module">
<sha256 value="5c319c569301724ed105416a0246f88ba186a04ec54c4d136aca468bc694b664" origin="Generated by Gradle"/>
</component>
<component group="org.robolectric" name="utils-reflector" version="4.10.3">
<artifact name="utils-reflector-4.10.3.jar">
<sha256 value="fcd2dde7623a5b47caa7efcbdf7dd2a95429e640b42490db5bc645367f0a0e1a" origin="Generated by Gradle"/>
</artifact>
<artifact name="utils-reflector-4.10.3.module">
<sha256 value="6afe78a35a9ce7f2b65f22b605a028b5a6ed43083a3c76a299f37a648ca3cf25" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.signal" name="aesgcmprovider" version="0.0.3">