Skip to content

Commit 4093682

Browse files
authored
fix(Android): Don't show camera options for a file upload when they can not be used (react-native-webview#1210)
* Don't show camera options for a file upload that would result in nothing happening for the user. On Android, if the application declares the camera permission, then even intents that use the camera require permission to be granted. This is a problem for apps that combine an in-app camera with a WebView that has file uploading and the user has not given permission for the camera. Note, this will not request permission for camera. This will simply prevent showing the camera options that would be a no-op action for users. It does this by checking if the camera permission is declared, and if so, checks that the user has granted permission. More information: https://blog.egorand.me/taking-photos-not-so-simply-how-i-got-bitten-by-action_image_capture/ * Add example and documentation about camera option availability in file uploads for Android.
1 parent 5024295 commit 4093682

File tree

5 files changed

+118
-7
lines changed

5 files changed

+118
-7
lines changed

android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.io.File;
3333
import java.io.IOException;
3434
import java.util.ArrayList;
35+
import java.util.Arrays;
3536

3637
import static android.app.Activity.RESULT_OK;
3738

@@ -180,11 +181,13 @@ public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final
180181
filePathCallback = callback;
181182

182183
ArrayList<Parcelable> extraIntents = new ArrayList<>();
183-
if (acceptsImages(acceptTypes)) {
184-
extraIntents.add(getPhotoIntent());
185-
}
186-
if (acceptsVideo(acceptTypes)) {
187-
extraIntents.add(getVideoIntent());
184+
if (! needsCameraPermission()) {
185+
if (acceptsImages(acceptTypes)) {
186+
extraIntents.add(getPhotoIntent());
187+
}
188+
if (acceptsVideo(acceptTypes)) {
189+
extraIntents.add(getVideoIntent());
190+
}
188191
}
189192

190193
Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple);
@@ -233,6 +236,23 @@ public boolean grantFileDownloaderPermissions() {
233236
return result;
234237
}
235238

239+
protected boolean needsCameraPermission() {
240+
boolean needed = false;
241+
242+
PackageManager packageManager = getCurrentActivity().getPackageManager();
243+
try {
244+
String[] requestedPermissions = packageManager.getPackageInfo(getReactApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
245+
if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA)
246+
&& ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
247+
needed = true;
248+
}
249+
} catch (PackageManager.NameNotFoundException e) {
250+
needed = true;
251+
}
252+
253+
return needed;
254+
}
255+
236256
private Intent getPhotoIntent() {
237257
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
238258
outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);

