Browse Source

feat(Android Webview): Add file download support for Android (#203)

Addresses #80.

Caveat: I am not an Android developer. This code comes from a fork of the original RN WebView that we have been using in production for some time, so all credit goes to @Oblongmana: https://github.com/Oblongmana/react-native-webview-file-upload-android.

Setting up a DownloadManager for the WebView is pretty straightforward, as is adding any known cookies to the request. Most of the complication comes from the requirement after SDK 23 to ask the user for the WRITE_EXTERNAL_STORAGE permission. Unfortunately there is no mechanism to suspend the download request until permission is resolved so this code stores off the request and sets up a listener that enqueues the download once permissions are resolved so the user experience is really nice.

I didn't see anything in the way of tests or documentation that needs to be added for this change, so let me know if I missed anything. Thanks!
Scott Mathson 6 years ago
parent
commit
2114a9b327

+ 56
- 0
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java View File

@@ -1,8 +1,13 @@
1 1
 package com.reactnativecommunity.webview;
2 2
 
3 3
 import android.annotation.TargetApi;
4
+import android.app.DownloadManager;
4 5
 import android.content.Context;
5 6
 import com.facebook.react.uimanager.UIManagerModule;
7
+
8
+import java.net.MalformedURLException;
9
+import java.net.URL;
10
+import java.net.URLDecoder;
6 11
 import java.util.LinkedList;
7 12
 import java.util.List;
8 13
 import java.util.regex.Pattern;
@@ -21,19 +26,23 @@ import android.graphics.Bitmap;
21 26
 import android.graphics.Picture;
22 27
 import android.net.Uri;
23 28
 import android.os.Build;
29
+import android.os.Environment;
24 30
 import android.text.TextUtils;
25 31
 import android.view.View;
26 32
 import android.view.ViewGroup.LayoutParams;
27 33
 import android.webkit.ConsoleMessage;
28 34
 import android.webkit.CookieManager;
35
+import android.webkit.DownloadListener;
29 36
 import android.webkit.GeolocationPermissions;
30 37
 import android.webkit.JavascriptInterface;
38
+import android.webkit.URLUtil;
31 39
 import android.webkit.ValueCallback;
32 40
 import android.webkit.WebChromeClient;
33 41
 import android.webkit.WebResourceRequest;
34 42
 import android.webkit.WebSettings;
35 43
 import android.webkit.WebView;
36 44
 import android.webkit.WebViewClient;
45
+
37 46
 import com.facebook.common.logging.FLog;
38 47
 import com.facebook.react.bridge.Arguments;
39 48
 import com.facebook.react.bridge.LifecycleEventListener;
@@ -446,6 +455,53 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
446 455
       WebView.setWebContentsDebuggingEnabled(true);
447 456
     }
448 457
 
458
+    webView.setDownloadListener(new DownloadListener() {
459
+      public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
460
+        RNCWebViewModule module = getModule();
461
+
462
+        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
463
+
464
+        //Try to extract filename from contentDisposition, otherwise guess using URLUtil
465
+        String fileName = "";
466
+        try {
467
+          fileName = contentDisposition.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1");
468
+          fileName = URLDecoder.decode(fileName, "UTF-8");
469
+        } catch (Exception e) {
470
+          System.out.println("Error extracting filename from contentDisposition: " + e);
471
+          System.out.println("Falling back to URLUtil.guessFileName");
472
+          fileName = URLUtil.guessFileName(url,contentDisposition,mimetype);
473
+        }
474
+        String downloadMessage = "Downloading " + fileName;
475
+
476
+        //Attempt to add cookie, if it exists
477
+        URL urlObj = null;
478
+        try {  
479
+          urlObj = new URL(url);
480
+          String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
481
+          String cookie = CookieManager.getInstance().getCookie(baseUrl);  
482
+          request.addRequestHeader("Cookie", cookie);
483
+          System.out.println("Got cookie for DownloadManager: " + cookie);
484
+        } catch (MalformedURLException e) {
485
+          System.out.println("Error getting cookie for DownloadManager: " + e.toString());
486
+          e.printStackTrace();  
487
+        }
488
+
489
+        //Finish setting up request
490
+        request.addRequestHeader("User-Agent", userAgent);
491
+        request.setTitle(fileName);
492
+        request.setDescription(downloadMessage);
493
+        request.allowScanningByMediaScanner();
494
+        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
495
+        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
496
+
497
+        module.setDownloadRequest(request);
498
+
499
+        if (module.grantFileDownloaderPermissions()) {
500
+          module.downloadFile();
501
+        }
502
+      }
503
+    });
504
+
449 505
     return webView;
