Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 194 additions & 86 deletions Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@

import android.webkit.CookieSyncManager;
import android.content.*;
import android.content.pm.*;
import android.content.res.Configuration;
import android.content.pm.*;
import android.content.res.AssetFileDescriptor;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
Expand Down Expand Up @@ -3603,41 +3604,70 @@ public Media createMedia(final String uri, boolean isVideo, final Runnable onCom
if (getActivity() == null) {
return null;
}
if(!uri.startsWith(FileSystemStorage.getInstance().getAppHomePath())) {
if(!PermissionsHelper.checkForPermission(isVideo ? DevicePermission.PERMISSION_READ_VIDEO : DevicePermission.PERMISSION_READ_AUDIO, "This is required to play media")){
return null;
}
}
if (uri.startsWith("file://")) {
return createMedia(removeFilePrefix(uri), isVideo, onCompletion);
}
File file = null;
if (uri.indexOf(':') < 0) {
// use a file object to play to try and workaround this issue:
// http://code.google.com/p/android/issues/detail?id=4124
file = new File(uri);
}

Media retVal;

if (isVideo) {
final AndroidImplementation.Video[] video = new AndroidImplementation.Video[1];
final boolean[] flag = new boolean[1];
final File f = file;
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
VideoView v = new VideoView(getActivity());
v.setZOrderMediaOverlay(true);
if (f != null) {
v.setVideoURI(Uri.fromFile(f));
} else {
v.setVideoURI(Uri.parse(uri));
}
video[0] = new AndroidImplementation.Video(v, getActivity(), onCompletion);
flag[0] = true;
synchronized (flag) {
flag.notify();
if (uri.startsWith("file://")) {
return createMedia(removeFilePrefix(uri), isVideo, onCompletion);
}
File file = null;
if (uri.indexOf(':') < 0) {
// use a file object to play to try and workaround this issue:
// http://code.google.com/p/android/issues/detail?id=4124
file = new File(uri);
}

Uri parsedUri = null;
boolean isContentUri = false;
if (file == null) {
parsedUri = Uri.parse(uri);
isContentUri = parsedUri != null && "content".equalsIgnoreCase(parsedUri.getScheme());
}

// The document picker grants temporary permissions for content URIs. Requesting
// READ_EXTERNAL_STORAGE again would surface a redundant prompt on Android 13+, so we only
// ask for classic file paths that require the legacy permission. MediaStore URIs still
// require an explicit permission grant, so they remain subject to the legacy check even
// though they also use the content:// scheme.
boolean requiresLegacyPermission = !uri.startsWith(FileSystemStorage.getInstance().getAppHomePath());
if (isContentUri && parsedUri != null) {
String authority = parsedUri.getAuthority();
if (authority != null) {
authority = authority.toLowerCase();
if (!"media".equals(authority) && !authority.startsWith("media.")) {
if (!"com.android.providers.media.documents".equals(authority)) {
requiresLegacyPermission = false;
}
}
} else {
requiresLegacyPermission = false;
}
}

if(requiresLegacyPermission) {
if(!PermissionsHelper.checkForPermission(isVideo ? DevicePermission.PERMISSION_READ_VIDEO : DevicePermission.PERMISSION_READ_AUDIO, "This is required to play media")){
return null;
}
}

Media retVal;

if (isVideo) {
final AndroidImplementation.Video[] video = new AndroidImplementation.Video[1];
final boolean[] flag = new boolean[1];
final File f = file;
final Uri videoUri = parsedUri;
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
VideoView v = new VideoView(getActivity());
v.setZOrderMediaOverlay(true);
if (f != null) {
v.setVideoURI(Uri.fromFile(f));
} else {
v.setVideoURI(videoUri != null ? videoUri : Uri.parse(uri));
}
video[0] = new AndroidImplementation.Video(v, getActivity(), onCompletion);
flag[0] = true;
synchronized (flag) {
flag.notify();
}
}
});
Expand All @@ -3652,18 +3682,47 @@ public void run() {
return video[0];
} else {
MediaPlayer player;
if (file != null) {
FileInputStream is = new FileInputStream(file);
player = new MediaPlayer();
player.setDataSource(is.getFD());
player.prepare();
} else {
player = MediaPlayer.create(getActivity(), Uri.parse(uri));
}
retVal = new Audio(getActivity(), player, null, onCompletion);
}
return retVal;
}
if (file != null) {
FileInputStream is = new FileInputStream(file);
player = new MediaPlayer();
player.setDataSource(is.getFD());
player.prepare();
} else {
player = MediaPlayer.create(getActivity(), parsedUri != null ? parsedUri : Uri.parse(uri));
if (player == null && isContentUri) {
// Android 13+ introduces stricter access rules for content:// URIs returned
// from the system document picker. The picker grants our activity a
// persistable read permission, but some OEM builds still reject the URI when it
// is passed directly to MediaPlayer. Opening the descriptor ourselves keeps the
// same permission grant while avoiding the OEM bug.
ContentResolver resolver = getContext().getContentResolver();
if (resolver != null && parsedUri != null) {
AssetFileDescriptor afd = null;
try {
afd = resolver.openAssetFileDescriptor(parsedUri, "r");
if (afd != null) {
player = new MediaPlayer();
player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
player.prepare();
}
} finally {
if (afd != null) {
try {
afd.close();
} catch (IOException ignore) {
}
}
}
}
}
}
if (player == null) {
throw new IOException("Unable to create media player for uri " + uri);
}
retVal = new Audio(getActivity(), player, null, onCompletion);
}
return retVal;
}

