Przeglądaj źródła

fix(android): Fix several Android file upload issues (#1302 by @hojason117)

[skip ci]
Jason Chia-Hsien Ho 4 lat temu
rodzic
commit
89886c820d
No account linked to committer's email address

+ 1
- 2
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java Wyświetl plik

@@ -971,8 +971,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
971 971
     public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
972 972
       String[] acceptTypes = fileChooserParams.getAcceptTypes();
973 973
       boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
974
-      Intent intent = fileChooserParams.createIntent();
975
-      return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
974
+      return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptTypes, allowMultiple);
976 975
     }
977 976
 
978 977
     @Override

+ 143
- 63
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java Wyświetl plik

@@ -11,9 +11,11 @@ import android.os.Build;
11 11
 import android.os.Environment;
12 12
 import android.os.Parcelable;
13 13
 import android.provider.MediaStore;
14
+
14 15
 import androidx.annotation.RequiresApi;
15 16
 import androidx.core.content.ContextCompat;
16 17
 import androidx.core.content.FileProvider;
18
+
17 19
 import android.util.Log;
18 20
 import android.webkit.MimeTypeMap;
19 21
 import android.webkit.ValueCallback;
@@ -42,11 +44,24 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
42 44
   private static final int PICKER = 1;
43 45
   private static final int PICKER_LEGACY = 3;
44 46
   private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
45
-  final String DEFAULT_MIME_TYPES = "*/*";
46 47
   private ValueCallback<Uri> filePathCallbackLegacy;
47 48
   private ValueCallback<Uri[]> filePathCallback;
48
-  private Uri outputFileUri;
49
+  private File outputImage;
50
+  private File outputVideo;
49 51
   private DownloadManager.Request downloadRequest;
52
+
53
+  private enum MimeType {
54
+    DEFAULT("*/*"),
55
+    IMAGE("image"),
56
+    VIDEO("video");
57
+
58
+    private final String value;
59
+
60
+    MimeType(String value) {
61
+      this.value = value;
62
+    }
63
+  }
64
+
50 65
   private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
51 66
     @Override
52 67
     public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
@@ -96,6 +111,16 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
96 111
       return;
97 112
     }
98 113
 
114
+    boolean imageTaken = false;
115
+    boolean videoTaken = false;
116
+
117
+    if (outputImage != null && outputImage.length() > 0) {
118
+      imageTaken = true;
119
+    }
120
+    if (outputVideo != null && outputVideo.length() > 0) {
121
+      videoTaken = true;
122
+    }
123
+
99 124
     // based off of which button was pressed, we get an activity result and a file
100 125
     // the camera activity doesn't properly return the filename* (I think?) so we use
101 126
     // this filename instead
@@ -106,23 +131,42 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
106 131
             filePathCallback.onReceiveValue(null);
107 132
           }
108 133
         } else {
109
-          Uri result[] = this.getSelectedFiles(data, resultCode);
110
-          if (result != null) {
111
-            filePathCallback.onReceiveValue(result);
134
+          if (imageTaken) {
135
+            filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputImage)});
136
+          } else if (videoTaken) {
137
+            filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputVideo)});
112 138
           } else {
113
-            filePathCallback.onReceiveValue(new Uri[]{outputFileUri});
139
+            filePathCallback.onReceiveValue(this.getSelectedFiles(data, resultCode));
114 140
           }
115 141
         }
116 142
         break;
117 143
       case PICKER_LEGACY:
118
-        Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
119
-        filePathCallbackLegacy.onReceiveValue(result);
144
+        if (resultCode != RESULT_OK) {
145
+          filePathCallbackLegacy.onReceiveValue(null);
146
+        } else {
147
+          if (imageTaken) {
148
+            filePathCallbackLegacy.onReceiveValue(getOutputUri(outputImage));
149
+          } else if (videoTaken) {
150
+            filePathCallbackLegacy.onReceiveValue(getOutputUri(outputVideo));
151
+          } else {
152
+            filePathCallbackLegacy.onReceiveValue(data.getData());
153
+          }
154
+        }
120 155
         break;
121 156
 
122 157
     }
158
+
159
+    if (outputImage != null && !imageTaken) {
160
+      outputImage.delete();
161
+    }
162
+    if (outputVideo != null && !videoTaken) {
163
+      outputVideo.delete();
164
+    }
165
+
123 166
     filePathCallback = null;
124 167
     filePathCallbackLegacy = null;
125
-    outputFileUri = null;
168
+    outputImage = null;
169
+    outputVideo = null;
126 170
   }
127 171
 
128 172
   public void onNewIntent(Intent intent) {
@@ -133,15 +177,6 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
133 177
       return null;
134 178
     }
135 179
 
136
-    // we have one file selected
137
-    if (data.getData() != null) {
138
-      if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
139
-        return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
140
-      } else {
141
-        return null;
142
-      }
143
-    }
144
-
145 180
     // we have multiple files selected
