Skip to content

Commit

Permalink
feat: add camera intent with file input capture (#1609)
Browse files Browse the repository at this point in the history
* Allow image file input to be from camera
* Reverting some irrellevant formatting changes
* Removing the openFileChooser functions as they are no longer necessary
* Update templates/project/res/xml/opener_paths.xml
* Code review feedback
* Adding license to provider paths xml file
* Adding a comment describing the proper use of the core cordova file provider
* Adding the ability to query the android.media.action.IMAGE_CAPTURE intent action
* Only including a cache path
* Applying code review feedback

---------

Co-authored-by: Ken Corbett <[email protected]>
Co-authored-by: エリス <[email protected]>
  • Loading branch information
3 people authored May 9, 2024
1 parent ebf0b10 commit b773ae4
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 24 deletions.
110 changes: 86 additions & 24 deletions framework/src/org/apache/cordova/engine/SystemWebChromeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@ Licensed to the Apache Software Foundation (ASF) under one
*/
package org.apache.cordova.engine;

import java.io.IOException;
import java.io.File;
import java.util.Arrays;
import android.annotation.TargetApi;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.content.ClipData;
import android.content.Context;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.webkit.ConsoleMessage;
import android.webkit.GeolocationPermissions.Callback;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
Expand All @@ -41,6 +45,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import androidx.core.content.FileProvider;

import org.apache.cordova.CordovaDialogsHelper;
import org.apache.cordova.CordovaPlugin;
Expand Down Expand Up @@ -212,53 +217,110 @@ public View getVideoLoadingProgressView() {
}

@Override
public boolean onShowFileChooser(WebView webView, final ValueCallback<Uri[]> filePathsCallback, final WebChromeClient.FileChooserParams fileChooserParams) {
public boolean onShowFileChooser(WebView webView, final ValueCallback<Uri[]> filePathsCallback,
final WebChromeClient.FileChooserParams fileChooserParams) {
Intent fileIntent = fileChooserParams.createIntent();

// Check if multiple-select is specified
Boolean selectMultiple = false;
if (fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE) {
selectMultiple = true;
}
Intent intent = fileChooserParams.createIntent();
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, selectMultiple);

fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, selectMultiple);

// Uses Intent.EXTRA_MIME_TYPES to pass multiple mime types.
String[] acceptTypes = fileChooserParams.getAcceptTypes();
if (acceptTypes.length > 1) {
intent.setType("*/*"); // Accept all, filter mime types by Intent.EXTRA_MIME_TYPES.
intent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes);
fileIntent.setType("*/*"); // Accept all, filter mime types by Intent.EXTRA_MIME_TYPES.
fileIntent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes);
}

// Image from camera intent
Uri tempUri = null;
Intent captureIntent = null;
if (fileChooserParams.isCaptureEnabled()) {
captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Context context = parentEngine.getView().getContext();
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
&& captureIntent.resolveActivity(context.getPackageManager()) != null) {
try {
File tempFile = createTempFile(context);
LOG.d(LOG_TAG, "Temporary photo capture file: " + tempFile);
tempUri = createUriForFile(context, tempFile);
LOG.d(LOG_TAG, "Temporary photo capture URI: " + tempUri);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, tempUri);
} catch (IOException e) {
LOG.e(LOG_TAG, "Unable to create temporary file for photo capture", e);
captureIntent = null;
}
} else {
LOG.w(LOG_TAG, "Device does not support photo capture");
captureIntent = null;
}
}
final Uri captureUri = tempUri;

// Chooser intent
Intent chooserIntent = Intent.createChooser(fileIntent, null);
if (captureIntent != null) {
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { captureIntent });
}

try {
LOG.i(LOG_TAG, "Starting intent for file chooser");
parentEngine.cordova.startActivityForResult(new CordovaPlugin() {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// Handle result
Uri[] result = null;
if (resultCode == Activity.RESULT_OK && intent != null) {
if (intent.getClipData() != null) {
// handle multiple-selected files
final int numSelectedFiles = intent.getClipData().getItemCount();
result = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
result[i] = intent.getClipData().getItemAt(i).getUri();
LOG.d(LOG_TAG, "Receive file chooser URL: " + result[i]);
if (resultCode == Activity.RESULT_OK) {
List<Uri> uris = new ArrayList<Uri>();

if (intent != null && intent.getData() != null) { // single file
LOG.v(LOG_TAG, "Adding file (single): " + intent.getData());
uris.add(intent.getData());
} else if (captureUri != null) { // camera
LOG.v(LOG_TAG, "Adding camera capture: " + captureUri);
uris.add(captureUri);
} else if (intent != null && intent.getClipData() != null) { // multiple files
ClipData clipData = intent.getClipData();
int count = clipData.getItemCount();
for (int i = 0; i < count; i++) {
Uri uri = clipData.getItemAt(i).getUri();
LOG.v(LOG_TAG, "Adding file (multiple): " + uri);
if (uri != null) {
uris.add(uri);
}
}
}
else if (intent.getData() != null) {
// handle single-selected file
result = WebChromeClient.FileChooserParams.parseResult(resultCode, intent);
LOG.d(LOG_TAG, "Receive file chooser URL: " + result);

if (!uris.isEmpty()) {
LOG.d(LOG_TAG, "Receive file chooser URL: " + uris.toString());
result = uris.toArray(new Uri[uris.size()]);
}
}
filePathsCallback.onReceiveValue(result);
}
}, intent, FILECHOOSER_RESULTCODE);
}, chooserIntent, FILECHOOSER_RESULTCODE);
} catch (ActivityNotFoundException e) {
LOG.w("No activity found to handle file chooser intent.", e);
LOG.w(LOG_TAG, "No activity found to handle file chooser intent.", e);
filePathsCallback.onReceiveValue(null);
}
return true;
}

@Override
private File createTempFile(Context context) throws IOException {
// Create an image file name
File tempFile = File.createTempFile("temp", ".jpg", context.getCacheDir());
return tempFile;
}

private Uri createUriForFile(Context context, File tempFile) throws IOException {
String appId = context.getPackageName();
Uri uri = FileProvider.getUriForFile(context, appId + ".cdv.core.file.provider", tempFile);
return uri;
}

public void onPermissionRequest(final PermissionRequest request) {
LOG.d(LOG_TAG, "onPermissionRequest: " + Arrays.toString(request.getResources()));
request.grant(request.getResources());
Expand Down
9 changes: 9 additions & 0 deletions templates/project/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.cdv.core.file.provider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/cdv_core_file_provider_paths" />
</provider>
</application>

<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>
</manifest>
30 changes: 30 additions & 0 deletions templates/project/res/xml/cdv_core_file_provider_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?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.
-->
<!--
Note: This File provider should only be used by the Cordova core
itself and should not be used for responding to Intents because
it will expose the app's private data folders.
For more information about FileProviders see:
https://developer.android.com/reference/androidx/core/content/FileProvider
-->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="." />
</paths>

0 comments on commit b773ae4

Please sign in to comment.