Skip to content

Commit

Permalink
Merge pull request #18183 from wordpress-mobile/android-13-media-perm…
Browse files Browse the repository at this point in the history
…issions

Android 13 media permissions
  • Loading branch information
irfano authored Apr 6, 2023
2 parents ca3e0b5 + 8b50e67 commit a76ef47
Show file tree
Hide file tree
Showing 22 changed files with 450 additions and 235 deletions.
3 changes: 3 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
*** PLEASE FOLLOW THIS FORMAT: [<priority indicator, more stars = higher priority>] <description> [<PR URL>]

22.2
[***] [internal] Adds media permissions support for Android 13 [https://github.com/wordpress-mobile/WordPress-Android/pull/18183]

22.1
-----
* [**] [WordPress-only] Warns user about sites with only individual plugins not supporting core app features and offers the option to switch to the Jetpack app. [https://github.com/wordpress-mobile/WordPress-Android/pull/18199]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package org.wordpress.android.e2e

import android.Manifest.permission
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.wordpress.android.e2e.pages.BlockEditorPage
import org.wordpress.android.e2e.pages.MySitesPage
Expand All @@ -14,9 +11,6 @@ import java.time.Instant

@HiltAndroidTest
class BlockEditorTests : BaseTest() {
@JvmField @Rule
var mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE)

@Before
fun setUp() {
logoutIfNecessary()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
package org.wordpress.android.e2e

import android.Manifest.permission
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.wordpress.android.e2e.pages.ReaderPage
import org.wordpress.android.support.BaseTest

@HiltAndroidTest
class ReaderTests : BaseTest() {
@JvmField @Rule
var mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE)

@Before
fun setUp() {
logoutIfNecessary()
Expand Down
13 changes: 10 additions & 3 deletions WordPress/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,16 @@
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<!-- Allows for storing and retrieving screenshots -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Allows for storing and retrieving screenshots, photos, videos and audios -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<!-- Allows changing locales for screenshot automation -->
<uses-permission
Expand Down
4 changes: 3 additions & 1 deletion WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
<!-- Dangerous permissions, access must be requested at runtime -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

<!-- GCM all build types configuration -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import android.content.ServiceConnection;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.text.TextUtils;
Expand Down Expand Up @@ -1002,13 +1003,17 @@ private void doAddMediaItemClicked(@NonNull AddMenuItem item) {

// stock photos item requires no permission, all other items do
if (item != AddMenuItem.ITEM_CHOOSE_STOCK_MEDIA) {
String[] permissions;
String[] permissions = null;
if (item == AddMenuItem.ITEM_CAPTURE_PHOTO || item == AddMenuItem.ITEM_CAPTURE_VIDEO) {
permissions = new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE};
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
permissions = new String[]{Manifest.permission.CAMERA};
} else {
permissions = new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
if (!PermissionUtils.checkAndRequestPermissions(
if (permissions != null && !PermissionUtils.checkAndRequestPermissions(
this, WPPermissionUtils.MEDIA_BROWSER_PERMISSION_REQUEST_CODE, permissions)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
Expand Down Expand Up @@ -987,14 +988,14 @@ private void updateImageSizeParameters() {
* saves the media to the local device using the Android DownloadManager
*/
private void saveMediaToDevice() {
// must request permissions even though they're already defined in the manifest
String[] permissionList = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
if (!PermissionUtils.checkAndRequestPermissions(this, WPPermissionUtils.MEDIA_PREVIEW_PERMISSION_REQUEST_CODE,
permissionList)) {
return;
// must request the permission even though it's already defined in the manifest
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
String[] permissionList = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
if (!PermissionUtils.checkAndRequestPermissions(this,
WPPermissionUtils.MEDIA_PREVIEW_PERMISSION_REQUEST_CODE,
permissionList)) {
return;
}
}

if (!NetworkUtils.checkConnection(this)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.Activity
import android.content.Intent.ACTION_GET_CONTENT
import android.content.Intent.ACTION_OPEN_DOCUMENT
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
Expand Down Expand Up @@ -52,8 +53,6 @@ import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.BrowseMenuUiMod
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.BrowseMenuUiModel.BrowseAction.SYSTEM_PICKER
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.BrowseMenuUiModel.BrowseAction.WP_MEDIA_LIBRARY
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.FabUiModel
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PermissionsRequested.CAMERA
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PermissionsRequested.STORAGE
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PhotoListUiModel
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel
import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel.Visible
Expand Down Expand Up @@ -202,6 +201,7 @@ class MediaPickerFragment : Fragment(), MenuProvider {
lateinit var uiHelpers: UiHelpers
private lateinit var viewModel: MediaPickerViewModel
private var binding: MediaPickerFragmentBinding? = null
private lateinit var mediaPickerSetup: MediaPickerSetup

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -226,7 +226,7 @@ class MediaPickerFragment : Fragment(), MenuProvider {
super.onViewCreated(view, savedInstanceState)
requireActivity().addMenuProvider(this, viewLifecycleOwner)

val mediaPickerSetup = MediaPickerSetup.fromBundle(requireArguments())
mediaPickerSetup = MediaPickerSetup.fromBundle(requireArguments())
val site = requireArguments().getSerializableCompat<SiteModel>(WordPress.SITE)
var selectedIds: List<Identifier>? = null
var lastTappedIcon: MediaPickerIcon? = null
Expand Down Expand Up @@ -276,12 +276,7 @@ class MediaPickerFragment : Fragment(), MenuProvider {
navigateEvent(navigationEvent)
}

viewModel.onPermissionsRequested.observeEvent(viewLifecycleOwner) {
when (it) {
CAMERA -> requestCameraPermission()
STORAGE -> requestStoragePermission()
}
}
viewModel.onCameraPermissionsRequested.observeEvent(viewLifecycleOwner) { requestCameraPermission() }
viewModel.onSnackbarMessage.observeEvent(viewLifecycleOwner) { messageHolder ->
showSnackbar(messageHolder)
}
Expand Down Expand Up @@ -456,7 +451,7 @@ class MediaPickerFragment : Fragment(), MenuProvider {
if (uiModel.isAlwaysDenied) {
WPPermissionUtils.showAppSettings(requireActivity())
} else {
requestStoragePermission()
requestMediaPermission()
}
}

Expand Down Expand Up @@ -621,44 +616,75 @@ class MediaPickerFragment : Fragment(), MenuProvider {

override fun onResume() {
super.onResume()
checkStoragePermission()
checkMediaPermission()
}

fun setMediaPickerListener(listener: MediaPickerListener?) {
this.listener = listener
}

private val isStoragePermissionAlwaysDenied: Boolean
get() = WPPermissionUtils.isPermissionAlwaysDenied(
requireActivity(), permission.WRITE_EXTERNAL_STORAGE
)

/*
* load the photos if we have the necessary permission, otherwise show the "soft ask" view
* which asks the user to allow the permission
*/
private fun checkStoragePermission() {
private fun checkMediaPermission() {
if (!isAdded) {
return
}
viewModel.checkStoragePermission(isStoragePermissionAlwaysDenied)

// Storage permission is available only for API lower than 33
val isStoragePermissionAlwaysDenied = WPPermissionUtils.isPermissionAlwaysDenied(
requireActivity(),
permission.WRITE_EXTERNAL_STORAGE
)

val isPhotosVideosPermissionAlwaysDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
WPPermissionUtils.isPermissionAlwaysDenied(requireActivity(), permission.READ_MEDIA_IMAGES) ||
WPPermissionUtils.isPermissionAlwaysDenied(requireActivity(), permission.READ_MEDIA_VIDEO)
} else {
// For devices lower than API 33, storage permission is the equivalent of Photos and Videos permission
isStoragePermissionAlwaysDenied
}
val isMusicAudioPermissionAlwaysDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
WPPermissionUtils.isPermissionAlwaysDenied(
requireActivity(),
permission.READ_MEDIA_AUDIO
)
} else {
// For devices lower than API 33, storage permission is the equivalent of Music and Audio permission
isStoragePermissionAlwaysDenied
}
viewModel.checkMediaPermissions(isPhotosVideosPermissionAlwaysDenied, isMusicAudioPermissionAlwaysDenied)
}

@Suppress("DEPRECATION")
private fun requestStoragePermission() {
val permissions = arrayOf(permission.WRITE_EXTERNAL_STORAGE, permission.READ_EXTERNAL_STORAGE)
requestPermissions(
permissions, WPPermissionUtils.PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE
)
private fun requestMediaPermission() {
val permissions = arrayListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (mediaPickerSetup.requiresPhotosVideosPermissions) {
permissions.add(permission.READ_MEDIA_IMAGES)
permissions.add(permission.READ_MEDIA_VIDEO)
}
if (mediaPickerSetup.requiresMusicAudioPermissions) {
permissions.add(permission.READ_MEDIA_AUDIO)
}
} else {
// READ_EXTERNAL_STORAGE is the equivalent of READ_MEDIA_IMAGES, READ_MEDIA_VIDEO and READ_MEDIA_AUDIO on
// devices lower than API 33.
permissions.add(permission.READ_EXTERNAL_STORAGE)
}
requestPermissions(permissions.toTypedArray(), WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE)
}

@Suppress("DEPRECATION")
private fun requestCameraPermission() {
// in addition to CAMERA permission we also need a storage permission, to store media from the camera
val permissions = arrayOf(
permission.CAMERA,
permission.WRITE_EXTERNAL_STORAGE
)
// For devices lower than API 30, in addition to CAMERA permission we also need a storage permission, to store
// media from the camera
val permissions = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
arrayOf(permission.CAMERA, permission.WRITE_EXTERNAL_STORAGE)
} else {
arrayOf(permission.CAMERA)
}
requestPermissions(permissions, WPPermissionUtils.PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE)
}

Expand All @@ -673,7 +699,7 @@ class MediaPickerFragment : Fragment(), MenuProvider {
requireActivity(), requestCode, permissions, grantResults, checkForAlwaysDenied
)
when (requestCode) {
WPPermissionUtils.PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE -> checkStoragePermission()
WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE -> checkMediaPermission()
WPPermissionUtils.PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE -> if (allGranted) {
viewModel.clickOnLastTappedIcon()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ data class MediaPickerSetup(
val primaryDataSource: DataSource,
val availableDataSources: Set<DataSource>,
val canMultiselect: Boolean,
val requiresStoragePermissions: Boolean,
val requiresPhotosVideosPermissions: Boolean,
val requiresMusicAudioPermissions: Boolean,
val allowedTypes: Set<MediaType>,
val cameraSetup: CameraSetup,
val systemPickerEnabled: Boolean,
Expand All @@ -31,7 +32,8 @@ data class MediaPickerSetup(
bundle.putIntegerArrayList(KEY_AVAILABLE_DATA_SOURCES, ArrayList(availableDataSources.map { it.ordinal }))
bundle.putIntegerArrayList(KEY_ALLOWED_TYPES, ArrayList(allowedTypes.map { it.ordinal }))
bundle.putBoolean(KEY_CAN_MULTISELECT, canMultiselect)
bundle.putBoolean(KEY_REQUIRES_STORAGE_PERMISSIONS, requiresStoragePermissions)
bundle.putBoolean(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS, requiresPhotosVideosPermissions)
bundle.putBoolean(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS, requiresMusicAudioPermissions)
bundle.putInt(KEY_CAMERA_SETUP, cameraSetup.ordinal)
bundle.putBoolean(KEY_SYSTEM_PICKER_ENABLED, systemPickerEnabled)
bundle.putBoolean(KEY_EDITING_ENABLED, editingEnabled)
Expand All @@ -45,7 +47,8 @@ data class MediaPickerSetup(
intent.putIntegerArrayListExtra(KEY_AVAILABLE_DATA_SOURCES, ArrayList(availableDataSources.map { it.ordinal }))
intent.putIntegerArrayListExtra(KEY_ALLOWED_TYPES, ArrayList(allowedTypes.map { it.ordinal }))
intent.putExtra(KEY_CAN_MULTISELECT, canMultiselect)
intent.putExtra(KEY_REQUIRES_STORAGE_PERMISSIONS, requiresStoragePermissions)
intent.putExtra(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS, requiresPhotosVideosPermissions)
intent.putExtra(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS, requiresMusicAudioPermissions)
intent.putExtra(KEY_CAMERA_SETUP, cameraSetup.ordinal)
intent.putExtra(KEY_SYSTEM_PICKER_ENABLED, systemPickerEnabled)
intent.putExtra(KEY_EDITING_ENABLED, editingEnabled)
Expand All @@ -58,7 +61,8 @@ data class MediaPickerSetup(
private const val KEY_PRIMARY_DATA_SOURCE = "key_primary_data_source"
private const val KEY_AVAILABLE_DATA_SOURCES = "key_available_data_sources"
private const val KEY_CAN_MULTISELECT = "key_can_multiselect"
private const val KEY_REQUIRES_STORAGE_PERMISSIONS = "key_requires_storage_permissions"
private const val KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS = "key_requires_photos_videos_permissions"
private const val KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS = "key_requires_music_audio_permissions"
private const val KEY_ALLOWED_TYPES = "key_allowed_types"
private const val KEY_CAMERA_SETUP = "key_camera_setup"
private const val KEY_SYSTEM_PICKER_ENABLED = "key_system_picker_enabled"
Expand All @@ -77,7 +81,8 @@ data class MediaPickerSetup(
}.toSet()
val multipleSelectionAllowed = bundle.getBoolean(KEY_CAN_MULTISELECT)
val cameraSetup = CameraSetup.values()[bundle.getInt(KEY_CAMERA_SETUP)]
val requiresStoragePermissions = bundle.getBoolean(KEY_REQUIRES_STORAGE_PERMISSIONS)
val requiresPhotosVideosPermissions = bundle.getBoolean(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS)
val requiresMusicAudioPermissions = bundle.getBoolean(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS)
val systemPickerEnabled = bundle.getBoolean(KEY_SYSTEM_PICKER_ENABLED)
val editingEnabled = bundle.getBoolean(KEY_EDITING_ENABLED)
val queueResults = bundle.getBoolean(KEY_QUEUE_RESULTS)
Expand All @@ -87,7 +92,8 @@ data class MediaPickerSetup(
dataSource,
availableDataSources,
multipleSelectionAllowed,
requiresStoragePermissions,
requiresPhotosVideosPermissions,
requiresMusicAudioPermissions,
allowedTypes,
cameraSetup,
systemPickerEnabled,
Expand All @@ -109,7 +115,8 @@ data class MediaPickerSetup(
}.toSet()
val multipleSelectionAllowed = intent.getBooleanExtra(KEY_CAN_MULTISELECT, false)
val cameraSetup = CameraSetup.values()[intent.getIntExtra(KEY_CAMERA_SETUP, -1)]
val requiresStoragePermissions = intent.getBooleanExtra(KEY_REQUIRES_STORAGE_PERMISSIONS, false)
val requiresPhotosVideosPermissions = intent.getBooleanExtra(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS, false)
val requiresMusicAudioPermissions = intent.getBooleanExtra(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS, false)
val systemPickerEnabled = intent.getBooleanExtra(KEY_SYSTEM_PICKER_ENABLED, false)
val editingEnabled = intent.getBooleanExtra(KEY_SYSTEM_PICKER_ENABLED, false)
val queueResults = intent.getBooleanExtra(KEY_QUEUE_RESULTS, false)
Expand All @@ -119,7 +126,8 @@ data class MediaPickerSetup(
dataSource,
availableDataSources,
multipleSelectionAllowed,
requiresStoragePermissions,
requiresPhotosVideosPermissions,
requiresMusicAudioPermissions,
allowedTypes,
cameraSetup,
systemPickerEnabled,
Expand Down
Loading

0 comments on commit a76ef47

Please sign in to comment.