diff --git a/android/app/build.gradle b/android/app/build.gradle index d89cdc7bbfd..a667ad8d084 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -82,8 +82,8 @@ tasks.withType(JavaCompile).configureEach { // Define library dependencies used by this app. dependencies { implementation(project(':titanium')) { -// Uncomment the below to exclude Google Play Services from app. -// exclude group: 'com.google.android.gms' + // Uncomment the below to exclude Google Play Services from app. + // exclude group: 'com.google.android.gms' } implementation "androidx.activity:activity:1.8.0" diff --git a/android/modules/media/src/java/ti/modules/titanium/media/MediaModule.java b/android/modules/media/src/java/ti/modules/titanium/media/MediaModule.java index 87ec0194fcf..0a65dc0bb8d 100644 --- a/android/modules/media/src/java/ti/modules/titanium/media/MediaModule.java +++ b/android/modules/media/src/java/ti/modules/titanium/media/MediaModule.java @@ -15,12 +15,14 @@ import java.util.Arrays; import java.util.Date; import java.util.HashMap; +import java.util.List; import org.appcelerator.kroll.KrollDict; import org.appcelerator.kroll.KrollFunction; import org.appcelerator.kroll.KrollModule; import org.appcelerator.kroll.KrollObject; import org.appcelerator.kroll.KrollPromise; +import org.appcelerator.kroll.KrollProxy; import org.appcelerator.kroll.annotations.Kroll; import org.appcelerator.kroll.common.Log; import org.appcelerator.titanium.ContextSpecific; @@ -43,11 +45,13 @@ import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.graphics.BitmapFactory; import android.graphics.ImageFormat; @@ -59,6 +63,7 @@ import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; @@ -67,7 +72,11 @@ import android.util.Size; import android.view.Window; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.PickVisualMediaRequest; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.camera.core.AspectRatio; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; @Kroll.module @ContextSpecific @@ -242,11 +251,19 @@ public class MediaModule extends KrollModule implements Handler.Callback private static ContentResolver contentResolver; private boolean useCameraX = false; private static boolean pathOnly = false; + ActivityResultLauncher pickMedia; + private final IntentFilter mIntentFilter; + LocalBroadcastReceiver localReceiver; + + KrollFunction fSuccessCallback; + KrollFunction fCancelCallback; + KrollFunction fErrorCallback; public MediaModule() { super(); - + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction("image"); if (contentResolver == null) { contentResolver = TiApplication.getInstance().getContentResolver(); } @@ -965,7 +982,7 @@ public void hideCamera() } /** - * @see org.appcelerator.kroll.KrollProxy#handleMessage(android.os.Message) + * @see KrollProxy#handleMessage(Message) */ @Override public boolean handleMessage(Message message) @@ -1105,27 +1122,15 @@ public void openPhotoGallery(KrollDict options) errorCallback = (KrollFunction) options.get(TiC.EVENT_ERROR); } - final KrollFunction fSuccessCallback = successCallback; - final KrollFunction fCancelCallback = cancelCallback; - final KrollFunction fErrorCallback = errorCallback; + fSuccessCallback = successCallback; + fCancelCallback = cancelCallback; + fErrorCallback = errorCallback; Log.d(TAG, "openPhotoGallery called", Log.DEBUG_MODE); Activity activity = TiApplication.getInstance().getCurrentActivity(); TiActivitySupport activitySupport = (TiActivitySupport) activity; - TiIntentWrapper galleryIntent = new TiIntentWrapper(new Intent()); - galleryIntent.getIntent().setAction(Intent.ACTION_GET_CONTENT); - - if (options.containsKeyAndNotNull(TiC.PROPERTY_MAX_IMAGES) - && options.containsKey(TiC.PROPERTY_ALLOW_MULTIPLE) - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // set max image count - galleryIntent = new TiIntentWrapper(new Intent(MediaStore.ACTION_PICK_IMAGES)); - galleryIntent.getIntent() - .putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, options.getInt(TiC.PROPERTY_MAX_IMAGES)); - } - boolean isSelectingPhoto = false; boolean isSelectingVideo = false; if (options.containsKey(TiC.PROPERTY_MEDIA_TYPES)) { @@ -1150,27 +1155,19 @@ public void openPhotoGallery(KrollDict options) isSelectingPhoto = true; } if (isSelectingPhoto && isSelectingVideo) { - galleryIntent.getIntent().setType("*/*"); - galleryIntent.getIntent().putExtra(Intent.EXTRA_MIME_TYPES, new String[] { "image/*", "video/*" }); MediaModule.mediaType = MEDIA_TYPE_PHOTO; } else if (isSelectingVideo) { - galleryIntent.getIntent().setType("video/*"); MediaModule.mediaType = MEDIA_TYPE_VIDEO; } else { - galleryIntent.getIntent().setType("image/*"); MediaModule.mediaType = MEDIA_TYPE_PHOTO; } - galleryIntent.getIntent().addCategory(Intent.CATEGORY_DEFAULT); - galleryIntent.setWindowId(TiIntentWrapper.createActivityName("GALLERY")); - final int PICK_IMAGE_SINGLE = activitySupport.getUniqueResultCode(); final int PICK_IMAGE_MULTIPLE = activitySupport.getUniqueResultCode(); boolean allowMultiple = false; if (options.containsKey(TiC.PROPERTY_ALLOW_MULTIPLE)) { allowMultiple = TiConvert.toBoolean(options.get(TiC.PROPERTY_ALLOW_MULTIPLE)); - galleryIntent.getIntent().putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); } pathOnly = false; @@ -1179,138 +1176,39 @@ public void openPhotoGallery(KrollDict options) } final int code = allowMultiple ? PICK_IMAGE_MULTIPLE : PICK_IMAGE_SINGLE; + int maxCount = 0; + if (options.containsKeyAndNotNull(TiC.PROPERTY_MAX_IMAGES)) { + maxCount = options.getInt(TiC.PROPERTY_MAX_IMAGES); + } + localReceiver = new LocalBroadcastReceiver(); + LocalBroadcastManager.getInstance(TiApplication.getAppRootOrCurrentActivity()) + .registerReceiver(localReceiver, mIntentFilter); - activitySupport.launchActivityForResult(galleryIntent.getIntent(), code, new TiActivityResultHandler() { - @Override - public void onResult(Activity activity, int requestCode, int resultCode, Intent data) - { - if (requestCode != code) { - return; - } - - // Do not continue if no selection was made. - Log.d(TAG, "OnResult called: " + resultCode, Log.DEBUG_MODE); - if ((resultCode == Activity.RESULT_CANCELED) || (data == null)) { - if (fCancelCallback != null) { - KrollDict response = new KrollDict(); - response.putCodeAndMessage(NO_ERROR, null); - fCancelCallback.callAsync(getKrollObject(), response); - } - return; - } - - // Fetch a URI to file selected. (Only applicable to single file selection.) - Uri uri = data.getData(); - String path = (uri != null) ? uri.toString() : null; - - // Handle multiple file selection, if enabled. - if (requestCode == PICK_IMAGE_MULTIPLE) { - // Wrap all selected file(s) in Titanium "CameraMediaItemType" dictionaries. - ArrayList selectedFiles = new ArrayList<>(); - ClipData clipData = data.getClipData(); - if (clipData != null) { - // Fetch file(s) from clip data. - int count = clipData.getItemCount(); - for (int index = 0; index < count; index++) { - ClipData.Item item = clipData.getItemAt(index); - if ((item == null) || (item.getUri() == null)) { - continue; - } - KrollDict dictionary = createDictForImage(item.getUri().toString()); - if (dictionary == null) { - continue; - } - selectedFiles.add(dictionary); - } - } else if (path != null) { - // Only a single file was found. - KrollDict dictionary = createDictForImage(path); - if (dictionary != null) { - selectedFiles.add(dictionary); - } - } - - // Copy each selected file to either an "images" or "videos" collection. - ArrayList selectedImages = new ArrayList<>(); - ArrayList selectedVideos = new ArrayList<>(); - for (KrollDict dictionary : selectedFiles) { - String mediaType = dictionary.getString("mediaType"); - if (mediaType != null) { - if (mediaType.equals(MEDIA_TYPE_PHOTO)) { - selectedImages.add(dictionary); - } else if (mediaType.equals(MEDIA_TYPE_VIDEO)) { - selectedVideos.add(dictionary); - } - } - } - - // Invoke a callback with the selection result. - if (selectedImages.isEmpty() && selectedVideos.isEmpty()) { - if (selectedFiles.isEmpty()) { - // Invoke the "cancel" callback if no files were selected. - if (fCancelCallback != null) { - KrollDict response = new KrollDict(); - response.putCodeAndMessage(NO_ERROR, null); - fCancelCallback.callAsync(getKrollObject(), response); - } - } else { - // Invoke the "error" callback if non-image/video files were selected. - String message = "Invalid file types were selected"; - Log.e(TAG, message); - if (fErrorCallback != null) { - fErrorCallback.callAsync(getKrollObject(), - createErrorResponse(UNKNOWN_ERROR, message)); - } - } - } else { - // Invoke the "success" callback with the selected file(s). - if (fSuccessCallback != null) { - KrollDict d = new KrollDict(); - d.putCodeAndMessage(NO_ERROR, null); - d.put("images", selectedImages.toArray(new KrollDict[0])); - d.put("videos", selectedVideos.toArray(new KrollDict[0])); - fSuccessCallback.callAsync(getKrollObject(), d); - } - } - return; - } - - // Handle single file selection. - try { - //Check for invalid path - if (path == null) { - String msg = "File path is invalid"; - Log.e(TAG, msg); - if (fErrorCallback != null) { - fErrorCallback.callAsync(getKrollObject(), createErrorResponse(UNKNOWN_ERROR, msg)); - } - return; - } - if (fSuccessCallback != null) { - fSuccessCallback.callAsync(getKrollObject(), createDictForImage(path)); - } - } catch (OutOfMemoryError e) { - String msg = "Not enough memory to get image: " + e.getMessage(); - Log.e(TAG, msg); - if (fErrorCallback != null) { - fErrorCallback.callAsync(getKrollObject(), createErrorResponse(UNKNOWN_ERROR, msg)); - } - } - } + PickVisualMediaRequest.Builder pickerBuilder = new PickVisualMediaRequest.Builder(); + if (isSelectingPhoto && isSelectingVideo) { + // photo and video + pickerBuilder.setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE); + } else if (isSelectingPhoto) { + // photo + pickerBuilder.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE); + } else { + // video + pickerBuilder.setMediaType(ActivityResultContracts.PickVisualMedia.VideoOnly.INSTANCE); + } - @Override - public void onError(Activity activity, int requestCode, Exception e) - { - if (requestCode != code) { - return; - } - String msg = "Gallery problem: " + e.getMessage(); - Log.e(TAG, msg, e); - if (fErrorCallback != null) { - fErrorCallback.callAsync(getKrollObject(), createErrorResponse(UNKNOWN_ERROR, msg)); - } + TiBaseActivity baseActivity = ((TiBaseActivity) activity); + if (allowMultiple) { + if (maxCount > 1) { + // use max item count + baseActivity.customPicker.updateMaxItems(maxCount); + baseActivity.pickMultipleMediaResultMax.launch(pickerBuilder.build()); + } else { + // single item picker + baseActivity.pickMultipleMediaResult.launch(pickerBuilder.build()); } - }); + } else { + baseActivity.pickMediaResult.launch(pickerBuilder.build()); + } } protected static KrollDict createDictForImage(String path) @@ -1768,4 +1666,87 @@ public String getApiName() { return "Ti.Media"; } + + public class LocalBroadcastReceiver extends BroadcastReceiver + { + @Override + public void onReceive(Context context, Intent intent) + { + KrollDict kd = new KrollDict(); + String action = intent.getAction(); + if (action.equals("image")) { + Bundle extras = intent.getExtras(); + if (extras.get("uri") != null) { + // single image + Uri uri = (Uri) extras.get("uri"); + String path = (uri != null) ? uri.toString() : null; + + if (fSuccessCallback != null && path != null) { + fSuccessCallback.callAsync(getKrollObject(), createDictForImage(path)); + } + } else if (extras.get("uris") != null) { + // multiple images + List uri = (List) extras.get("uris"); + + ArrayList selectedFiles = new ArrayList<>(); + // Fetch file(s) from clip data. + int count = uri.size(); + for (int index = 0; index < count; index++) { + KrollDict dictionary = createDictForImage(uri.get(index).toString()); + if (dictionary == null) { + continue; + } + selectedFiles.add(dictionary); + } + + // Copy each selected file to either an "images" or "videos" collection. + ArrayList selectedImages = new ArrayList<>(); + ArrayList selectedVideos = new ArrayList<>(); + for (KrollDict dictionary : selectedFiles) { + String mediaType = dictionary.getString("mediaType"); + if (mediaType != null) { + if (mediaType.equals(MEDIA_TYPE_PHOTO)) { + selectedImages.add(dictionary); + } else if (mediaType.equals(MEDIA_TYPE_VIDEO)) { + selectedVideos.add(dictionary); + } + } + } + + // Invoke a callback with the selection result. + if (selectedImages.isEmpty() && selectedVideos.isEmpty()) { + if (selectedFiles.isEmpty()) { + // Invoke the "cancel" callback if no files were selected. + if (fCancelCallback != null) { + KrollDict response = new KrollDict(); + response.putCodeAndMessage(NO_ERROR, null); + fCancelCallback.callAsync(getKrollObject(), response); + } + } else { + // Invoke the "error" callback if non-image/video files were selected. + String message = "Invalid file types were selected"; + Log.e(TAG, message); + if (fErrorCallback != null) { + fErrorCallback.callAsync(getKrollObject(), + createErrorResponse(UNKNOWN_ERROR, message)); + } + } + } else { + // Invoke the "success" callback with the selected file(s). + if (fSuccessCallback != null) { + KrollDict d = new KrollDict(); + d.putCodeAndMessage(NO_ERROR, null); + d.put("images", selectedImages.toArray(new KrollDict[0])); + d.put("videos", selectedVideos.toArray(new KrollDict[0])); + fSuccessCallback.callAsync(getKrollObject(), d); + } + } + } + + LocalBroadcastManager.getInstance(TiApplication.getAppRootOrCurrentActivity()) + .unregisterReceiver(localReceiver); + } + } + + } } diff --git a/android/templates/build/app.build.gradle b/android/templates/build/app.build.gradle index dcb91f38827..1df2eb902aa 100644 --- a/android/templates/build/app.build.gradle +++ b/android/templates/build/app.build.gradle @@ -98,6 +98,7 @@ android { tasks.lint.enabled = false dependencies { + implementation "androidx.activity:activity:1.6.0" implementation "androidx.appcompat:appcompat:${project.ext.tiAndroidXAppCompatLibVersion}" implementation fileTree(dir: "${rootDir}/libs", include: ['*.aar', '*.jar']) implementation fileTree(dir: "${projectDir}/src/main", include: ['*.aar', '*.jar']) diff --git a/android/titanium/build.gradle b/android/titanium/build.gradle index a5909b6a936..15289722f15 100644 --- a/android/titanium/build.gradle +++ b/android/titanium/build.gradle @@ -278,6 +278,7 @@ dependencies { compileOnly project(':kroll-apt') // AndroidX Library dependencies. + implementation "androidx.activity:activity:1.6.0" implementation "androidx.appcompat:appcompat:${project.ext.tiAndroidXAppCompatLibVersion}" implementation 'androidx.cardview:cardview:1.0.0' implementation "androidx.core:core:${project.ext.tiAndroidXCoreLibVersion}" diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java index 341e8018c0f..7f073040433 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java @@ -6,6 +6,7 @@ */ package org.appcelerator.titanium; +import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Iterator; @@ -38,6 +39,7 @@ import org.appcelerator.titanium.util.TiConvert; import org.appcelerator.titanium.util.TiLocaleManager; import org.appcelerator.titanium.util.TiMenuSupport; +import org.appcelerator.titanium.util.TiPickMultipleVisualMedia; import org.appcelerator.titanium.util.TiUIHelper; import org.appcelerator.titanium.util.TiWeakList; import org.appcelerator.titanium.view.TiActionBarStyleHandler; @@ -48,6 +50,9 @@ import android.app.Activity; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.PickVisualMediaRequest; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import android.app.Dialog; @@ -68,6 +73,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -138,6 +144,31 @@ void onRequestPermissionsResult( public static boolean canFinishRoot = true; private boolean overridenLayout; + public TiPickMultipleVisualMedia customPicker = new TiPickMultipleVisualMedia(10); + + public ActivityResultLauncher pickMediaResult + = registerForActivityResult(new ActivityResultContracts.PickVisualMedia(), uri -> { + Intent intent = new Intent(); + intent.setAction("image"); + intent.putExtra("uri", uri); + LocalBroadcastManager.getInstance(TiApplication.getAppCurrentActivity()).sendBroadcast(intent); + }); + + public ActivityResultLauncher pickMultipleMediaResultMax + = registerForActivityResult(customPicker, uris -> { + Intent intent = new Intent(); + intent.setAction("image"); + intent.putExtra("uris", (Serializable) uris); + LocalBroadcastManager.getInstance(TiApplication.getAppCurrentActivity()).sendBroadcast(intent); + }); + + public ActivityResultLauncher pickMultipleMediaResult + = registerForActivityResult(new ActivityResultContracts.PickMultipleVisualMedia(), uris -> { + Intent intent = new Intent(); + intent.setAction("image"); + intent.putExtra("uris", (Serializable) uris); + LocalBroadcastManager.getInstance(TiApplication.getAppCurrentActivity()).sendBroadcast(intent); + }); public static class DialogWrapper { diff --git a/android/titanium/src/java/org/appcelerator/titanium/util/TiPickMultipleVisualMedia.java b/android/titanium/src/java/org/appcelerator/titanium/util/TiPickMultipleVisualMedia.java new file mode 100644 index 00000000000..bb2efad3704 --- /dev/null +++ b/android/titanium/src/java/org/appcelerator/titanium/util/TiPickMultipleVisualMedia.java @@ -0,0 +1,45 @@ +/** + * Titanium SDK + * Copyright TiDev, Inc. 04/07/2022-Present + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +package org.appcelerator.titanium.util; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.ext.SdkExtensions; +import android.provider.MediaStore; + +import androidx.activity.result.PickVisualMediaRequest; +import androidx.activity.result.contract.ActivityResultContracts; + +public class TiPickMultipleVisualMedia extends ActivityResultContracts.PickMultipleVisualMedia +{ + private int maxItems; + + public TiPickMultipleVisualMedia(int maxItems) + { + super(maxItems); + this.maxItems = maxItems; + } + + public void updateMaxItems(int newMaxItems) + { + if (maxItems > 1) { + this.maxItems = newMaxItems; + } + } + + @Override + public Intent createIntent(Context context, PickVisualMediaRequest input) + { + Intent intent = super.createIntent(context, input); + if (maxItems > 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= 2) { + intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxItems); + } + return intent; + } +}