docs/Guide.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ Add permission in AndroidManifest.xml:
191191
</manifest>
192192
```
193193

194+
###### Camera option availability in uploading for Android
195+
196+
If the file input indicates that images or video is desired with [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept), then the WebView will attempt to provide options to the user to use their camera to take a picture or video.
197+
198+
Normally, apps that do not have permission to use the camera can prompt the user to use an external app so that the requesting app has no need for permission. However, Android has made a special exception for this around the camera to reduce confusion for users. If an app *can* request the camera permission because it has been declared, and the user has not granted the permission, it may not fire an intent that would use the camera (`MediaStore.ACTION_IMAGE_CAPTURE` or `MediaStore.ACTION_VIDEO_CAPTURE`). In this scenario, it is up to the developer to request camera permission before a file upload directly using the camera is necessary.
199+
194200
##### Check for File Upload support, with `static isFileUploadSupported()`
195201

196202
File Upload using `<input type="file" />` is not supported for Android 4.4 KitKat (see [details](https://github.com/delight-im/Android-AdvancedWebView/issues/4#issuecomment-70372146)):
@@ -301,7 +307,7 @@ _Under the hood_
301307

302308
#### The `injectedJavaScriptBeforeContentLoaded` prop
303309

304-
This is a script that runs **before** the web page loads for the first time. It only runs once, even if the page is reloaded or navigated away. This is useful if you want to inject anything into the window, localstorage, or document prior to the web code executing.
310+
This is a script that runs **before** the web page loads for the first time. It only runs once, even if the page is reloaded or navigated away. This is useful if you want to inject anything into the window, localstorage, or document prior to the web code executing.
305311

306312
```jsx
307313
import React, { Component } from 'react';
@@ -329,7 +335,7 @@ export default class App extends Component {
329335
}
330336
```
331337

332-
This runs the JavaScript in the `runFirst` string before the page is loaded. In this case, the value of `window.isNativeApp` will be set to true before the web code executes.
338+
This runs the JavaScript in the `runFirst` string before the page is loaded. In this case, the value of `window.isNativeApp` will be set to true before the web code executes.
333339

334340
#### The `injectJavaScript` method
335341

example/App.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
View,
88
Keyboard,
99
Button,
10+
Platform,
1011
} from 'react-native';
1112

1213
import Alerts from './examples/Alerts';
1314
import Scrolling from './examples/Scrolling';
1415
import Background from './examples/Background';
16+
import Uploads from './examples/Uploads';
1517

1618
const TESTS = {
1719
Alerts: {
@@ -38,6 +40,14 @@ const TESTS = {
3840
return <Background />;
3941
},
4042
},
43+
Uploads: {
44+
title: 'Uploads',
45+
testId: 'uploads',
46+
description: 'Upload test',
47+
render() {
48+
return <Uploads />;
49+
},
50+
},
4151
};
4252

4353
type Props = {};
@@ -91,6 +101,11 @@ export default class App extends Component<Props, State> {
91101
title="Background"
92102
onPress={() => this._changeTest('Background')}
93103
/>
104+
{Platform.OS === 'android' && <Button
105+
testID="testType_uploads"
106+
title="Uploads"
107+
onPress={() => this._changeTest('Uploads')}
108+
/>}
94109
</View>
95110

96111
{restarting ? null : (

example/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package="com.example">
33

44
<uses-permission android:name="android.permission.INTERNET" />
5+
<uses-permission android:name="android.permission.CAMERA" />
56

67
<application
78
android:name=".MainApplication"

example/examples/Uploads.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, {Component} from 'react';
2+
import {Button, Linking, Text, View} from 'react-native';
3+
4+
import WebView from 'react-native-webview';
5+
6+
const HTML = `
7+
<!DOCTYPE html>\n
8+
<html>
9+
<head>
10+
<title>Uploads</title>
11+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
12+
<meta name="viewport" content="width=320, user-scalable=no">
13+
<style type="text/css">
14+
body {
15+
margin: 0;
16+
padding: 0;
17+
font: 62.5% arial, sans-serif;
18+
background: #ccc;
19+
}
20+
</style>
21+
</head>
22+
<body>
23+
<p>
24+
<label for="images-only">Images only file upload</label>
25+
<input name="images-only" type="file" accept="image/*">
26+
</p>
27+
<p>
28+
<label for="video-only">Video only file upload</label>
29+
<input name="video-only" type="file" accept="video/*">
30+
</p>
31+
<p>
32+
<label for="any-file">Any file upload</label>
33+
<input name="any-file" type="file">
34+
</p>
35+
</body>
36+
</html>
37+
`;
38+
39+
type Props = {};
40+
type State = {};
41+
42+
export default class Uploads extends Component<Props, State> {
43+
state = {};
44+
45+
render() {
46+
return (
47+
<View>
48+
<View style={{ height: 120 }}>
49+
<WebView
50+
source={{html: HTML}}
51+
automaticallyAdjustContentInsets={false}
52+
/>
53+
</View>
54+
<Text>
55+
Android limitation: If the file input should show camera options for the user,
56+
and the app has the ability to request the camera permission, then the user must
57+
grant permission first in order to see the options. Since this example app does
58+
have the permission declared, you must allow it in settings to be able to see
59+
camera options. If your app does not have the camera permission declared, then
60+
there is no restriction to showing the camera options.
61+
</Text>
62+
<Button
63+
title="Open settings"
64+
onPress={() => Linking.openSettings()}
65+
/>
66+
</View>
67+
);
68+
}
69+
}

0 commit comments

Comments
 (0)