From c95b18072804ee9add44064a516fa08b24dcf251 Mon Sep 17 00:00:00 2001 From: mtang-signal Date: Wed, 1 May 2024 16:38:26 -0400 Subject: [PATCH] Update gallery permission UI --- .../v2/ConversationActivityResultContracts.kt | 9 +---- .../v2/keyboard/AttachmentKeyboardFragment.kt | 5 ++- .../v2/gallery/MediaGalleryFragment.kt | 34 ++++++++++++++++- .../v2/gallery/MediaGallerySelectableItem.kt | 16 ++++++-- .../v2/gallery/MediaGalleryViewModel.kt | 4 ++ .../main/res/drawable/permission_gallery.xml | 32 ++++++++++++++++ .../main/res/layout/attachment_keyboard.xml | 4 +- .../res/layout/v2_media_gallery_fragment.xml | 38 +++++++++++++++++++ .../v2_media_gallery_placeholder_item.xml | 19 ++++++++++ app/src/main/res/values/strings.xml | 11 +++++- 10 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 app/src/main/res/drawable/permission_gallery.xml create mode 100644 app/src/main/res/layout/v2_media_gallery_placeholder_item.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt index 1b3c845b00..7f27799662 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt @@ -29,7 +29,6 @@ import org.thoughtcrime.securesms.maps.PlacePickerActivity import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity -import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.RecipientId @@ -71,13 +70,7 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat } fun launchGallery(recipientId: RecipientId, text: CharSequence?, isReply: Boolean) { - Permissions - .with(fragment) - .request(*PermissionCompat.forImagesAndVideos()) - .ifNecessary() - .withPermanentDenialDialog(fragment.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio)) - .onAllGranted { mediaGalleryLauncher.launch(MediaSelectionInput(emptyList(), recipientId, text, isReply)) } - .execute() + mediaGalleryLauncher.launch(MediaSelectionInput(emptyList(), recipientId, text, isReply)) } fun launchCamera(recipientId: RecipientId, isReply: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt index 02cd1db237..86dfd09caf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.conversation.v2.keyboard import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels @@ -94,8 +95,10 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_ override fun onAttachmentPermissionsRequested() { Permissions.with(requireParentFragment()) .request(*PermissionCompat.forImagesAndVideos()) + .ifNecessary() .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), null, R.string.AttachmentManager_signal_allow_storage, R.string.AttachmentManager_signal_to_show_photos, parentFragmentManager) + .onAnyDenied { Toast.makeText(requireContext(), R.string.AttachmentManager_signal_needs_storage_access, Toast.LENGTH_LONG).show() } .execute() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt index 5965d8815b..28869f97d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.mediasend.v2.gallery import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.WindowInsetsCompat @@ -18,6 +19,8 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration import org.thoughtcrime.securesms.databinding.V2MediaGalleryFragmentBinding import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaRepository +import org.thoughtcrime.securesms.permissions.PermissionCompat +import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.Material3OnScrollHelper import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter import org.thoughtcrime.securesms.util.ViewUtil @@ -39,6 +42,7 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { private lateinit var callbacks: Callbacks private var selectedMediaTouchHelper: ItemTouchHelper? = null + private var shouldEnableScrolling: Boolean = true private val galleryAdapter = MappingAdapter() private val selectedAdapter = MappingAdapter() @@ -65,6 +69,10 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { height = ViewUtil.getStatusBarHeight(view) } + binding.mediaGalleryGrid.layoutManager = object : GridLayoutManager(requireContext(), 4) { + override fun canScrollVertically() = shouldEnableScrolling + } + (binding.mediaGalleryGrid.layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { val isFolder: Boolean = (binding.mediaGalleryGrid.adapter as MappingAdapter).getModel(position).map { it is MediaGallerySelectableItem.FolderModel }.orElse(false) @@ -143,6 +151,8 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { binding.mediaGalleryToolbar.title = state.bucketTitle ?: requireContext().getString(R.string.AttachmentKeyboard_gallery) } + binding.mediaGalleryAllowAccess.setOnClickListener { requestRequiredPermissions() } + val galleryItemsWithSelection = LiveDataUtil.combineLatest( viewModel.state.map { it.items }, viewStateLiveData.map { it.selectedMedia } @@ -157,12 +167,34 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { } galleryItemsWithSelection.observe(viewLifecycleOwner) { - galleryAdapter.submitList(it) + if (!Permissions.hasAll(requireContext(), *PermissionCompat.forImagesAndVideos())) { + binding.mediaGalleryMissingPermissions.visibility = View.VISIBLE + shouldEnableScrolling = false + galleryAdapter.submitList((1..100).map { MediaGallerySelectableItem.PlaceholderModel() }) + } else { + binding.mediaGalleryMissingPermissions.visibility = View.GONE + shouldEnableScrolling = true + galleryAdapter.submitList(it) + } } requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback) } + private fun refreshMediaGallery() { + viewModel.refreshMediaGallery() + } + + private fun requestRequiredPermissions() { + Permissions.with(requireParentFragment()) + .request(*PermissionCompat.forImagesAndVideos()) + .ifNecessary() + .onAllGranted { refreshMediaGallery() } + .withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio), null, R.string.AttachmentManager_signal_allow_storage, R.string.AttachmentManager_signal_to_show_photos, parentFragmentManager) + .onAnyDenied { Toast.makeText(requireContext(), R.string.AttachmentManager_signal_needs_storage_access, Toast.LENGTH_LONG).show() } + .execute() + } + fun onBack() { if (viewModel.pop()) { onBackPressedCallback.isEnabled = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt index 849fdd11d0..00ab0d08ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGallerySelectableItem.kt @@ -43,6 +43,16 @@ object MediaGallerySelectableItem { ) { mappingAdapter.registerFactory(FolderModel::class.java, LayoutFactory({ FolderViewHolder(it, onMediaFolderClicked) }, R.layout.v2_media_gallery_folder_item)) mappingAdapter.registerFactory(FileModel::class.java, LayoutFactory({ FileViewHolder(it, onMediaClicked) }, if (isMultiselectEnabled) R.layout.v2_media_gallery_item else R.layout.v2_media_gallery_item_no_check)) + mappingAdapter.registerFactory(PlaceholderModel::class.java, LayoutFactory({ PlaceholderViewHolder(it) }, R.layout.v2_media_gallery_placeholder_item)) + } + + class PlaceholderViewHolder(itemView: View) : BaseViewHolder(itemView) { + override fun bind(model: PlaceholderModel) = Unit + } + + class PlaceholderModel : MappingModel { + override fun areItemsTheSame(newItem: PlaceholderModel): Boolean = true + override fun areContentsTheSame(newItem: PlaceholderModel): Boolean = true } class FolderModel(val mediaFolder: MediaFolder) : MappingModel { @@ -58,7 +68,7 @@ object MediaGallerySelectableItem { abstract class BaseViewHolder>(itemView: View) : MappingViewHolder(itemView) { protected val imageView: ShapeableImageView = itemView.findViewById(R.id.media_gallery_image) - protected val playOverlay: ImageView = itemView.findViewById(R.id.media_gallery_play_overlay) + protected val playOverlay: ImageView? = itemView.findViewById(R.id.media_gallery_play_overlay) protected val checkView: TextView? = itemView.findViewById(R.id.media_gallery_check) protected val title: TextView? = itemView.findViewById(R.id.media_gallery_title) } @@ -69,7 +79,7 @@ object MediaGallerySelectableItem { .load(DecryptableStreamUriLoader.DecryptableUri(model.mediaFolder.thumbnailUri)) .into(imageView) - playOverlay.visible = false + playOverlay?.visible = false itemView.setOnClickListener { onMediaFolderClicked(model.mediaFolder) } title?.text = model.mediaFolder.title title?.visible = true @@ -105,7 +115,7 @@ object MediaGallerySelectableItem { checkView?.visible = model.isSelected checkView?.text = "${model.selectionOneBasedIndex}" itemView.setOnClickListener { onMediaClicked(model.media, model.isSelected) } - playOverlay.visible = MediaUtil.isVideo(model.media.mimeType) && !model.media.isVideoGif + playOverlay?.visible = MediaUtil.isVideo(model.media.mimeType) && !model.media.isVideoGif title?.visible = false if (PAYLOAD_INDEX_CHANGED in payload) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryViewModel.kt index 28f908a22a..7fb707e360 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryViewModel.kt @@ -29,6 +29,10 @@ class MediaGalleryViewModel(bucketId: String?, bucketTitle: String?, private val loadItemsForBucket(mediaFolder.bucketId, mediaFolder.title) } + fun refreshMediaGallery() { + loadItemsForBucket(null, null) + } + private fun loadItemsForBucket(bucketId: String?, bucketTitle: String?) { if (bucketId == null) { repository.getFolders { folders -> diff --git a/app/src/main/res/drawable/permission_gallery.xml b/app/src/main/res/drawable/permission_gallery.xml new file mode 100644 index 0000000000..892e0daefc --- /dev/null +++ b/app/src/main/res/drawable/permission_gallery.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/attachment_keyboard.xml b/app/src/main/res/layout/attachment_keyboard.xml index ed029dd1ba..d3c6b0c951 100644 --- a/app/src/main/res/layout/attachment_keyboard.xml +++ b/app/src/main/res/layout/attachment_keyboard.xml @@ -57,9 +57,11 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a2632380e..bea3c47d56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,8 +80,9 @@ File Contact Location - Signal needs permission to show your photos and videos. - Give Access + Signal needs permission to show your photos and videos + + Allow Access Payment @@ -98,6 +99,12 @@ Allow Signal access to send your location. Signal needs location access to send your location. + + Allow access to storage + + To show photos and videos: + + Signal needs storage access to show your photos and videos. %1$s hasn\'t activated Payments