146 181
     if (data.getClipData() != null) {
147 182
       final int numSelectedFiles = data.getClipData().getItemCount();
@@ -151,6 +186,12 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
151 186
       }
152 187
       return result;
153 188
     }
189
+
190
+    // we have one file selected
191
+    if (data.getData() != null && resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
192
+      return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
193
+    }
194
+
154 195
     return null;
155 196
   }
156 197
 
@@ -162,10 +203,16 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
162 203
 
163 204
     ArrayList<Parcelable> extraIntents = new ArrayList<>();
164 205
     if (acceptsImages(acceptType)) {
165
-      extraIntents.add(getPhotoIntent());
206
+      Intent photoIntent = getPhotoIntent();
207
+      if (photoIntent != null) {
208
+        extraIntents.add(photoIntent);
209
+      }
166 210
     }
167 211
     if (acceptsVideo(acceptType)) {
168
-      extraIntents.add(getVideoIntent());
212
+      Intent videoIntent = getVideoIntent();
213
+      if (videoIntent != null) {
214
+        extraIntents.add(videoIntent);
215
+      }
169 216
     }
170 217
     chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
171 218
 
@@ -177,16 +224,22 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
177 224
   }
178 225
 
179 226
   @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
180
-  public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
227
+  public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final String[] acceptTypes, final boolean allowMultiple) {
181 228
     filePathCallback = callback;
182 229
 
183 230
     ArrayList<Parcelable> extraIntents = new ArrayList<>();
184
-    if (! needsCameraPermission()) {
231
+    if (!needsCameraPermission()) {
185 232
       if (acceptsImages(acceptTypes)) {
186
-        extraIntents.add(getPhotoIntent());
233
+        Intent photoIntent = getPhotoIntent();
234
+        if (photoIntent != null) {
235
+          extraIntents.add(photoIntent);
236
+        }
187 237
       }
188 238
       if (acceptsVideo(acceptTypes)) {
189
-        extraIntents.add(getVideoIntent());
239
+        Intent videoIntent = getVideoIntent();
240
+        if (videoIntent != null) {
241
+          extraIntents.add(videoIntent);
242
+        }
190 243
       }
191 244
     }
192 245
 
@@ -254,23 +307,41 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
254 307
   }
255 308
 
256 309
   private Intent getPhotoIntent() {
257
-    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
258
-    outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);
259
-    intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
310
+    Intent intent = null;
311
+
312
+    try {
313
+      outputImage = getCapturedFile(MimeType.IMAGE);
314
+      Uri outputImageUri = getOutputUri(outputImage);
315
+      intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
316
+      intent.putExtra(MediaStore.EXTRA_OUTPUT, outputImageUri);
317
+    } catch (IOException | IllegalArgumentException e) {
318
+      Log.e("CREATE FILE", "Error occurred while creating the File", e);
319
+      e.printStackTrace();
320
+    }
321
+
260 322
     return intent;
261 323
   }
262 324
 
263 325
   private Intent getVideoIntent() {
264
-    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
265
-    outputFileUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE);
266
-    intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
326
+    Intent intent = null;
327
+
328
+    try {
329
+      outputVideo = getCapturedFile(MimeType.VIDEO);
330
+      Uri outputVideoUri = getOutputUri(outputVideo);
331
+      intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
332
+      intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri);
333
+    } catch (IOException | IllegalArgumentException e) {
334
+      Log.e("CREATE FILE", "Error occurred while creating the File", e);
335
+      e.printStackTrace();
336
+    }
337
+    
267 338
     return intent;
268 339
   }
269 340
 
270 341
   private Intent getFileChooserIntent(String acceptTypes) {
271 342
     String _acceptTypes = acceptTypes;
272 343
     if (acceptTypes.isEmpty()) {
273
-      _acceptTypes = DEFAULT_MIME_TYPES;
344
+      _acceptTypes = MimeType.DEFAULT.value;
274 345
     }
275 346
     if (acceptTypes.matches("\\.\\w+")) {
276 347
       _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", ""));
@@ -284,7 +355,7 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
284 355
   private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) {
285 356
     Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
286 357
     intent.addCategory(Intent.CATEGORY_OPENABLE);
287
-    intent.setType("*/*");
358
+    intent.setType(MimeType.DEFAULT.value);
288 359
     intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
289 360
     intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
290 361
     return intent;
@@ -295,25 +366,33 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
295 366
     if (types.matches("\\.\\w+")) {
296 367
       mimeType = getMimeTypeFromExtension(types.replace(".", ""));
297 368
     }
298
-    return mimeType.isEmpty() || mimeType.toLowerCase().contains("image");
369
+    return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.IMAGE.value);
299 370
   }
300 371
 
301 372
   private Boolean acceptsImages(String[] types) {
302 373
     String[] mimeTypes = getAcceptedMimeType(types);
303
-    return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "image");
374
+    return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.IMAGE.value);
304 375
   }
