Last active
March 15, 2021 15:47
-
-
Save Aulig/792bbeffa50d3a3a88ee5123f80b9bb3 to your computer and use it in GitHub Desktop.
Flutter PR 3225 add support for picking multiple files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2019 The Chromium Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
package io.flutter.plugins.webviewflutter; | |
public class Constants { | |
static final String ACTION_REQUEST_CAMERA_PERMISSION_FINISHED = | |
"action_request_camera_permission_denied"; | |
static final String ACTION_FILE_CHOOSER_FINISHED = "action_file_chooser_completed"; | |
static final String EXTRA_TITLE = "extra_title"; | |
static final String EXTRA_ACCEPT_TYPES = "extra_types"; | |
static final String EXTRA_SHOW_VIDEO_OPTION = "extra_show_video_option"; | |
static final String EXTRA_SHOW_IMAGE_OPTION = "extra_show_image_option"; | |
static final String EXTRA_FILE_URIS = "extra_file_uris"; | |
static final String EXTRA_ALLOW_MULTIPLE_FILES = "extra_allow_multiple_files"; | |
static final String WEBVIEW_STORAGE_DIRECTORY = "storage"; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2019 The Chromium Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
package io.flutter.plugins.webviewflutter; | |
import static io.flutter.plugins.webviewflutter.Constants.ACTION_FILE_CHOOSER_FINISHED; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ALLOW_MULTIPLE_FILES; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_FILE_URIS; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_IMAGE_OPTION; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_VIDEO_OPTION; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_TITLE; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ACCEPT_TYPES; | |
import static io.flutter.plugins.webviewflutter.Constants.WEBVIEW_STORAGE_DIRECTORY; | |
import android.app.Activity; | |
import android.content.Intent; | |
import android.database.Cursor; | |
import android.net.Uri; | |
import android.os.Bundle; | |
import android.provider.MediaStore; | |
import android.provider.OpenableColumns; | |
import android.util.Log; | |
import androidx.annotation.Nullable; | |
import androidx.core.content.FileProvider; | |
import java.io.File; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.text.SimpleDateFormat; | |
import java.util.ArrayList; | |
import java.util.Date; | |
public class FileChooserActivity extends Activity { | |
private static final int FILE_CHOOSER_REQUEST_CODE = 12322; | |
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); | |
// List of Uris that point to files where there MIGHT be the output of the capture. At most one of these can be valid | |
private final ArrayList<Uri> potentialCaptureOutputUris = new ArrayList<>(); | |
@Override | |
protected void onCreate(@Nullable Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
showFileChooser(getIntent().getBooleanExtra(EXTRA_SHOW_IMAGE_OPTION, false), getIntent().getBooleanExtra(EXTRA_SHOW_VIDEO_OPTION, false)); | |
} | |
private void showFileChooser(boolean showImageIntent, boolean showVideoIntent) { | |
Intent getContentIntent = createGetContentIntent(); | |
Intent captureImageIntent = showImageIntent ? createCaptureIntent(MediaStore.ACTION_IMAGE_CAPTURE, "jpg") : null; | |
Intent captureVideoIntent = showVideoIntent ? createCaptureIntent(MediaStore.ACTION_VIDEO_CAPTURE, "mp4") : null; | |
if (getContentIntent == null && captureImageIntent == null && captureVideoIntent == null) { | |
// cannot open anything: cancel file chooser | |
sendBroadcast(new Intent(ACTION_FILE_CHOOSER_FINISHED)); | |
finish(); | |
} else { | |
ArrayList<Intent> intentList = new ArrayList<>(); | |
if (getContentIntent != null) { | |
intentList.add(getContentIntent); | |
} | |
if (captureImageIntent != null) { | |
intentList.add(captureImageIntent); | |
} | |
if (captureVideoIntent != null) { | |
intentList.add(captureVideoIntent); | |
} | |
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); | |
chooserIntent.putExtra(Intent.EXTRA_TITLE, getIntent().getStringExtra(EXTRA_TITLE)); | |
chooserIntent.putExtra(Intent.EXTRA_INTENT, intentList.get(0)); | |
intentList.remove(0); | |
if (intentList.size() > 0) { | |
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentList.toArray(new Intent[0])); | |
} | |
startActivityForResult(chooserIntent, FILE_CHOOSER_REQUEST_CODE); | |
} | |
} | |
private Intent createGetContentIntent() { | |
Intent filesIntent = new Intent(Intent.ACTION_GET_CONTENT); | |
if (getIntent().getBooleanExtra(EXTRA_ALLOW_MULTIPLE_FILES, false)) { | |
filesIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); | |
} | |
String[] acceptTypes = getIntent().getStringArrayExtra(EXTRA_ACCEPT_TYPES); | |
if (acceptTypes.length == 0 || (acceptTypes.length == 1 && acceptTypes[0].length() == 0)) { | |
// empty array or only 1 empty string? -> accept all types | |
filesIntent.setType("*/*"); | |
} else if (acceptTypes.length == 1) { | |
filesIntent.setType(acceptTypes[0]); | |
} else { | |
// acceptTypes.length > 1 | |
filesIntent.setType("*/*"); | |
filesIntent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes); | |
} | |
return (filesIntent.resolveActivity(getPackageManager()) != null) ? filesIntent : null; | |
} | |
private Intent createCaptureIntent(String type, String fileFormat) { | |
Intent captureIntent = new Intent(type); | |
if (captureIntent.resolveActivity(getPackageManager()) == null) { | |
return null; | |
} | |
// Create the File where the output should go | |
Uri captureOutputUri = getTempUri(fileFormat); | |
potentialCaptureOutputUris.add(captureOutputUri); | |
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureOutputUri); | |
return captureIntent; | |
} | |
private File getStorageDirectory() { | |
File imageDirectory = new File(getCacheDir(), WEBVIEW_STORAGE_DIRECTORY); | |
if (!imageDirectory.exists() && !imageDirectory.mkdir()) { | |
Log.e("WEBVIEW", "Unable to create storage directory"); | |
} | |
return imageDirectory; | |
} | |
private Uri getTempUri(String format) { | |
String fileName = "CAPTURE-" + simpleDateFormat.format(new Date()) + "." + format; | |
File file = new File(getStorageDirectory(), fileName); | |
return FileProvider.getUriForFile( | |
this, getApplicationContext().getPackageName() + ".generic.provider", file); | |
} | |
private String getFileNameFromUri(Uri uri) { | |
Cursor returnCursor = getContentResolver().query(uri, null, null, null, null); | |
assert returnCursor != null; | |
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); | |
returnCursor.moveToFirst(); | |
String name = returnCursor.getString(nameIndex); | |
returnCursor.close(); | |
return name; | |
} | |
private Uri copyToLocalUri(Uri uri) { | |
File destination = new File(getStorageDirectory(), getFileNameFromUri(uri)); | |
try (InputStream in = getContentResolver().openInputStream(uri); OutputStream out = new FileOutputStream(destination)) { | |
byte[] buffer = new byte[1024]; | |
int len; | |
while ((len = in.read(buffer)) != -1) { | |
out.write(buffer, 0, len); | |
} | |
return FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ".generic.provider", destination); | |
} catch (IOException e) { | |
Log.e("WEBVIEW", "Unable to copy selected image", e); | |
e.printStackTrace(); | |
return null; | |
} | |
} | |
@Override | |
protected void onActivityResult(int requestCode, int resultCode, Intent data) { | |
if (requestCode == FILE_CHOOSER_REQUEST_CODE) { | |
Intent fileChooserFinishedIntent = new Intent(ACTION_FILE_CHOOSER_FINISHED); | |
if (resultCode == Activity.RESULT_OK) { | |
if (data != null && (data.getDataString() != null || data.getClipData() != null)) { | |
if (data.getDataString() != null) { | |
// single result from file browser OR video from camera | |
Uri localUri = copyToLocalUri(data.getData()); | |
if (localUri != null) { | |
fileChooserFinishedIntent.putExtra(EXTRA_FILE_URIS, new String[]{localUri.toString()}); | |
} | |
} else if (data.getClipData() != null) { | |
// multiple results from file browser | |
int uriCount = data.getClipData().getItemCount(); | |
String[] uriStrings = new String[uriCount]; | |
for (int i = 0; i < uriCount; i++) { | |
Uri localUri = copyToLocalUri(data.getClipData().getItemAt(i).getUri()); | |
if (localUri != null) { | |
uriStrings[i] = localUri.toString(); | |
} | |
} | |
fileChooserFinishedIntent.putExtra(EXTRA_FILE_URIS, uriStrings); | |
} | |
} else { | |
// image result from camera (videos from the camera are handled above, but this if-branch could handle them too if this varies from device to device) | |
for (Uri captureOutputUri : potentialCaptureOutputUris) { | |
try { | |
// just opening an input stream (and closing immediately) to test if the Uri points to a valid file | |
// if it's not a real file, the below catch-clause gets executed and we continue with the next Uri in the loop. | |
getContentResolver().openInputStream(captureOutputUri).close(); | |
fileChooserFinishedIntent.putExtra(EXTRA_FILE_URIS, new String[]{captureOutputUri.toString()}); | |
// leave the loop, as only one of the potentialCaptureOutputUris is valid and we just found it | |
break; | |
} catch (IOException ignored) {} | |
} | |
} | |
} | |
sendBroadcast(fileChooserFinishedIntent); | |
finish(); | |
} else { | |
super.onActivityResult(requestCode, resultCode, data); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2019 The Chromium Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
package io.flutter.plugins.webviewflutter; | |
import static io.flutter.plugins.webviewflutter.Constants.ACTION_FILE_CHOOSER_FINISHED; | |
import static io.flutter.plugins.webviewflutter.Constants.ACTION_REQUEST_CAMERA_PERMISSION_FINISHED; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ALLOW_MULTIPLE_FILES; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_FILE_URIS; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_IMAGE_OPTION; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_VIDEO_OPTION; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_TITLE; | |
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ACCEPT_TYPES; | |
import android.Manifest; | |
import android.content.BroadcastReceiver; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.content.IntentFilter; | |
import android.content.pm.PackageManager; | |
import android.net.Uri; | |
import android.webkit.ValueCallback; | |
import androidx.core.content.ContextCompat; | |
import java.util.Arrays; | |
public class FileChooserLauncher extends BroadcastReceiver { | |
private Context context; | |
private String title; | |
private boolean allowMultipleFiles; | |
private boolean videoAcceptable; | |
private boolean imageAcceptable; | |
private ValueCallback<Uri[]> filePathCallback; | |
private String[] acceptTypes; | |
public FileChooserLauncher( | |
Context context, | |
boolean allowMultipleFiles, | |
ValueCallback<Uri[]> filePathCallback, | |
String[] acceptTypes) { | |
this.context = context; | |
this.allowMultipleFiles = allowMultipleFiles; | |
this.filePathCallback = filePathCallback; | |
this.acceptTypes = acceptTypes; | |
if (acceptTypes.length == 0 || (acceptTypes.length == 1 && acceptTypes[0].length() == 0)) { | |
// acceptTypes empty -> accept anything | |
imageAcceptable = true; | |
videoAcceptable = true; | |
} | |
else { | |
for (String acceptType : acceptTypes) { | |
if (acceptType.startsWith("image/")) { | |
imageAcceptable = true; | |
} | |
else if (acceptType.startsWith("video/")) { | |
videoAcceptable = true; | |
} | |
} | |
} | |
if (imageAcceptable && !videoAcceptable) { | |
title = context.getResources().getString(R.string.webview_image_chooser_title); | |
} | |
else if (videoAcceptable && !imageAcceptable) { | |
title = context.getResources().getString(R.string.webview_video_chooser_title); | |
} | |
else { | |
title = context.getResources().getString(R.string.webview_file_chooser_title); | |
} | |
} | |
private boolean canCameraProduceAcceptableType() { | |
return imageAcceptable || videoAcceptable; | |
} | |
private boolean hasCameraPermission() { | |
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) | |
== PackageManager.PERMISSION_GRANTED; | |
} | |
public void start() { | |
if (!canCameraProduceAcceptableType() || hasCameraPermission()) { | |
showFileChooser(); | |
} else { | |
IntentFilter intentFilter = new IntentFilter(); | |
intentFilter.addAction(ACTION_REQUEST_CAMERA_PERMISSION_FINISHED); | |
context.registerReceiver(this, intentFilter); | |
Intent intent = new Intent(context, RequestCameraPermissionActivity.class); | |
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
context.startActivity(intent); | |
} | |
} | |
private void showFileChooser() { | |
IntentFilter intentFilter = new IntentFilter(ACTION_FILE_CHOOSER_FINISHED); | |
context.registerReceiver(this, intentFilter); | |
Intent intent = new Intent(context, FileChooserActivity.class); | |
intent.putExtra(EXTRA_TITLE, title); | |
intent.putExtra(EXTRA_ACCEPT_TYPES, acceptTypes); | |
intent.putExtra(EXTRA_SHOW_IMAGE_OPTION, imageAcceptable && hasCameraPermission()); | |
intent.putExtra(EXTRA_SHOW_VIDEO_OPTION, videoAcceptable && hasCameraPermission()); | |
intent.putExtra(EXTRA_ALLOW_MULTIPLE_FILES, allowMultipleFiles); | |
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
context.startActivity(intent); | |
} | |
@Override | |
public void onReceive(Context context, Intent intent) { | |
if (intent.getAction().equals(ACTION_REQUEST_CAMERA_PERMISSION_FINISHED)) { | |
context.unregisterReceiver(this); | |
showFileChooser(); | |
} else if (intent.getAction().equals(ACTION_FILE_CHOOSER_FINISHED)) { | |
String[] uriStrings = intent.getStringArrayExtra(EXTRA_FILE_URIS); | |
Uri[] result = null; | |
if (uriStrings != null) { | |
int uriStringCount = uriStrings.length; | |
result = new Uri[uriStringCount]; | |
for (int i = 0; i < uriStringCount; i++) { | |
result[i] = Uri.parse(uriStrings[i]); | |
} | |
} | |
filePathCallback.onReceiveValue(result); | |
context.unregisterReceiver(this); | |
filePathCallback = null; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2018 The Chromium Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
package io.flutter.plugins.webviewflutter; | |
import android.annotation.TargetApi; | |
import android.content.Context; | |
import android.hardware.display.DisplayManager; | |
import android.net.Uri; | |
import android.os.Build; | |
import android.os.Handler; | |
import android.os.Message; | |
import android.view.View; | |
import android.webkit.ValueCallback; | |
import android.webkit.WebChromeClient; | |
import android.webkit.WebResourceRequest; | |
import android.webkit.WebStorage; | |
import android.webkit.WebView; | |
import android.webkit.WebViewClient; | |
import androidx.annotation.NonNull; | |
import io.flutter.plugin.common.BinaryMessenger; | |
import io.flutter.plugin.common.MethodCall; | |
import io.flutter.plugin.common.MethodChannel; | |
import io.flutter.plugin.common.MethodChannel.MethodCallHandler; | |
import io.flutter.plugin.common.MethodChannel.Result; | |
import io.flutter.plugin.platform.PlatformView; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.List; | |
import java.util.Map; | |
public class FlutterWebView implements PlatformView, MethodCallHandler { | |
private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; | |
private final WebView webView; | |
private final MethodChannel methodChannel; | |
private final FlutterWebViewClient flutterWebViewClient; | |
private final Handler platformThreadHandler; | |
// Verifies that a url opened by `Window.open` has a secure url. | |
private class FlutterWebChromeClient extends WebChromeClient { | |
@Override | |
public boolean onCreateWindow( | |
final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { | |
final WebViewClient webViewClient = | |
new WebViewClient() { | |
@TargetApi(Build.VERSION_CODES.LOLLIPOP) | |
@Override | |
public boolean shouldOverrideUrlLoading( | |
@NonNull WebView view, @NonNull WebResourceRequest request) { | |
final String url = request.getUrl().toString(); | |
if (!flutterWebViewClient.shouldOverrideUrlLoading( | |
FlutterWebView.this.webView, request)) { | |
webView.loadUrl(url); | |
} | |
return true; | |
} | |
@Override | |
public boolean shouldOverrideUrlLoading(WebView view, String url) { | |
if (!flutterWebViewClient.shouldOverrideUrlLoading( | |
FlutterWebView.this.webView, url)) { | |
webView.loadUrl(url); | |
} | |
return true; | |
} | |
}; | |
final WebView newWebView = new WebView(view.getContext()); | |
newWebView.setWebViewClient(webViewClient); | |
final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; | |
transport.setWebView(newWebView); | |
resultMsg.sendToTarget(); | |
return true; | |
} | |
@Override | |
public boolean onShowFileChooser( | |
WebView webView, | |
ValueCallback<Uri[]> filePathCallback, | |
FileChooserParams fileChooserParams) { | |
// info as of 2021-03-08: | |
// don't use fileChooserParams.getTitle() as it is (always? on Mi 9T Pro Android 10 at least) null | |
// don't use fileChooserParams.isCaptureEnabled() as it is (always? on Mi 9T Pro Android 10 at least) false, even when the file upload allows images or any file | |
final Context context = webView.getContext(); | |
final boolean allowMultipleFiles = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE; | |
new FileChooserLauncher(context, allowMultipleFiles, filePathCallback, fileChooserParams.getAcceptTypes()).start(); | |
return true; | |
} | |
@Override | |
public void onProgressChanged(WebView view, int progress) { | |
flutterWebViewClient.onLoadingProgress(progress); | |
} | |
} | |
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) | |
@SuppressWarnings("unchecked") | |
FlutterWebView( | |
final Context context, | |
BinaryMessenger messenger, | |
int id, | |
Map<String, Object> params, | |
View containerView) { | |
DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); | |
DisplayManager displayManager = | |
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); | |
displayListenerProxy.onPreWebViewInitialization(displayManager); | |
Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition"); | |
webView = | |
(usesHybridComposition) | |
? new WebView(context) | |
: new InputAwareWebView(context, containerView); | |
displayListenerProxy.onPostWebViewInitialization(displayManager); | |
platformThreadHandler = new Handler(context.getMainLooper()); | |
// Allow local storage. | |
webView.getSettings().setDomStorageEnabled(true); | |
webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); | |
// Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679. | |
webView.getSettings().setSupportMultipleWindows(true); | |
webView.setWebChromeClient(new FlutterWebChromeClient()); | |
methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); | |
methodChannel.setMethodCallHandler(this); | |
flutterWebViewClient = new FlutterWebViewClient(methodChannel); | |
Map<String, Object> settings = (Map<String, Object>) params.get("settings"); | |
if (settings != null) applySettings(settings); | |
if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { | |
List<String> names = (List<String>) params.get(JS_CHANNEL_NAMES_FIELD); | |
if (names != null) registerJavaScriptChannelNames(names); | |
} | |
Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); | |
if (autoMediaPlaybackPolicy != null) updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); | |
if (params.containsKey("userAgent")) { | |
String userAgent = (String) params.get("userAgent"); | |
updateUserAgent(userAgent); | |
} | |
if (params.containsKey("initialUrl")) { | |
String url = (String) params.get("initialUrl"); | |
webView.loadUrl(url); | |
} | |
} | |
@Override | |
public View getView() { | |
return webView; | |
} | |
// @Override | |
// This is overriding a method that hasn't rolled into stable Flutter yet. Including the | |
// annotation would cause compile time failures in versions of Flutter too old to include the new | |
// method. However leaving it raw like this means that the method will be ignored in old versions | |
// of Flutter but used as an override anyway wherever it's actually defined. | |
// TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. | |
public void onInputConnectionUnlocked() { | |
if (webView instanceof InputAwareWebView) { | |
((InputAwareWebView) webView).unlockInputConnection(); | |
} | |
} | |
// @Override | |
// This is overriding a method that hasn't rolled into stable Flutter yet. Including the | |
// annotation would cause compile time failures in versions of Flutter too old to include the new | |
// method. However leaving it raw like this means that the method will be ignored in old versions | |
// of Flutter but used as an override anyway wherever it's actually defined. | |
// TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. | |
public void onInputConnectionLocked() { | |
if (webView instanceof InputAwareWebView) { | |
((InputAwareWebView) webView).lockInputConnection(); | |
} | |
} | |
// @Override | |
// This is overriding a method that hasn't rolled into stable Flutter yet. Including the | |
// annotation would cause compile time failures in versions of Flutter too old to include the new | |
// method. However leaving it raw like this means that the method will be ignored in old versions | |
// of Flutter but used as an override anyway wherever it's actually defined. | |
// TODO(mklim): Add the @Override annotation once stable passes v1.10.9. | |
public void onFlutterViewAttached(View flutterView) { | |
if (webView instanceof InputAwareWebView) { | |
((InputAwareWebView) webView).setContainerView(flutterView); | |
} | |
} | |
// @Override | |
// This is overriding a method that hasn't rolled into stable Flutter yet. Including the | |
// annotation would cause compile time failures in versions of Flutter too old to include the new | |
// method. However leaving it raw like this means that the method will be ignored in old versions | |
// of Flutter but used as an override anyway wherever it's actually defined. | |
// TODO(mklim): Add the @Override annotation once stable passes v1.10.9. | |
public void onFlutterViewDetached() { | |
if (webView instanceof InputAwareWebView) { | |
((InputAwareWebView) webView).setContainerView(null); | |
} | |
} | |
@Override | |
public void onMethodCall(MethodCall methodCall, Result result) { | |
switch (methodCall.method) { | |
case "loadUrl": | |
loadUrl(methodCall, result); | |
break; | |
case "updateSettings": | |
updateSettings(methodCall, result); | |
break; | |
case "canGoBack": | |
canGoBack(result); | |
break; | |
case "canGoForward": | |
canGoForward(result); | |
break; | |
case "goBack": | |
goBack(result); | |
break; | |
case "goForward": | |
goForward(result); | |
break; | |
case "reload": | |
reload(result); | |
break; | |
case "currentUrl": | |
currentUrl(result); | |
break; | |
case "evaluateJavascript": | |
evaluateJavaScript(methodCall, result); | |
break; | |
case "addJavascriptChannels": | |
addJavaScriptChannels(methodCall, result); | |
break; | |
case "removeJavascriptChannels": | |
removeJavaScriptChannels(methodCall, result); | |
break; | |
case "clearCache": | |
clearCache(result); | |
break; | |
case "getTitle": | |
getTitle(result); | |
break; | |
case "scrollTo": | |
scrollTo(methodCall, result); | |
break; | |
case "scrollBy": | |
scrollBy(methodCall, result); | |
break; | |
case "getScrollX": | |
getScrollX(result); | |
break; | |
case "getScrollY": | |
getScrollY(result); | |
break; | |
default: | |
result.notImplemented(); | |
} | |
} | |
@SuppressWarnings("unchecked") | |
private void loadUrl(MethodCall methodCall, Result result) { | |
Map<String, Object> request = (Map<String, Object>) methodCall.arguments; | |
String url = (String) request.get("url"); | |
Map<String, String> headers = (Map<String, String>) request.get("headers"); | |
if (headers == null) { | |
headers = Collections.emptyMap(); | |
} | |
webView.loadUrl(url, headers); | |
result.success(null); | |
} | |
private void canGoBack(Result result) { | |
result.success(webView.canGoBack()); | |
} | |
private void canGoForward(Result result) { | |
result.success(webView.canGoForward()); | |
} | |
private void goBack(Result result) { | |
if (webView.canGoBack()) { | |
webView.goBack(); | |
} | |
result.success(null); | |
} | |
private void goForward(Result result) { | |
if (webView.canGoForward()) { | |
webView.goForward(); | |
} | |
result.success(null); | |
} | |
private void reload(Result result) { | |
webView.reload(); | |
result.success(null); | |
} | |
private void currentUrl(Result result) { | |
result.success(webView.getUrl()); | |
} | |
@SuppressWarnings("unchecked") | |
private void updateSettings(MethodCall methodCall, Result result) { | |
applySettings((Map<String, Object>) methodCall.arguments); | |
result.success(null); | |
} | |
@TargetApi(Build.VERSION_CODES.KITKAT) | |
private void evaluateJavaScript(MethodCall methodCall, final Result result) { | |
String jsString = (String) methodCall.arguments; | |
if (jsString == null) { | |
throw new UnsupportedOperationException("JavaScript string cannot be null"); | |
} | |
webView.evaluateJavascript( | |
jsString, | |
new android.webkit.ValueCallback<String>() { | |
@Override | |
public void onReceiveValue(String value) { | |
result.success(value); | |
} | |
}); | |
} | |
@SuppressWarnings("unchecked") | |
private void addJavaScriptChannels(MethodCall methodCall, Result result) { | |
List<String> channelNames = (List<String>) methodCall.arguments; | |
registerJavaScriptChannelNames(channelNames); | |
result.success(null); | |
} | |
@SuppressWarnings("unchecked") | |
private void removeJavaScriptChannels(MethodCall methodCall, Result result) { | |
List<String> channelNames = (List<String>) methodCall.arguments; | |
for (String channelName : channelNames) { | |
webView.removeJavascriptInterface(channelName); | |
} | |
result.success(null); | |
} | |
private void clearCache(Result result) { | |
webView.clearCache(true); | |
WebStorage.getInstance().deleteAllData(); | |
result.success(null); | |
} | |
private void getTitle(Result result) { | |
result.success(webView.getTitle()); | |
} | |
private void scrollTo(MethodCall methodCall, Result result) { | |
Map<String, Object> request = methodCall.arguments(); | |
int x = (int) request.get("x"); | |
int y = (int) request.get("y"); | |
webView.scrollTo(x, y); | |
result.success(null); | |
} | |
private void scrollBy(MethodCall methodCall, Result result) { | |
Map<String, Object> request = methodCall.arguments(); | |
int x = (int) request.get("x"); | |
int y = (int) request.get("y"); | |
webView.scrollBy(x, y); | |
result.success(null); | |
} | |
private void getScrollX(Result result) { | |
result.success(webView.getScrollX()); | |
} | |
private void getScrollY(Result result) { | |
result.success(webView.getScrollY()); | |
} | |
private void applySettings(Map<String, Object> settings) { | |
for (String key : settings.keySet()) { | |
switch (key) { | |
case "jsMode": | |
Integer mode = (Integer) settings.get(key); | |
if (mode != null) updateJsMode(mode); | |
break; | |
case "hasNavigationDelegate": | |
final boolean hasNavigationDelegate = (boolean) settings.get(key); | |
final WebViewClient webViewClient = | |
flutterWebViewClient.createWebViewClient(hasNavigationDelegate); | |
webView.setWebViewClient(webViewClient); | |
break; | |
case "debuggingEnabled": | |
final boolean debuggingEnabled = (boolean) settings.get(key); | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | |
webView.setWebContentsDebuggingEnabled(debuggingEnabled); | |
} | |
break; | |
case "hasProgressTracking": | |
flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); | |
break; | |
case "gestureNavigationEnabled": | |
break; | |
case "userAgent": | |
updateUserAgent((String) settings.get(key)); | |
break; | |
case "allowsInlineMediaPlayback": | |
// no-op inline media playback is always allowed on Android. | |
break; | |
default: | |
throw new IllegalArgumentException("Unknown WebView setting: " + key); | |
} | |
} | |
} | |
private void updateJsMode(int mode) { | |
switch (mode) { | |
case 0: // disabled | |
webView.getSettings().setJavaScriptEnabled(false); | |
break; | |
case 1: // unrestricted | |
webView.getSettings().setJavaScriptEnabled(true); | |
break; | |
default: | |
throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); | |
} | |
} | |
private void updateAutoMediaPlaybackPolicy(int mode) { | |
// This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all | |
// other values we require a user gesture. | |
boolean requireUserGesture = mode != 1; | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { | |
webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); | |
} | |
} | |
private void registerJavaScriptChannelNames(List<String> channelNames) { | |
for (String channelName : channelNames) { | |
webView.addJavascriptInterface( | |
new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); | |
} | |
} | |
private void updateUserAgent(String userAgent) { | |
webView.getSettings().setUserAgentString(userAgent); | |
} | |
@Override | |
public void dispose() { | |
methodChannel.setMethodCallHandler(null); | |
if (webView instanceof InputAwareWebView) { | |
((InputAwareWebView) webView).dispose(); | |
} | |
webView.destroy(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<string name="webview_file_chooser_title">Choose a file</string> | |
<string name="webview_image_chooser_title">Choose an image</string> | |
<string name="webview_video_chooser_title">Choose a video</string> | |
</resources> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment