3939
4040import android .webkit .CookieSyncManager ;
4141import android .content .*;
42- import android .content .pm .*;
43- import android .content .res .Configuration ;
42+ import android .content .pm .*;
43+ import android .content .res .AssetFileDescriptor ;
44+ import android .content .res .Configuration ;
4445import android .graphics .Bitmap ;
4546import android .graphics .BitmapFactory ;
4647import android .graphics .Canvas ;
@@ -3603,41 +3604,70 @@ public Media createMedia(final String uri, boolean isVideo, final Runnable onCom
36033604 if (getActivity () == null ) {
36043605 return null ;
36053606 }
3606- if (!uri .startsWith (FileSystemStorage .getInstance ().getAppHomePath ())) {
3607- if (!PermissionsHelper .checkForPermission (isVideo ? DevicePermission .PERMISSION_READ_VIDEO : DevicePermission .PERMISSION_READ_AUDIO , "This is required to play media" )){
3608- return null ;
3609- }
3610- }
3611- if (uri .startsWith ("file://" )) {
3612- return createMedia (removeFilePrefix (uri ), isVideo , onCompletion );
3613- }
3614- File file = null ;
3615- if (uri .indexOf (':' ) < 0 ) {
3616- // use a file object to play to try and workaround this issue:
3617- // http://code.google.com/p/android/issues/detail?id=4124
3618- file = new File (uri );
3619- }
3620-
3621- Media retVal ;
3622-
3623- if (isVideo ) {
3624- final AndroidImplementation .Video [] video = new AndroidImplementation .Video [1 ];
3625- final boolean [] flag = new boolean [1 ];
3626- final File f = file ;
3627- getActivity ().runOnUiThread (new Runnable () {
3628- @ Override
3629- public void run () {
3630- VideoView v = new VideoView (getActivity ());
3631- v .setZOrderMediaOverlay (true );
3632- if (f != null ) {
3633- v .setVideoURI (Uri .fromFile (f ));
3634- } else {
3635- v .setVideoURI (Uri .parse (uri ));
3636- }
3637- video [0 ] = new AndroidImplementation .Video (v , getActivity (), onCompletion );
3638- flag [0 ] = true ;
3639- synchronized (flag ) {
3640- flag .notify ();
3607+ if (uri .startsWith ("file://" )) {
3608+ return createMedia (removeFilePrefix (uri ), isVideo , onCompletion );
3609+ }
3610+ File file = null ;
3611+ if (uri .indexOf (':' ) < 0 ) {
3612+ // use a file object to play to try and workaround this issue:
3613+ // http://code.google.com/p/android/issues/detail?id=4124
3614+ file = new File (uri );
3615+ }
3616+
3617+ Uri parsedUri = null ;
3618+ boolean isContentUri = false ;
3619+ if (file == null ) {
3620+ parsedUri = Uri .parse (uri );
3621+ isContentUri = parsedUri != null && "content" .equalsIgnoreCase (parsedUri .getScheme ());
3622+ }
3623+
3624+ // The document picker grants temporary permissions for content URIs. Requesting
3625+ // READ_EXTERNAL_STORAGE again would surface a redundant prompt on Android 13+, so we only
3626+ // ask for classic file paths that require the legacy permission. MediaStore URIs still
3627+ // require an explicit permission grant, so they remain subject to the legacy check even
3628+ // though they also use the content:// scheme.
3629+ boolean requiresLegacyPermission = !uri .startsWith (FileSystemStorage .getInstance ().getAppHomePath ());
3630+ if (isContentUri && parsedUri != null ) {
3631+ String authority = parsedUri .getAuthority ();
3632+ if (authority != null ) {
3633+ authority = authority .toLowerCase ();
3634+ if (!"media" .equals (authority ) && !authority .startsWith ("media." )) {
3635+ if (!"com.android.providers.media.documents" .equals (authority )) {
3636+ requiresLegacyPermission = false ;
3637+ }
3638+ }
3639+ } else {
3640+ requiresLegacyPermission = false ;
3641+ }
3642+ }
3643+
3644+ if (requiresLegacyPermission ) {
3645+ if (!PermissionsHelper .checkForPermission (isVideo ? DevicePermission .PERMISSION_READ_VIDEO : DevicePermission .PERMISSION_READ_AUDIO , "This is required to play media" )){
3646+ return null ;
3647+ }
3648+ }
3649+
3650+ Media retVal ;
3651+
3652+ if (isVideo ) {
3653+ final AndroidImplementation .Video [] video = new AndroidImplementation .Video [1 ];
3654+ final boolean [] flag = new boolean [1 ];
3655+ final File f = file ;
3656+ final Uri videoUri = parsedUri ;
3657+ getActivity ().runOnUiThread (new Runnable () {
3658+ @ Override
3659+ public void run () {
3660+ VideoView v = new VideoView (getActivity ());
3661+ v .setZOrderMediaOverlay (true );
3662+ if (f != null ) {
3663+ v .setVideoURI (Uri .fromFile (f ));
3664+ } else {
3665+ v .setVideoURI (videoUri != null ? videoUri : Uri .parse (uri ));
3666+ }
3667+ video [0 ] = new AndroidImplementation .Video (v , getActivity (), onCompletion );
3668+ flag [0 ] = true ;
3669+ synchronized (flag ) {
3670+ flag .notify ();
36413671 }
36423672 }
36433673 });
@@ -3652,18 +3682,47 @@ public void run() {
36523682 return video [0 ];
36533683 } else {
36543684 MediaPlayer player ;
3655- if (file != null ) {
3656- FileInputStream is = new FileInputStream (file );
3657- player = new MediaPlayer ();
3658- player .setDataSource (is .getFD ());
3659- player .prepare ();
3660- } else {
3661- player = MediaPlayer .create (getActivity (), Uri .parse (uri ));
3662- }
3663- retVal = new Audio (getActivity (), player , null , onCompletion );
3664- }
3665- return retVal ;
3666- }
3685+ if (file != null ) {
3686+ FileInputStream is = new FileInputStream (file );
3687+ player = new MediaPlayer ();
3688+ player .setDataSource (is .getFD ());
3689+ player .prepare ();
3690+ } else {
3691+ player = MediaPlayer .create (getActivity (), parsedUri != null ? parsedUri : Uri .parse (uri ));
3692+ if (player == null && isContentUri ) {
3693+ // Android 13+ introduces stricter access rules for content:// URIs returned
3694+ // from the system document picker. The picker grants our activity a
3695+ // persistable read permission, but some OEM builds still reject the URI when it
3696+ // is passed directly to MediaPlayer. Opening the descriptor ourselves keeps the
3697+ // same permission grant while avoiding the OEM bug.
3698+ ContentResolver resolver = getContext ().getContentResolver ();
3699+ if (resolver != null && parsedUri != null ) {
3700+ AssetFileDescriptor afd = null ;
3701+ try {
3702+ afd = resolver .openAssetFileDescriptor (parsedUri , "r" );
3703+ if (afd != null ) {
3704+ player = new MediaPlayer ();
3705+ player .setDataSource (afd .getFileDescriptor (), afd .getStartOffset (), afd .getLength ());
3706+ player .prepare ();
3707+ }
3708+ } finally {
3709+ if (afd != null ) {
3710+ try {
3711+ afd .close ();
3712+ } catch (IOException ignore ) {
3713+ }
3714+ }
3715+ }
3716+ }
3717+ }
3718+ }
3719+ if (player == null ) {
3720+ throw new IOException ("Unable to create media player for uri " + uri );
3721+ }
3722+ retVal = new Audio (getActivity (), player , null , onCompletion );
3723+ }
3724+ return retVal ;
3725+ }
36673726
36683727 @ Override
36693728 public void addCompletionHandler (Media media , Runnable onCompletion ) {
@@ -8072,15 +8131,17 @@ private String getImageFilePath(Uri uri) {
80728131 @ Override
80738132 public void onActivityResult (int requestCode , int resultCode , Intent intent ) {
80748133
8075- if (requestCode == ZOOZ_PAYMENT ) {
8076- ((IntentResultListener ) pur ).onActivityResult (requestCode , resultCode , intent );
8077- return ;
8078- }
8079-
8080- if (requestCode == REQUEST_SELECT_FILE || requestCode == FILECHOOSER_RESULTCODE ) {
8081- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
8082- if (requestCode == REQUEST_SELECT_FILE ) {
8083- if (uploadMessage == null ) return ;
8134+ if (requestCode == ZOOZ_PAYMENT ) {
8135+ ((IntentResultListener ) pur ).onActivityResult (requestCode , resultCode , intent );
8136+ return ;
8137+ }
8138+
8139+ takePersistablePermissionsFromIntent (intent );
8140+
8141+ if (requestCode == REQUEST_SELECT_FILE || requestCode == FILECHOOSER_RESULTCODE ) {
8142+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
8143+ if (requestCode == REQUEST_SELECT_FILE ) {
8144+ if (uploadMessage == null ) return ;
80848145 Uri [] results = null ;
80858146
80868147 // Check that the response is a good one
@@ -8618,45 +8679,92 @@ public void openGallery(final ActionListener response, int type){
86188679
86198680 callback = new EventDispatcher ();
86208681 callback .addListener (response );
8621- Intent galleryIntent = new Intent (Intent .ACTION_PICK , android .provider .MediaStore .Images .Media .INTERNAL_CONTENT_URI );
8622- if (multi ) {
8623- galleryIntent .putExtra (Intent .EXTRA_ALLOW_MULTIPLE , true );
8624- }
8682+ Intent galleryIntent = new Intent (Intent .ACTION_PICK , android .provider .MediaStore .Images .Media .INTERNAL_CONTENT_URI );
8683+ galleryIntent .addFlags (Intent .FLAG_GRANT_READ_URI_PERMISSION );
8684+ if (multi ) {
8685+ galleryIntent .putExtra (Intent .EXTRA_ALLOW_MULTIPLE , true );
8686+ }
86258687 if (type == Display .GALLERY_VIDEO ){
86268688 galleryIntent .setType ("video/*" );
86278689 }else if (type == Display .GALLERY_IMAGE ){
86288690 galleryIntent .setType ("image/*" );
86298691 }else if (type == Display .GALLERY_ALL ){
86308692 galleryIntent .setType ("image/* video/*" );
8631- }else if (type == -9999 ) {
8632- galleryIntent = new Intent ();
8633- if (android .os .Build .VERSION .SDK_INT >= android .os .Build .VERSION_CODES .KITKAT ) {
8634- galleryIntent .setAction (Intent .ACTION_OPEN_DOCUMENT );
8635- } else {
8636- galleryIntent .setAction (Intent .ACTION_GET_CONTENT );
8637- }
8638- galleryIntent .addCategory (Intent .CATEGORY_OPENABLE );
8639-
8640- // set MIME type for image
8641- galleryIntent .setType ("*/*" );
8642- galleryIntent .putExtra (Intent .EXTRA_MIME_TYPES , Display .getInstance ().getProperty ("android.openGallery.accept" , "*/*" ).split ("," ));
8643- }else {
8693+ }else if (type == -9999 ) {
8694+ galleryIntent = new Intent ();
8695+ if (android .os .Build .VERSION .SDK_INT >= android .os .Build .VERSION_CODES .KITKAT ) {
8696+ galleryIntent .setAction (Intent .ACTION_OPEN_DOCUMENT );
8697+ } else {
8698+ galleryIntent .setAction (Intent .ACTION_GET_CONTENT );
8699+ }
8700+ galleryIntent .addCategory (Intent .CATEGORY_OPENABLE );
8701+ galleryIntent .addFlags (Intent .FLAG_GRANT_READ_URI_PERMISSION );
8702+ if (android .os .Build .VERSION .SDK_INT >= android .os .Build .VERSION_CODES .KITKAT ) {
8703+ galleryIntent .addFlags (Intent .FLAG_GRANT_PERSISTABLE_URI_PERMISSION );
8704+ }
8705+
8706+ // set MIME type for image
8707+ galleryIntent .setType ("*/*" );
8708+ galleryIntent .putExtra (Intent .EXTRA_MIME_TYPES , Display .getInstance ().getProperty ("android.openGallery.accept" , "*/*" ).split ("," ));
8709+ }else {
86448710 galleryIntent .setType ("*/*" );
86458711 }
86468712 this .getActivity ().startActivityForResult (galleryIntent , multi ? OPEN_GALLERY_MULTI : OPEN_GALLERY );
86478713 }
86488714
8649- class NativeImage extends Image {
8650-
8651- public NativeImage (Bitmap nativeImage ) {
8652- super (nativeImage );
8653- }
8654- }
8655-
8656- /**
8657- * Create a File for saving an image or video
8658- */
8659- private File getOutputMediaFile (boolean isVideo ) {
8715+ class NativeImage extends Image {
8716+
8717+ public NativeImage (Bitmap nativeImage ) {
8718+ super (nativeImage );
8719+ }
8720+ }
8721+
8722+ /**
8723+ * Persist read permissions that were granted by an activity result so that media playback can
8724+ * continue after {@link Activity#onActivityResult(int, int, Intent)} returns.
8725+ *
8726+ * <p>Android 13 and newer revoke temporary grants immediately after the callback unless the
8727+ * app calls {@link ContentResolver#takePersistableUriPermission(Uri, int)}. Without this call
8728+ * {@link #createMedia(String, boolean, Runnable)} loses access to the {@code content://} URI
8729+ * provided by the system picker and playback fails on Android 15.</p>
8730+ */
8731+ private void takePersistablePermissionsFromIntent (Intent intent ) {
8732+ if (intent == null || Build .VERSION .SDK_INT < Build .VERSION_CODES .KITKAT ) {
8733+ return ;
8734+ }
8735+ int takeFlags = intent .getFlags () & (Intent .FLAG_GRANT_READ_URI_PERMISSION | Intent .FLAG_GRANT_WRITE_URI_PERMISSION );
8736+ if (takeFlags == 0 ) {
8737+ return ;
8738+ }
8739+ ContentResolver resolver = getContext ().getContentResolver ();
8740+ if (resolver == null ) {
8741+ return ;
8742+ }
8743+ ClipData clip = intent .getClipData ();
8744+ if (clip != null ) {
8745+ for (int i = 0 ; i < clip .getItemCount (); i ++) {
8746+ Uri uri = clip .getItemAt (i ).getUri ();
8747+ if (uri != null ) {
8748+ try {
8749+ resolver .takePersistableUriPermission (uri , takeFlags );
8750+ } catch (SecurityException ignored ) {
8751+ }
8752+ }
8753+ }
8754+ }
8755+ Uri dataUri = intent .getData ();
8756+ if (dataUri != null ) {
8757+ try {
8758+ resolver .takePersistableUriPermission (dataUri , takeFlags );
8759+ } catch (SecurityException ignored ) {
8760+ }
8761+ }
8762+ }
8763+
8764+ /**
8765+ * Create a File for saving an image or video
8766+ */
8767+ private File getOutputMediaFile (boolean isVideo ) {
86608768 // To be safe, you should check that the SDCard is mounted
86618769 // using Environment.getExternalStorageState() before doing this.
86628770 if (getActivity () != null ) {
0 commit comments