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.robolectric.shadows.multidex
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) { version { strictly "1.70" } } // Used by roboelectric 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.bouncycastle.bcpkix.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
testImplementation testLibs.conscrypt.openjdk.uber // Used by robolectric
testImplementation testLibs.hamcrest.hamcrest testImplementation testLibs.hamcrest.hamcrest
testImplementation testLibs.mockk 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_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_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.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_CALL_STATE"/> <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.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" <application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" 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.groups.ParcelableGroupId
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@ -238,7 +239,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun openGallery() { private fun openGallery() {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE) .request(*PermissionCompat.forImages())
.ifNecessary() .ifNecessary()
.onAllGranted { .onAllGranted {
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext()) 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.RadioListPreference
import org.thoughtcrime.securesms.components.settings.RadioListPreferenceViewHolder import org.thoughtcrime.securesms.components.settings.RadioListPreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.configure 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.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels 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.RingtoneUtil
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
@ -62,6 +65,11 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
private lateinit var viewModel: NotificationsSettingsViewModel private lateinit var viewModel: NotificationsSettingsViewModel
override fun onResume() {
super.onResume()
viewModel.refresh()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == MESSAGE_SOUND_SELECT && resultCode == Activity.RESULT_OK && data != null) { if (requestCode == MESSAGE_SOUND_SELECT && resultCode == Activity.RESULT_OK && data != null) {
val uri: Uri? = data.getParcelableExtraCompat(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java) 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) LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item)
) )
Banner.register(adapter)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val factory = NotificationsSettingsViewModel.Factory(sharedPreferences) val factory = NotificationsSettingsViewModel.Factory(sharedPreferences)
@ -90,10 +100,23 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
private fun getConfiguration(state: NotificationsSettingsState): DSLConfiguration { private fun getConfiguration(state: NotificationsSettingsState): DSLConfiguration {
return configure { 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) sectionHeaderPref(R.string.NotificationsSettingsFragment__messages)
switchPref( switchPref(
title = DSLSettingsText.from(R.string.preferences__notifications), title = DSLSettingsText.from(R.string.preferences__notifications),
isEnabled = state.messageNotificationsState.canEnableNotifications,
isChecked = state.messageNotificationsState.notificationsEnabled, isChecked = state.messageNotificationsState.notificationsEnabled,
onClick = { onClick = {
viewModel.setMessageNotificationsEnabled(!state.messageNotificationsState.notificationsEnabled) viewModel.setMessageNotificationsEnabled(!state.messageNotificationsState.notificationsEnabled)
@ -223,6 +246,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
switchPref( switchPref(
title = DSLSettingsText.from(R.string.preferences__notifications), title = DSLSettingsText.from(R.string.preferences__notifications),
isEnabled = state.callNotificationsState.canEnableNotifications,
isChecked = state.callNotificationsState.notificationsEnabled, isChecked = state.callNotificationsState.notificationsEnabled,
onClick = { onClick = {
viewModel.setCallNotificationsEnabled(!state.callNotificationsState.notificationsEnabled) viewModel.setCallNotificationsEnabled(!state.callNotificationsState.notificationsEnabled)

View file

@ -10,6 +10,7 @@ data class NotificationsSettingsState(
data class MessageNotificationsState( data class MessageNotificationsState(
val notificationsEnabled: Boolean, val notificationsEnabled: Boolean,
val canEnableNotifications: Boolean,
val sound: Uri, val sound: Uri,
val vibrateEnabled: Boolean, val vibrateEnabled: Boolean,
val ledColor: String, val ledColor: String,
@ -23,6 +24,7 @@ data class MessageNotificationsState(
data class CallNotificationsState( data class CallNotificationsState(
val notificationsEnabled: Boolean, val notificationsEnabled: Boolean,
val canEnableNotifications: Boolean,
val ringtone: Uri, val ringtone: Uri,
val vibrateEnabled: Boolean val vibrateEnabled: Boolean
) )

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.notifications
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -26,78 +27,83 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
val state: LiveData<NotificationsSettingsState> = store.stateLiveData val state: LiveData<NotificationsSettingsState> = store.stateLiveData
fun refresh() {
store.update { getState() }
}
fun setMessageNotificationsEnabled(enabled: Boolean) { fun setMessageNotificationsEnabled(enabled: Boolean) {
SignalStore.settings().isMessageNotificationsEnabled = enabled SignalStore.settings().isMessageNotificationsEnabled = enabled
store.update { getState() } refresh()
} }
fun setMessageNotificationsSound(sound: Uri?) { fun setMessageNotificationsSound(sound: Uri?) {
val messageSound = sound ?: Uri.EMPTY val messageSound = sound ?: Uri.EMPTY
SignalStore.settings().messageNotificationSound = messageSound SignalStore.settings().messageNotificationSound = messageSound
NotificationChannels.getInstance().updateMessageRingtone(messageSound) NotificationChannels.getInstance().updateMessageRingtone(messageSound)
store.update { getState() } refresh()
} }
fun setMessageNotificationVibration(enabled: Boolean) { fun setMessageNotificationVibration(enabled: Boolean) {
SignalStore.settings().isMessageVibrateEnabled = enabled SignalStore.settings().isMessageVibrateEnabled = enabled
NotificationChannels.getInstance().updateMessageVibrate(enabled) NotificationChannels.getInstance().updateMessageVibrate(enabled)
store.update { getState() } refresh()
} }
fun setMessageNotificationLedColor(color: String) { fun setMessageNotificationLedColor(color: String) {
SignalStore.settings().messageLedColor = color SignalStore.settings().messageLedColor = color
NotificationChannels.getInstance().updateMessagesLedColor(color) NotificationChannels.getInstance().updateMessagesLedColor(color)
store.update { getState() } refresh()
} }
fun setMessageNotificationLedBlink(blink: String) { fun setMessageNotificationLedBlink(blink: String) {
SignalStore.settings().messageLedBlinkPattern = blink SignalStore.settings().messageLedBlinkPattern = blink
store.update { getState() } refresh()
} }
fun setMessageNotificationInChatSoundsEnabled(enabled: Boolean) { fun setMessageNotificationInChatSoundsEnabled(enabled: Boolean) {
SignalStore.settings().isMessageNotificationsInChatSoundsEnabled = enabled SignalStore.settings().isMessageNotificationsInChatSoundsEnabled = enabled
store.update { getState() } refresh()
} }
fun setMessageRepeatAlerts(repeats: Int) { fun setMessageRepeatAlerts(repeats: Int) {
SignalStore.settings().messageNotificationsRepeatAlerts = repeats SignalStore.settings().messageNotificationsRepeatAlerts = repeats
store.update { getState() } refresh()
} }
fun setMessageNotificationPrivacy(preference: String) { fun setMessageNotificationPrivacy(preference: String) {
SignalStore.settings().messageNotificationsPrivacy = NotificationPrivacyPreference(preference) SignalStore.settings().messageNotificationsPrivacy = NotificationPrivacyPreference(preference)
store.update { getState() } refresh()
} }
fun setMessageNotificationPriority(priority: Int) { fun setMessageNotificationPriority(priority: Int) {
sharedPreferences.edit().putString(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF, priority.toString()).apply() sharedPreferences.edit().putString(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF, priority.toString()).apply()
store.update { getState() } refresh()
} }
fun setCallNotificationsEnabled(enabled: Boolean) { fun setCallNotificationsEnabled(enabled: Boolean) {
SignalStore.settings().isCallNotificationsEnabled = enabled SignalStore.settings().isCallNotificationsEnabled = enabled
store.update { getState() } refresh()
} }
fun setCallRingtone(ringtone: Uri?) { fun setCallRingtone(ringtone: Uri?) {
SignalStore.settings().callRingtone = ringtone ?: Uri.EMPTY SignalStore.settings().callRingtone = ringtone ?: Uri.EMPTY
store.update { getState() } refresh()
} }
fun setCallVibrateEnabled(enabled: Boolean) { fun setCallVibrateEnabled(enabled: Boolean) {
SignalStore.settings().isCallVibrateEnabled = enabled SignalStore.settings().isCallVibrateEnabled = enabled
store.update { getState() } refresh()
} }
fun setNotifyWhenContactJoinsSignal(enabled: Boolean) { fun setNotifyWhenContactJoinsSignal(enabled: Boolean) {
SignalStore.settings().isNotifyWhenContactJoinsSignal = enabled SignalStore.settings().isNotifyWhenContactJoinsSignal = enabled
store.update { getState() } refresh()
} }
private fun getState(): NotificationsSettingsState = NotificationsSettingsState( private fun getState(): NotificationsSettingsState = NotificationsSettingsState(
messageNotificationsState = MessageNotificationsState( messageNotificationsState = MessageNotificationsState(
notificationsEnabled = SignalStore.settings().isMessageNotificationsEnabled, notificationsEnabled = SignalStore.settings().isMessageNotificationsEnabled && canEnableNotifications(),
canEnableNotifications = canEnableNotifications(),
sound = SignalStore.settings().messageNotificationSound, sound = SignalStore.settings().messageNotificationSound,
vibrateEnabled = SignalStore.settings().isMessageVibrateEnabled, vibrateEnabled = SignalStore.settings().isMessageVibrateEnabled,
ledColor = SignalStore.settings().messageLedColor, ledColor = SignalStore.settings().messageLedColor,
@ -109,13 +115,24 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
troubleshootNotifications = SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations() && SlowNotificationHeuristics.isHavingDelayedNotifications() troubleshootNotifications = SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations() && SlowNotificationHeuristics.isHavingDelayedNotifications()
), ),
callNotificationsState = CallNotificationsState( callNotificationsState = CallNotificationsState(
notificationsEnabled = SignalStore.settings().isCallNotificationsEnabled, notificationsEnabled = SignalStore.settings().isCallNotificationsEnabled && canEnableNotifications(),
canEnableNotifications = canEnableNotifications(),
ringtone = SignalStore.settings().callRingtone, ringtone = SignalStore.settings().callRingtone,
vibrateEnabled = SignalStore.settings().isCallVibrateEnabled vibrateEnabled = SignalStore.settings().isCallVibrateEnabled
), ),
notifyWhenContactJoinsSignal = SignalStore.settings().isNotifyWhenContactJoinsSignal 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 { class Factory(private val sharedPreferences: SharedPreferences) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(NotificationsSettingsViewModel(sharedPreferences))) 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.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.recipients.RecipientId 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) { fun launchGallery(recipientId: RecipientId, text: CharSequence?, isReply: Boolean) {
Permissions Permissions
.with(fragment) .with(fragment)
.request(Manifest.permission.READ_EXTERNAL_STORAGE) .request(*PermissionCompat.forImagesAndVideos())
.ifNecessary() .ifNecessary()
.withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) .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)) } .onAllGranted { mediaGalleryLauncher.launch(MediaSelectionInput(emptyList(), recipientId, text, isReply)) }

View file

@ -5,7 +5,6 @@
package org.thoughtcrime.securesms.conversation.v2.keyboard package org.thoughtcrime.securesms.conversation.v2.keyboard
import android.Manifest
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.os.bundleOf 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.conversation.v2.ConversationViewModel
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import java.util.function.Predicate import java.util.function.Predicate
@ -93,7 +93,7 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_
override fun onAttachmentPermissionsRequested() { override fun onAttachmentPermissionsRequested() {
Permissions.with(requireParentFragment()) Permissions.with(requireParentFragment())
.request(Manifest.permission.READ_EXTERNAL_STORAGE) .request(*PermissionCompat.forImagesAndVideos())
.onAllGranted { viewModel.refreshRecentMedia() } .onAllGranted { viewModel.refreshRecentMedia() }
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) .withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.execute() .execute()

View file

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.devicetransfer; package org.thoughtcrime.securesms.devicetransfer;
import android.Manifest;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Intent; import android.content.Intent;
import android.location.LocationManager; import android.location.LocationManager;
@ -16,7 +15,6 @@ import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group; import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@ -100,7 +98,7 @@ public abstract class DeviceTransferSetupFragment extends LoggingFragment {
case INITIAL: case INITIAL:
status.setText(""); status.setText("");
case PERMISSIONS_CHECK: case PERMISSIONS_CHECK:
requestLocationPermission(); requestRequiredPermission();
break; break;
case PERMISSIONS_DENIED: case PERMISSIONS_DENIED:
error.setText(getErrorTextForStep(step)); error.setText(getErrorTextForStep(step));
@ -280,9 +278,9 @@ public abstract class DeviceTransferSetupFragment extends LoggingFragment {
super.onDestroyView(); super.onDestroyView();
} }
private void requestLocationPermission() { private void requestRequiredPermission() {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.ACCESS_FINE_LOCATION) .request(WifiDirect.requiredPermission())
.ifNecessary() .ifNecessary()
.withRationaleDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)), false, R.drawable.ic_location_on_white_24dp) .withRationaleDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)), false, R.drawable.ic_location_on_white_24dp)
.withPermanentDenialDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED))) .withPermanentDenialDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)))

View file

@ -78,7 +78,7 @@ public final class DeviceTransferSetupViewModel extends ViewModel {
public void onWifiDirectUnavailable(WifiDirect.AvailableStatus availability) { public void onWifiDirectUnavailable(WifiDirect.AvailableStatus availability) {
Log.i(TAG, "Wifi Direct unavailable: " + 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)); store.update(s -> s.updateStep(SetupStep.PERMISSIONS_CHECK));
} else { } else {
store.update(s -> s.updateStep(SetupStep.WIFI_DIRECT_UNAVAILABLE)); 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.fragment.app.Fragment
import androidx.navigation.NavController import androidx.navigation.NavController
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
@ -46,7 +47,7 @@ class MediaSelectionNavigator(
onGranted: () -> Unit onGranted: () -> Unit
) { ) {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE) .request(*PermissionCompat.forImagesAndVideos())
.ifNecessary() .ifNecessary()
.withPermanentDenialDialog(getString(R.string.AttachmentKeyboard_Signal_needs_permission_to_show_your_photos_and_videos)) .withPermanentDenialDialog(getString(R.string.AttachmentKeyboard_Signal_needs_permission_to_show_your_photos_and_videos))
.onAllGranted(onGranted) .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.lock.v2.SvrMigrationActivity;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.TurnOnNotificationsBottomSheet;
import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity; import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -232,17 +233,8 @@ public final class Megaphones {
.setBody(R.string.NotificationsMegaphone_never_miss_a_message) .setBody(R.string.NotificationsMegaphone_never_miss_a_message)
.setImage(R.drawable.megaphone_notifications_64) .setImage(R.drawable.megaphone_notifications_64)
.setActionButton(R.string.NotificationsMegaphone_turn_on, (megaphone, controller) -> { .setActionButton(R.string.NotificationsMegaphone_turn_on, (megaphone, controller) -> {
if (Build.VERSION.SDK_INT >= 26 && !NotificationChannels.getInstance().isMessageChannelEnabled()) { if (Build.VERSION.SDK_INT >= 26) {
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); controller.onMegaphoneDialogFragmentRequested(new TurnOnNotificationsBottomSheet());
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);
} else { } else {
controller.onMegaphoneNavigationRequested(AppSettingsActivity.notifications(context)); 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.PaymentsActivity;
import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog; import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog;
import org.thoughtcrime.securesms.payments.preferences.model.PayeeParcelable; import org.thoughtcrime.securesms.payments.preferences.model.PayeeParcelable;
import org.thoughtcrime.securesms.permissions.PermissionCompat;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider; 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) { public static void selectGallery(Fragment fragment, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull MessageSendType messageSendType, boolean hasQuote) {
Permissions.with(fragment) Permissions.with(fragment)
.request(Manifest.permission.READ_EXTERNAL_STORAGE) .request(PermissionCompat.forImagesAndVideos())
.ifNecessary() .ifNecessary()
.withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) .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)) .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)); return new PermissionsBuilder(new FragmentPermissionObject(fragment));
} }
public static boolean isRuntimePermissionsRequired() {
return Build.VERSION.SDK_INT >= 23;
}
public static class PermissionsBuilder { public static class PermissionsBuilder {
private final PermissionObject permissionObject; private final PermissionObject permissionObject;
@ -239,13 +243,13 @@ public class Permissions {
} }
public static boolean hasAny(@NonNull Context context, String... 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); Stream.of(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
} }
public static boolean hasAll(@NonNull Context context, String... permissions) { 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); 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.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment;
import androidx.annotation.StringRes;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.ActivityNavigator; import androidx.navigation.ActivityNavigator;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation; import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment; 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 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 CircularProgressMaterialButton continueButton;
private RegistrationViewModel viewModel; private RegistrationViewModel viewModel;
@ -97,7 +74,7 @@ public final class WelcomeFragment extends LoggingFragment {
return; return;
} }
initializeNumber(); initializeNumber(requireContext(), viewModel);
Log.i(TAG, "Skipping restore because this is a reregistration."); Log.i(TAG, "Skipping restore because this is a reregistration.");
viewModel.setWelcomeSkippedOnRestore(); viewModel.setWelcomeSkippedOnRestore();
@ -109,10 +86,10 @@ public final class WelcomeFragment extends LoggingFragment {
setDebugLogSubmitMultiTapView(view.findViewById(R.id.title)); setDebugLogSubmitMultiTapView(view.findViewById(R.id.title));
continueButton = view.findViewById(R.id.welcome_continue_button); 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); 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); TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
welcomeTermsButton.setOnClickListener(v -> onTermsClicked()); welcomeTermsButton.setOnClickListener(v -> onTermsClicked());
@ -139,70 +116,116 @@ public final class WelcomeFragment extends LoggingFragment {
} }
} }
private void continueClicked(@NonNull View view) { private void onContinueClicked() {
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext()); 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) private void onRestoreFromBackupClicked() {
.request(getContinuePermissions(isUserSelectionRequired)) 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() .ifNecessary()
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired)) .onAnyResult(() -> gatherInformationAndContinue(fragment,
.onAnyResult(() -> gatherInformationAndContinue(continueButton)) viewModel,
onSearchForBackupStarted,
onSearchForBackupFinished,
actionSkipRestore,
actionRestore))
.execute(); .execute();
} }
private void restoreFromBackupClicked(@NonNull View view) { static void restoreFromBackupClicked(@NonNull Fragment fragment,
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext()); @NonNull RegistrationViewModel viewModel,
@NonNull NavDirections actionTransferOrRestore)
{
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(fragment.requireContext());
Permissions.with(this) Permissions.with(fragment)
.request(getContinuePermissions(isUserSelectionRequired)) .request(WelcomePermissions.getWelcomePermissions(isUserSelectionRequired))
.ifNecessary() .ifNecessary()
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired)) .onAnyResult(() -> gatherInformationAndChooseBackup(fragment, viewModel, actionTransferOrRestore))
.onAnyResult(() -> gatherInformationAndChooseBackup(continueButton))
.execute(); .execute();
} }
private void gatherInformationAndContinue(@NonNull View view) { static void gatherInformationAndContinue(
continueButton.setSpinning(); @NonNull Fragment fragment,
@NonNull RegistrationViewModel viewModel,
@NonNull Runnable onSearchForBackupStarted,
@NonNull Runnable onSearchForBackupFinished,
@NonNull NavDirections actionSkipRestore,
@NonNull NavDirections actionRestore
) {
onSearchForBackupStarted.run();
RestoreBackupFragment.searchForBackup(backup -> { RestoreBackupFragment.searchForBackup(backup -> {
Context context = getContext(); Context context = fragment.getContext();
if (context == null) { if (context == null) {
Log.i(TAG, "No context on fragment, must have navigated away."); Log.i(TAG, "No context on fragment, must have navigated away.");
return; return;
} }
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true); TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
initializeNumber(); initializeNumber(fragment.requireContext(), viewModel);
continueButton.cancelSpinning(); onSearchForBackupFinished.run();
if (backup == null) { if (backup == null) {
Log.i(TAG, "Skipping backup. No backup found, or no permission to look."); Log.i(TAG, "Skipping backup. No backup found, or no permission to look.");
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
WelcomeFragmentDirections.actionSkipRestore()); actionSkipRestore);
} else { } else {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
WelcomeFragmentDirections.actionRestore()); actionRestore);
} }
}); });
} }
private void gatherInformationAndChooseBackup(@NonNull View view) { static void gatherInformationAndChooseBackup(@NonNull Fragment fragment,
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true); @NonNull RegistrationViewModel viewModel,
@NonNull NavDirections actionTransferOrRestore) {
TextSecurePreferences.setHasSeenWelcomeScreen(fragment.requireContext(), true);
initializeNumber(); initializeNumber(fragment.requireContext(), viewModel);
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), SafeNavigation.safeNavigate(NavHostFragment.findNavController(fragment),
WelcomeFragmentDirections.actionTransferOrRestore()); actionTransferOrRestore);
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private void initializeNumber() { private static void initializeNumber(@NonNull Context context, @NonNull RegistrationViewModel viewModel) {
Optional<Phonenumber.PhoneNumber> localNumber = Optional.empty(); Optional<Phonenumber.PhoneNumber> localNumber = Optional.empty();
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
localNumber = Util.getDeviceNumber(requireContext()); localNumber = Util.getDeviceNumber(context);
} else { } else {
Log.i(TAG, "No phone permission"); Log.i(TAG, "No phone permission");
} }
@ -215,7 +238,7 @@ public final class WelcomeFragment extends LoggingFragment {
viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber); viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
} else { } else {
Log.i(TAG, "No number detected"); 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())) { if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), ""); viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
@ -232,23 +255,4 @@ public final class WelcomeFragment extends LoggingFragment {
!viewModel.isReregister() && !viewModel.isReregister() &&
!SignalStore.settings().isBackupEnabled(); !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.R;
import org.thoughtcrime.securesms.database.NoExternalStorageException; import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.permissions.PermissionCompat;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import java.io.File; 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 { private static File getSignalStorageDir() throws NoExternalStorageException {
final File storage = Environment.getExternalStorageDirectory(); final File storage = Environment.getExternalStorageDirectory();
@ -108,17 +105,13 @@ public class StorageUtil {
return storage.canWrite(); return storage.canWrite();
} }
public static File getLegacyBackupDirectory() throws NoExternalStorageException {
return getSignalStorageDir();
}
public static boolean canWriteToMediaStore() { public static boolean canWriteToMediaStore() {
return Build.VERSION.SDK_INT > 28 || return Build.VERSION.SDK_INT > 28 ||
Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE); Permissions.hasAll(ApplicationDependencies.getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
} }
public static boolean canReadFromMediaStore() { 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() { public static @NonNull Uri getVideoUri() {

View file

@ -18,6 +18,7 @@ import androidx.navigation.Navigation;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.PermissionCompat;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.wallpaper.crop.WallpaperImageSelectionActivity; import org.thoughtcrime.securesms.wallpaper.crop.WallpaperImageSelectionActivity;
@ -76,7 +77,7 @@ public class ChatWallpaperSelectionFragment extends Fragment {
private void askForPermissionIfNeededAndLaunchPhotoSelection() { private void askForPermissionIfNeededAndLaunchPhotoSelection() {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE) .request(PermissionCompat.forImages())
.ifNecessary() .ifNecessary()
.onAllGranted(() -> { .onAllGranted(() -> {
startActivityForResult(WallpaperImageSelectionActivity.getIntent(requireContext(), viewModel.getRecipientId()), CHOOSE_WALLPAPER); startActivityForResult(WallpaperImageSelectionActivity.getIntent(requireContext(), viewModel.getRecipientId()), CHOOSE_WALLPAPER);

View file

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.wallpaper.crop; package org.thoughtcrime.securesms.wallpaper.crop;
import android.Manifest;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
@ -8,7 +7,6 @@ import android.view.WindowManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate; 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 String EXTRA_RECIPIENT_ID = "RECIPIENT_ID";
private static final int CROP = 901; private static final int CROP = 901;
@RequiresPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
public static Intent getIntent(@NonNull Context context, public static Intent getIntent(@NonNull Context context,
@Nullable RecipientId recipientId) @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:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_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>
<fragment <fragment

View file

@ -202,4 +202,5 @@
<color name="message_request_bar_background_wallpaper">@color/signal_colorNeutralVariant</color> <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_denyForeground_wallpaper">@color/signal_colorError</color>
<color name="message_request_bar_acceptForeground_wallpaper">@color/signal_colorNeutralInverse</color> <color name="message_request_bar_acceptForeground_wallpaper">@color/signal_colorNeutralInverse</color>
</resources> </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> <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 --> <!-- Notification Channels -->
<string name="NotificationChannel_channel_messages">Messages</string> <string name="NotificationChannel_channel_messages">Messages</string>
<string name="NotificationChannel_calls">Calls</string> <string name="NotificationChannel_calls">Calls</string>
@ -3117,6 +3129,32 @@
<!-- Alert dialog button to update now --> <!-- Alert dialog button to update now -->
<string name="PaymentsHomeFragment__update_now">Update now</string> <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 --> <!-- PaymentsSecuritySetupFragment -->
<!-- Toolbar title --> <!-- Toolbar title -->
<string name="PaymentsSecuritySetupFragment__security_setup">Security setup</string> <string name="PaymentsSecuritySetupFragment__security_setup">Security setup</string>
@ -4991,6 +5029,10 @@
<!-- Displayed in a toast when we fail to open the ringtone picker --> <!-- Displayed in a toast when we fail to open the ringtone picker -->
<string name="NotificationSettingsFragment__failed_to_open_picker">Failed to open picker.</string> <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 --> <!-- Description shown for the Signal Release Notes channel -->
<string name="ReleaseNotes__signal_release_notes_and_news">Signal Release Notes &amp; News</string> <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 signalBuildToolsVersion by extra("34.0.0")
val signalCompileSdkVersion by extra("android-34") val signalCompileSdkVersion by extra("android-34")
val signalTargetSdkVersion by extra(31) val signalTargetSdkVersion by extra(33)
val signalMinSdkVersion by extra(21) val signalMinSdkVersion by extra(21)
val signalJavaVersion by extra(JavaVersion.VERSION_17) val signalJavaVersion by extra(JavaVersion.VERSION_17)
val signalKotlinJvmTarget by extra("17") val signalKotlinJvmTarget by extra("17")

View file

@ -172,7 +172,7 @@ dependencyResolutionManagement {
testLibs { testLibs {
version('androidx-test', '1.4.0') version('androidx-test', '1.4.0')
version('androidx-test-ext-junit', '1.1.1') 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('junit-junit', 'junit:junit:4.13.2')
library('androidx-test-core', 'androidx.test', 'core').versionRef('androidx-test') library('androidx-test-core', 'androidx.test', 'core').versionRef('androidx-test')

View file

@ -65,6 +65,14 @@ public final class WifiDirect {
private WifiP2pDnsSdServiceRequest serviceRequest; private WifiP2pDnsSdServiceRequest serviceRequest;
private final HandlerThread wifiDirectCallbacksHandler; 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 * Determine the ability to use WiFi Direct by checking if the device supports WiFi Direct
* and the appropriate permissions have been granted. * and the appropriate permissions have been granted.
@ -81,9 +89,12 @@ public final class WifiDirect {
return AvailableStatus.WIFI_MANAGER_NOT_AVAILABLE; 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"); 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 return Build.VERSION.SDK_INT <= 23 || wifiManager.isP2pSupported() ? AvailableStatus.AVAILABLE
@ -464,7 +475,7 @@ public final class WifiDirect {
public enum AvailableStatus { public enum AvailableStatus {
FEATURE_NOT_AVAILABLE, FEATURE_NOT_AVAILABLE,
WIFI_MANAGER_NOT_AVAILABLE, WIFI_MANAGER_NOT_AVAILABLE,
FINE_LOCATION_PERMISSION_NOT_GRANTED, REQUIRED_PERMISSION_NOT_GRANTED,
WIFI_DIRECT_NOT_AVAILABLE, WIFI_DIRECT_NOT_AVAILABLE,
AVAILABLE AVAILABLE
} }

View file

@ -1538,6 +1538,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="c0754928effe1968c3a9a7b55d1dfc7ceb1e1e7c9f3f09f98afd42431f712492" origin="Generated by Gradle"/> <sha256 value="c0754928effe1968c3a9a7b55d1dfc7ceb1e1e7c9f3f09f98afd42431f712492" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="androidx.test" name="core" version="1.4.0">
<artifact name="core-1.4.0.aar"> <artifact name="core-1.4.0.aar">
<sha256 value="671284e62e393f16ceae1a99a3a9a07bf1aacda29f8fe7b6b884355ef34c09cf" origin="Generated by Gradle"/> <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"/> <sha256 value="10b1723c436beecb5884c69f8473504bc59611f9463ae549c48b3cf8e73b09c0" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="androidx.test" name="orchestrator" version="1.4.1">
<artifact name="orchestrator-1.4.1.apk"> <artifact name="orchestrator-1.4.1.apk">
<sha256 value="0c7a02b619e4a97ee8ab55983ab19c6559d02aaff55710e6d2c1b8581832d4c1" origin="Generated by Gradle"/> <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"/> <sha256 value="8845d93979f09fffcc974c0be7d6b6ce4cf4275a4e3ba26bf0f83402e7f0cca5" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="androidx.test.ext" name="junit" version="1.1.1">
<artifact name="junit-1.1.1.aar"> <artifact name="junit-1.1.1.aar">
<sha256 value="449df418d2916a0f86fe7dafb1edb09480fafb6e995d5c751c7d0d1970d4ae72" origin="Generated by Gradle"/> <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"/> <sha256 value="5ce71656118618731e34a5d4c61aa3a031be23446dc7de8b5a5e77b66ebcd6ef" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="com.google.auto.value" name="auto-value-annotations" version="1.6.2">
<artifact name="auto-value-annotations-1.6.2.jar"> <artifact name="auto-value-annotations-1.6.2.jar">
<sha256 value="b48b04ddba40e8ac33bf036f06fc43995fc5084bd94bdaace807ce27d3bea3fb" origin="Generated by Gradle"/> <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"/> <sha256 value="2b4d8d4e098e86aa5f905ec81c46751d218b16afd3f7fc02b64f80dd20fffa20" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="com.jakewharton.android.repackaged" name="dalvik-dx" version="9.0.0_r3">
<artifact name="dalvik-dx-9.0.0_r3.jar"> <artifact name="dalvik-dx-9.0.0_r3.jar">
<sha256 value="b29c1c21e52ed6238cd3fed39d880a17ecf2360118604548cea8821be6801e1c" origin="Generated by Gradle"/> <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"/> <sha256 value="8f3c20e3e2d565d26f33e8d4857a37d0d7f8ac39b62a7026496fcab1bdac30d4" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.bouncycastle" name="bcutil-jdk15on" version="1.70">
<artifact name="bcutil-jdk15on-1.70.jar"> <artifact name="bcutil-jdk15on-1.70.jar">
<sha256 value="52dc5551b0257666526c5095424567fed7dc7b00d2b1ba7bd52298411112b1d0" origin="Generated by Gradle"/> <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"/> <sha256 value="b9d4fe4d71938df38839f0eca42aaaa64cf8b313d678da036f0cb3ca199b47f5" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.ow2.asm" name="asm-analysis" version="9.1">
<artifact name="asm-analysis-9.1.jar"> <artifact name="asm-analysis-9.1.jar">
<sha256 value="81a88041b1b8beda5a8a99646098046c48709538270c49def68abff25ac3be34" origin="Generated by Gradle"/> <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"/> <sha256 value="878fbe521731c072d14d2d65b983b1beae6ad06fda0007b6a8bae81f73f433c4" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.ow2.asm" name="asm-commons" version="9.2">
<artifact name="asm-commons-9.2.jar"> <artifact name="asm-commons-9.2.jar">
<sha256 value="be4ce53138a238bb522cd781cf91f3ba5ce2f6ca93ec62d46a162a127225e0a6" origin="Generated by Gradle"/> <sha256 value="be4ce53138a238bb522cd781cf91f3ba5ce2f6ca93ec62d46a162a127225e0a6" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.ow2.asm" name="asm-tree" version="9.1">
<artifact name="asm-tree-9.1.jar"> <artifact name="asm-tree-9.1.jar">
<sha256 value="fd00afa49e9595d7646205b09cecb4a776a8ff0ba06f2d59b8f7bf9c704b4a73" origin="Generated by Gradle"/> <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"/> <sha256 value="aabf9bd23091a4ebfc109c1f3ee7cf3e4b89f6ba2d3f51c5243f16b3cffae011" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.ow2.asm" name="asm-util" version="9.2">
<artifact name="asm-util-9.2.jar"> <artifact name="asm-util-9.2.jar">
<sha256 value="ff5b3cd331ae8a9a804768280da98f50f424fef23dd3c788bb320e08c94ee598" origin="Generated by Gradle"/> <sha256 value="ff5b3cd331ae8a9a804768280da98f50f424fef23dd3c788bb320e08c94ee598" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.reactivestreams" name="reactive-streams" version="1.0.3">
<artifact name="reactive-streams-1.0.3.jar"> <artifact name="reactive-streams-1.0.3.jar">
<sha256 value="1dee0481072d19c929b623e155e14d2f6085dc011529a0a0dbefc84cf571d865" origin="Generated by Gradle"/> <sha256 value="1dee0481072d19c929b623e155e14d2f6085dc011529a0a0dbefc84cf571d865" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="annotations" version="4.8.1"> <component group="org.robolectric" name="annotations" version="4.10.3">
<artifact name="annotations-4.8.1.jar"> <artifact name="annotations-4.10.3.jar">
<sha256 value="d3924abb4c9f7c20df582e4553ac13203bb40b7c367bf5bf776a9acbb252e181" origin="Generated by Gradle"/> <sha256 value="f3d6b921b7bf9d541577414c3b3124293eb09ced71f939e0c325c8d8abad0b6f" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="annotations-4.8.1.module"> <artifact name="annotations-4.10.3.module">
<sha256 value="a5e9f2ee5794c9ac22921e12646869025540924abfeacc5f39152dcee5b4a628" origin="Generated by Gradle"/> <sha256 value="a8a2a9b49333cba29c6598fdd18f111c562e5c2c37a7e92826ae0093e812bb7d" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="junit" version="4.8.1"> <component group="org.robolectric" name="junit" version="4.10.3">
<artifact name="junit-4.8.1.jar"> <artifact name="junit-4.10.3.jar">
<sha256 value="1475e1a271dce425d95ef53e837c07542023d718b0a320a9af6b879fa26c49ac" origin="Generated by Gradle"/> <sha256 value="815f0bae88eb198889e1878ef65b904c4ec59131be2458829bcc942bd7b9f6da" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="junit-4.8.1.module"> <artifact name="junit-4.10.3.module">
<sha256 value="e4b95ac9e273333dfa3938e50790fc5d3ba8bfe78f38ad61376b1baf6fa50b70" origin="Generated by Gradle"/> <sha256 value="fc951c7170ebe9b8da140bffdb3822c158ef4ace7b21d1e81d8752cd7ed92cda" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="nativeruntime" version="4.8.1"> <component group="org.robolectric" name="nativeruntime" version="4.10.3">
<artifact name="nativeruntime-4.8.1.jar"> <artifact name="nativeruntime-4.10.3.jar">
<sha256 value="62e3e2de0e205a5a9a017f71de661d9529ac1a1f6bf4014609493a0ec95178f8" origin="Generated by Gradle"/> <sha256 value="71fd2d1e8e78f2d70cc4879f5aa6910bf05a68274d3ca87179fb6f9447db5fb9" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="nativeruntime-4.8.1.module"> <artifact name="nativeruntime-4.10.3.module">
<sha256 value="796f67b8d2a5a93dd9917034089a8dfae1eb96c59e61223ac319a298475022c8" origin="Generated by Gradle"/> <sha256 value="989c43aacf186fc92edd59ee7be4d562a658130b7d3bfba3df9a5115a14f105e" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="pluginapi" version="4.8.1"> <component group="org.robolectric" name="nativeruntime-dist-compat" version="1.0.1">
<artifact name="pluginapi-4.8.1.jar"> <artifact name="nativeruntime-dist-compat-1.0.1.jar">
<sha256 value="bea89e22d02b946563cf320312bb3eb479f8f31aa9cf96f65ce35c2b80cd02bc" origin="Generated by Gradle"/> <sha256 value="2dd7aae2332b8f57932e1ef78fb8d973aac1da631ec9fb471752280df50d140c" origin="Generated by Gradle"/>
</artifact>
<artifact name="pluginapi-4.8.1.module">
<sha256 value="d2580abc08a992fe0256e209305dacdcc8e523d2c8d2ff0ddc7b25eaa7a39ac3" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="plugins-maven-dependency-resolver" version="4.8.1"> <component group="org.robolectric" name="pluginapi" version="4.10.3">
<artifact name="plugins-maven-dependency-resolver-4.8.1.jar"> <artifact name="pluginapi-4.10.3.jar">
<sha256 value="dc87b3d6c55f5a6691246038ef4ba53311f406c784c8d9469d1f1112f885f72d" origin="Generated by Gradle"/> <sha256 value="56be2717854add52e3437bb3be1b898dfea8ce8c6fcd26c4d0de68bf605274b0" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="plugins-maven-dependency-resolver-4.8.1.module"> <artifact name="pluginapi-4.10.3.module">
<sha256 value="e47aca957baeb036487497d52242c2fc5300435e6d31a106773baa9fae85b302" origin="Generated by Gradle"/> <sha256 value="2581c1cdc15888ce414f719763a8c784035189833f28debe049f9d686d1aeb0e" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="resources" version="4.8.1"> <component group="org.robolectric" name="plugins-maven-dependency-resolver" version="4.10.3">
<artifact name="resources-4.8.1.jar"> <artifact name="plugins-maven-dependency-resolver-4.10.3.jar">
<sha256 value="fd15dedf3721e46412c23c238a0b905166fbfba65628db546b1c121546d4712a" origin="Generated by Gradle"/> <sha256 value="54618c67214824dd5ebd72c5ed9c56fb62b776902455d0b0efc0e0940d8ebcf6" origin="Generated by Gradle"/>
</artifact>
<artifact name="resources-4.8.1.module">
<sha256 value="e8625ecc2f8c4476bc0a67bdf41824fd8a4dd871246d2eaa06a5cf01ea8d89c2" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="robolectric" version="4.8.1"> <component group="org.robolectric" name="resources" version="4.10.3">
<artifact name="robolectric-4.8.1.jar"> <artifact name="resources-4.10.3.jar">
<sha256 value="c17a5358ca9f1adb5d182bda0cf276efa01a0cc1a7ba456325ecffd6e9c70fef" origin="Generated by Gradle"/> <sha256 value="8decd0518e147c1038d38f6d33632e3310886194d7a8afeeb62849495f36e5f7" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="robolectric-4.8.1.module"> <artifact name="resources-4.10.3.module">
<sha256 value="34b34fdcf60982097fcad301036e379378a1c3d2cced824318fa43ed0b2fbcc3" origin="Generated by Gradle"/> <sha256 value="723823f373052ed4cb4c660039880f21ca88dc731260333bb8c7f01eb95f7039" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="sandbox" version="4.8.1"> <component group="org.robolectric" name="robolectric" version="4.10.3">
<artifact name="sandbox-4.8.1.jar"> <artifact name="robolectric-4.10.3.jar">
<sha256 value="ec132754fbd4a9b0c6658a2d3b12a2c6c2f2f56dfbd4ab1bd429441853697a41" origin="Generated by Gradle"/> <sha256 value="e61c4733bd64f57ba9884bf232b293fdd19b233608dd3481cd0e3c99f0f7c0fc" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="sandbox-4.8.1.module"> <artifact name="robolectric-4.10.3.module">
<sha256 value="09f45b4a6240134d8859245017b8d18f654dc1e0e56605b4840cf49dbd01f8ee" origin="Generated by Gradle"/> <sha256 value="f4491fc2ede8245609502e1bf6f41c0eaad532eec16c568e63091583fb30ece0" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="shadowapi" version="4.8.1"> <component group="org.robolectric" name="sandbox" version="4.10.3">
<artifact name="shadowapi-4.8.1.jar"> <artifact name="sandbox-4.10.3.jar">
<sha256 value="48ea81a61603d316f971a1c59f74b66d5914a8a5f96f657c8a57a3a5104455fb" origin="Generated by Gradle"/> <sha256 value="59611ce3f110f21d464003a7a812dc8155f4132173cb13cf4e246da496cf17d0" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="shadowapi-4.8.1.module"> <artifact name="sandbox-4.10.3.module">
<sha256 value="ecc1e0e90857cb1dcde67b0ca27cf3cbc7be84a09c4a6dd107b45b40588b7b9c" origin="Generated by Gradle"/> <sha256 value="3a2c82c681484a66288016006ec30dca02c48cc7a9855ca5ece8538fbf062c75" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="shadows-framework" version="4.8.1"> <component group="org.robolectric" name="shadowapi" version="4.10.3">
<artifact name="shadows-framework-4.8.1.jar"> <artifact name="shadowapi-4.10.3.jar">
<sha256 value="5fda3468ab58877ade2fab5c0a954a947a7b89d67c4e9eeda939eb8a91e1bf34" origin="Generated by Gradle"/> <sha256 value="1ba648a76968f1bb9f4fc64321af70c4eeed94b2a3fa1b2a848a7706ec25c75a" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="shadows-framework-4.8.1.module"> <artifact name="shadowapi-4.10.3.module">
<sha256 value="13e1f7e83dec909ef0f08130b4433e47a15821ccb8af1dad51fd344103ba7cfc" origin="Generated by Gradle"/> <sha256 value="09790eeb56f1a2ffa997fb3373d0b62ca763487e9540d7ae83198ab999f793ee" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="shadows-multidex" version="4.8.1"> <component group="org.robolectric" name="shadows-framework" version="4.10.3">
<artifact name="shadows-multidex-4.8.1.jar"> <artifact name="shadows-framework-4.10.3.jar">
<sha256 value="010216f7e8b05598e2b15c1091afc93e8e23f4258e87413ef28f38831afc352b" origin="Generated by Gradle"/> <sha256 value="106f6a19abc9d5786a18461a2554afbf782a30e799f867d7f1a9f26bcbb873a7" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="shadows-multidex-4.8.1.module"> <artifact name="shadows-framework-4.10.3.module">
<sha256 value="fbc182dca3e86887e055dff179eaaf68caf04f9c7532b29cdaa85b52b851be5a" origin="Generated by Gradle"/> <sha256 value="126f485b5f1570021ab79a2b819bf44a2f64405bd248f7e819236f13728b9ce7" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="utils" version="4.8.1"> <component group="org.robolectric" name="shadows-multidex" version="4.10.3">
<artifact name="utils-4.8.1.jar"> <artifact name="shadows-multidex-4.10.3.jar">
<sha256 value="c40afc0a140aada6830b3c2f7a331ad96d8e2f2a905458b6624b8fb12f7552d4" origin="Generated by Gradle"/> <sha256 value="34d1da27044528c07ca43c6334af3890868f2570d48114459c25ec4331ea6966" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="utils-4.8.1.module"> <artifact name="shadows-multidex-4.10.3.module">
<sha256 value="816c8a178a9346c9e7a9ecf3081f81df42ecb5e7b77ded0e36c6944635079aba" origin="Generated by Gradle"/> <sha256 value="ad1276a3820bbe37672624ee412b14bf35660604fb4802ac9b2ce8de494b72ff" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.robolectric" name="utils-reflector" version="4.8.1"> <component group="org.robolectric" name="utils" version="4.10.3">
<artifact name="utils-reflector-4.8.1.jar"> <artifact name="utils-4.10.3.jar">
<sha256 value="d17357e5254e1e14ee258fc1604bbe011aa425fe5e7c768a04fd043476e41267" origin="Generated by Gradle"/> <sha256 value="0081b1a65c2c6d7cf56a56f6b4ed85b35a91f5a9f40a4b81c6771b497265518e" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="utils-reflector-4.8.1.module"> </component>
<sha256 value="5c319c569301724ed105416a0246f88ba186a04ec54c4d136aca468bc694b664" origin="Generated by Gradle"/> <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> </artifact>
</component> </component>
<component group="org.signal" name="aesgcmprovider" version="0.0.3"> <component group="org.signal" name="aesgcmprovider" version="0.0.3">