450 506
   }
451 507
 

+ 72
- 0
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java View File

@@ -1,24 +1,32 @@
1 1
 
2 2
 package com.reactnativecommunity.webview;
3 3
 
4
+import android.Manifest;
4 5
 import android.app.Activity;
6
+import android.app.DownloadManager;
7
+import android.content.Context;
5 8
 import android.content.Intent;
9
+import android.content.pm.PackageManager;
6 10
 import android.net.Uri;
7 11
 import android.os.Build;
8 12
 import android.os.Environment;
9 13
 import android.os.Parcelable;
10 14
 import android.provider.MediaStore;
11 15
 import android.support.annotation.RequiresApi;
16
+import android.support.v4.content.ContextCompat;
12 17
 import android.support.v4.content.FileProvider;
13 18
 import android.util.Log;
14 19
 import android.webkit.ValueCallback;
15 20
 import android.webkit.WebChromeClient;
21
+import android.widget.Toast;
16 22
 
17 23
 import com.facebook.react.bridge.ActivityEventListener;
18 24
 import com.facebook.react.bridge.Promise;
19 25
 import com.facebook.react.bridge.ReactApplicationContext;
20 26
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
21 27
 import com.facebook.react.bridge.ReactMethod;
28
+import com.facebook.react.modules.core.PermissionAwareActivity;
29
+import com.facebook.react.modules.core.PermissionListener;
22 30
 
23 31
 import java.io.File;
24 32
 import java.io.IOException;
@@ -38,6 +46,9 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
38 46
   private ValueCallback<Uri[]> filePathCallback;
39 47
   private Uri outputFileUri;
40 48
 
49
+  private DownloadManager.Request downloadRequest;
50
+  private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
51
+
41 52
   final String DEFAULT_MIME_TYPES = "*/*";
42 53
 
43 54
   public RNCWebViewModule(ReactApplicationContext reactContext) {
@@ -177,6 +188,37 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
177 188
     return true;
178 189
   }
179 190
 
191
+  public void setDownloadRequest(DownloadManager.Request request) {
192
+    this.downloadRequest = request;
193
+  }
194
+
195
+  public void downloadFile() {
196
+    DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
197
+    String downloadMessage = "Downloading";
198
+
199
+    dm.enqueue(this.downloadRequest);
200
+
201
+    Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
202
+  }
203
+
204
+  public boolean grantFileDownloaderPermissions() {
205
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
206
+      return true;
207
+    }
208
+
209
+    boolean result = true;
210
+    if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
211
+      result = false;
212
+    }
213
+
214
+    if (!result) {
215
+      PermissionAwareActivity activity = getPermissionAwareActivity();
216
+      activity.requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
217
+    }
218
+
219
+    return result;
220
+  }
221
+
180 222
   public RNCWebViewPackage getPackage() {
181 223
     return this.aPackage;
182 224
   }
@@ -306,4 +348,34 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
306 348
     // will be an array with one empty string element, afaik
307 349
     return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0);
308 350
   }
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
+
362
+  private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
363
+    @Override
364
+    public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
365
+      switch (requestCode) {
366
+        case FILE_DOWNLOAD_PERMISSION_REQUEST: {
367
+          // If request is cancelled, the result arrays are empty.
368
+          if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
369
+            if (downloadRequest != null) {
370
+              downloadFile();
371
+            }
372
+          } else {
373
+            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();
374
+          }
375
+          return true;
376
+        }
377
+      }
378
+      return false;
379
+    }
380
+  };
309 381
 }

+ 25
- 0
docs/Guide.md View File

@@ -105,3 +105,28 @@ WebView.isFileUploadSupported().then(res => {
105 105
 
106 106
 ```
107 107
 
108
+### Add support for File Download
109
+
110
+##### iOS
111
+
112
+For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
113
+
114
+Save to gallery:
115
+```
116
+<key>NSPhotoLibraryAddUsageDescription</key>
117
+<string>Save pictures for certain activities.</string>
118
+```
119
+
120
+##### Android
121
+
122
+Add permission in AndroidManifest.xml:
123
+```xml
124
+<manifest ...>
125
+  ......
126
+
127
+  <!-- this is required to save files on Android  -->
128
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
129
+
130
+  ......
131
+</manifest>
132
+```