From a58248a2ee642e82956f146cb8235bbdddded4ae Mon Sep 17 00:00:00 2001 From: aaronchu Date: Fri, 3 Jul 2020 11:52:05 +0800 Subject: [PATCH 1/7] TWECACAPP-675 Cherry-pick commit 5943059b1dc2ab6ab67cb0664365a745cc37e91c --- .../InAppWebView/InAppWebView.java | 400 +++++++++++++++--- .../drawable/floating_action_mode_shape.xml | 6 + .../main/res/layout/floating_action_mode.xml | 16 + .../res/layout/floating_action_mode_item.xml | 12 + 4 files changed, 379 insertions(+), 55 deletions(-) create mode 100644 android/src/main/res/drawable/floating_action_mode_shape.xml create mode 100644 android/src/main/res/layout/floating_action_mode.xml create mode 100644 android/src/main/res/layout/floating_action_mode_item.xml diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index c0faf56fc..944ea7882 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -12,12 +12,16 @@ import android.print.PrintManager; import android.util.AttributeSet; import android.util.Log; +import android.view.ActionMode; import android.view.ContextMenu; +import android.view.GestureDetector; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.ViewParent; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.webkit.CookieManager; import android.webkit.DownloadListener; import android.webkit.ValueCallback; @@ -25,6 +29,9 @@ import android.webkit.WebHistoryItem; import android.webkit.WebSettings; import android.webkit.WebStorage; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; import androidx.annotation.RequiresApi; import androidx.appcompat.widget.PopupMenu; @@ -39,6 +46,7 @@ import com.pichillilorenzo.flutter_inappwebview.FlutterWebView; import com.pichillilorenzo.flutter_inappwebview.InAppBrowserActivity; import com.pichillilorenzo.flutter_inappwebview.JavaScriptBridgeInterface; +import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.Shared; import com.pichillilorenzo.flutter_inappwebview.Util; @@ -72,6 +80,17 @@ final public class InAppWebView extends InputAwareWebView { int okHttpClientCacheSize = 10 * 1024 * 1024; // 10MB public ContentBlockerHandler contentBlockerHandler = new ContentBlockerHandler(); public Pattern regexToCancelSubFramesLoadingCompiled; + private GestureDetector gestureDetector = null; + private MotionEvent motionEvent = null; + private LinearLayout floatingContextMenu = null; + public Handler headlessHandler = new Handler(Looper.getMainLooper()); + + private Runnable checkScrollStoppedTask; + private int initialPositionScrollStoppedTask; + private int newCheckScrollStoppedTask = 100; + + private Runnable selectedTextTask; + private int newCheckSelectedTextTask = 100; static final String consoleLogJS = "(function(console) {" + " var oldLogs = {" + @@ -515,6 +534,18 @@ final public class InAppWebView extends InputAwareWebView { " };" + "})(window.fetch);"; + static final String getSelectedTextJS = "(function(){" + + " var txt;" + + " if (window.getSelection) {" + + " txt = window.getSelection().toString();" + + " } else if (window.document.getSelection) {" + + " txt = window.document.getSelection().toString();" + + " } else if (window.document.selection) {" + + " txt = window.document.selection.createRange().text;" + + " }" + + " return txt;" + + "})();"; + public InAppWebView(Context context) { super(context); } @@ -536,7 +567,7 @@ else if (obj instanceof FlutterWebView) this.channel = (this.inAppBrowserActivity != null) ? this.inAppBrowserActivity.channel : this.flutterWebView.channel; this.id = id; this.options = options; - //Shared.activity.registerForContextMenu(this); + Shared.activity.registerForContextMenu(this); } @Override @@ -689,12 +720,67 @@ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, bo } }); - setOnTouchListener(new View.OnTouchListener() { + setVerticalScrollBarEnabled(!options.disableVerticalScroll); + setHorizontalScrollBarEnabled(!options.disableHorizontalScroll); + + gestureDetector = new GestureDetector(this.getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent ev) { + if (floatingContextMenu != null) { + hideContextMenu(); + } + return super.onSingleTapUp(ev); + } + }); + + checkScrollStoppedTask = new Runnable() { + @Override + public void run() { + int newPosition = getScrollY(); + if(initialPositionScrollStoppedTask - newPosition == 0){ + // has stopped + onScrollStopped(); + } else { + initialPositionScrollStoppedTask = getScrollY(); + headlessHandler.postDelayed(checkScrollStoppedTask, newCheckScrollStoppedTask); + } + } + }; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + selectedTextTask = new Runnable() { + @Override + public void run() { + if (floatingContextMenu != null) { + getSelectedText(new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value == null || value.length() == 0) { + if (floatingContextMenu != null) { + hideContextMenu(); + } + } else { + headlessHandler.postDelayed(selectedTextTask, newCheckSelectedTextTask); + } + } + }); + } + } + }; + } + + setOnTouchListener(new OnTouchListener() { float m_downX; float m_downY; @Override public boolean onTouch(View v, MotionEvent event) { + gestureDetector.onTouchEvent(event); + + if (event.getAction() == MotionEvent.ACTION_UP) { + checkScrollStoppedTask.run(); + } + if (options.disableHorizontalScroll && options.disableVerticalScroll) { return (event.getAction() == MotionEvent.ACTION_MOVE); } @@ -743,13 +829,7 @@ public boolean onLongClick(View v) { }); } - private Point lastTouch; - - @Override - public boolean onTouchEvent(MotionEvent ev) { - lastTouch = new Point((int) ev.getX(), (int) ev.getY()) ; - return super.onTouchEvent(ev); - } + private MotionEvent lastMotionEvent = null; public void setIncognito(boolean enabled) { WebSettings settings = getSettings(); @@ -888,7 +968,7 @@ public void clearAllCache() { } public void takeScreenshot(final MethodChannel.Result result) { - post(new Runnable() { + headlessHandler.post(new Runnable() { @Override public void run() { int height = (int) (getContentHeight() * scale + 0.5); @@ -1190,7 +1270,7 @@ public void injectDeferredObject(String source, String jsWrapper, final MethodCh scriptToInject = String.format(jsWrapper, jsonSourceString); } final String finalScriptToInject = scriptToInject; - post(new Runnable() { + headlessHandler.post(new Runnable() { @Override public void run() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { @@ -1266,6 +1346,11 @@ protected void onScrollChanged (int l, int x = (int) (l/scale); int y = (int) (t/scale); + if (floatingContextMenu != null) { + floatingContextMenu.setAlpha(0f); + floatingContextMenu.setVisibility(View.GONE); + } + Map obj = new HashMap<>(); if (inAppBrowserActivity != null) obj.put("uuid", inAppBrowserActivity.uuid); @@ -1348,69 +1433,273 @@ public void printCurrentPage() { public Float getUpdatedScale() { return scale; } -/* + @Override public void onCreateContextMenu(ContextMenu menu) { - Log.d(LOG_TAG, getHitTestResult().getType() + ""); - String extra = getHitTestResult().getExtra(); - //if (getHitTestResult().getType() == 7 || getHitTestResult().getType() == 5) - if (extra != null) - Log.d(LOG_TAG, extra); - Log.d(LOG_TAG, "\n\nonCreateContextMenu\n\n"); - - for(int i = 0; i < menu.size(); i++) { - Log.d(LOG_TAG, menu.getItem(i).toString()); - } + super.onCreateContextMenu(menu); + sendOnCreateContextMenuEvent(); } - private Integer mActionMode; - private CustomActionModeCallback mActionModeCallback; + private void sendOnCreateContextMenuEvent() { + HitTestResult hitTestResult = getHitTestResult(); + Map hitTestResultMap = new HashMap<>(); + hitTestResultMap.put("type", hitTestResult.getType()); + hitTestResultMap.put("extra", hitTestResult.getExtra()); + + Map obj = new HashMap<>(); + if (inAppBrowserActivity != null) + obj.put("uuid", inAppBrowserActivity.uuid); + obj.put("hitTestResult", hitTestResultMap); + channel.invokeMethod("onCreateContextMenu", obj); + } + + private Point contextMenuPoint = new Point(0, 0); + private Point lastTouch = new Point(0, 0); @Override - public ActionMode startActionMode(ActionMode.Callback callback, int mode) { - Log.d(LOG_TAG, "startActionMode"); - ViewParent parent = getParent(); - if (parent == null || mActionMode != null) { - return null; + public boolean onTouchEvent(MotionEvent ev) { + lastTouch = new Point((int) ev.getX(), (int) ev.getY()); + return super.onTouchEvent(ev); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + return rebuildActionMode(super.startActionMode(callback), callback); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public ActionMode startActionMode(ActionMode.Callback callback, int type) { + return rebuildActionMode(super.startActionMode(callback, type), callback); + } + + public ActionMode rebuildActionMode( + final ActionMode actionMode, + final ActionMode.Callback callback + ) { + boolean hasBeenRemovedAndRebuilt = false; + if (floatingContextMenu != null) { + hideContextMenu(); + hasBeenRemovedAndRebuilt = true; } - mActionModeCallback = new CustomActionModeCallback(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - mActionMode = ActionMode.TYPE_FLOATING; - //return Shared.activity.getWindow().getDecorView().startActionMode(mActionModeCallback, mActionMode); - return parent.startActionModeForChild(this, mActionModeCallback, mActionMode); - } else { - return parent.startActionModeForChild(this, mActionModeCallback); + if (actionMode == null) { + return null; } - } - private class CustomActionModeCallback implements ActionMode.Callback { + Menu actionMenu = actionMode.getMenu(); + if (options.disableContextMenu) { + actionMenu.clear(); + return actionMode; + } - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { + floatingContextMenu = (LinearLayout) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode, this, false); + HorizontalScrollView horizontalScrollView = (HorizontalScrollView) floatingContextMenu.getChildAt(0); + LinearLayout menuItemListLayout = (LinearLayout) horizontalScrollView.getChildAt(0); + + for (int i = 0; i < actionMenu.size(); i++) { + final MenuItem menuItem = actionMenu.getItem(i); + final int itemId = menuItem.getItemId(); + final String itemTitle = menuItem.getTitle().toString(); + TextView text = (TextView) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode_item, this, false); + text.setText(itemTitle); + text.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + hideContextMenu(); + callback.onActionItemClicked(actionMode, menuItem); + + Map obj = new HashMap<>(); + if (inAppBrowserActivity != null) + obj.put("uuid", inAppBrowserActivity.uuid); + obj.put("androidId", itemId); + obj.put("iosId", null); + obj.put("title", itemTitle); + channel.invokeMethod("onContextMenuActionItemClicked", obj); + } + }); + if (floatingContextMenu != null) { + menuItemListLayout.addView(text); + } + } - menu.add("ciao"); + if (contextMenu != null) { + List> customMenuItems = (List>) contextMenu.get("menuItems"); + if (customMenuItems != null) { + for (final HashMap menuItem : customMenuItems) { + final int itemId = (int) menuItem.get("androidId"); + final String itemTitle = (String) menuItem.get("title"); + TextView text = (TextView) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode_item, this, false); + text.setText(itemTitle); + text.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + clearFocus(); + hideContextMenu(); + + Map obj = new HashMap<>(); + if (inAppBrowserActivity != null) + obj.put("uuid", inAppBrowserActivity.uuid); + obj.put("androidId", itemId); + obj.put("iosId", null); + obj.put("title", itemTitle); + channel.invokeMethod("onContextMenuActionItemClicked", obj); + } + }); + if (floatingContextMenu != null) { + menuItemListLayout.addView(text); - return true; + } + } + } } - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; + final int x = (lastTouch != null) ? lastTouch.x : 0; + final int y = (lastTouch != null) ? lastTouch.y : 0; + contextMenuPoint = new Point(x, y); + + if (floatingContextMenu != null) { + floatingContextMenu.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + if (floatingContextMenu != null) { + floatingContextMenu.getViewTreeObserver().removeOnGlobalLayoutListener(this); + if (getSettings().getJavaScriptEnabled()) { + onScrollStopped(); + } else { + onFloatingActionGlobalLayout(x, y); + } + } + } + }); + addView(floatingContextMenu, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, x, y)); + if (hasBeenRemovedAndRebuilt) { + sendOnCreateContextMenuEvent(); + } + if (selectedTextTask != null) { + selectedTextTask.run(); + } } + actionMenu.clear(); - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - mode.finish(); - return true; + return actionMode; + } + + public void onFloatingActionGlobalLayout(int x, int y) { + int maxWidth = getWidth(); + int maxHeight = getHeight(); + int width = floatingContextMenu.getWidth(); + int height = floatingContextMenu.getHeight(); + int curx = x - (width / 2); + if (curx < 0) { + curx = 0; + } else if (curx + width > maxWidth) { + curx = maxWidth - width; + } + // float size = 12 * scale; + float cury = y - (height * 1.5f); + if (cury < 0) { + cury = y + height; } - // Called when the user exits the action mode - @Override - public void onDestroyActionMode(ActionMode mode) { - clearFocus(); + updateViewLayout( + floatingContextMenu, + new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, curx, ((int) cury) + getScrollY()) + ); + + headlessHandler.post(new Runnable() { + @Override + public void run() { + if (floatingContextMenu != null) { + floatingContextMenu.setVisibility(View.VISIBLE); + floatingContextMenu.animate().alpha(1f).setDuration(100).setListener(null); + } + } + }); + } + + public void hideContextMenu() { + removeView(floatingContextMenu); + floatingContextMenu = null; + onHideContextMenu(); + } + + public void onHideContextMenu() { + Map obj = new HashMap<>(); + if (inAppBrowserActivity != null) + obj.put("uuid", inAppBrowserActivity.uuid); + channel.invokeMethod("onHideContextMenu", obj); + } + + public void onScrollStopped() { + if (floatingContextMenu != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + adjustFloatingContextMenuPosition(); } } -*/ + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void adjustFloatingContextMenuPosition() { + evaluateJavascript("(function(){" + + " var selection = window.getSelection();" + + " var rangeY = null;" + + " if (selection != null && selection.rangeCount > 0) {" + + " var range = selection.getRangeAt(0);" + + " var clientRect = range.getClientRects();" + + " if (clientRect.length > 0) {" + + " rangeY = clientRect[0].y;" + + " } else if (document.activeElement) {" + + " var boundingClientRect = document.activeElement.getBoundingClientRect();" + + " rangeY = boundingClientRect.y;" + + " }" + + " }" + + " return rangeY;" + + "})();", new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (floatingContextMenu != null) { + if (value != null) { + int x = contextMenuPoint.x; + int y = (int) ((Float.parseFloat(value) * scale) + (floatingContextMenu.getHeight() / 3.5)); + contextMenuPoint.y = y; + onFloatingActionGlobalLayout(x, y); + } else { + floatingContextMenu.setVisibility(View.VISIBLE); + floatingContextMenu.animate().alpha(1f).setDuration(100).setListener(null); + } + } + } + }); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void getSelectedText(final ValueCallback resultCallback) { + evaluateJavascript(getSelectedTextJS, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + value = (value != null) ? value.substring(1, value.length() - 1) : null; + resultCallback.onReceiveValue(value); + } + }); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void getSelectedText(final MethodChannel.Result result) { + getSelectedText(new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } + @Override public void dispose() { super.dispose(); @@ -1418,6 +1707,7 @@ public void dispose() { @Override public void destroy() { + headlessHandler.removeCallbacksAndMessages(null); super.destroy(); } } diff --git a/android/src/main/res/drawable/floating_action_mode_shape.xml b/android/src/main/res/drawable/floating_action_mode_shape.xml new file mode 100644 index 000000000..9ad086b97 --- /dev/null +++ b/android/src/main/res/drawable/floating_action_mode_shape.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/src/main/res/layout/floating_action_mode.xml b/android/src/main/res/layout/floating_action_mode.xml new file mode 100644 index 000000000..b58cc7cbc --- /dev/null +++ b/android/src/main/res/layout/floating_action_mode.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/android/src/main/res/layout/floating_action_mode_item.xml b/android/src/main/res/layout/floating_action_mode_item.xml new file mode 100644 index 000000000..3ce3c1117 --- /dev/null +++ b/android/src/main/res/layout/floating_action_mode_item.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file From afa51df7647b5227ae6baea40403aec5f7036117 Mon Sep 17 00:00:00 2001 From: aaronchu Date: Fri, 3 Jul 2020 12:00:57 +0800 Subject: [PATCH 2/7] TWECACAPP-675 Cherry-pick commit f569e369f433d218dac78f43462c83be1c5c4b1d --- .../InAppWebView/InAppWebView.java | 149 ++++++++++-------- .../InAppWebView/InAppWebViewClient.java | 4 + .../JavaScriptBridgeInterface.java | 15 ++ 3 files changed, 103 insertions(+), 65 deletions(-) diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index 944ea7882..7b6c96b8a 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -5,8 +5,9 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Point; -import android.graphics.Rect; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintManager; @@ -33,12 +34,6 @@ import android.widget.LinearLayout; import android.widget.TextView; -import androidx.annotation.RequiresApi; -import androidx.appcompat.widget.PopupMenu; - -import android.view.ActionMode; -import android.webkit.WebView; - import com.pichillilorenzo.flutter_inappwebview.ContentBlocker.ContentBlocker; import com.pichillilorenzo.flutter_inappwebview.ContentBlocker.ContentBlockerAction; import com.pichillilorenzo.flutter_inappwebview.ContentBlocker.ContentBlockerHandler; @@ -58,6 +53,7 @@ import java.util.Map; import java.util.regex.Pattern; +import androidx.annotation.RequiresApi; import io.flutter.plugin.common.MethodChannel; import okhttp3.OkHttpClient; @@ -80,17 +76,16 @@ final public class InAppWebView extends InputAwareWebView { int okHttpClientCacheSize = 10 * 1024 * 1024; // 10MB public ContentBlockerHandler contentBlockerHandler = new ContentBlockerHandler(); public Pattern regexToCancelSubFramesLoadingCompiled; - private GestureDetector gestureDetector = null; - private MotionEvent motionEvent = null; - private LinearLayout floatingContextMenu = null; + public GestureDetector gestureDetector = null; + public LinearLayout floatingContextMenu = null; public Handler headlessHandler = new Handler(Looper.getMainLooper()); - private Runnable checkScrollStoppedTask; - private int initialPositionScrollStoppedTask; - private int newCheckScrollStoppedTask = 100; + public Runnable checkScrollStoppedTask; + public int initialPositionScrollStoppedTask; + public int newCheckScrollStoppedTask = 100; // ms - private Runnable selectedTextTask; - private int newCheckSelectedTextTask = 100; + public Runnable checkContextMenuShouldBeClosedTask; + public int newCheckContextMenuShouldBeClosedTaskTask = 100; // ms static final String consoleLogJS = "(function(console) {" + " var oldLogs = {" + @@ -120,7 +115,7 @@ final public class InAppWebView extends InputAwareWebView { static final String printJS = "window.print = function() {" + " window." + JavaScriptBridgeInterface.name + ".callHandler('onPrint', window.location.href);" + - "}"; + "};"; static final String platformReadyJS = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));"; @@ -534,6 +529,14 @@ final public class InAppWebView extends InputAwareWebView { " };" + "})(window.fetch);"; + static final String isActiveElementInputEditableJS = + "var activeEl = document.activeElement;" + + "var nodeName = (activeEl != null) ? activeEl.nodeName.toLowerCase() : '';" + + "var isActiveElementInputEditable = activeEl != null && " + + "(activeEl.nodeType == 1 && (nodeName == 'textarea' || (nodeName == 'input' && /^(?:text|email|number|search|tel|url|password)$/i.test(activeEl.type != null ? activeEl.type : 'text')))) && " + + "!activeEl.disabled && !activeEl.readOnly;" + + "var isActiveElementEditable = isActiveElementInputEditable || (activeEl != null && activeEl.isContentEditable) || document.designMode === 'on';"; + static final String getSelectedTextJS = "(function(){" + " var txt;" + " if (window.getSelection) {" + @@ -546,6 +549,48 @@ final public class InAppWebView extends InputAwareWebView { " return txt;" + "})();"; + // android Workaround to hide context menu when selected text is empty + // and the document active element is not an input element. + static final String checkContextMenuShouldBeHiddenJS = "(function(){" + + " var txt;" + + " if (window.getSelection) {" + + " txt = window.getSelection().toString();" + + " } else if (window.document.getSelection) {" + + " txt = window.document.getSelection().toString();" + + " } else if (window.document.selection) {" + + " txt = window.document.selection.createRange().text;" + + " }" + + isActiveElementInputEditableJS + + " return txt === '' && !isActiveElementEditable;" + + "})();"; + + // android Workaround to hide context menu when user emit a keydown event + static final String checkGlobalKeyDownEventToHideContextMenuJS = "(function(){" + + " document.addEventListener('keydown', function(e) {" + + " window." + JavaScriptBridgeInterface.name + "._hideContextMenu();" + + " });" + + "})();"; + + // android Workaround to hide the Keyboard when the user click outside + // on something not focusable such as input or a textarea. + static final String androidKeyboardWorkaroundFocusoutEventJS = "(function(){" + + " var isFocusin = false;" + + " document.addEventListener('focusin', function(e) {" + + " var nodeName = e.target.nodeName.toLowerCase();" + + " var isInputButton = nodeName === 'input' && e.target.type != null && e.target.type === 'button';" + + " isFocusin = (['a', 'area', 'button', 'details', 'iframe', 'select', 'summary'].indexOf(nodeName) >= 0 || isInputButton) ? false : true;" + + " });" + + " document.addEventListener('focusout', function(e) {" + + " isFocusin = false;" + + " setTimeout(function() {" + + isActiveElementInputEditableJS + + " if (!isFocusin && !isActiveElementEditable) {" + + " window." + JavaScriptBridgeInterface.name + ".callHandler('androidKeyboardWorkaroundFocusoutEvent');" + + " }" + + " }, 300);" + + " });" + + "})();"; + public InAppWebView(Context context) { super(context); } @@ -748,19 +793,19 @@ public void run() { }; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - selectedTextTask = new Runnable() { + checkContextMenuShouldBeClosedTask = new Runnable() { @Override public void run() { if (floatingContextMenu != null) { - getSelectedText(new ValueCallback() { + evaluateJavascript(checkContextMenuShouldBeHiddenJS, new ValueCallback() { @Override public void onReceiveValue(String value) { - if (value == null || value.length() == 0) { + if (value == null || value.equals("true")) { if (floatingContextMenu != null) { hideContextMenu(); } } else { - headlessHandler.postDelayed(selectedTextTask, newCheckSelectedTextTask); + headlessHandler.postDelayed(checkContextMenuShouldBeClosedTask, newCheckContextMenuShouldBeClosedTaskTask); } } }); @@ -1420,14 +1465,18 @@ public void printCurrentPage() { PrintManager printManager = (PrintManager) Shared.activity.getApplicationContext() .getSystemService(Context.PRINT_SERVICE); - String jobName = getTitle() + " Document"; + if (printManager != null) { + String jobName = getTitle() + " Document"; - // Get a printCurrentPage adapter instance - PrintDocumentAdapter printAdapter = createPrintDocumentAdapter(jobName); + // Get a printCurrentPage adapter instance + PrintDocumentAdapter printAdapter = createPrintDocumentAdapter(jobName); - // Create a printCurrentPage job with name and adapter instance - printManager.print(jobName, printAdapter, - new PrintAttributes.Builder().build()); + // Create a printCurrentPage job with name and adapter instance + printManager.print(jobName, printAdapter, + new PrintAttributes.Builder().build()); + } else { + Log.e(LOG_TAG, "No PrintManager available"); + } } public Float getUpdatedScale() { @@ -1440,6 +1489,12 @@ public void onCreateContextMenu(ContextMenu menu) { sendOnCreateContextMenuEvent(); } + @Override + public boolean onCheckIsTextEditor() { + Log.d(LOG_TAG, "onCheckIsTextEditor"); + return super.onCheckIsTextEditor(); + } + private void sendOnCreateContextMenuEvent() { HitTestResult hitTestResult = getHitTestResult(); Map hitTestResultMap = new HashMap<>(); @@ -1491,17 +1546,13 @@ public ActionMode rebuildActionMode( return null; } - Menu actionMenu = actionMode.getMenu(); - if (options.disableContextMenu) { - actionMenu.clear(); - return actionMode; - } floatingContextMenu = (LinearLayout) LayoutInflater.from(this.getContext()) .inflate(R.layout.floating_action_mode, this, false); HorizontalScrollView horizontalScrollView = (HorizontalScrollView) floatingContextMenu.getChildAt(0); LinearLayout menuItemListLayout = (LinearLayout) horizontalScrollView.getChildAt(0); + Menu actionMenu = actionMode.getMenu(); for (int i = 0; i < actionMenu.size(); i++) { final MenuItem menuItem = actionMenu.getItem(i); final int itemId = menuItem.getItemId(); @@ -1529,38 +1580,6 @@ public void onClick(View v) { } } - if (contextMenu != null) { - List> customMenuItems = (List>) contextMenu.get("menuItems"); - if (customMenuItems != null) { - for (final HashMap menuItem : customMenuItems) { - final int itemId = (int) menuItem.get("androidId"); - final String itemTitle = (String) menuItem.get("title"); - TextView text = (TextView) LayoutInflater.from(this.getContext()) - .inflate(R.layout.floating_action_mode_item, this, false); - text.setText(itemTitle); - text.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - clearFocus(); - hideContextMenu(); - - Map obj = new HashMap<>(); - if (inAppBrowserActivity != null) - obj.put("uuid", inAppBrowserActivity.uuid); - obj.put("androidId", itemId); - obj.put("iosId", null); - obj.put("title", itemTitle); - channel.invokeMethod("onContextMenuActionItemClicked", obj); - } - }); - if (floatingContextMenu != null) { - menuItemListLayout.addView(text); - - } - } - } - } - final int x = (lastTouch != null) ? lastTouch.x : 0; final int y = (lastTouch != null) ? lastTouch.y : 0; contextMenuPoint = new Point(x, y); @@ -1584,8 +1603,8 @@ public void onGlobalLayout() { if (hasBeenRemovedAndRebuilt) { sendOnCreateContextMenuEvent(); } - if (selectedTextTask != null) { - selectedTextTask.run(); + if (checkContextMenuShouldBeClosedTask != null) { + checkContextMenuShouldBeClosedTask.run(); } } actionMenu.clear(); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java index 023e8f961..e35ef824f 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java @@ -181,6 +181,10 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) { if (webView.options.useOnLoadResource) { js += InAppWebView.resourceObserverJS.replaceAll("[\r\n]+", ""); } + js += InAppWebView.checkGlobalKeyDownEventToHideContextMenuJS.replaceAll("[\r\n]+", ""); + if (flutterWebView != null) { + js += InAppWebView.androidKeyboardWorkaroundFocusoutEventJS.replaceAll("[\r\n]+", ""); + } js += InAppWebView.printJS.replaceAll("[\r\n]+", ""); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java index 0ab277c13..9811fa347 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java @@ -42,6 +42,21 @@ else if (obj instanceof FlutterWebView) this.channel = (this.inAppBrowserActivity != null) ? this.inAppBrowserActivity.channel : this.flutterWebView.channel; } + @JavascriptInterface + public void _hideContextMenu() { + final InAppWebView webView = (inAppBrowserActivity != null) ? inAppBrowserActivity.webView : flutterWebView.webView; + + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + if (webView != null && webView.floatingContextMenu != null) { + webView.hideContextMenu(); + } + } + }); + } + @JavascriptInterface public void _callHandler(final String handlerName, final String _callHandlerID, final String args) { final InAppWebView webView = (inAppBrowserActivity != null) ? inAppBrowserActivity.webView : flutterWebView.webView; From 937226a313c48349ee7fe22cc04115b8fd9b9512 Mon Sep 17 00:00:00 2001 From: aaronchu Date: Fri, 3 Jul 2020 18:45:54 +0800 Subject: [PATCH 3/7] TWECACAPP-698 Remove unnecessary code --- .../InAppWebView/InAppWebView.java | 74 ------------------- 1 file changed, 74 deletions(-) diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index 7b6c96b8a..8c116b983 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -612,7 +612,6 @@ else if (obj instanceof FlutterWebView) this.channel = (this.inAppBrowserActivity != null) ? this.inAppBrowserActivity.channel : this.flutterWebView.channel; this.id = id; this.options = options; - Shared.activity.registerForContextMenu(this); } @Override @@ -1483,31 +1482,6 @@ public Float getUpdatedScale() { return scale; } - @Override - public void onCreateContextMenu(ContextMenu menu) { - super.onCreateContextMenu(menu); - sendOnCreateContextMenuEvent(); - } - - @Override - public boolean onCheckIsTextEditor() { - Log.d(LOG_TAG, "onCheckIsTextEditor"); - return super.onCheckIsTextEditor(); - } - - private void sendOnCreateContextMenuEvent() { - HitTestResult hitTestResult = getHitTestResult(); - Map hitTestResultMap = new HashMap<>(); - hitTestResultMap.put("type", hitTestResult.getType()); - hitTestResultMap.put("extra", hitTestResult.getExtra()); - - Map obj = new HashMap<>(); - if (inAppBrowserActivity != null) - obj.put("uuid", inAppBrowserActivity.uuid); - obj.put("hitTestResult", hitTestResultMap); - channel.invokeMethod("onCreateContextMenu", obj); - } - private Point contextMenuPoint = new Point(0, 0); private Point lastTouch = new Point(0, 0); @@ -1517,11 +1491,6 @@ public boolean onTouchEvent(MotionEvent ev) { return super.onTouchEvent(ev); } - @Override - public boolean dispatchTouchEvent(MotionEvent event) { - return super.dispatchTouchEvent(event); - } - @Override public ActionMode startActionMode(ActionMode.Callback callback) { return rebuildActionMode(super.startActionMode(callback), callback); @@ -1537,10 +1506,8 @@ public ActionMode rebuildActionMode( final ActionMode actionMode, final ActionMode.Callback callback ) { - boolean hasBeenRemovedAndRebuilt = false; if (floatingContextMenu != null) { hideContextMenu(); - hasBeenRemovedAndRebuilt = true; } if (actionMode == null) { return null; @@ -1555,7 +1522,6 @@ public ActionMode rebuildActionMode( Menu actionMenu = actionMode.getMenu(); for (int i = 0; i < actionMenu.size(); i++) { final MenuItem menuItem = actionMenu.getItem(i); - final int itemId = menuItem.getItemId(); final String itemTitle = menuItem.getTitle().toString(); TextView text = (TextView) LayoutInflater.from(this.getContext()) .inflate(R.layout.floating_action_mode_item, this, false); @@ -1565,14 +1531,6 @@ public ActionMode rebuildActionMode( public void onClick(View v) { hideContextMenu(); callback.onActionItemClicked(actionMode, menuItem); - - Map obj = new HashMap<>(); - if (inAppBrowserActivity != null) - obj.put("uuid", inAppBrowserActivity.uuid); - obj.put("androidId", itemId); - obj.put("iosId", null); - obj.put("title", itemTitle); - channel.invokeMethod("onContextMenuActionItemClicked", obj); } }); if (floatingContextMenu != null) { @@ -1600,9 +1558,6 @@ public void onGlobalLayout() { } }); addView(floatingContextMenu, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, x, y)); - if (hasBeenRemovedAndRebuilt) { - sendOnCreateContextMenuEvent(); - } if (checkContextMenuShouldBeClosedTask != null) { checkContextMenuShouldBeClosedTask.run(); } @@ -1648,14 +1603,6 @@ public void run() { public void hideContextMenu() { removeView(floatingContextMenu); floatingContextMenu = null; - onHideContextMenu(); - } - - public void onHideContextMenu() { - Map obj = new HashMap<>(); - if (inAppBrowserActivity != null) - obj.put("uuid", inAppBrowserActivity.uuid); - channel.invokeMethod("onHideContextMenu", obj); } public void onScrollStopped() { @@ -1698,27 +1645,6 @@ public void onReceiveValue(String value) { }); } - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - public void getSelectedText(final ValueCallback resultCallback) { - evaluateJavascript(getSelectedTextJS, new ValueCallback() { - @Override - public void onReceiveValue(String value) { - value = (value != null) ? value.substring(1, value.length() - 1) : null; - resultCallback.onReceiveValue(value); - } - }); - } - - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - public void getSelectedText(final MethodChannel.Result result) { - getSelectedText(new ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - @Override public void dispose() { super.dispose(); From 2a2fba723c0834d4270a8fee4aff4a2a6876dcc2 Mon Sep 17 00:00:00 2001 From: aaronchu Date: Mon, 6 Jul 2020 12:04:14 +0800 Subject: [PATCH 4/7] TWECACAPP-698 Show copy, paste, cut, and select all actions only --- .../InAppWebView/InAppWebView.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index 8c116b983..aea3212c7 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -48,9 +48,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Pattern; import androidx.annotation.RequiresApi; @@ -1513,6 +1516,20 @@ public ActionMode rebuildActionMode( return null; } + // We only support copy, cut, paste, and selectAll action. For other actions provided by other + // applications, the callback.onActionItemClicked(actionMode, menuItem) causes crash due to the + // following error: + // + // java.lang.RuntimeException: This method is only implemented for Activity-based Contexts. + // Check canStartActivityForResult() before calling. + // at android.content.Context.startActivityForResult(Context.java:1774) + // ... + final Set supportedActionSet = new HashSet<>(Arrays.asList( + getContext().getString(android.R.string.copy), + getContext().getString(android.R.string.cut), + getContext().getString(android.R.string.paste), + getContext().getString(android.R.string.selectAll) + )); floatingContextMenu = (LinearLayout) LayoutInflater.from(this.getContext()) .inflate(R.layout.floating_action_mode, this, false); @@ -1523,6 +1540,11 @@ public ActionMode rebuildActionMode( for (int i = 0; i < actionMenu.size(); i++) { final MenuItem menuItem = actionMenu.getItem(i); final String itemTitle = menuItem.getTitle().toString(); + + if (!supportedActionSet.contains(itemTitle)) { + continue; + } + TextView text = (TextView) LayoutInflater.from(this.getContext()) .inflate(R.layout.floating_action_mode_item, this, false); text.setText(itemTitle); From 0223fa717bd361830de23b0c0b61de7cf7fee7dd Mon Sep 17 00:00:00 2001 From: aaronchu Date: Mon, 6 Jul 2020 12:08:23 +0800 Subject: [PATCH 5/7] TWECACAPP-698 Remove unnecessary setVerticalScrollBarEnabled and setHorizontalScrollBarEnabled --- .../flutter_inappwebview/InAppWebView/InAppWebView.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index aea3212c7..ef3750ec8 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -767,9 +767,6 @@ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, bo } }); - setVerticalScrollBarEnabled(!options.disableVerticalScroll); - setHorizontalScrollBarEnabled(!options.disableHorizontalScroll); - gestureDetector = new GestureDetector(this.getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent ev) { From 15a4bd815704a6df98ab165935478b4d1861590a Mon Sep 17 00:00:00 2001 From: aaronchu Date: Mon, 6 Jul 2020 12:11:47 +0800 Subject: [PATCH 6/7] TWECACAPP-698 Update minSdkVersion to 19 and remove KITKAT if-check --- android/build.gradle | 4 +-- .../InAppWebView/InAppWebView.java | 34 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index b80e9be95..aef6f6978 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ android { compileSdkVersion 29 defaultConfig { - minSdkVersion 17 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -74,4 +74,4 @@ afterEvaluate { } } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index ef3750ec8..df0e25d98 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -791,27 +791,25 @@ public void run() { } }; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - checkContextMenuShouldBeClosedTask = new Runnable() { - @Override - public void run() { - if (floatingContextMenu != null) { - evaluateJavascript(checkContextMenuShouldBeHiddenJS, new ValueCallback() { - @Override - public void onReceiveValue(String value) { - if (value == null || value.equals("true")) { - if (floatingContextMenu != null) { - hideContextMenu(); - } - } else { - headlessHandler.postDelayed(checkContextMenuShouldBeClosedTask, newCheckContextMenuShouldBeClosedTaskTask); + checkContextMenuShouldBeClosedTask = new Runnable() { + @Override + public void run() { + if (floatingContextMenu != null) { + evaluateJavascript(checkContextMenuShouldBeHiddenJS, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value == null || value.equals("true")) { + if (floatingContextMenu != null) { + hideContextMenu(); } + } else { + headlessHandler.postDelayed(checkContextMenuShouldBeClosedTask, newCheckContextMenuShouldBeClosedTaskTask); } - }); - } + } + }); } - }; - } + } + }; setOnTouchListener(new OnTouchListener() { float m_downX; From 32c3f49c9716b36c327b82dfce703ea91dad0084 Mon Sep 17 00:00:00 2001 From: aaronchu Date: Mon, 6 Jul 2020 12:13:23 +0800 Subject: [PATCH 7/7] TWECACAPP-698 Remove unused getSelectedTextJS String --- .../InAppWebView/InAppWebView.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index df0e25d98..2788dbfcd 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -540,18 +540,6 @@ final public class InAppWebView extends InputAwareWebView { "!activeEl.disabled && !activeEl.readOnly;" + "var isActiveElementEditable = isActiveElementInputEditable || (activeEl != null && activeEl.isContentEditable) || document.designMode === 'on';"; - static final String getSelectedTextJS = "(function(){" + - " var txt;" + - " if (window.getSelection) {" + - " txt = window.getSelection().toString();" + - " } else if (window.document.getSelection) {" + - " txt = window.document.getSelection().toString();" + - " } else if (window.document.selection) {" + - " txt = window.document.selection.createRange().text;" + - " }" + - " return txt;" + - "})();"; - // android Workaround to hide context menu when selected text is empty // and the document active element is not an input element. static final String checkContextMenuShouldBeHiddenJS = "(function(){" +