-
Notifications
You must be signed in to change notification settings - Fork 495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Android] Store image/audio/video in FileProvider due to Android 11 updates #215
Changes from 8 commits
b75c66d
b8fb20d
f7c2230
54d2652
b25e2f3
bf28483
0473bb7
51392e8
937b084
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -25,7 +25,9 @@ Licensed to the Apache Software Foundation (ASF) under one | |||||
import java.lang.reflect.InvocationTargetException; | ||||||
import java.lang.reflect.Field; | ||||||
import java.lang.reflect.Method; | ||||||
import java.text.SimpleDateFormat; | ||||||
import java.util.Arrays; | ||||||
import java.util.Date; | ||||||
|
||||||
import android.content.ActivityNotFoundException; | ||||||
import android.os.Build; | ||||||
|
@@ -57,6 +59,8 @@ Licensed to the Apache Software Foundation (ASF) under one | |||||
import android.net.Uri; | ||||||
import android.os.Environment; | ||||||
import android.provider.MediaStore; | ||||||
import android.support.v4.content.FileProvider; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace this with AndroidX |
||||||
import org.apache.cordova.BuildHelper; | ||||||
|
||||||
public class Capture extends CordovaPlugin { | ||||||
|
||||||
|
@@ -83,7 +87,12 @@ 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; | ||||||
|
||||||
private String applicationId; | ||||||
|
||||||
|
||||||
// public void setContext(Context mCtx) | ||||||
// { | ||||||
|
@@ -122,6 +131,9 @@ protected void pluginInitialize() { | |||||
|
||||||
@Override | ||||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { | ||||||
this.applicationId = (String) BuildHelper.getBuildConfigValue(this.cordova.getActivity(), "APPLICATION_ID"); | ||||||
this.applicationId = preferences.getString("applicationId", this.applicationId); | ||||||
|
||||||
if (action.equals("getFormatData")) { | ||||||
JSONObject obj = getFormatData(args.getString(0), args.getString(1)); | ||||||
callbackContext.success(obj); | ||||||
|
@@ -234,6 +246,17 @@ private void captureAudio(Request req) { | |||||
try { | ||||||
Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION); | ||||||
|
||||||
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); | ||||||
String fileName = "AUDIO_" + timeStamp + ".wav"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
When I use the recording software, I get m4a files. I dont suggest changing format. |
||||||
File audio = new File(getTempDirectoryPath(), fileName); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (for all 3 capture types) I would drop the use of See the Taking photos doc : https://developer.android.com/training/camera/photobasics#TaskPath, really similar to what you have done
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Android training link I shared gives example of using
Do you agree? Or to which dirs will we agree to write by default here? Any way to capture and write all 3 types to public dirs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://developer.android.com/guide/topics/media/camera.html#saving-media but deprecated since API 29: what to use? are this SO or other correct? with pre and post Android Q code? through ContentResolver? through ACTION_CREATE_DOCUMENT? ... |
||||||
|
||||||
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); | ||||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this
|
||||||
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.")); | ||||||
|
@@ -276,13 +299,17 @@ private void captureImage(Request req) { | |||||
|
||||||
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 = "IMG_" + timeStamp + ".jpg"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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); | ||||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); | ||||||
LOG.d(LOG_TAG, "Taking a picture and saving to: " + this.imageAbsolutePath); | ||||||
|
||||||
this.cordova.startActivityForResult((CordovaPlugin) this, intent, req.requestCode); | ||||||
} | ||||||
|
@@ -301,6 +328,16 @@ private void captureVideo(Request req) { | |||||
PermissionHelper.requestPermission(this, req.requestCode, Manifest.permission.CAMERA); | ||||||
} else { | ||||||
Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); | ||||||
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); | ||||||
String fileName = "VID_" + timeStamp + ".avi"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it save to assume file extension to begin with? I imagine this could differ depending on the underlying camera app. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @breautek I think you're right :/ "but" one more link in Android training docs should be interesting to follow : https://developer.android.com/guide/topics/media/camera.html#saving-media a similar output extension of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, well if the docs shows an assumption of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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); | ||||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); | ||||||
|
||||||
if(Build.VERSION.SDK_INT > 7){ | ||||||
intent.putExtra("android.intent.extra.durationLimit", req.duration); | ||||||
|
@@ -369,10 +406,8 @@ else if (resultCode == Activity.RESULT_CANCELED) { | |||||
|
||||||
|
||||||
public void onAudioActivityResult(Request req, Intent intent) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can remove the |
||||||
// Get the uri of the audio clip | ||||||
Uri data = intent.getData(); | ||||||
// create a file object from the uri | ||||||
req.results.put(createMediaFile(data)); | ||||||
// create a file object from the audio absolute path | ||||||
req.results.put(createMediaFileWithAbsolutePath(this.audioAbsolutePath)); | ||||||
|
||||||
if (req.results.length() >= req.limit) { | ||||||
// Send Uri back to JavaScript for listening to audio | ||||||
|
@@ -385,9 +420,7 @@ public void onAudioActivityResult(Request req, Intent intent) { | |||||
|
||||||
public void onImageActivityResult(Request req) { | ||||||
// Add image to results | ||||||
req.results.put(createMediaFile(imageUri)); | ||||||
|
||||||
checkForDuplicateImage(); | ||||||
req.results.put(createMediaFileWithAbsolutePath(this.imageAbsolutePath)); | ||||||
|
||||||
if (req.results.length() >= req.limit) { | ||||||
// Send Uri back to JavaScript for viewing image | ||||||
|
@@ -399,32 +432,18 @@ public void onImageActivityResult(Request req) { | |||||
} | ||||||
|
||||||
public void onVideoActivityResult(Request req, Intent intent) { | ||||||
Uri data = null; | ||||||
|
||||||
if (intent != null){ | ||||||
// Get the uri of the video clip | ||||||
data = intent.getData(); | ||||||
} | ||||||
|
||||||
if( data == null){ | ||||||
File movie = new File(getTempDirectoryPath(), "Capture.avi"); | ||||||
data = Uri.fromFile(movie); | ||||||
} | ||||||
|
||||||
// create a file object from the uri | ||||||
if(data == null) { | ||||||
if(this.videoAbsolutePath != null) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest to remove the check on |
||||||
req.results.put(createMediaFileWithAbsolutePath(this.videoAbsolutePath)); | ||||||
} else { | ||||||
pendingRequests.resolveWithFailure(req, createErrorObject(CAPTURE_NO_MEDIA_FILES, "Error: data is null")); | ||||||
} | ||||||
else { | ||||||
req.results.put(createMediaFile(data)); | ||||||
|
||||||
if (req.results.length() >= req.limit) { | ||||||
// Send Uri back to JavaScript for viewing video | ||||||
pendingRequests.resolveWithSuccess(req); | ||||||
} else { | ||||||
// still need to capture more video clips | ||||||
captureVideo(req); | ||||||
} | ||||||
if (req.results.length() >= req.limit) { | ||||||
// Send Uri back to JavaScript for viewing video | ||||||
pendingRequests.resolveWithSuccess(req); | ||||||
} else { | ||||||
// still need to capture more videos | ||||||
captureVideo(req); | ||||||
} | ||||||
} | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chriskhongqarma Sorry if this is the incorrect place for this comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and other unused elements now like |
||||||
|
@@ -488,6 +507,62 @@ private JSONObject createMediaFile(Uri data) { | |||||
return obj; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Creates a JSONObject that represents a File from the Uri | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
* | ||||||
* @param path the absolute path saved in FileProvider of the audio/image/video | ||||||
* @return a JSONObject that represents a File | ||||||
* @throws IOException | ||||||
*/ | ||||||
private JSONObject createMediaFileWithAbsolutePath(String path) { | ||||||
File fp = new File(path); | ||||||
JSONObject obj = new JSONObject(); | ||||||
|
||||||
Class webViewClass = webView.getClass(); | ||||||
PluginManager pm = null; | ||||||
try { | ||||||
Method gpm = webViewClass.getMethod("getPluginManager"); | ||||||
pm = (PluginManager) gpm.invoke(webView); | ||||||
} catch (NoSuchMethodException e) { | ||||||
} catch (IllegalAccessException e) { | ||||||
} catch (InvocationTargetException e) { | ||||||
} | ||||||
if (pm == null) { | ||||||
try { | ||||||
Field pmf = webViewClass.getField("pluginManager"); | ||||||
pm = (PluginManager)pmf.get(webView); | ||||||
} catch (NoSuchFieldException e) { | ||||||
} catch (IllegalAccessException e) { | ||||||
} | ||||||
} | ||||||
FileUtils filePlugin = (FileUtils) pm.getPlugin("File"); | ||||||
LocalFilesystemURL url = filePlugin.filesystemURLforLocalPath(fp.getAbsolutePath()); | ||||||
|
||||||
try { | ||||||
// File properties | ||||||
obj.put("name", fp.getName()); | ||||||
obj.put("fullPath", Uri.fromFile(fp)); | ||||||
if (url != null) { | ||||||
obj.put("localURL", url.toString()); | ||||||
} | ||||||
// Because of an issue with MimeTypeMap.getMimeTypeFromExtension() all .3gpp files | ||||||
// 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")) { | ||||||
obj.put("type", VIDEO_3GPP); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why just this line? was previously
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the target file is always defined now with file extension/type? (video always as avi/mp4, and audio as wav) so this block about .3gpp files may be removed? |
||||||
} else { | ||||||
obj.put("type", FileHelper.getMimeType(Uri.fromFile(fp), cordova)); | ||||||
} | ||||||
|
||||||
obj.put("lastModifiedDate", fp.lastModified()); | ||||||
obj.put("size", fp.length()); | ||||||
} catch (JSONException e) { | ||||||
// this will never happen | ||||||
e.printStackTrace(); | ||||||
} | ||||||
return obj; | ||||||
} | ||||||
|
||||||
private JSONObject createErrorObject(int code, String message) { | ||||||
JSONObject obj = new JSONObject(); | ||||||
try { | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
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 android.support.v4.content.FileProvider {} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,21 @@ | ||||||||
<?xml version="1.0" encoding="utf-8"?> | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cordova plugins usually store res files into |
||||||||
<!-- | ||||||||
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="." /> | ||||||||
</paths> | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Add blank line |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this. We do not support the older Android Support Library anymore