Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android 13 media permissions #18183

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