react-native-webview.git

RNCWebViewModule.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. package com.reactnativecommunity.webview;
  2. import android.Manifest;
  3. import android.app.Activity;
  4. import android.app.DownloadManager;
  5. import android.content.Context;
  6. import android.content.Intent;
  7. import android.content.pm.PackageManager;
  8. import android.net.Uri;
  9. import android.os.Build;
  10. import android.os.Environment;
  11. import android.os.Parcelable;
  12. import android.provider.MediaStore;
  13. import androidx.annotation.RequiresApi;
  14. import androidx.core.content.ContextCompat;
  15. import androidx.core.content.FileProvider;
  16. import android.util.Log;
  17. import android.webkit.MimeTypeMap;
  18. import android.webkit.ValueCallback;
  19. import android.webkit.WebChromeClient;
  20. import android.widget.Toast;
  21. import com.facebook.react.bridge.ActivityEventListener;
  22. import com.facebook.react.bridge.Promise;
  23. import com.facebook.react.bridge.ReactApplicationContext;
  24. import com.facebook.react.bridge.ReactContextBaseJavaModule;
  25. import com.facebook.react.bridge.ReactMethod;
  26. import com.facebook.react.module.annotations.ReactModule;
  27. import com.facebook.react.modules.core.PermissionAwareActivity;
  28. import com.facebook.react.modules.core.PermissionListener;
  29. import java.io.File;
  30. import java.io.IOException;
  31. import java.util.ArrayList;
  32. import java.util.Arrays;
  33. import static android.app.Activity.RESULT_OK;
  34. @ReactModule(name = RNCWebViewModule.MODULE_NAME)
  35. public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
  36. public static final String MODULE_NAME = "RNCWebView";
  37. private static final int PICKER = 1;
  38. private static final int PICKER_LEGACY = 3;
  39. private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
  40. final String DEFAULT_MIME_TYPES = "*/*";
  41. private ValueCallback<Uri> filePathCallbackLegacy;
  42. private ValueCallback<Uri[]> filePathCallback;
  43. private Uri outputFileUri;
  44. private DownloadManager.Request downloadRequest;
  45. private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
  46. @Override
  47. public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
  48. switch (requestCode) {
  49. case FILE_DOWNLOAD_PERMISSION_REQUEST: {
  50. // If request is cancelled, the result arrays are empty.
  51. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  52. if (downloadRequest != null) {
  53. downloadFile();
  54. }
  55. } else {
  56. Toast.makeText(getCurrentActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show();
  57. }
  58. return true;
  59. }
  60. }
  61. return false;
  62. }
  63. };
  64. public RNCWebViewModule(ReactApplicationContext reactContext) {
  65. super(reactContext);
  66. reactContext.addActivityEventListener(this);
  67. }
  68. @Override
  69. public String getName() {
  70. return MODULE_NAME;
  71. }
  72. @ReactMethod
  73. public void isFileUploadSupported(final Promise promise) {
  74. Boolean result = false;
  75. int current = Build.VERSION.SDK_INT;
  76. if (current >= Build.VERSION_CODES.LOLLIPOP) {
  77. result = true;
  78. }
  79. if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
  80. result = true;
  81. }
  82. promise.resolve(result);
  83. }
  84. public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
  85. if (filePathCallback == null && filePathCallbackLegacy == null) {
  86. return;
  87. }
  88. // based off of which button was pressed, we get an activity result and a file
  89. // the camera activity doesn't properly return the filename* (I think?) so we use
  90. // this filename instead
  91. switch (requestCode) {
  92. case PICKER:
  93. if (resultCode != RESULT_OK) {
  94. if (filePathCallback != null) {
  95. filePathCallback.onReceiveValue(null);
  96. }
  97. } else {
  98. Uri result[] = this.getSelectedFiles(data, resultCode);
  99. if (result != null) {
  100. filePathCallback.onReceiveValue(result);
  101. } else {
  102. filePathCallback.onReceiveValue(new Uri[]{outputFileUri});
  103. }
  104. }
  105. break;
  106. case PICKER_LEGACY:
  107. Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
  108. filePathCallbackLegacy.onReceiveValue(result);
  109. break;
  110. }
  111. filePathCallback = null;
  112. filePathCallbackLegacy = null;
  113. outputFileUri = null;
  114. }
  115. public void onNewIntent(Intent intent) {
  116. }
  117. private Uri[] getSelectedFiles(Intent data, int resultCode) {
  118. if (data == null) {
  119. return null;
  120. }
  121. // we have one file selected
  122. if (data.getData() != null) {
  123. if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  124. return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
  125. } else {
  126. return null;
  127. }
  128. }
  129. // we have multiple files selected
  130. if (data.getClipData() != null) {
  131. final int numSelectedFiles = data.getClipData().getItemCount();
  132. Uri[] result = new Uri[numSelectedFiles];
  133. for (int i = 0; i < numSelectedFiles; i++) {
  134. result[i] = data.getClipData().getItemAt(i).getUri();
  135. }
  136. return result;
  137. }
  138. return null;
  139. }
  140. public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
  141. filePathCallbackLegacy = filePathCallback;
  142. Intent fileChooserIntent = getFileChooserIntent(acceptType);
  143. Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
  144. ArrayList<Parcelable> extraIntents = new ArrayList<>();
  145. if (acceptsImages(acceptType)) {
  146. extraIntents.add(getPhotoIntent());
  147. }
  148. if (acceptsVideo(acceptType)) {
  149. extraIntents.add(getVideoIntent());
  150. }
  151. chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
  152. if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
  153. getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY);
  154. } else {
  155. Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
  156. }
  157. }
  158. @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  159. public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
  160. filePathCallback = callback;
  161. ArrayList<Parcelable> extraIntents = new ArrayList<>();
  162. if (! needsCameraPermission()) {
  163. if (acceptsImages(acceptTypes)) {
  164. extraIntents.add(getPhotoIntent());
  165. }
  166. if (acceptsVideo(acceptTypes)) {
  167. extraIntents.add(getVideoIntent());
  168. }
  169. }
  170. Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple);
  171. Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
  172. chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent);
  173. chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
  174. if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
  175. getCurrentActivity().startActivityForResult(chooserIntent, PICKER);
  176. } else {
  177. Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
  178. }
  179. return true;
  180. }
  181. public void setDownloadRequest(DownloadManager.Request request) {
  182. this.downloadRequest = request;
  183. }
  184. public void downloadFile() {
  185. DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
  186. String downloadMessage = "Downloading";
  187. dm.enqueue(this.downloadRequest);
  188. Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
  189. }
  190. public boolean grantFileDownloaderPermissions() {
  191. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
  192. return true;
  193. }
  194. boolean result = true;
  195. if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
  196. result = false;
  197. }
  198. if (!result) {
  199. PermissionAwareActivity activity = getPermissionAwareActivity();
  200. activity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
  201. }
  202. return result;
  203. }
  204. protected boolean needsCameraPermission() {
  205. boolean needed = false;
  206. PackageManager packageManager = getCurrentActivity().getPackageManager();
  207. try {
  208. String[] requestedPermissions = packageManager.getPackageInfo(getReactApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
  209. if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA)
  210. && ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
  211. needed = true;
  212. }
  213. } catch (PackageManager.NameNotFoundException e) {
  214. needed = true;
  215. }
  216. return needed;
  217. }
  218. private Intent getPhotoIntent() {
  219. Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
  220. outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);
  221. intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
  222. return intent;
  223. }
  224. private Intent getVideoIntent() {
  225. Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
  226. outputFileUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE);
  227. intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
  228. return intent;
  229. }
  230. private Intent getFileChooserIntent(String acceptTypes) {
  231. String _acceptTypes = acceptTypes;
  232. if (acceptTypes.isEmpty()) {
  233. _acceptTypes = DEFAULT_MIME_TYPES;
  234. }
  235. if (acceptTypes.matches("\\.\\w+")) {
  236. _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", ""));
  237. }
  238. Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
  239. intent.addCategory(Intent.CATEGORY_OPENABLE);
  240. intent.setType(_acceptTypes);
  241. return intent;
  242. }
  243. private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) {
  244. Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
  245. intent.addCategory(Intent.CATEGORY_OPENABLE);
  246. intent.setType("*/*");
  247. intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
  248. intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
  249. return intent;
  250. }
  251. private Boolean acceptsImages(String types) {
  252. String mimeType = types;
  253. if (types.matches("\\.\\w+")) {
  254. mimeType = getMimeTypeFromExtension(types.replace(".", ""));
  255. }
  256. return mimeType.isEmpty() || mimeType.toLowerCase().contains("image");
  257. }
  258. private Boolean acceptsImages(String[] types) {
  259. String[] mimeTypes = getAcceptedMimeType(types);
  260. return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "image");
  261. }
  262. private Boolean acceptsVideo(String types) {
  263. String mimeType = types;
  264. if (types.matches("\\.\\w+")) {
  265. mimeType = getMimeTypeFromExtension(types.replace(".", ""));
  266. }
  267. return mimeType.isEmpty() || mimeType.toLowerCase().contains("video");
  268. }
  269. private Boolean acceptsVideo(String[] types) {
  270. String[] mimeTypes = getAcceptedMimeType(types);
  271. return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "video");
  272. }
  273. private Boolean arrayContainsString(String[] array, String pattern) {
  274. for (String content : array) {
  275. if (content.contains(pattern)) {
  276. return true;
  277. }
  278. }
  279. return false;
  280. }
  281. private String[] getAcceptedMimeType(String[] types) {
  282. if (isArrayEmpty(types)) {
  283. return new String[]{DEFAULT_MIME_TYPES};
  284. }
  285. String[] mimeTypes = new String[types.length];
  286. for (int i = 0; i < types.length; i++) {
  287. String t = types[i];
  288. // convert file extensions to mime types
  289. if (t.matches("\\.\\w+")) {
  290. String mimeType = getMimeTypeFromExtension(t.replace(".", ""));
  291. mimeTypes[i] = mimeType;
  292. } else {
  293. mimeTypes[i] = t;
  294. }
  295. }
  296. return mimeTypes;
  297. }
  298. private String getMimeTypeFromExtension(String extension) {
  299. String type = null;
  300. if (extension != null) {
  301. type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
  302. }
  303. return type;
  304. }
  305. private Uri getOutputUri(String intentType) {
  306. File capturedFile = null;
  307. try {
  308. capturedFile = getCapturedFile(intentType);
  309. } catch (IOException e) {
  310. Log.e("CREATE FILE", "Error occurred while creating the File", e);
  311. e.printStackTrace();
  312. }
  313. // for versions below 6.0 (23) we use the old File creation & permissions model
  314. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
  315. return Uri.fromFile(capturedFile);
  316. }
  317. // for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions
  318. String packageName = getReactApplicationContext().getPackageName();
  319. return FileProvider.getUriForFile(getReactApplicationContext(), packageName + ".fileprovider", capturedFile);
  320. }
  321. private File getCapturedFile(String intentType) throws IOException {
  322. String prefix = "";
  323. String suffix = "";
  324. String dir = "";
  325. String filename = "";
  326. if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) {
  327. prefix = "image-";
  328. suffix = ".jpg";
  329. dir = Environment.DIRECTORY_PICTURES;
  330. } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) {
  331. prefix = "video-";
  332. suffix = ".mp4";
  333. dir = Environment.DIRECTORY_MOVIES;
  334. }
  335. filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
  336. // for versions below 6.0 (23) we use the old File creation & permissions model
  337. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
  338. // only this Directory works on all tested Android versions
  339. // ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
  340. File storageDir = Environment.getExternalStoragePublicDirectory(dir);
  341. return new File(storageDir, filename);
  342. }
  343. File storageDir = getReactApplicationContext().getExternalFilesDir(null);
  344. return File.createTempFile(filename, suffix, storageDir);
  345. }
  346. private Boolean isArrayEmpty(String[] arr) {
  347. // when our array returned from getAcceptTypes() has no values set from the webview
  348. // i.e. <input type="file" />, without any "accept" attr
  349. // will be an array with one empty string element, afaik
  350. return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0);
  351. }
  352. private PermissionAwareActivity getPermissionAwareActivity() {
  353. Activity activity = getCurrentActivity();
  354. if (activity == null) {
  355. throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
  356. } else if (!(activity instanceof PermissionAwareActivity)) {
  357. throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
  358. }
  359. return (PermissionAwareActivity) activity;
  360. }
  361. }