Skip to content

Commit 6c90efd

Browse files
committed
Improve camera capability of the file chooser
This builds on LuckyLuky's [commit](LuckyLuky@876a12a) in [PR apache#857](apache#857) to improve the camera path for file inputs (`<input type="file"/>`) on Android. The changes are: - Light refactoring to extract work such as configuring the intent into functions - Only offer the "Image Capture" intent if the device has a camera app and the file input has an accept type of an image (e.g. "image/png" or "image/*") - Use a cache directory to store files, rather than the external storage. This mimics how cordova-plugin-camera stores files. - Mark the photos for later deletion with `deleteOnExit`. I'm not sure how robust this is, so if we truly wanted to clean up files, we would need to do it explicitly, each time `onShowFileChooser` was fired. Both the change in the original commit and in this one rely on the FileProvider API to allow the passing of content URIs (rather than File URIs) between this plugin and the camera app. As such, the FileProvider must be configured. The Camera Plugin does this in plugin.xml, but no such change has been made here yet. Therefore, configuration similar to the following must be added to the main app. ```xml <!-- config.xml --> <widget> ... <platform name="android"> <!-- Configure our FileProvider--> <config-file target="AndroidManifest.xml" parent="application"> <provider android:name="androidx.core.content.FileProvider" android:authorities="YOUR_APP_ID.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/camera_provider_paths" /> </provider> </config-file> <resource-file src="camera_provider_paths.xml" target="app/src/main/res/xml/camera_provider_paths.xml" /> </platform> ... </widget> ``` ```xml <!-- camera_provider_paths.xml --> <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <cache-path name="cache_files" path="." /> </paths> ``` Fore more information on the above configuration, see the ["Take photos" Guide on Android Developers](https://developer.android.com/training/camera/photobasics) The following improvements could be made to this feature of the plugin: - Use the `captureEnabled()` boolean on the file chooser params to only offer the camera when capture is enabled on the HTML element - Image downscaling - it would be very nice to allow control over the quality and size of the captured image. But, at this point, could we just delegate to the existing Camera plugin? (cordova-plugin-camera)
1 parent 046fecc commit 6c90efd

File tree

1 file changed

+58
-27
lines changed

1 file changed

+58
-27
lines changed

src/android/InAppBrowser.java

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ Licensed to the Apache Software Foundation (ASF) under one
8484
import java.util.StringTokenizer;
8585

8686
import android.Manifest;
87-
import android.os.Environment;
8887
import android.provider.MediaStore;
8988
import android.util.Log;
9089
import androidx.core.content.FileProvider;
@@ -157,7 +156,7 @@ public class InAppBrowser extends CordovaPlugin {
157156
private boolean fullscreen = true;
158157
private String[] allowedSchemes;
159158
private InAppBrowserClient currentClient;
160-
private String photoFilePath;
159+
private File photoFile;
161160
private Uri photoFileUri;
162161

163162
/**
@@ -947,29 +946,13 @@ public boolean onShowFileChooser (WebView webView, ValueCallback<Uri[]> filePath
947946
}
948947
mUploadCallback = filePathCallback;
949948

950-
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
951-
952-
if (takePictureIntent.resolveActivity(cordova.getActivity().getPackageManager()) != null) {
953-
954-
File photoFile = null;
955-
try {
956-
photoFile = createImageFile();
957-
takePictureIntent.putExtra("PhotoPath", photoFilePath);
958-
} catch (IOException ex) {
959-
Log.e(LOG_TAG, "Image file creation failed", ex);
960-
}
961-
if (photoFile != null) {
962-
photoFilePath = "file:/" + photoFile.getAbsolutePath();
963-
photoFileUri = FileProvider.getUriForFile(
964-
cordova.getActivity().getApplicationContext(),
965-
cordova.getActivity().getPackageName() + ".fileprovider",
966-
photoFile
967-
);
968-
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoFileUri);
969-
} else {
970-
takePictureIntent = null;
971-
}
949+
// Set up the intent to take a picture, if the file input has the capture attribute and any `image/` in the accept attribute.
950+
// Ex: <input type="file" accept="image/*" capture />
951+
Intent takePictureIntent = null;
952+
if (shouldOfferCameraForFileInput(fileChooserParams)) {
953+
takePictureIntent = buildTakePictureIntent();
972954
}
955+
973956
// Create File Chooser Intent
974957
Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
975958
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
@@ -1104,9 +1087,54 @@ public void postMessage(String data) {
11041087
private File createImageFile() throws IOException{
11051088
@SuppressLint("SimpleDateFormat") String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
11061089
String imageFileName = "img_"+timeStamp+"_";
1107-
File storageDir = this.cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
1108-
File file = new File(storageDir, imageFileName + ".jpg");
1109-
return file;
1090+
File cacheDir = this.cordova.getActivity().getCacheDir();
1091+
// Create the cache directory if it doesn't exist
1092+
cacheDir.mkdirs();
1093+
return new File(cacheDir.getAbsolutePath(), imageFileName + ".jpg");
1094+
}
1095+
1096+
// Returns true if there is a system camera and the accept types on the file input want an image (e.g. image/png or image/*).
1097+
// Note: another good indicator would be to check fileChooserParams.isCaptureEnabled(), but that is not done here.
1098+
private boolean shouldOfferCameraForFileInput(WebChromeClient.FileChooserParams fileChooserParams) {
1099+
boolean hasCamera = cordova.getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
1100+
boolean wantsImage = false;
1101+
for (String acceptType : fileChooserParams.getAcceptTypes()) {
1102+
if (acceptType.startsWith("image/")) {
1103+
wantsImage = true;
1104+
break;
1105+
}
1106+
}
1107+
1108+
return hasCamera && wantsImage;
1109+
}
1110+
1111+
private Intent buildTakePictureIntent() {
1112+
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
1113+
1114+
photoFile = null;
1115+
try {
1116+
photoFile = createImageFile();
1117+
} catch (IOException ex) {
1118+
Log.e(LOG_TAG, "Image file creation failed", ex);
1119+
}
1120+
if (photoFile != null) {
1121+
try {
1122+
// Compute the Content URI for our file. See: https://developer.android.com/training/camera/photobasics#TaskPath
1123+
photoFileUri = FileProvider.getUriForFile(
1124+
cordova.getActivity().getApplicationContext(),
1125+
cordova.getActivity().getPackageName() + ".fileprovider",
1126+
photoFile
1127+
);
1128+
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoFileUri);
1129+
} catch(IllegalArgumentException ex) {
1130+
Log.e(LOG_TAG, "File provider initialization failed - configure your FileProvider in the app manifest", ex);
1131+
takePictureIntent = null;
1132+
}
1133+
} else {
1134+
takePictureIntent = null;
1135+
}
1136+
1137+
return takePictureIntent;
11101138
}
11111139

11121140
/**
@@ -1160,6 +1188,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
11601188
}
11611189
mUploadCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, customIntent));
11621190
mUploadCallback = null;
1191+
if (photoFile != null) {
1192+
photoFile.deleteOnExit();
1193+
}
11631194
}
11641195

11651196
/**

0 commit comments

Comments
 (0)