Background: I'm trying to build a very basic WebView app for accessing a responsive web app/website I recently built. One feature in the website involves a "choose photo" button that allows the user to upload an image to an HTML5 canvas for temporary image review and data extraction. This button is based on a simple HTML file input:
<input type="file" accept="image/*;capture=camera">
This feature works perfectly in "regular" mobile browsers (Chrome, Safari, Firefox, etc. on various Android and iOS devices). When users click on the "choose photo" button in any of these browsers, they are able to either (1) use their device's camera to take/save/upload a brand new image to the HTML5 canvas, or (2) upload an existing image stored on their phone.
Here's the problem: In my WebView app, I can upload an existing photo from local storage to the HTML5 canvas, but I can't upload photos taken with the camera when the camera is opened from the WebView app.
When I click the website's "choose photo" button in the WebView app, I am prompted to choose between camera and local storage (this is good). Browsing/uploading from local storage to the HTML5 canvas works fine. However, when I click on the camera option, I can take a photo as usual with the camera (front or back camera), but the photo isn't saved or uploaded to the HTML5 canvas (this is bad!). I just end up with an empty HTML5 canvas after taking/accepting the photo. The photo doesn't appear in local storage either.
I can't find any related errors in the logs (tested on several emulators/various Android releases and my Pixel 3a running Android 10). The app prompts for camera and storage/write permissions and the app seems to retain those permissions when granted. My best guess is that the app can't save/get new camera photos because I'm specifying the wrong directory or filename for the new photo file. Not sure how to address this.
What I've tried: I've been trying to get this app to upload photos from the camera for the past few days. The closest I've gotten is some code from this post (as reflected in my code below).
I've tried augmenting the code from that post with some info from Android developer docs (especially camera > saving media files) and Google's chromium input-file-example on GitHub. I've rewritten the WebView app in part and in full several times. Also tried variations on this post, and changing the directory path, along with every suggestion here, and this native method "Image Capture from a Camera using File Provider", and more.
Here is my current MainActivity.java code. Please let me know if you need to see any other files. (note: url replaced with example.com)
package com.example.placeholderexamplename;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.KeyEvent;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.widget.Toast;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private static final int INPUT_FILE_REQUEST_CODE = 2;
private static final int FILECHOOSER_RESULTCODE = 1;
private static final String TAG = MainActivity.class.getSimpleName();
Context context;
WebView webView;
private WebSettings webSettings;
private ValueCallback<Uri> mUploadMessage;
private Uri mCapturedImageURI = null;
private ValueCallback<Uri[]> mFilePathCallback;
private String mCameraPhotoPath;
private static final int CAMERA_PERMISSION_CODE = 100;
private static final int STORAGE_PERMISSION_CODE = 101;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode != INPUT_FILE_REQUEST_CODE || mFilePathCallback == null) {
super.onActivityResult(requestCode, resultCode, data);
return;
}
Uri[] results = null;
// Check response
if (resultCode == Activity.RESULT_OK) {
if (data == null) {
// If no data, we may have taken a photo
if (mCameraPhotoPath != null) {
results = new Uri[]{Uri.parse(mCameraPhotoPath)};
}
} else {
String dataString = data.getDataString();
if (dataString != null) {
results = new Uri[]{Uri.parse(dataString)};
}
}
}
mFilePathCallback.onReceiveValue(results);
mFilePathCallback = null;
return;
}
//start check permissions
private final static int REQUEST_CODE_ASK_PERMISSIONS = 1;
private static final String[] REQUIRED_SDK_PERMISSIONS = new String[] {
Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE };
protected void checkPermissions() {
final List<String> missingPermissions = new ArrayList<String>();
// check all required dynamic permissions
for (final String permission : REQUIRED_SDK_PERMISSIONS) {
final int result = ContextCompat.checkSelfPermission(this, permission);
if (result != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(permission);
}
}
if (!missingPermissions.isEmpty()) {
// request all missing permissions
final String[] permissions = missingPermissions
.toArray(new String[missingPermissions.size()]);
ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE_ASK_PERMISSIONS);
} else {
final int[] grantResults = new int[REQUIRED_SDK_PERMISSIONS.length];
Arrays.fill(grantResults, PackageManager.PERMISSION_GRANTED);
onRequestPermissionsResult(REQUEST_CODE_ASK_PERMISSIONS, REQUIRED_SDK_PERMISSIONS,
grantResults);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
@NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
for (int index = permissions.length - 1; index >= 0; --index) {
if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "To search by photo, please grant camera and storage access via device settings.", Toast.LENGTH_LONG).show();
}
}
}
}
//end check permissions
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
context = this;
webView = (WebView) findViewById(R.id.activity_main_webview);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
webView.loadUrl("https://www.example.com");
webView.setWebChromeClient(new ChromeClient());
checkPermissions();
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
boolean isLocalUrl = false;
try {
URL givenUrl = new URL(url);
String host = givenUrl.getHost();
if(host.contains("example.com"))
isLocalUrl = true;
} catch (MalformedURLException e) {
}
if (isLocalUrl)
return super.shouldOverrideUrlLoading(view, url);
else
{
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
return true;
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.w("----------", "page finish : " + url);
}
public void onReceivedError(WebView webView, int errorCode, String description, String failingUrl) {
try {
webView.stopLoading();
} catch (Exception e) {
}
if (webView.canGoBack()) {
webView.goBack();
}
webView.loadUrl("about:blank");
AlertDialog alertDialog = new AlertDialog.Builder(context).create();
alertDialog.setTitle("Error");
alertDialog.setCancelable(false);
alertDialog.setMessage("Check your internet connection and try again.");
alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, "Try Again", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
startActivity(getIntent());
finish();
}
});
alertDialog.show();
super.onReceivedError(webView, errorCode, description, failingUrl);
}
});
}
private File createImageFile() throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
//File storageDir = Environment.getExternalStoragePublicDirectory(Environment.getExternalStorageState());
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File imageFile = File.createTempFile(
imageFileName, // prefix
".jpg", // suffix
storageDir // directory
);
return imageFile;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Check if the key event was the Back button and if there's history
if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
webView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
public class ChromeClient extends WebChromeClient {
// For Android 5.0
public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> filePath, WebChromeClient.FileChooserParams fileChooserParams) {
// Double check that we don't have any existing callbacks
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(null);
}
mFilePathCallback = filePath;
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
// startActivity(intent);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
// Create the File where the photo should go
File photoFile = null;
try {
photoFile = createImageFile();
takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath);
} catch (IOException ex) {
// Error occurred while creating the File
Log.e(TAG, "Unable to create Image File", ex);
}
// Continue only if the File was successfully created
if (photoFile != null) {
mCameraPhotoPath = "file:" + photoFile.getAbsolutePath();
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(photoFile));
} else {
takePictureIntent = null;
}
}
Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
contentSelectionIntent.setType("image/*");
//contentSelectionIntent.setType("*/*");
Intent[] intentArray;
if (takePictureIntent != null) {
intentArray = new Intent[]{takePictureIntent};
} else {
intentArray = new Intent[0];
}
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, "Choose from...");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);
startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE);
return true;
}
}
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
}
}
}
(1st edit) Here's my AndroidManifest.xml code (placeholder values are used for url, etc.). This includes some unused code left over from previous attempts (like the section); removing the unused code hasn't affected the problem. Currently I am only prompting for and recording "CAMERA" and "WRITE_EXTERNAL_STORAGE" permissions (are these the correct permissions?):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.placeholderexamplename">
<application
android:allowBackup="true"
android:icon="@mipmap/example_icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/example_icon_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="com.example.placeholderexamplename.MainActivity"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="www.example.com" />
<data android:scheme="https" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
</provider>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA2" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera2" />
</manifest>
And my activity_main.xml code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.placeholderexamplename.MainActivity">
<WebView
android:id="@+id/activity_main_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
I would like to support KitKat and later, but a solution limited to Android 10 would be ok for now. The WebView app needs to be able to get an image to an HTML5 canvas from either (1) the device's camera, or (2) local storage. New photos taken from within the app can be saved either to a public directory or a directory tied to the app -- no preference, I just need to be able to take a photo from within the app and show it in this HTML5 canvas to perform a quick action. I'll work up from there once unblocked.
This is my first attempt at Android development/Android Studio. I'm a noob and I'm stuck. I would greatly appreciate any help! Everything else in the WebView app is working, this is my last blocker.
Thank you in advance for your input.