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!
This commit is contained in:
Scott Mathson 2019-01-07 06:02:47 -08:00 committed by Thibault Malbranche
parent 8427a8db69
commit 2114a9b327
3 changed files with 153 additions and 0 deletions

View File

@ -1,8 +1,13 @@
package com.reactnativecommunity.webview;
import android.annotation.TargetApi;
import android.app.DownloadManager;
import android.content.Context;
import com.facebook.react.uimanager.UIManagerModule;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
@ -21,19 +26,23 @@ import android.graphics.Bitmap;
import android.graphics.Picture;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.DownloadListener;
import android.webkit.GeolocationPermissions;
import android.webkit.JavascriptInterface;
import android.webkit.URLUtil;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
@ -446,6 +455,53 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
WebView.setWebContentsDebuggingEnabled(true);
}
webView.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
RNCWebViewModule module = getModule();
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
//Try to extract filename from contentDisposition, otherwise guess using URLUtil
String fileName = "";
try {
fileName = contentDisposition.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1");
fileName = URLDecoder.decode(fileName, "UTF-8");
} catch (Exception e) {
System.out.println("Error extracting filename from contentDisposition: " + e);
System.out.println("Falling back to URLUtil.guessFileName");
fileName = URLUtil.guessFileName(url,contentDisposition,mimetype);
}
String downloadMessage = "Downloading " + fileName;
//Attempt to add cookie, if it exists
URL urlObj = null;
try {
urlObj = new URL(url);
String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
String cookie = CookieManager.getInstance().getCookie(baseUrl);
request.addRequestHeader("Cookie", cookie);
System.out.println("Got cookie for DownloadManager: " + cookie);
} catch (MalformedURLException e) {
System.out.println("Error getting cookie for DownloadManager: " + e.toString());
e.printStackTrace();
}
//Finish setting up request
request.addRequestHeader("User-Agent", userAgent);
request.setTitle(fileName);
request.setDescription(downloadMessage);
request.allowScanningByMediaScanner();
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
module.setDownloadRequest(request);
if (module.grantFileDownloaderPermissions()) {
module.downloadFile();
}
}
});
return webView;
}

View File

@ -1,24 +1,32 @@
package com.reactnativecommunity.webview;
import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.support.annotation.RequiresApi;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.widget.Toast;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
import java.io.File;
import java.io.IOException;
@ -38,6 +46,9 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private ValueCallback<Uri[]> filePathCallback;
private Uri outputFileUri;
private DownloadManager.Request downloadRequest;
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
final String DEFAULT_MIME_TYPES = "*/*";
public RNCWebViewModule(ReactApplicationContext reactContext) {
@ -177,6 +188,37 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
return true;
}
public void setDownloadRequest(DownloadManager.Request request) {
this.downloadRequest = request;
}
public void downloadFile() {
DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
String downloadMessage = "Downloading";
dm.enqueue(this.downloadRequest);
Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
}
public boolean grantFileDownloaderPermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
boolean result = true;
if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
result = false;
}
if (!result) {
PermissionAwareActivity activity = getPermissionAwareActivity();
activity.requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
}
return result;
}
public RNCWebViewPackage getPackage() {
return this.aPackage;
}
@ -306,4 +348,34 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
// will be an array with one empty string element, afaik
return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0);
}
private PermissionAwareActivity getPermissionAwareActivity() {
Activity activity = getCurrentActivity();
if (activity == null) {
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
} else if (!(activity instanceof PermissionAwareActivity)) {
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
}
return (PermissionAwareActivity) activity;
}
private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case FILE_DOWNLOAD_PERMISSION_REQUEST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (downloadRequest != null) {
downloadFile();
}
} else {
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();
}
return true;
}
}
return false;
}
};
}

View File

@ -105,3 +105,28 @@ WebView.isFileUploadSupported().then(res => {
```
### Add support for File Download
##### iOS
For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
Save to gallery:
```
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save pictures for certain activities.</string>
```
##### Android
Add permission in AndroidManifest.xml:
```xml
<manifest ...>
......
<!-- this is required to save files on Android -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
......
</manifest>
```