@Override
public void addCompletionHandler(Media media, Runnable onCompletion) {
Expand Down Expand Up @@ -8072,15 +8131,17 @@ private String getImageFilePath(Uri uri) {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {

if (requestCode == ZOOZ_PAYMENT) {
((IntentResultListener) pur).onActivityResult(requestCode, resultCode, intent);
return;
}

if (requestCode == REQUEST_SELECT_FILE || requestCode == FILECHOOSER_RESULTCODE) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (requestCode == REQUEST_SELECT_FILE) {
if (uploadMessage == null) return;
if (requestCode == ZOOZ_PAYMENT) {
((IntentResultListener) pur).onActivityResult(requestCode, resultCode, intent);
return;
}

takePersistablePermissionsFromIntent(intent);

if (requestCode == REQUEST_SELECT_FILE || requestCode == FILECHOOSER_RESULTCODE) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (requestCode == REQUEST_SELECT_FILE) {
if (uploadMessage == null) return;
Uri[] results = null;

// Check that the response is a good one
Expand Down Expand Up @@ -8618,45 +8679,92 @@ public void openGallery(final ActionListener response, int type){

callback = new EventDispatcher();
callback.addListener(response);
Intent galleryIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI);
if (multi) {
galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
Intent galleryIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI);
galleryIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (multi) {
galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
if(type == Display.GALLERY_VIDEO){
galleryIntent.setType("video/*");
}else if(type == Display.GALLERY_IMAGE){
galleryIntent.setType("image/*");
}else if(type == Display.GALLERY_ALL){
galleryIntent.setType("image/* video/*");
}else if (type == -9999) {
galleryIntent = new Intent();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
galleryIntent.setAction(Intent.ACTION_OPEN_DOCUMENT);
} else {
galleryIntent.setAction(Intent.ACTION_GET_CONTENT);
}
galleryIntent.addCategory(Intent.CATEGORY_OPENABLE);

// set MIME type for image
galleryIntent.setType("*/*");
galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, Display.getInstance().getProperty("android.openGallery.accept", "*/*").split(","));
}else{
}else if (type == -9999) {
galleryIntent = new Intent();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
galleryIntent.setAction(Intent.ACTION_OPEN_DOCUMENT);
} else {
galleryIntent.setAction(Intent.ACTION_GET_CONTENT);
}
galleryIntent.addCategory(Intent.CATEGORY_OPENABLE);
galleryIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
galleryIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}

// set MIME type for image
galleryIntent.setType("*/*");
galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, Display.getInstance().getProperty("android.openGallery.accept", "*/*").split(","));
}else{
galleryIntent.setType("*/*");
}
this.getActivity().startActivityForResult(galleryIntent, multi ? OPEN_GALLERY_MULTI: OPEN_GALLERY);
}

class NativeImage extends Image {

public NativeImage(Bitmap nativeImage) {
super(nativeImage);
}
}

