diff --git a/api.txt b/api.txt index 163431944a..0b8e1a3c79 100644 --- a/api.txt +++ b/api.txt @@ -1246,8 +1246,9 @@ package androidx.media3.common.util { method @androidx.media3.common.C.ContentType public static int inferContentType(android.net.Uri); method @androidx.media3.common.C.ContentType public static int inferContentTypeForExtension(String); method @androidx.media3.common.C.ContentType public static int inferContentTypeForUriAndMimeType(android.net.Uri, @Nullable String); - method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, android.net.Uri...); - method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...); + method @Deprecated public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, android.net.Uri...); + method @Deprecated public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...); + method public static boolean maybeRequestReadStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...); method @org.checkerframework.checker.nullness.qual.EnsuresNonNullIf(result=false, expression="#1") public static boolean shouldShowPlayButton(@Nullable androidx.media3.common.Player); } diff --git a/constants.gradle b/constants.gradle index 118ccb2017..0f30434e0f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -16,9 +16,6 @@ project.ext { releaseVersionCode = 1_001_000_3_00 minSdkVersion = 16 appTargetSdkVersion = 34 - // API version before restricting local file access. - // https://developer.android.com/training/data-storage/app-specific - mainDemoAppTargetSdkVersion = 29 // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some // additional robolectric config. targetSdkVersion = 30 diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 8038d49ee7..850a24ac0a 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -30,9 +30,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - // Not using appTargetSDKVersion to allow local file access on API 29 - // and higher [Internal ref: b/191644662] - targetSdkVersion project.ext.mainDemoAppTargetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion multiDexEnabled true } diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 63266bfbd4..da13a42ca4 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -21,6 +21,9 @@ + + + @@ -36,7 +39,6 @@ android:banner="@drawable/ic_banner" android:largeHeap="true" android:allowBackup="false" - android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:name="androidx.multidex.MultiDexApplication" tools:targetApi="29"> diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index d812c2ffbf..ad4b4c0fa7 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -354,7 +354,7 @@ public class PlayerActivity extends AppCompatActivity finish(); return Collections.emptyList(); } - if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) { + if (Util.maybeRequestReadStoragePermission(/* activity= */ this, mediaItem)) { // The player will be reinitialized if the permission is granted. return Collections.emptyList(); } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java index 5afa2613e3..7d10a65966 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java @@ -80,7 +80,6 @@ public class SampleChooserActivity extends AppCompatActivity private static final String TAG = "SampleChooserActivity"; private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position"; private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position"; - private static final int POST_NOTIFICATION_PERMISSION_REQUEST_CODE = 100; private String[] uris; private boolean useExtensionRenderers; @@ -179,14 +178,6 @@ public class SampleChooserActivity extends AppCompatActivity public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == POST_NOTIFICATION_PERMISSION_REQUEST_CODE) { - handlePostNotificationPermissionGrantResults(grantResults); - } else { - handleExternalStoragePermissionGrantResults(grantResults); - } - } - - private void handlePostNotificationPermissionGrantResults(int[] grantResults) { if (!notificationPermissionToastShown && (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED)) { Toast.makeText( @@ -201,30 +192,8 @@ public class SampleChooserActivity extends AppCompatActivity } } - private void handleExternalStoragePermissionGrantResults(int[] grantResults) { - if (grantResults.length == 0) { - // Empty results are triggered if a permission is requested while another request was already - // pending and can be safely ignored in this case. - return; - } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - loadSample(); - } else { - Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) - .show(); - finish(); - } - } - private void loadSample() { checkNotNull(uris); - - for (int i = 0; i < uris.length; i++) { - Uri uri = Uri.parse(uris[i]); - if (Util.maybeRequestReadExternalStoragePermission(this, uri)) { - return; - } - } - SampleListLoader loaderTask = new SampleListLoader(); loaderTask.execute(uris); } @@ -279,8 +248,7 @@ public class SampleChooserActivity extends AppCompatActivity != PackageManager.PERMISSION_GRANTED) { downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0); requestPermissions( - new String[] {Api33.getPostNotificationPermissionString()}, - /* requestCode= */ POST_NOTIFICATION_PERMISSION_REQUEST_CODE); + new String[] {Api33.getPostNotificationPermissionString()}, /* requestCode= */ 0); } else { toggleDownload(playlistHolder.mediaItems.get(0)); } diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 8e76c0bada..374e70f689 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -73,6 +73,7 @@ import android.util.SparseLongArray; import android.view.Display; import android.view.SurfaceView; import android.view.WindowManager; +import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.DoNotInline; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; @@ -331,19 +332,12 @@ public final class Util { } /** - * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE} - * permission read the specified {@link Uri}s, requesting the permission if necessary. - * - * @param activity The host activity for checking and requesting the permission. - * @param uris {@link Uri}s that may require {@link permission#READ_EXTERNAL_STORAGE} to read. - * @return Whether a permission request was made. + * @deprecated Use {@link #maybeRequestReadStoragePermission(Activity, MediaItem...)} instead. */ + @Deprecated public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) { - if (SDK_INT < 23) { - return false; - } for (Uri uri : uris) { - if (maybeRequestReadExternalStoragePermission(activity, uri)) { + if (maybeRequestReadStoragePermission(activity, uri)) { return true; } } @@ -351,16 +345,24 @@ public final class Util { } /** - * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE} - * permission for the specified {@link MediaItem media items}, requesting the permission if - * necessary. + * @deprecated Use {@link #maybeRequestReadStoragePermission(Activity, MediaItem...)} instead. + */ + @Deprecated + public static boolean maybeRequestReadExternalStoragePermission( + Activity activity, MediaItem... mediaItems) { + return maybeRequestReadStoragePermission(activity, mediaItems); + } + + /** + * Checks whether it's necessary to request storage reading permissions for the specified {@link + * MediaItem media items}, requesting the permissions if necessary. * * @param activity The host activity for checking and requesting the permission. - * @param mediaItems {@link MediaItem Media items}s that may require {@link - * permission#READ_EXTERNAL_STORAGE} to read. + * @param mediaItems {@link MediaItem Media items}s that may require storage reading permissions + * to read. * @return Whether a permission request was made. */ - public static boolean maybeRequestReadExternalStoragePermission( + public static boolean maybeRequestReadStoragePermission( Activity activity, MediaItem... mediaItems) { if (SDK_INT < 23) { return false; @@ -369,13 +371,13 @@ public final class Util { if (mediaItem.localConfiguration == null) { continue; } - if (maybeRequestReadExternalStoragePermission(activity, mediaItem.localConfiguration.uri)) { + if (maybeRequestReadStoragePermission(activity, mediaItem.localConfiguration.uri)) { return true; } List subtitleConfigs = mediaItem.localConfiguration.subtitleConfigurations; for (int i = 0; i < subtitleConfigs.size(); i++) { - if (maybeRequestReadExternalStoragePermission(activity, subtitleConfigs.get(i).uri)) { + if (maybeRequestReadStoragePermission(activity, subtitleConfigs.get(i).uri)) { return true; } } @@ -383,10 +385,50 @@ public final class Util { return false; } - private static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri uri) { - return SDK_INT >= 23 - && (isLocalFileUri(uri) || isMediaStoreExternalContentUri(uri)) - && requestExternalStoragePermission(activity); + private static boolean maybeRequestReadStoragePermission(Activity activity, Uri uri) { + if (!isReadStoragePermissionRequestNeeded(activity, uri)) { + return false; + } + if (SDK_INT < 33) { + return requestExternalStoragePermission(activity); + } else { + return requestReadMediaPermissions(activity); + } + } + + @ChecksSdkIntAtLeast(api = 23) + private static boolean isReadStoragePermissionRequestNeeded(Activity activity, Uri uri) { + if (SDK_INT < 23) { + // Permission automatically granted via manifest below API 23. + return false; + } + if (isLocalFileUri(uri)) { + return !isAppSpecificStorageFileUri(activity, uri); + } + if (isMediaStoreExternalContentUri(uri)) { + return true; + } + return false; + } + + private static boolean isAppSpecificStorageFileUri(Activity activity, Uri uri) { + try { + @Nullable String uriPath = uri.getPath(); + if (uriPath == null) { + return false; + } + String filePath = new File(uriPath).getCanonicalPath(); + String internalAppDirectoryPath = activity.getFilesDir().getCanonicalPath(); + @Nullable File externalAppDirectory = activity.getExternalFilesDir(/* type= */ null); + @Nullable + String externalAppDirectoryPath = + externalAppDirectory == null ? null : externalAppDirectory.getCanonicalPath(); + return filePath.startsWith(internalAppDirectoryPath) + || (externalAppDirectoryPath != null && filePath.startsWith(externalAppDirectoryPath)); + } catch (IOException e) { + // Error while querying canonical paths. + return false; + } } private static boolean isMediaStoreExternalContentUri(Uri uri) { @@ -3225,6 +3267,24 @@ public final class Util { return false; } + @RequiresApi(api = 33) + private static boolean requestReadMediaPermissions(Activity activity) { + if (activity.checkSelfPermission(permission.READ_MEDIA_AUDIO) + != PackageManager.PERMISSION_GRANTED + || activity.checkSelfPermission(permission.READ_MEDIA_VIDEO) + != PackageManager.PERMISSION_GRANTED + || activity.checkSelfPermission(permission.READ_MEDIA_IMAGES) + != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions( + new String[] { + permission.READ_MEDIA_AUDIO, permission.READ_MEDIA_IMAGES, permission.READ_MEDIA_VIDEO + }, + /* requestCode= */ 0); + return true; + } + return false; + } + @RequiresApi(api = Build.VERSION_CODES.N) private static boolean isTrafficRestricted(Uri uri) { return "http".equals(uri.getScheme())