2018-08-15 20:30:06 -07:00
package com.reactnativecommunity.webview ;
2018-08-07 23:02:40 -07:00
2019-01-07 06:02:47 -08:00
import android.Manifest ;
2018-11-21 12:46:43 +02:00
import android.app.Activity ;
2019-01-07 06:02:47 -08:00
import android.app.DownloadManager ;
import android.content.Context ;
2018-11-21 12:46:43 +02:00
import android.content.Intent ;
2019-01-07 06:02:47 -08:00
import android.content.pm.PackageManager ;
2018-11-21 12:46:43 +02:00
import android.net.Uri ;
import android.os.Build ;
import android.os.Environment ;
import android.os.Parcelable ;
import android.provider.MediaStore ;
2019-05-16 23:33:25 +01:00
import androidx.annotation.RequiresApi ;
import androidx.core.content.ContextCompat ;
import androidx.core.content.FileProvider ;
2018-11-21 12:46:43 +02:00
import android.util.Log ;
2019-02-28 13:12:27 -08:00
import android.webkit.MimeTypeMap ;
2018-11-21 12:46:43 +02:00
import android.webkit.ValueCallback ;
import android.webkit.WebChromeClient ;
2019-01-07 06:02:47 -08:00
import android.widget.Toast ;
2018-11-21 12:46:43 +02:00
import com.facebook.react.bridge.ActivityEventListener ;
import com.facebook.react.bridge.Promise ;
2018-08-07 23:02:40 -07:00
import com.facebook.react.bridge.ReactApplicationContext ;
import com.facebook.react.bridge.ReactContextBaseJavaModule ;
import com.facebook.react.bridge.ReactMethod ;
2019-03-27 10:52:36 +01:00
import com.facebook.react.module.annotations.ReactModule ;
2019-01-07 06:02:47 -08:00
import com.facebook.react.modules.core.PermissionAwareActivity ;
import com.facebook.react.modules.core.PermissionListener ;
2018-08-07 23:02:40 -07:00
2018-11-21 12:46:43 +02:00
import java.io.File ;
import java.io.IOException ;
import java.util.ArrayList ;
2020-02-18 18:40:30 -08:00
import java.util.Arrays ;
2018-11-21 12:46:43 +02:00
import static android.app.Activity.RESULT_OK ;
2019-03-27 10:52:36 +01:00
@ReactModule ( name = RNCWebViewModule . MODULE_NAME )
2018-11-21 12:46:43 +02:00
public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
2019-03-27 10:52:36 +01:00
public static final String MODULE_NAME = "RNCWebView" ;
2018-11-21 12:46:43 +02:00
private static final int PICKER = 1 ;
private static final int PICKER_LEGACY = 3 ;
2019-03-19 15:42:47 +01:00
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1 ;
final String DEFAULT_MIME_TYPES = "*/*" ;
2018-11-21 12:46:43 +02:00
private ValueCallback < Uri > filePathCallbackLegacy ;
private ValueCallback < Uri []> filePathCallback ;
private Uri outputFileUri ;
2019-01-07 06:02:47 -08:00
private DownloadManager . Request downloadRequest ;
2019-03-19 15:42:47 +01:00
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 ;
}
};
2018-08-07 23:02:40 -07:00
2018-09-19 01:19:06 +02:00
public RNCWebViewModule ( ReactApplicationContext reactContext ) {
2018-08-07 23:02:40 -07:00
super ( reactContext );
2018-11-21 12:46:43 +02:00
reactContext . addActivityEventListener ( this );
2018-08-07 23:02:40 -07:00
}
@Override
public String getName () {
2019-03-27 10:52:36 +01:00
return MODULE_NAME ;
2018-08-07 23:02:40 -07:00
}
2018-11-21 12:46:43 +02:00
@ReactMethod
public void isFileUploadSupported ( final Promise promise ) {
2019-03-19 15:42:47 +01:00
Boolean result = false ;
int current = Build . VERSION . SDK_INT ;
if ( current >= Build . VERSION_CODES . LOLLIPOP ) {
result = true ;
}
if ( current >= Build . VERSION_CODES . JELLY_BEAN && current <= Build . VERSION_CODES . JELLY_BEAN_MR2 ) {
result = true ;
}
promise . resolve ( result );
2018-11-21 12:46:43 +02:00
}
public void onActivityResult ( Activity activity , int requestCode , int resultCode , Intent data ) {
if ( filePathCallback == null && filePathCallbackLegacy == null ) {
2019-03-19 15:42:47 +01:00
return ;
2018-11-21 12:46:43 +02:00
}
// based off of which button was pressed, we get an activity result and a file
// the camera activity doesn't properly return the filename* (I think?) so we use
// this filename instead
switch ( requestCode ) {
2019-03-19 15:42:47 +01:00
case PICKER :
2018-11-21 12:46:43 +02:00
if ( resultCode != RESULT_OK ) {
2019-03-19 15:42:47 +01:00
if ( filePathCallback != null ) {
filePathCallback . onReceiveValue ( null );
}
2018-11-21 12:46:43 +02:00
} else {
2019-03-19 15:42:47 +01:00
Uri result [] = this . getSelectedFiles ( data , resultCode );
if ( result != null ) {
filePathCallback . onReceiveValue ( result );
} else {
filePathCallback . onReceiveValue ( new Uri [] { outputFileUri });
}
2018-11-21 12:46:43 +02:00
}
break ;
2019-03-19 15:42:47 +01:00
case PICKER_LEGACY :
2018-11-21 12:46:43 +02:00
Uri result = resultCode != Activity . RESULT_OK ? null : data == null ? outputFileUri : data . getData ();
filePathCallbackLegacy . onReceiveValue ( result );
break ;
}
filePathCallback = null ;
2019-03-19 15:42:47 +01:00
filePathCallbackLegacy = null ;
2018-11-21 12:46:43 +02:00
outputFileUri = null ;
}
public void onNewIntent ( Intent intent ) {
}
private Uri [] getSelectedFiles ( Intent data , int resultCode ) {
if ( data == null ) {
2019-03-19 15:42:47 +01:00
return null ;
2018-11-21 12:46:43 +02:00
}
// we have one file selected
if ( data . getData () != null ) {
2019-03-19 15:42:47 +01:00
if ( resultCode == RESULT_OK && Build . VERSION . SDK_INT >= Build . VERSION_CODES . LOLLIPOP ) {
return WebChromeClient . FileChooserParams . parseResult ( resultCode , data );
} else {
return null ;
}
2018-11-21 12:46:43 +02:00
}
// we have multiple files selected
if ( data . getClipData () != null ) {
2019-03-19 15:42:47 +01:00
final int numSelectedFiles = data . getClipData (). getItemCount ();
Uri [] result = new Uri [ numSelectedFiles ] ;
for ( int i = 0 ; i < numSelectedFiles ; i ++ ) {
result [ i ] = data . getClipData (). getItemAt ( i ). getUri ();
}
return result ;
2018-11-21 12:46:43 +02:00
}
return null ;
}
public void startPhotoPickerIntent ( ValueCallback < Uri > filePathCallback , String acceptType ) {
2019-03-19 15:42:47 +01:00
filePathCallbackLegacy = filePathCallback ;
2018-11-21 12:46:43 +02:00
2019-03-19 15:42:47 +01:00
Intent fileChooserIntent = getFileChooserIntent ( acceptType );
Intent chooserIntent = Intent . createChooser ( fileChooserIntent , "" );
2018-11-21 12:46:43 +02:00
2019-03-19 15:42:47 +01:00
ArrayList < Parcelable > extraIntents = new ArrayList <> ();
if ( acceptsImages ( acceptType )) {
extraIntents . add ( getPhotoIntent ());
}
if ( acceptsVideo ( acceptType )) {
extraIntents . add ( getVideoIntent ());
}
chooserIntent . putExtra ( Intent . EXTRA_INITIAL_INTENTS , extraIntents . toArray ( new Parcelable [] {}));
2018-11-21 12:46:43 +02:00
2019-03-19 15:42:47 +01:00
if ( chooserIntent . resolveActivity ( getCurrentActivity (). getPackageManager ()) != null ) {
getCurrentActivity (). startActivityForResult ( chooserIntent , PICKER_LEGACY );
} else {
Log . w ( "RNCWebViewModule" , "there is no Activity to handle this Intent" );
}
2018-11-21 12:46:43 +02:00
}
@RequiresApi ( api = Build . VERSION_CODES . LOLLIPOP )
public boolean startPhotoPickerIntent ( final ValueCallback < Uri []> callback , final Intent intent , final String [] acceptTypes , final boolean allowMultiple ) {
filePathCallback = callback ;
ArrayList < Parcelable > extraIntents = new ArrayList <> ();
2020-02-18 18:40:30 -08:00
if ( ! needsCameraPermission ()) {
if ( acceptsImages ( acceptTypes )) {
extraIntents . add ( getPhotoIntent ());
}
if ( acceptsVideo ( acceptTypes )) {
extraIntents . add ( getVideoIntent ());
}
2018-11-21 12:46:43 +02:00
}
Intent fileSelectionIntent = getFileChooserIntent ( acceptTypes , allowMultiple );
Intent chooserIntent = new Intent ( Intent . ACTION_CHOOSER );
chooserIntent . putExtra ( Intent . EXTRA_INTENT , fileSelectionIntent );
chooserIntent . putExtra ( Intent . EXTRA_INITIAL_INTENTS , extraIntents . toArray ( new Parcelable [] {}));
if ( chooserIntent . resolveActivity ( getCurrentActivity (). getPackageManager ()) != null ) {
2019-03-19 15:42:47 +01:00
getCurrentActivity (). startActivityForResult ( chooserIntent , PICKER );
2018-11-21 12:46:43 +02:00
} else {
2019-03-19 15:42:47 +01:00
Log . w ( "RNCWebViewModule" , "there is no Activity to handle this Intent" );
2018-11-21 12:46:43 +02:00
}
return true ;
}
2019-01-07 06:02:47 -08:00
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 ();
2019-03-19 15:42:47 +01:00
activity . requestPermissions ( new String [] { Manifest . permission . WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST , webviewFileDownloaderPermissionListener );
2019-01-07 06:02:47 -08:00
}
return result ;
}
2020-02-18 18:40:30 -08:00
protected boolean needsCameraPermission () {
boolean needed = false ;
PackageManager packageManager = getCurrentActivity (). getPackageManager ();
try {
String [] requestedPermissions = packageManager . getPackageInfo ( getReactApplicationContext (). getPackageName (), PackageManager . GET_PERMISSIONS ). requestedPermissions ;
if ( Arrays . asList ( requestedPermissions ). contains ( Manifest . permission . CAMERA )
&& ContextCompat . checkSelfPermission ( getCurrentActivity (), Manifest . permission . CAMERA ) != PackageManager . PERMISSION_GRANTED ) {
needed = true ;
}
} catch ( PackageManager . NameNotFoundException e ) {
needed = true ;
}
return needed ;
}
2018-11-21 12:46:43 +02:00
private Intent getPhotoIntent () {
Intent intent = new Intent ( MediaStore . ACTION_IMAGE_CAPTURE );
outputFileUri = getOutputUri ( MediaStore . ACTION_IMAGE_CAPTURE );
intent . putExtra ( MediaStore . EXTRA_OUTPUT , outputFileUri );
return intent ;
}
private Intent getVideoIntent () {
Intent intent = new Intent ( MediaStore . ACTION_VIDEO_CAPTURE );
2019-08-29 20:16:42 +05:30
outputFileUri = getOutputUri ( MediaStore . ACTION_VIDEO_CAPTURE );
intent . putExtra ( MediaStore . EXTRA_OUTPUT , outputFileUri );
2018-11-21 12:46:43 +02:00
return intent ;
}
private Intent getFileChooserIntent ( String acceptTypes ) {
String _acceptTypes = acceptTypes ;
if ( acceptTypes . isEmpty ()) {
_acceptTypes = DEFAULT_MIME_TYPES ;
}
2019-02-28 13:12:27 -08:00
if ( acceptTypes . matches ( "\\.\\w+" )) {
_acceptTypes = getMimeTypeFromExtension ( acceptTypes . replace ( "." , "" ));
}
2018-11-21 12:46:43 +02:00
Intent intent = new Intent ( Intent . ACTION_GET_CONTENT );
intent . addCategory ( Intent . CATEGORY_OPENABLE );
intent . setType ( _acceptTypes );
return intent ;
}
private Intent getFileChooserIntent ( String [] acceptTypes , boolean allowMultiple ) {
Intent intent = new Intent ( Intent . ACTION_GET_CONTENT );
intent . addCategory ( Intent . CATEGORY_OPENABLE );
intent . setType ( "*/*" );
intent . putExtra ( Intent . EXTRA_MIME_TYPES , getAcceptedMimeType ( acceptTypes ));
intent . putExtra ( Intent . EXTRA_ALLOW_MULTIPLE , allowMultiple );
return intent ;
}
private Boolean acceptsImages ( String types ) {
2019-02-28 13:12:27 -08:00
String mimeType = types ;
if ( types . matches ( "\\.\\w+" )) {
2019-03-19 15:42:47 +01:00
mimeType = getMimeTypeFromExtension ( types . replace ( "." , "" ));
2019-02-28 13:12:27 -08:00
}
return mimeType . isEmpty () || mimeType . toLowerCase (). contains ( "image" );
2018-11-21 12:46:43 +02:00
}
2019-03-19 15:42:47 +01:00
2018-11-21 12:46:43 +02:00
private Boolean acceptsImages ( String [] types ) {
2019-02-28 13:12:27 -08:00
String [] mimeTypes = getAcceptedMimeType ( types );
return isArrayEmpty ( mimeTypes ) || arrayContainsString ( mimeTypes , "image" );
2018-11-21 12:46:43 +02:00
}
private Boolean acceptsVideo ( String types ) {
2019-02-28 13:12:27 -08:00
String mimeType = types ;
if ( types . matches ( "\\.\\w+" )) {
2019-03-19 15:42:47 +01:00
mimeType = getMimeTypeFromExtension ( types . replace ( "." , "" ));
2019-02-28 13:12:27 -08:00
}
return mimeType . isEmpty () || mimeType . toLowerCase (). contains ( "video" );
2018-11-21 12:46:43 +02:00
}
2019-03-19 15:42:47 +01:00
2018-11-21 12:46:43 +02:00
private Boolean acceptsVideo ( String [] types ) {
2019-02-28 13:12:27 -08:00
String [] mimeTypes = getAcceptedMimeType ( types );
return isArrayEmpty ( mimeTypes ) || arrayContainsString ( mimeTypes , "video" );
2018-11-21 12:46:43 +02:00
}
2019-03-19 15:42:47 +01:00
private Boolean arrayContainsString ( String [] array , String pattern ) {
for ( String content : array ) {
if ( content . contains ( pattern )) {
return true ;
}
2018-11-21 12:46:43 +02:00
}
return false ;
}
private String [] getAcceptedMimeType ( String [] types ) {
if ( isArrayEmpty ( types )) {
2019-03-19 15:42:47 +01:00
return new String [] { DEFAULT_MIME_TYPES };
2018-11-21 12:46:43 +02:00
}
2019-02-28 13:12:27 -08:00
String [] mimeTypes = new String [ types . length ] ;
for ( int i = 0 ; i < types . length ; i ++ ) {
2019-03-19 15:42:47 +01:00
String t = types [ i ] ;
// convert file extensions to mime types
if ( t . matches ( "\\.\\w+" )) {
String mimeType = getMimeTypeFromExtension ( t . replace ( "." , "" ));
mimeTypes [ i ] = mimeType ;
} else {
mimeTypes [ i ] = t ;
}
2019-02-28 13:12:27 -08:00
}
return mimeTypes ;
}
private String getMimeTypeFromExtension ( String extension ) {
String type = null ;
if ( extension != null ) {
2019-03-19 15:42:47 +01:00
type = MimeTypeMap . getSingleton (). getMimeTypeFromExtension ( extension );
2019-02-28 13:12:27 -08:00
}
return type ;
2018-11-21 12:46:43 +02:00
}
private Uri getOutputUri ( String intentType ) {
File capturedFile = null ;
try {
2019-03-19 15:42:47 +01:00
capturedFile = getCapturedFile ( intentType );
2018-11-21 12:46:43 +02:00
} catch ( IOException e ) {
2019-03-19 15:42:47 +01:00
Log . e ( "CREATE FILE" , "Error occurred while creating the File" , e );
e . printStackTrace ();
2018-11-21 12:46:43 +02:00
}
// for versions below 6.0 (23) we use the old File creation & permissions model
if ( Build . VERSION . SDK_INT < Build . VERSION_CODES . M ) {
2019-03-19 15:42:47 +01:00
return Uri . fromFile ( capturedFile );
2018-11-21 12:46:43 +02:00
}
// for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions
String packageName = getReactApplicationContext (). getPackageName ();
2019-03-19 15:42:47 +01:00
return FileProvider . getUriForFile ( getReactApplicationContext (), packageName + ".fileprovider" , capturedFile );
2018-11-21 12:46:43 +02:00
}
private File getCapturedFile ( String intentType ) throws IOException {
String prefix = "" ;
String suffix = "" ;
String dir = "" ;
String filename = "" ;
if ( intentType . equals ( MediaStore . ACTION_IMAGE_CAPTURE )) {
prefix = "image-" ;
suffix = ".jpg" ;
dir = Environment . DIRECTORY_PICTURES ;
} else if ( intentType . equals ( MediaStore . ACTION_VIDEO_CAPTURE )) {
prefix = "video-" ;
suffix = ".mp4" ;
dir = Environment . DIRECTORY_MOVIES ;
}
filename = prefix + String . valueOf ( System . currentTimeMillis ()) + suffix ;
// for versions below 6.0 (23) we use the old File creation & permissions model
if ( Build . VERSION . SDK_INT < Build . VERSION_CODES . M ) {
2019-03-19 15:42:47 +01:00
// only this Directory works on all tested Android versions
// ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
File storageDir = Environment . getExternalStoragePublicDirectory ( dir );
return new File ( storageDir , filename );
2018-11-21 12:46:43 +02:00
}
File storageDir = getReactApplicationContext (). getExternalFilesDir ( null );
return File . createTempFile ( filename , suffix , storageDir );
}
private Boolean isArrayEmpty ( String [] arr ) {
// when our array returned from getAcceptTypes() has no values set from the webview
// i.e. <input type="file" />, without any "accept" attr
// will be an array with one empty string element, afaik
return arr . length == 0 || ( arr . length == 1 && arr [ 0 ] . length () == 0 );
}
2019-01-07 06:02:47 -08:00
private PermissionAwareActivity getPermissionAwareActivity () {
Activity activity = getCurrentActivity ();
if ( activity == null ) {
2019-03-19 15:42:47 +01:00
throw new IllegalStateException ( "Tried to use permissions API while not attached to an Activity." );
2019-01-07 06:02:47 -08:00
} else if ( ! ( activity instanceof PermissionAwareActivity )) {
2019-03-19 15:42:47 +01:00
throw new IllegalStateException ( "Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity." );
2019-01-07 06:02:47 -08:00
}
return ( PermissionAwareActivity ) activity ;
}
2018-11-21 12:46:43 +02:00
}