/**
* Create a File for saving an image or video
*/
private File getOutputMediaFile(boolean isVideo) {
class NativeImage extends Image {

public NativeImage(Bitmap nativeImage) {
super(nativeImage);
}
}

/**
* Persist read permissions that were granted by an activity result so that media playback can
* continue after {@link Activity#onActivityResult(int, int, Intent)} returns.
*
* <p>Android 13 and newer revoke temporary grants immediately after the callback unless the
* app calls {@link ContentResolver#takePersistableUriPermission(Uri, int)}. Without this call
* {@link #createMedia(String, boolean, Runnable)} loses access to the {@code content://} URI
* provided by the system picker and playback fails on Android 15.</p>
*/
private void takePersistablePermissionsFromIntent(Intent intent) {
if (intent == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return;
}
int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (takeFlags == 0) {
return;
}
ContentResolver resolver = getContext().getContentResolver();
if (resolver == null) {
return;
}
ClipData clip = intent.getClipData();
if (clip != null) {
for (int i = 0; i < clip.getItemCount(); i++) {
Uri uri = clip.getItemAt(i).getUri();
if (uri != null) {
try {
resolver.takePersistableUriPermission(uri, takeFlags);
} catch (SecurityException ignored) {
}
}
}
}
Uri dataUri = intent.getData();
if (dataUri != null) {
try {
resolver.takePersistableUriPermission(dataUri, takeFlags);
} catch (SecurityException ignored) {
}
}
}

/**
* Create a File for saving an image or video
*/
private File getOutputMediaFile(boolean isVideo) {
// To be safe, you should check that the SDCard is mounted
// using Environment.getExternalStorageState() before doing this.
if (getActivity() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ public File getGradleProjectDirectory() {
private boolean contactsPermission;
private boolean wakeLock;
private boolean recordAudio;
private boolean mediaPlaybackPermission;
private boolean phonePermission;
private boolean purchasePermissions;
private boolean accessNetworkStatePermission;
Expand Down Expand Up @@ -1162,6 +1163,7 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc
playFlag = "true";

gpsPermission = request.getArg("android.gpsPermission", "false").equals("true");
mediaPlaybackPermission = false;
try {
scanClassesForPermissions(dummyClassesDir, new Executor.ClassScanner() {

Expand Down Expand Up @@ -1288,6 +1290,12 @@ public void usesClassMethod(String cls, String method) {
if (cls.indexOf("com/codename1/ui/Display") == 0 && method.indexOf("createMediaRecorder") > -1) {
recordAudio = true;
}
if (cls.indexOf("com/codename1/media/MediaManager") == 0 && method.indexOf("createMedia") > -1 && method.indexOf("createMediaRecorder") < 0) {
mediaPlaybackPermission = true;
}
if (cls.indexOf("com/codename1/ui/Display") == 0 && method.indexOf("createMedia") > -1 && method.indexOf("createMediaRecorder") < 0) {
mediaPlaybackPermission = true;
}
if (cls.indexOf("com/codename1/ui/Display") == 0 && method.indexOf("createContact") > -1) {
contactsWritePermission = true;
}
Expand Down Expand Up @@ -2203,9 +2211,18 @@ public void usesClassMethod(String cls, String method) {
if (request.getArg("android.removeBasePermissions", "false").equals("true")) {
basePermissions = "";
}
String externalStoragePermission = " <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" android:required=\"false\" android:maxSdkVersion=\"32\" />\n";
if (request.getArg("android.blockExternalStoragePermission", "false").equals("true")) {
externalStoragePermission = "";
boolean blockExternalStoragePermission = request.getArg("android.blockExternalStoragePermission", "false").equals("true");
String externalStoragePermission = "";
if (!blockExternalStoragePermission) {
externalStoragePermission = " <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" android:required=\"false\" android:maxSdkVersion=\"32\" />\n";
}
boolean blockReadMediaPermissions = request.getArg("android.blockReadMediaPermissions", blockExternalStoragePermission ? "true" : "false").equals("true");
boolean requestReadMediaPermissions = request.getArg("android.requestReadMediaPermissions", "false").equals("true");
String readMediaPermissions = "";
if (!blockReadMediaPermissions && targetSDKVersionInt >= 33 && (mediaPlaybackPermission || requestReadMediaPermissions)) {
readMediaPermissions += permissionAdd(request, "\"android.permission.READ_MEDIA_IMAGES\"", " <uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" android:required=\"false\" />\n");
readMediaPermissions += permissionAdd(request, "\"android.permission.READ_MEDIA_VIDEO\"", " <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" android:required=\"false\" />\n");
readMediaPermissions += permissionAdd(request, "\"android.permission.READ_MEDIA_AUDIO\"", " <uses-permission android:name=\"android.permission.READ_MEDIA_AUDIO\" android:required=\"false\" />\n");
}
String xmlizedDisplayName = xmlize(request.getDisplayName());

Expand Down Expand Up @@ -2356,6 +2373,7 @@ public void usesClassMethod(String cls, String method) {
+ " <uses-feature android:name=\"android.hardware.touchscreen\" android:required=\"false\" />\n"
+ basePermissions
+ externalStoragePermission
+ readMediaPermissions
+ permissions
+ " " + xPermissions
+ " " + xQueries
Expand Down
3 changes: 2 additions & 1 deletion scripts/device-runner-app/tests/Cn1ssDeviceRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
public final class Cn1ssDeviceRunner extends DeviceRunner {
private static final String[] TEST_CLASSES = new String[] {
MainScreenScreenshotTest.class.getName(),
BrowserComponentScreenshotTest.class.getName()
BrowserComponentScreenshotTest.class.getName(),
MediaPlaybackScreenshotTest.class.getName()
};

public void runSuite() {
Expand Down
Loading
Loading