This is an automated email from the ASF dual-hosted git repository. normanbreau pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cordova-plugin-media-capture.git
The following commit(s) were added to refs/heads/master by this push: new 7dc2f87 Fix(android): save media capture to File Provider (#302) 7dc2f87 is described below commit 7dc2f87c6dba6835812c514e338337fdabd03b8d Author: Mallat <29370498+xaviermal...@users.noreply.github.com> AuthorDate: Thu Dec 19 15:05:46 2024 +0100 Fix(android): save media capture to File Provider (#302) * Fix(android): save media capture to File Provider - Save image/audio/video to FileProvider instead of MediaStore external storage * Chore(android): save media to plugin's cache folder --------- Co-authored-by: xaviermallat <xavier.mal...@inetum.com> --- plugin.xml | 14 ++ src/android/Capture.java | 147 +++++++++++---------- src/android/FileProvider.java | 20 +++ .../res/xml/mediacapture_provider_paths.xml | 21 +++ 4 files changed, 130 insertions(+), 72 deletions(-) diff --git a/plugin.xml b/plugin.xml index b3c6715..2f01cba 100644 --- a/plugin.xml +++ b/plugin.xml @@ -76,6 +76,18 @@ xmlns:android="http://schemas.android.com/apk/res/android" </feature> </config-file> + <config-file target="AndroidManifest.xml" parent="application"> + <provider + android:name="org.apache.cordova.mediacapture.FileProvider" + android:authorities="${applicationId}.cordova.plugin.mediacapture.provider" + android:exported="false" + android:grantUriPermissions="true" > + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/mediacapture_provider_paths"/> + </provider> + </config-file> + <config-file target="AndroidManifest.xml" parent="/*"> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> @@ -85,6 +97,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" <source-file src="src/android/Capture.java" target-dir="src/org/apache/cordova/mediacapture" /> <source-file src="src/android/FileHelper.java" target-dir="src/org/apache/cordova/mediacapture" /> <source-file src="src/android/PendingRequests.java" target-dir="src/org/apache/cordova/mediacapture" /> + <source-file src="src/android/FileProvider.java" target-dir="src/org/apache/cordova/mediacapture" /> + <source-file src="src/android/res/xml/mediacapture_provider_paths.xml" target-dir="res/xml" /> <js-module src="www/android/init.js" name="init"> <runs /> diff --git a/src/android/Capture.java b/src/android/Capture.java index c915c39..f589615 100644 --- a/src/android/Capture.java +++ b/src/android/Capture.java @@ -23,9 +23,11 @@ import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Date; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; @@ -93,15 +95,11 @@ public class Capture extends CordovaPlugin { private final PendingRequests pendingRequests = new PendingRequests(); private int numPics; // Number of pictures before capture activity - private Uri imageUri; + private String audioAbsolutePath; + private String imageAbsolutePath; + private String videoAbsolutePath; -// public void setContext(Context mCtx) -// { -// if (CordovaInterface.class.isInstance(mCtx)) -// cordova = (CordovaInterface) mCtx; -// else -// LOG.d(LOG_TAG, "ERROR: You must use the CordovaInterface for this to work correctly. Please implement it in your activity"); -// } + private String applicationId; @Override protected void pluginInitialize() { @@ -132,6 +130,8 @@ public class Capture extends CordovaPlugin { @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + this.applicationId = cordova.getContext().getPackageName(); + if (action.equals("getFormatData")) { JSONObject obj = getFormatData(args.getString(0), args.getString(1)); callbackContext.success(obj); @@ -142,14 +142,11 @@ public class Capture extends CordovaPlugin { if (action.equals("captureAudio")) { this.captureAudio(pendingRequests.createRequest(CAPTURE_AUDIO, options, callbackContext)); - } - else if (action.equals("captureImage")) { + } else if (action.equals("captureImage")) { this.captureImage(pendingRequests.createRequest(CAPTURE_IMAGE, options, callbackContext)); - } - else if (action.equals("captureVideo")) { + } else if (action.equals("captureVideo")) { this.captureVideo(pendingRequests.createRequest(CAPTURE_VIDEO, options, callbackContext)); - } - else { + } else { return false; } @@ -175,18 +172,16 @@ public class Capture extends CordovaPlugin { // If the mimeType isn't set the rest will fail // so let's see if we can determine it. - if (mimeType == null || mimeType.equals("") || "null".equals(mimeType)) { + if (mimeType == null || mimeType.isEmpty() || "null".equals(mimeType)) { mimeType = FileHelper.getMimeType(fileUrl, cordova); } LOG.d(LOG_TAG, "Mime type = " + mimeType); if (mimeType.equals(IMAGE_JPEG) || filePath.endsWith(".jpg")) { obj = getImageData(fileUrl, obj); - } - else if (Arrays.asList(AUDIO_TYPES).contains(mimeType)) { + } else if (Arrays.asList(AUDIO_TYPES).contains(mimeType)) { obj = getAudioVideoData(filePath, obj, false); - } - else if (mimeType.equals(VIDEO_3GPP) || mimeType.equals(VIDEO_MP4)) { + } else if (mimeType.equals(VIDEO_3GPP) || mimeType.equals(VIDEO_MP4)) { obj = getAudioVideoData(filePath, obj, true); } return obj; @@ -242,7 +237,7 @@ public class Capture extends CordovaPlugin { } } - boolean isMissingPermissions = missingPermissions.size() > 0; + boolean isMissingPermissions = !missingPermissions.isEmpty(); if (isMissingPermissions) { String[] missing = missingPermissions.toArray(new String[missingPermissions.size()]); PermissionHelper.requestPermissions(this, req.requestCode, missing); @@ -262,6 +257,14 @@ public class Capture extends CordovaPlugin { return isMissingPermissions(req, cameraPermissions); } + private String getTempDirectoryPath() { + File cache = new File(cordova.getActivity().getCacheDir(), "org.apache.cordova.mediacapture"); + + // Create the cache directory if it doesn't exist + cache.mkdirs(); + return cache.getAbsolutePath(); + } + /** * Sets up an intent to capture audio. Result handled by onActivityResult() */ @@ -270,6 +273,16 @@ public class Capture extends CordovaPlugin { try { Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION); + String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); + String fileName = "cdv_media_capture_audio_" + timeStamp + ".m4a"; + File audio = new File(getTempDirectoryPath(), fileName); + Uri audioUri = FileProvider.getUriForFile(this.cordova.getActivity(), + this.applicationId + ".cordova.plugin.mediacapture.provider", + audio); + this.audioAbsolutePath = audio.getAbsolutePath(); + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, audioUri); + LOG.d(LOG_TAG, "Recording an audio and saving to: " + this.audioAbsolutePath); + this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode); } catch (ActivityNotFoundException ex) { pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NOT_SUPPORTED, "No Activity found to handle Audio Capture.")); @@ -287,11 +300,16 @@ public class Capture extends CordovaPlugin { Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); - ContentResolver contentResolver = this.cordova.getActivity().getContentResolver(); - ContentValues cv = new ContentValues(); - cv.put(MediaStore.Images.Media.MIME_TYPE, IMAGE_JPEG); - imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv); - LOG.d(LOG_TAG, "Taking a picture and saving to: " + imageUri.toString()); + String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); + String fileName = "cdv_media_capture_image_" + timeStamp + ".jpg"; + File image = new File(getTempDirectoryPath(), fileName); + + Uri imageUri = FileProvider.getUriForFile(this.cordova.getActivity(), + this.applicationId + ".cordova.plugin.mediacapture.provider", + image); + this.imageAbsolutePath = image.getAbsolutePath(); + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri); + LOG.d(LOG_TAG, "Taking a picture and saving to: " + this.imageAbsolutePath); intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri); @@ -305,6 +323,16 @@ public class Capture extends CordovaPlugin { if (isMissingCameraPermissions(req)) return; Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); + String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); + String fileName = "cdv_media_capture_video_" + timeStamp + ".mp4"; + File movie = new File(getTempDirectoryPath(), fileName); + + Uri videoUri = FileProvider.getUriForFile(this.cordova.getActivity(), + this.applicationId + ".cordova.plugin.mediacapture.provider", + movie); + this.videoAbsolutePath = movie.getAbsolutePath(); + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, videoUri); + LOG.d(LOG_TAG, "Recording a video and saving to: " + this.videoAbsolutePath); if(Build.VERSION.SDK_INT > 7){ intent.putExtra("android.intent.extra.durationLimit", req.duration); @@ -332,13 +360,13 @@ public class Capture extends CordovaPlugin { public void run() { switch(req.action) { case CAPTURE_AUDIO: - onAudioActivityResult(req, intent); + onAudioActivityResult(req); break; case CAPTURE_IMAGE: onImageActivityResult(req); break; case CAPTURE_VIDEO: - onVideoActivityResult(req, intent); + onVideoActivityResult(req); break; } } @@ -371,18 +399,11 @@ public class Capture extends CordovaPlugin { } - public void onAudioActivityResult(Request req, Intent intent) { - // Get the uri of the audio clip - Uri data = intent.getData(); - if (data == null) { - pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null")); - return; - } - - // Create a file object from the uri - JSONObject mediaFile = createMediaFile(data); + public void onAudioActivityResult(Request req) { + // create a file object from the audio absolute path + JSONObject mediaFile = createMediaFileWithAbsolutePath(this.audioAbsolutePath); if (mediaFile == null) { - pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: no mediaFile created from " + data)); + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: no mediaFile created from " + this.audioAbsolutePath)); return; } @@ -398,17 +419,10 @@ public class Capture extends CordovaPlugin { } public void onImageActivityResult(Request req) { - // Get the uri of the image - Uri data = imageUri; - if (data == null) { - pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null")); - return; - } - - // Create a file object from the uri - JSONObject mediaFile = createMediaFile(data); + // create a file object from the image absolute path + JSONObject mediaFile = createMediaFileWithAbsolutePath(this.imageAbsolutePath); if (mediaFile == null) { - pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: no mediaFile created from " + data)); + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: no mediaFile created from " + this.imageAbsolutePath)); return; } @@ -425,18 +439,11 @@ public class Capture extends CordovaPlugin { } } - public void onVideoActivityResult(Request req, Intent intent) { - // Get the uri of the video clip - Uri data = intent.getData(); - if (data == null) { - pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null")); - return; - } - - // Create a file object from the uri - JSONObject mediaFile = createMediaFile(data); + public void onVideoActivityResult(Request req) { + // create a file object from the video absolute path + JSONObject mediaFile = createMediaFileWithAbsolutePath(this.videoAbsolutePath); if (mediaFile == null) { - pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: no mediaFile created from " + data)); + pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_INTERNAL_ERR, "Error: no mediaFile created from " + this.videoAbsolutePath)); return; } @@ -452,18 +459,14 @@ public class Capture extends CordovaPlugin { } /** - * Creates a JSONObject that represents a File from the Uri + * Creates a JSONObject that represents a File from the absolute path * - * @param data the Uri of the audio/image/video + * @param path the absolute path saved in FileProvider of the audio/image/video * @return a JSONObject that represents a File * @throws IOException */ - private JSONObject createMediaFile(Uri data) { - File fp = webView.getResourceApi().mapUriToFile(data); - if (fp == null) { - return null; - } - + private JSONObject createMediaFileWithAbsolutePath(String path) { + File fp = new File(path); JSONObject obj = new JSONObject(); Class webViewClass = webView.getClass(); @@ -471,16 +474,15 @@ public class Capture extends CordovaPlugin { try { Method gpm = webViewClass.getMethod("getPluginManager"); pm = (PluginManager) gpm.invoke(webView); - } catch (NoSuchMethodException e) { - } catch (IllegalAccessException e) { - } catch (InvocationTargetException e) { + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + // Do Nothing } if (pm == null) { try { Field pmf = webViewClass.getField("pluginManager"); pm = (PluginManager)pmf.get(webView); - } catch (NoSuchFieldException e) { - } catch (IllegalAccessException e) { + } catch (NoSuchFieldException | IllegalAccessException e) { + // Do Nothing } } FileUtils filePlugin = (FileUtils) pm.getPlugin("File"); @@ -497,6 +499,7 @@ public class Capture extends CordovaPlugin { // are reported as video/3gpp. I'm doing this hacky check of the URI to see if it // is stored in the audio or video content store. if (fp.getAbsoluteFile().toString().endsWith(".3gp") || fp.getAbsoluteFile().toString().endsWith(".3gpp")) { + Uri data = Uri.fromFile(fp); if (data.toString().contains("/audio/")) { obj.put("type", AUDIO_3GPP); } else { diff --git a/src/android/FileProvider.java b/src/android/FileProvider.java new file mode 100644 index 0000000..0dc2d04 --- /dev/null +++ b/src/android/FileProvider.java @@ -0,0 +1,20 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.mediacapture; + +public class FileProvider extends androidx.core.content.FileProvider { +} diff --git a/src/android/res/xml/mediacapture_provider_paths.xml b/src/android/res/xml/mediacapture_provider_paths.xml new file mode 100644 index 0000000..4adfd0c --- /dev/null +++ b/src/android/res/xml/mediacapture_provider_paths.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> + +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <cache-path name="cache_files" path="org.apache.cordova.mediacapture" /> +</paths> --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cordova.apache.org For additional commands, e-mail: commits-h...@cordova.apache.org