| // 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 org.chromium.weblayer; |
| |
| import android.graphics.Bitmap; |
| import android.net.Uri; |
| import android.os.RemoteException; |
| import android.util.Pair; |
| import android.webkit.ValueCallback; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import org.chromium.weblayer_private.interfaces.APICallException; |
| import org.chromium.weblayer_private.interfaces.IClientNavigation; |
| import org.chromium.weblayer_private.interfaces.IErrorPageCallbackClient; |
| import org.chromium.weblayer_private.interfaces.IFullscreenCallbackClient; |
| import org.chromium.weblayer_private.interfaces.IGoogleAccountsCallbackClient; |
| import org.chromium.weblayer_private.interfaces.IObjectWrapper; |
| import org.chromium.weblayer_private.interfaces.ITab; |
| import org.chromium.weblayer_private.interfaces.ITabClient; |
| import org.chromium.weblayer_private.interfaces.IWebMessageCallbackClient; |
| import org.chromium.weblayer_private.interfaces.IWebMessageReplyProxy; |
| import org.chromium.weblayer_private.interfaces.ObjectWrapper; |
| import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; |
| |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Represents a single tab in a browser. More specifically, owns a NavigationController, and allows |
| * configuring state of the tab, such as delegates and callbacks. |
| */ |
| public class Tab { |
| /** The top level key of the JSON object returned by executeScript(). */ |
| public static final String SCRIPT_RESULT_KEY = "result"; |
| |
| // Maps from id (as returned from ITab.getId()) to Tab. |
| private static final Map<Integer, Tab> sTabMap = new HashMap<Integer, Tab>(); |
| |
| private ITab mImpl; |
| private final NavigationController mNavigationController; |
| private final FindInPageController mFindInPageController; |
| private final MediaCaptureController mMediaCaptureController; |
| private final ObserverList<TabCallback> mCallbacks; |
| private Browser mBrowser; |
| private Profile.DownloadCallbackClientImpl mDownloadCallbackClient; |
| private FullscreenCallbackClientImpl mFullscreenCallbackClient; |
| private NewTabCallback mNewTabCallback; |
| // Id from the remote side. |
| private final int mId; |
| |
| // Constructor for test mocking. |
| protected Tab() { |
| mImpl = null; |
| mNavigationController = null; |
| mFindInPageController = null; |
| mMediaCaptureController = null; |
| mCallbacks = null; |
| mId = 0; |
| } |
| |
| Tab(ITab impl, Browser browser) { |
| mImpl = impl; |
| mBrowser = browser; |
| try { |
| mId = impl.getId(); |
| mImpl.setClient(new TabClientImpl()); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| |
| mCallbacks = new ObserverList<TabCallback>(); |
| mNavigationController = NavigationController.create(mImpl); |
| mFindInPageController = new FindInPageController(mImpl); |
| mMediaCaptureController = new MediaCaptureController(mImpl); |
| registerTab(this); |
| } |
| |
| static void registerTab(Tab tab) { |
| assert getTabById(tab.getId()) == null; |
| sTabMap.put(tab.getId(), tab); |
| } |
| |
| static void unregisterTab(Tab tab) { |
| assert getTabById(tab.getId()) != null; |
| sTabMap.remove(tab.getId()); |
| } |
| |
| static Tab getTabById(int id) { |
| return sTabMap.get(id); |
| } |
| |
| static Set<Tab> getTabsInBrowser(Browser browser) { |
| Set<Tab> tabs = new HashSet<Tab>(); |
| for (Tab tab : sTabMap.values()) { |
| if (tab.getBrowser() == browser) tabs.add(tab); |
| } |
| return tabs; |
| } |
| |
| int getId() { |
| return mId; |
| } |
| |
| void setBrowser(Browser browser) { |
| mBrowser = browser; |
| } |
| |
| @NonNull |
| public Browser getBrowser() { |
| ThreadCheck.ensureOnUiThread(); |
| return mBrowser; |
| } |
| |
| /** |
| * Deprecated. Use Profile.setDownloadCallback instead. |
| */ |
| public void setDownloadCallback(@Nullable DownloadCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| if (callback != null) { |
| mDownloadCallbackClient = new Profile.DownloadCallbackClientImpl(callback); |
| mImpl.setDownloadCallbackClient(mDownloadCallbackClient); |
| } else { |
| mDownloadCallbackClient = null; |
| mImpl.setDownloadCallbackClient(null); |
| } |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| public void setErrorPageCallback(@Nullable ErrorPageCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| mImpl.setErrorPageCallbackClient( |
| callback == null ? null : new ErrorPageCallbackClientImpl(callback)); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| public void setFullscreenCallback(@Nullable FullscreenCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| if (callback != null) { |
| mFullscreenCallbackClient = new FullscreenCallbackClientImpl(callback); |
| mImpl.setFullscreenCallbackClient(mFullscreenCallbackClient); |
| } else { |
| mImpl.setFullscreenCallbackClient(null); |
| mFullscreenCallbackClient = null; |
| } |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Creates a {@link FaviconFetcher} that notifies a {@link FaviconCallback} when the favicon |
| * changes. |
| * |
| * When the fetcher is no longer necessary, call {@link destroy}. Destroying the Tab implicitly |
| * destroys any fetchers that were created. |
| * |
| * A page may provide any number of favicons. This favors a largish favicon. If a previously |
| * cached icon is available, it is used, otherwise the icon is downloaded. |
| * |
| * {@link callback} may be called multiple times for the same navigation. This happens if the |
| * page dynamically updates the favicon. |
| * |
| * @param callback The callback to notify of changes. |
| * |
| * @since 86 |
| */ |
| public @NonNull FaviconFetcher createFaviconFetcher(@NonNull FaviconCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 86) { |
| throw new UnsupportedOperationException(); |
| } |
| return new FaviconFetcher(mImpl, callback); |
| } |
| |
| /** |
| * Sets the target language for translation such that whenever the translate UI shows in this |
| * Tab, the target language will be |targetLanguage|. Notes: |
| * - |targetLanguage| should be specified as the language code (e.g., "de" for German). |
| * - Passing an empty string causes behavior to revert to default. |
| * - Even with the target language specified, the translate UI will not trigger for pages in the |
| * user's locale. |
| * @since 86 |
| */ |
| public void setTranslateTargetLanguage(@NonNull String targetLanguage) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 86) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.setTranslateTargetLanguage(targetLanguage); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Executes the script, and returns the result as a JSON object to the callback if provided. The |
| * object passed to the callback will have a single key SCRIPT_RESULT_KEY which will hold the |
| * result of running the script. |
| * @param useSeparateIsolate If true, runs the script in a separate v8 Isolate. This uses more |
| * memory, but separates the injected scrips from scripts in the page. This prevents any |
| * potentially malicious interaction between first-party scripts in the page, and injected |
| * scripts. Use with caution, only pass false for this argument if you know this isn't an issue |
| * or you need to interact with first-party scripts. |
| */ |
| public void executeScript(@NonNull String script, boolean useSeparateIsolate, |
| @Nullable ValueCallback<JSONObject> callback) { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| ValueCallback<String> stringCallback = (String result) -> { |
| if (callback == null) { |
| return; |
| } |
| |
| try { |
| callback.onReceiveValue( |
| new JSONObject("{\"" + SCRIPT_RESULT_KEY + "\":" + result + "}")); |
| } catch (JSONException e) { |
| // This should never happen since the result should be well formed. |
| throw new RuntimeException(e); |
| } |
| }; |
| mImpl.executeScript(script, useSeparateIsolate, ObjectWrapper.wrap(stringCallback)); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Runs the beforeunload handler for the main frame or any sub frame, if necessary; otherwise, |
| * asynchronously closes the tab. |
| * |
| * If there is a beforeunload handler a dialog is shown to the user which will allow them to |
| * choose whether to proceed with closing the tab. If the WebLayer implementation is < 84 the |
| * closure will be notified via {@link NewTabCallback#onCloseTab}; on 84 and above, WebLayer |
| * closes the tab internally and the embedder will be notified via |
| * TabListCallback#onTabRemoved(). The tab will not close if the user chooses to cancel the |
| * action. If there is no beforeunload handler, the tab closure will be asynchronous (but |
| * immediate) and will be notified in the same way. |
| * |
| * To close the tab synchronously without running beforeunload, use {@link Browser#destroyTab}. |
| * |
| * @since 82 |
| */ |
| public void dispatchBeforeUnloadAndClose() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 82) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.dispatchBeforeUnloadAndClose(); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Dismisses one active transient UI, if any. |
| * |
| * This is useful, for example, to handle presses on the system back button. UI such as tab |
| * modal dialogs, text selection popups and fullscreen will be dismissed. At most one piece of |
| * UI will be dismissed, but this distinction isn't very meaningful in practice since only one |
| * such kind of UI would tend to be active at a time. |
| * |
| * @return true if some piece of UI was dismissed, or false if nothing happened. |
| * |
| * @since 82 |
| */ |
| public boolean dismissTransientUi() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 82) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| return mImpl.dismissTransientUi(); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| public void setNewTabCallback(@Nullable NewTabCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| mNewTabCallback = callback; |
| try { |
| mImpl.setNewTabsEnabled(mNewTabCallback != null); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| @Nullable |
| public FullscreenCallback getFullscreenCallback() { |
| ThreadCheck.ensureOnUiThread(); |
| return mFullscreenCallbackClient != null ? mFullscreenCallbackClient.getCallback() : null; |
| } |
| |
| @NonNull |
| public NavigationController getNavigationController() { |
| ThreadCheck.ensureOnUiThread(); |
| return mNavigationController; |
| } |
| |
| @NonNull |
| public FindInPageController getFindInPageController() { |
| ThreadCheck.ensureOnUiThread(); |
| return mFindInPageController; |
| } |
| |
| @NonNull |
| public MediaCaptureController getMediaCaptureController() { |
| ThreadCheck.ensureOnUiThread(); |
| return mMediaCaptureController; |
| } |
| |
| public void registerTabCallback(@NonNull TabCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| mCallbacks.addObserver(callback); |
| } |
| |
| public void unregisterTabCallback(@NonNull TabCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| mCallbacks.removeObserver(callback); |
| } |
| |
| /** |
| Â * Take a screenshot of this tab and return it as a Bitmap. |
| Â * This API captures only the web content, not any Java Views, including the |
| Â * view in Browser.setTopView. The browser top view shrinks the height of |
| Â * the screenshot if it is not completely hidden. |
| Â * This method will fail if |
| Â * * the Fragment of this Tab is not started during the operation |
| Â * * this tab is not the active tab in its Browser |
| Â * * if scale is not in the range (0, 1] |
| Â * * Bitmap allocation fails |
| Â * The API is asynchronous when successful, but can be synchronous on |
| Â * failure. So embedder must take care when implementing resultCallback to |
| Â * allow reentrancy. |
| * @param scale Scale applied to the Bitmap. |
| Â * @param resultCallback Called when operation is complete. |
| * @since 84 |
| */ |
| public void captureScreenShot(float scale, @NonNull CaptureScreenShotCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 84) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.captureScreenShot(scale, |
| ObjectWrapper.wrap( |
| (ValueCallback<Pair<Bitmap, Integer>>) (Pair<Bitmap, Integer> pair) -> { |
| callback.onScreenShotCaptured(pair.first, pair.second); |
| })); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| ITab getITab() { |
| return mImpl; |
| } |
| |
| /** |
| * Returns a unique id that persists across restarts. |
| * |
| * @return the unique id. |
| * @since 82 |
| */ |
| @NonNull |
| public String getGuid() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 82) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| return mImpl.getGuid(); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Set arbitrary data on the tab. This will be saved and restored with the browser, so it is |
| * important to keep this data as small as possible. |
| * |
| * @param data The data to set, must be smaller than 4K when serialized. A snapshot of this data |
| * is taken, so any changes to the passed in object after this call will not be reflected. |
| * |
| * @throws IllegalArgumentException if the serialzed size of the data exceeds 4K. |
| * |
| * @since 85 |
| */ |
| public void setData(@NonNull Map<String, String> data) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 85) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| if (!mImpl.setData(data)) { |
| throw new IllegalArgumentException("Data given to Tab.setData() was too large."); |
| } |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Get arbitrary data set on the tab with setData(). |
| * |
| * @return the data or an empty map if no data was set. |
| * @since 85 |
| */ |
| @NonNull |
| public Map<String, String> getData() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 85) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| return (Map<String, String>) mImpl.getData(); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Sets a callback to intercept interaction with GAIA accounts. If this callback is set, any |
| * link that would result in a change to a user's GAIA account state will trigger a call to |
| * {@link GoogleAccountsCallback#onGoogleAccountsRequest}. |
| * |
| * @since 86 |
| */ |
| public void setGoogleAccountsCallback(@Nullable GoogleAccountsCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 86) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.setGoogleAccountsCallbackClient( |
| callback == null ? null : new GoogleAccountsCallbackClientImpl(callback)); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Adds a WebMessageCallback and injects a JavaScript object into each frame that the |
| * WebMessageCallback will listen on. |
| * |
| * <p> |
| * The injected JavaScript object will be named {@code jsObjectName} in the global scope. This |
| * will inject the JavaScript object in any frame whose origin matches {@code |
| * allowedOriginRules} for every navigation after this call, and the JavaScript object will be |
| * available immediately when the page begins to load. |
| * |
| * <p> |
| * Each {@code allowedOriginRules} entry must follow the format {@code SCHEME "://" [ |
| * HOSTNAME_PATTERN [ ":" PORT ] ]}, each part is explained in the below table: |
| * |
| * <table> |
| * <col width="25%"> |
| * <tr><th>Rule</th><th>Description</th><th>Example</th></tr> |
| * |
| * <tr> |
| * <td>http/https with hostname</td> |
| * <td>{@code SCHEME} is http or https; {@code HOSTNAME_PATTERN} is a regular hostname; {@code |
| * PORT} is optional, when not present, the rule will match port {@code 80} for http and port |
| * {@code 443} for https.</td> |
| * <td><ul> |
| * <li>{@code https://foobar.com:8080} - Matches https:// URL on port 8080, whose normalized |
| * host is foobar.com.</li> |
| * <li>{@code https://www.example.com} - Matches https:// URL on port 443, whose normalized host |
| * is www.example.com.</li> |
| * </ul></td> |
| * </tr> |
| * |
| * <tr> |
| * <td>http/https with pattern matching</td> |
| * <td>{@code SCHEME} is http or https; {@code HOSTNAME_PATTERN} is a sub-domain matching |
| * pattern with a leading {@code *.}; {@code PORT} is optional, when not present, the rule will |
| * match port {@code 80} for http and port {@code 443} for https.</td> |
| * |
| * <td><ul> |
| * <li>{@code https://*.example.com} - Matches https://calendar.example.com and |
| * https://foo.bar.example.com but not https://example.com.</li> |
| * <li>{@code https://*.example.com:8080} - Matches https://calendar.example.com:8080</li> |
| * </ul></td> |
| * </tr> |
| * |
| * <tr> |
| * <td>http/https with IP literal</td> |
| * <td>{@code SCHEME} is https or https; {@code HOSTNAME_PATTERN} is IP literal; {@code PORT} is |
| * optional, when not present, the rule will match port {@code 80} for http and port {@code 443} |
| * for https.</td> |
| * |
| * <td><ul> |
| * <li>{@code https://127.0.0.1} - Matches https:// URL on port 443, whose IPv4 address is |
| * 127.0.0.1</li> |
| * <li>{@code https://[::1]} or {@code https://[0:0::1]}- Matches any URL to the IPv6 loopback |
| * address with port 443.</li> |
| * <li>{@code https://[::1]:99} - Matches any https:// URL to the IPv6 loopback on port 99.</li> |
| * </ul></td> |
| * </tr> |
| * |
| * <tr> |
| * <td>Custom scheme</td> |
| * <td>{@code SCHEME} is a custom scheme; {@code HOSTNAME_PATTERN} and {@code PORT} must not be |
| * present.</td> |
| * <td><ul> |
| * <li>{@code my-app-scheme://} - Matches any my-app-scheme:// URL.</li> |
| * </ul></td> |
| * </tr> |
| * |
| * <tr><td>{@code *}</td> |
| * <td>Wildcard rule, matches any origin.</td> |
| * <td><ul><li>{@code *}</li></ul></td> |
| * </table> |
| * |
| * <p> |
| * Note that this is a powerful API, as the JavaScript object will be injected when the frame's |
| * origin matches any one of the allowed origins. The HTTPS scheme is strongly recommended for |
| * security; allowing HTTP origins exposes the injected object to any potential network-based |
| * attackers. If a wildcard {@code "*"} is provided, it will inject the JavaScript object to all |
| * frames. A wildcard should only be used if the app wants <b>any</b> third party web page to be |
| * able to use the injected object. When using a wildcard, the app must treat received messages |
| * as untrustworthy and validate any data carefully. |
| * |
| * <p> |
| * This method can be called multiple times to inject multiple JavaScript objects. |
| * |
| * <p> |
| * Let's say the injected JavaScript object is named {@code myObject}. We will have following |
| * methods on that object once it is available to use: |
| * <pre class="prettyprint"> |
| * // message needs to be a JavaScript String. |
| * myObject.postMessage(message) |
| * |
| * // To receive the message posted from the app side. event has a "data" property, which is the |
| * // message string from the app side. |
| * myObject.onmessage(event) |
| * |
| * // To be compatible with DOM EventTarget's addEventListener, it accepts type and listener |
| * // parameters, where type can be only "message" type and listener can only be a JavaScript |
| * // function for myObject. An event object will be passed to listener with a "data" property, |
| * // which is the message string from the app side. |
| * myObject.addEventListener(type, listener) |
| * |
| * // To be compatible with DOM EventTarget's removeEventListener, it accepts type and listener |
| * // parameters, where type can be only "message" type and listener can only be a JavaScript |
| * // function for myObject. |
| * myObject.removeEventListener(type, listener) |
| * </pre> |
| * |
| * <p> |
| * We start the communication between JavaScript and the app from the JavaScript side. In order |
| * to send message from the app to JavaScript, it needs to post a message from JavaScript first, |
| * so the app will have a {@link WebMessageReplyProxy} object to respond. Example: |
| * <pre class="prettyprint"> |
| * // Web page (in JavaScript) |
| * myObject.onmessage = function(event) { |
| * // prints "Got it!" when we receive the app's response. |
| * console.log(event.data); |
| * } |
| * myObject.postMessage("I'm ready!"); |
| * |
| * // App (in Java) |
| * WebMessageCallback callback = new WebMessageCallback() { |
| * @Override |
| * public void onWebMessageReceived(WebMessageReplyProxy proxy, |
| * WebMessage message) { |
| * // do something about view, message, sourceOrigin and isMainFrame. |
| * proxy.postMessage(new WebMessage("Got it!")); |
| * } |
| * }; |
| * tab.registerWebMessageCallback(callback, "myObject", rules); |
| * </pre> |
| * |
| * @param callback The WebMessageCallback to notify of messages. |
| * @param jsObjectName The name to give the injected JavaScript object. |
| * @param allowedOrigins The set of allowed origins. |
| * |
| * @throws IllegalArgumentException if jsObjectName or allowedOrigins is invalid. |
| * |
| * @since 85 |
| */ |
| public void registerWebMessageCallback(@NonNull WebMessageCallback callback, |
| @NonNull String jsObjectName, @NonNull List<String> allowedOrigins) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 85) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.registerWebMessageCallback( |
| jsObjectName, allowedOrigins, new WebMessageCallbackClientImpl(callback)); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Removes the JavaScript object previously registered by way of registerWebMessageCallback. |
| * This impacts future navigations (not any already loaded navigations). |
| * |
| * @param jsObjectName Name of the JavaScript object. |
| * @since 85 |
| */ |
| public void unregisterWebMessageCallback(@NonNull String jsObjectName) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 85) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.unregisterWebMessageCallback(jsObjectName); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Returns true if the content displayed in this tab can be translated. |
| * |
| * @since 85 |
| */ |
| public boolean canTranslate() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 85) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| return mImpl.canTranslate(); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Shows the UI which allows the user to translate the content displayed in this tab. |
| * |
| * @since 85 |
| */ |
| public void showTranslateUi() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 85) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.showTranslateUi(); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| private static final class WebMessageCallbackClientImpl extends IWebMessageCallbackClient.Stub { |
| private final WebMessageCallback mCallback; |
| // Maps from id of IWebMessageReplyProxy to WebMessageReplyProxy. This is done to avoid AIDL |
| // creating an implementation of IWebMessageReplyProxy every time onPostMessage() is called. |
| private final Map<Integer, WebMessageReplyProxy> mProxyIdToProxy = |
| new HashMap<Integer, WebMessageReplyProxy>(); |
| |
| private WebMessageCallbackClientImpl(WebMessageCallback callback) { |
| mCallback = callback; |
| } |
| |
| @Override |
| public void onNewReplyProxy(IWebMessageReplyProxy proxy, int proxyId, boolean isMainFrame, |
| String sourceOrigin) { |
| StrictModeWorkaround.apply(); |
| assert mProxyIdToProxy.get(proxyId) == null; |
| mProxyIdToProxy.put( |
| proxyId, new WebMessageReplyProxy(proxy, isMainFrame, sourceOrigin)); |
| } |
| |
| @Override |
| public void onPostMessage(int proxyId, String string) { |
| StrictModeWorkaround.apply(); |
| assert mProxyIdToProxy.get(proxyId) != null; |
| mCallback.onWebMessageReceived(mProxyIdToProxy.get(proxyId), new WebMessage(string)); |
| } |
| |
| @Override |
| public void onReplyProxyDestroyed(int proxyId) { |
| StrictModeWorkaround.apply(); |
| assert mProxyIdToProxy.get(proxyId) != null; |
| mProxyIdToProxy.remove(proxyId); |
| } |
| } |
| |
| private final class TabClientImpl extends ITabClient.Stub { |
| @Override |
| public void visibleUriChanged(String uriString) { |
| StrictModeWorkaround.apply(); |
| Uri uri = Uri.parse(uriString); |
| for (TabCallback callback : mCallbacks) { |
| callback.onVisibleUriChanged(uri); |
| } |
| } |
| |
| @Override |
| public void onNewTab(int tabId, int mode) { |
| StrictModeWorkaround.apply(); |
| // This should only be hit if setNewTabCallback() has been called with a non-null |
| // value. |
| assert mNewTabCallback != null; |
| Tab tab = getTabById(tabId); |
| // Tab should have already been created by way of BrowserClient. |
| assert tab != null; |
| assert tab.getBrowser() == getBrowser(); |
| mNewTabCallback.onNewTab(tab, mode); |
| } |
| |
| @Override |
| public void onCloseTab() { |
| StrictModeWorkaround.apply(); |
| |
| // Prior to 84 this method was used to signify that the embedder should take action to |
| // close the Tab; 84+ it's deprecated and no longer sent.. |
| assert WebLayer.getSupportedMajorVersionInternal() < 84; |
| |
| // This should only be hit if setNewTabCallback() has been called with a non-null |
| // value. |
| assert mNewTabCallback != null; |
| mNewTabCallback.onCloseTab(); |
| } |
| |
| @Override |
| public void onTabDestroyed() { |
| // Prior to 87 this was called *before* onTabRemoved(). Post 87 this is called after |
| // onTabRemoved(). onTabRemoved() needs the Tab to be registered, so unregisterTab() |
| // should only be called in >= 87 (in < 87 it is called from |
| // Browser.prepareForDestroy()). |
| if (WebLayer.getSupportedMajorVersionInternal() >= 87) { |
| unregisterTab(Tab.this); |
| } |
| // Ensure that the app will fail fast if the embedder mistakenly tries to call back |
| // into the implementation via this Tab. |
| mImpl = null; |
| } |
| |
| @Override |
| public void onRenderProcessGone() { |
| StrictModeWorkaround.apply(); |
| for (TabCallback callback : mCallbacks) { |
| callback.onRenderProcessGone(); |
| } |
| } |
| |
| @Override |
| public void showContextMenu(IObjectWrapper pageUrl, IObjectWrapper linkUrl, |
| IObjectWrapper linkText, IObjectWrapper titleOrAltText, IObjectWrapper srcUrl) { |
| StrictModeWorkaround.apply(); |
| String pageUrlString = ObjectWrapper.unwrap(pageUrl, String.class); |
| String linkUrlString = ObjectWrapper.unwrap(linkUrl, String.class); |
| String srcUrlString = ObjectWrapper.unwrap(srcUrl, String.class); |
| ContextMenuParams params = new ContextMenuParams(Uri.parse(pageUrlString), |
| linkUrlString != null ? Uri.parse(linkUrlString) : null, |
| ObjectWrapper.unwrap(linkText, String.class), |
| ObjectWrapper.unwrap(titleOrAltText, String.class), |
| srcUrlString != null ? Uri.parse(srcUrlString) : null); |
| for (TabCallback callback : mCallbacks) { |
| callback.showContextMenu(params); |
| } |
| } |
| |
| @Override |
| public void onTabModalStateChanged(boolean isTabModalShowing) { |
| StrictModeWorkaround.apply(); |
| for (TabCallback callback : mCallbacks) { |
| callback.onTabModalStateChanged(isTabModalShowing); |
| } |
| } |
| |
| @Override |
| public void onTitleUpdated(IObjectWrapper title) { |
| StrictModeWorkaround.apply(); |
| String titleString = ObjectWrapper.unwrap(title, String.class); |
| for (TabCallback callback : mCallbacks) { |
| callback.onTitleUpdated(titleString); |
| } |
| } |
| |
| @Override |
| public void bringTabToFront() { |
| StrictModeWorkaround.apply(); |
| for (TabCallback callback : mCallbacks) { |
| callback.bringTabToFront(); |
| } |
| } |
| |
| @Override |
| public void onBackgroundColorChanged(int color) { |
| StrictModeWorkaround.apply(); |
| for (TabCallback callback : mCallbacks) { |
| callback.onBackgroundColorChanged(color); |
| } |
| } |
| |
| @Override |
| public void onScrollNotification( |
| @ScrollNotificationType int notificationType, float currentScrollRatio) { |
| StrictModeWorkaround.apply(); |
| for (TabCallback callback : mCallbacks) { |
| callback.onScrollNotification(notificationType, currentScrollRatio); |
| } |
| } |
| } |
| |
| private static final class ErrorPageCallbackClientImpl extends IErrorPageCallbackClient.Stub { |
| private final ErrorPageCallback mCallback; |
| |
| ErrorPageCallbackClientImpl(ErrorPageCallback callback) { |
| mCallback = callback; |
| } |
| |
| public ErrorPageCallback getCallback() { |
| return mCallback; |
| } |
| |
| @Override |
| public boolean onBackToSafety() { |
| StrictModeWorkaround.apply(); |
| return mCallback.onBackToSafety(); |
| } |
| @Override |
| public String getErrorPageContent(IClientNavigation navigation) { |
| StrictModeWorkaround.apply(); |
| ErrorPage errorPage = mCallback.getErrorPage((Navigation) navigation); |
| return errorPage == null ? null : errorPage.htmlContent; |
| } |
| } |
| |
| private static final class FullscreenCallbackClientImpl extends IFullscreenCallbackClient.Stub { |
| private FullscreenCallback mCallback; |
| |
| /* package */ FullscreenCallbackClientImpl(FullscreenCallback callback) { |
| mCallback = callback; |
| } |
| |
| public FullscreenCallback getCallback() { |
| return mCallback; |
| } |
| |
| @Override |
| public void enterFullscreen(IObjectWrapper exitFullscreenWrapper) { |
| StrictModeWorkaround.apply(); |
| ValueCallback<Void> exitFullscreenCallback = (ValueCallback<Void>) ObjectWrapper.unwrap( |
| exitFullscreenWrapper, ValueCallback.class); |
| mCallback.onEnterFullscreen(() -> exitFullscreenCallback.onReceiveValue(null)); |
| } |
| |
| @Override |
| public void exitFullscreen() { |
| StrictModeWorkaround.apply(); |
| mCallback.onExitFullscreen(); |
| } |
| } |
| |
| private static final class GoogleAccountsCallbackClientImpl |
| extends IGoogleAccountsCallbackClient.Stub { |
| private GoogleAccountsCallback mCallback; |
| |
| GoogleAccountsCallbackClientImpl(GoogleAccountsCallback callback) { |
| mCallback = callback; |
| } |
| |
| @Override |
| public void onGoogleAccountsRequest( |
| int serviceType, String email, String continueUrl, boolean isSameTab) { |
| StrictModeWorkaround.apply(); |
| mCallback.onGoogleAccountsRequest(new GoogleAccountsParams( |
| serviceType, email, Uri.parse(continueUrl), isSameTab)); |
| } |
| |
| @Override |
| public String getGaiaId() { |
| StrictModeWorkaround.apply(); |
| return mCallback.getGaiaId(); |
| } |
| |
| @Override |
| public String getFullName() { |
| StrictModeWorkaround.apply(); |
| return mCallback.getFullName(); |
| } |
| |
| @Override |
| public void getAvatar(int desiredSize, IObjectWrapper avatarLoadedWrapper) { |
| StrictModeWorkaround.apply(); |
| ValueCallback<Bitmap> avatarLoadedCallback = |
| (ValueCallback<Bitmap>) ObjectWrapper.unwrap( |
| avatarLoadedWrapper, ValueCallback.class); |
| mCallback.getAvatar(desiredSize, avatarLoadedCallback); |
| } |
| } |
| } |