No Description

RNCWebViewModule.java 14KB

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