Skip to content

Commit

Permalink
Issue react-native-webview#3287: improve Download feature in Android
Browse files Browse the repository at this point in the history
  • Loading branch information
HoangNgocDai committed Feb 5, 2024
1 parent 39d4293 commit 32b82f4
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 0 deletions.
19 changes: 19 additions & 0 deletions android/src/main/assets/SearchWebView.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,22 @@ function getFavicons() {
}
window.FaviconWebView.postFavicon(`${favi}`);
}

function getBase64StringFromBlobUrl(blobUrl) {
const xhr = new XMLHttpRequest();
xhr.open('GET', blobUrl, true);
xhr.setRequestHeader('Content-type', 'application/octet-stream');
xhr.responseType = 'blob';
xhr.onload = () => {
if (xhr.status === 200) {
const blobResponse = xhr.response;
const fileReaderInstance = new FileReader();
fileReaderInstance.readAsDataURL(blobResponse);
fileReaderInstance.onloadend = () => {
const base64data = fileReaderInstance.result;
nativeScriptHandler.processBase64Data(base64data);
};
}
};
xhr.send();
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ public void handleMessage(Message msg) {

webView.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
if (url.startsWith("blob")) {
String jsConvert = "getBase64StringFromBlobUrl('" + url + "');";
webView.loadUrl("javascript:" + jsConvert);
return;
}

// block non-http/https download links
if (!URLUtil.isNetworkUrl(url)) {
Toast.makeText(reactContext.getCurrentActivity(), R.string.download_protocol_not_supported,
Expand Down Expand Up @@ -1846,6 +1852,15 @@ public void run() {
});
}
}

@JavascriptInterface
public void processBase64Data(String base64Data) {
RNCWebViewModule module = getModule((ReactContext) mContext.getContext());
module.setDownloadBase64Data(base64Data);
if (module.grantFileDownloaderPermissions()) {
module.saveBase64DataToFile();
}
}
}

public void captureScreen(String type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;

import android.util.Base64;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.webkit.ValueCallback;
Expand All @@ -34,6 +36,8 @@

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
Expand All @@ -45,6 +49,7 @@
import static android.app.Activity.RESULT_OK;

import com.brave.adblock.Engine;
import com.reactnativecommunity.webview.utils.Utils;

@ReactModule(name = RNCWebViewModule.MODULE_NAME)
public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
Expand All @@ -67,6 +72,7 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private Uri outputFileUri;
private String intentTypeAfterPermissionGranted;
private DownloadManager.Request downloadRequest;
private String downloadBase64Data;
private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
Expand All @@ -77,6 +83,9 @@ public boolean onRequestPermissionsResult(int requestCode, String[] permissions,
if (downloadRequest != null) {
downloadFile();
}
if (downloadBase64Data != null) {
saveBase64DataToFile();
}
} 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();
}
Expand Down Expand Up @@ -259,6 +268,48 @@ public void downloadFile() {
Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
}

public void setDownloadBase64Data(String base64Data) {
this.downloadBase64Data = base64Data;
}

public void saveBase64DataToFile() {
if (downloadBase64Data != null) {
String fileName = String.valueOf(System.currentTimeMillis());
String fileExtension = Utils.getFileExtensionFromBase64Data(downloadBase64Data);
String mimeType = Utils.getMimeTypeFromBase64Data(downloadBase64Data);
if (fileExtension != null) {
fileName = fileName + "." + fileExtension;
}

try {
File downloadPath = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
byte[] decodedData = Base64.decode(Utils.getBase64Data(downloadBase64Data), Base64.DEFAULT);
FileOutputStream fileOutputStream = new FileOutputStream(downloadPath, false);
fileOutputStream.write(decodedData);
fileOutputStream.flush();

if (downloadPath.exists()) {
Uri fileUri = FileProvider.getUriForFile(
getReactApplicationContext(),
getReactApplicationContext().getPackageName() + ".provider",
downloadPath
);
Utils.makeNotificationDownloadedBlobFile(getReactApplicationContext(), fileUri, mimeType, fileName);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}

downloadBase64Data = null;
}
}

public boolean grantFileDownloaderPermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.reactnativecommunity.webview.utils;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;

import androidx.core.app.NotificationCompat;

import com.reactnativecommunity.webview.R;

public class Utils {

public static String getBase64Data(String base64Data) {
String[] parts = base64Data.split(",");
if (parts.length > 1) {
return parts[1];
}
return base64Data;
}

public static String getMimeTypeFromBase64Data(String base64Data) {
String[] parts = base64Data.split(";");
if (parts.length > 0) {
String[] typePart = parts[0].split(":");
if (typePart.length > 1) {
return typePart[1];
}
}
return null;
}

public static String getFileExtensionFromBase64Data(String base64Data) {
String[] parts = base64Data.split(";");
if (parts.length > 0) {
String[] extensionPart = parts[0].split("/");
if (extensionPart.length > 1) {
return extensionPart[1];
}
}
return null;
}

public static void makeNotificationDownloadedBlobFile(Context context, Uri fileUri, String mimeType, String fileName) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (mimeType != null) {
intent.setDataAndType(fileUri, mimeType);
} else {
intent.setData(fileUri);
}
PendingIntent pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
String channel = "LunascapeChannel";

NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, channel)
.setAutoCancel(true)
.setContentTitle("File downloaded")
.setContentText(fileName)
.setSmallIcon(R.drawable.ic_download_done_24)
.setWhen(System.currentTimeMillis())
.setPriority(Notification.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(channel, channel, NotificationManager.IMPORTANCE_DEFAULT);
notificationChannel.setDescription(channel);
notificationChannel.enableLights(true);
notificationChannel.enableVibration(true);
notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
notificationManager.createNotificationChannel(notificationChannel);
notificationBuilder.setChannelId(channel);
}

notificationManager.notify(1, notificationBuilder.build());
}

}
6 changes: 6 additions & 0 deletions android/src/main/res/drawable/ic_download_done_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20.13,5.41l-1.41,-1.41l-9.19,9.19l-4.25,-4.24l-1.41,1.41l5.66,5.66z"/>
<path android:fillColor="@android:color/white" android:pathData="M5,18h14v2h-14z"/>
</vector>

0 comments on commit 32b82f4

Please sign in to comment.