Skip to content

Commit ddd015e

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 df841d3 commit ddd015e

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
@@ -85,7 +85,6 @@ Licensed to the Apache Software Foundation (ASF) under one
8585
import java.util.StringTokenizer;
8686

8787
import android.Manifest;
88-
import android.os.Environment;
8988
import android.provider.MediaStore;
9089
import android.util.Log;
9190
import androidx.core.content.FileProvider;
@@ -161,7 +160,7 @@ public class InAppBrowser extends CordovaPlugin {
161160
private boolean fullscreen = true;
162161
private String[] allowedSchemes;
163162
private InAppBrowserClient currentClient;
164-
private String photoFilePath;
163+
private File photoFile;
165164
private Uri photoFileUri;
166165

167166
/**
@@ -951,29 +950,13 @@ public boolean onShowFileChooser (WebView webView, ValueCallback<Uri[]> filePath
951950
}
952951
mUploadCallback = filePathCallback;
953952

954-
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
955-
956-
if (takePictureIntent.resolveActivity(cordova.getActivity().getPackageManager()) != null) {
957-
958-
File photoFile = null;
959-
try {
960-
photoFile = createImageFile();
961-
takePictureIntent.putExtra("PhotoPath", photoFilePath);
962-
} catch (IOException ex) {
963-
Log.e(LOG_TAG, "Image file creation failed", ex);
964-
}
965-
if (photoFile != null) {
966-
photoFilePath = "file:/" + photoFile.getAbsolutePath();
967-
photoFileUri = FileProvider.getUriForFile(
968-
cordova.getActivity().getApplicationContext(),
969-
cordova.getActivity().getPackageName() + ".fileprovider",
970-
photoFile
971-
);
972-
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoFileUri);
973-
} else {
974-
takePictureIntent = null;
975-
}
953+
// Set up the intent to take a picture, if the file input has the capture attribute and any `image/` in the accept attribute.
954+
// Ex: <input type="file" accept="image/*" capture />
955+
Intent takePictureIntent = null;
956+
if (shouldOfferCameraForFileInput(fileChooserParams)) {
957+
takePictureIntent = buildTakePictureIntent();
976958
}
959+
977960
// Create File Chooser Intent
978961
Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
979962
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
@@ -1132,9 +1115,54 @@ public void postMessage(String data) {
11321115
private File createImageFile() throws IOException{
11331116
@SuppressLint("SimpleDateFormat") String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
11341117
String imageFileName = "img_"+timeStamp+"_";
1135-
File storageDir = this.cordova.getActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
1136-
File file = new File(storageDir, imageFileName + ".jpg");
1137-
return file;
1118+
File cacheDir = this.cordova.getActivity().getCacheDir();
1119+
// Create the cache directory if it doesn't exist
1120+
cacheDir.mkdirs();
1121+
return new File(cacheDir.getAbsolutePath(), imageFileName + ".jpg");
1122+
}
1123+
1124+
// 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/*).
1125+
// Note: another good indicator would be to check fileChooserParams.isCaptureEnabled(), but that is not done here.
1126+
private boolean shouldOfferCameraForFileInput(WebChromeClient.FileChooserParams fileChooserParams) {
1127+
boolean hasCamera = cordova.getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
1128+
boolean wantsImage = false;
1129+
for (String acceptType : fileChooserParams.getAcceptTypes()) {
1130+
if (acceptType.startsWith("image/")) {
1131+
wantsImage = true;
1132+
break;
1133+
}
1134+
}
1135+
1136+
return hasCamera && wantsImage;
1137+
}
1138+
1139+
private Intent buildTakePictureIntent() {
1140+
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
1141+
1142+
photoFile = null;
1143+
try {
1144+
photoFile = createImageFile();
1145+
} catch (IOException ex) {
1146+
Log.e(LOG_TAG, "Image file creation failed", ex);
1147+
}
1148+
if (photoFile != null) {
1149+
try {
1150+
// Compute the Content URI for our file. See: https://developer.android.com/training/camera/photobasics#TaskPath
1151+
photoFileUri = FileProvider.getUriForFile(
1152+
cordova.getActivity().getApplicationContext(),
1153+
cordova.getActivity().getPackageName() + ".fileprovider",
1154+
photoFile
1155+
);
1156+
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoFileUri);
1157+
} catch(IllegalArgumentException ex) {
1158+
Log.e(LOG_TAG, "File provider initialization failed - configure your FileProvider in the app manifest", ex);
1159+
takePictureIntent = null;
1160+
}
1161+
} else {
1162+
takePictureIntent = null;
1163+
}
1164+
1165+
return takePictureIntent;
11381166
}
11391167

11401168
/**
@@ -1188,6 +1216,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
11881216
}
11891217
mUploadCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, customIntent));
11901218
mUploadCallback = null;
1219+
if (photoFile != null) {
1220+
photoFile.deleteOnExit();
1221+
}
11911222
}
11921223

11931224
/**

0 commit comments

Comments
 (0)