305 376
 
306 377
   private Boolean acceptsVideo(String types) {
378
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
379
+      return false;
380
+    }
381
+
307 382
     String mimeType = types;
308 383
     if (types.matches("\\.\\w+")) {
309 384
       mimeType = getMimeTypeFromExtension(types.replace(".", ""));
310 385
     }
311
-    return mimeType.isEmpty() || mimeType.toLowerCase().contains("video");
386
+    return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.VIDEO.value);
312 387
   }
313 388
 
314 389
   private Boolean acceptsVideo(String[] types) {
390
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
391
+      return false;
392
+    }
393
+
315 394
     String[] mimeTypes = getAcceptedMimeType(types);
316
-    return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "video");
395
+    return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.VIDEO.value);
317 396
   }
318 397
 
319 398
   private Boolean arrayContainsString(String[] array, String pattern) {
@@ -326,8 +405,8 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
326 405
   }
327 406
 
328 407
   private String[] getAcceptedMimeType(String[] types) {
329
-    if (isArrayEmpty(types)) {
330
-      return new String[]{DEFAULT_MIME_TYPES};
408
+    if (noAcceptTypesSet(types)) {
409
+      return new String[]{MimeType.DEFAULT.value};
331 410
     }
332 411
     String[] mimeTypes = new String[types.length];
333 412
     for (int i = 0; i < types.length; i++) {
@@ -355,15 +434,7 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
355 434
     return type;
356 435
   }
357 436
 
358
-  private Uri getOutputUri(String intentType) {
359
-    File capturedFile = null;
360
-    try {
361
-      capturedFile = getCapturedFile(intentType);
362
-    } catch (IOException e) {
363
-      Log.e("CREATE FILE", "Error occurred while creating the File", e);
364
-      e.printStackTrace();
365
-    }
366
-
437
+  private Uri getOutputUri(File capturedFile) {
367 438
     // for versions below 6.0 (23) we use the old File creation & permissions model
368 439
     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
369 440
       return Uri.fromFile(capturedFile);
@@ -374,41 +445,50 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
374 445
     return FileProvider.getUriForFile(getReactApplicationContext(), packageName + ".fileprovider", capturedFile);
375 446
   }
376 447
 
377
-  private File getCapturedFile(String intentType) throws IOException {
448
+  private File getCapturedFile(MimeType mimeType) throws IOException {
378 449
     String prefix = "";
379 450
     String suffix = "";
380 451
     String dir = "";
381
-    String filename = "";
382 452
 
383
-    if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) {
384
-      prefix = "image-";
385
-      suffix = ".jpg";
386
-      dir = Environment.DIRECTORY_PICTURES;
387
-    } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) {
388
-      prefix = "video-";
389
-      suffix = ".mp4";
390
-      dir = Environment.DIRECTORY_MOVIES;
453
+    switch (mimeType) {
454
+      case IMAGE:
455
+        prefix = "image-";
456
+        suffix = ".jpg";
457
+        dir = Environment.DIRECTORY_PICTURES;
458
+        break;
459
+      case VIDEO:
460
+        prefix = "video-";
461
+        suffix = ".mp4";
462
+        dir = Environment.DIRECTORY_MOVIES;
463
+        break;
464
+
465
+      default:
466
+        break;
391 467
     }
392 468
 
393
-    filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
469
+    String filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
470
+    File outputFile = null;
394 471
 
395 472
     // for versions below 6.0 (23) we use the old File creation & permissions model
396 473
     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
397 474
       // only this Directory works on all tested Android versions
398 475
       // ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
399 476
       File storageDir = Environment.getExternalStoragePublicDirectory(dir);
400
-      return new File(storageDir, filename);
477
+      outputFile = new File(storageDir, filename);
478
+    } else {
479
+      File storageDir = getReactApplicationContext().getExternalFilesDir(null);
480
+      outputFile = File.createTempFile(prefix, suffix, storageDir);
401 481
     }
402 482
 
403
-    File storageDir = getReactApplicationContext().getExternalFilesDir(null);
404
-    return File.createTempFile(filename, suffix, storageDir);
483
+    return outputFile;
405 484
   }
406 485
 
407
-  private Boolean isArrayEmpty(String[] arr) {
486
+  private Boolean noAcceptTypesSet(String[] types) {
408 487
     // when our array returned from getAcceptTypes() has no values set from the webview
409 488
     // i.e. <input type="file" />, without any "accept" attr
410 489
     // will be an array with one empty string element, afaik
411
-    return arr.length == 0 || (arr.length == 1 && arr[0] != null && arr[0].length() == 0);
490
+
491
+    return types.length == 0 || (types.length == 1 && types[0] != null && types[0].length() == 0);
412 492
   }
413 493
 
414 494
   private PermissionAwareActivity getPermissionAwareActivity() {