| // 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.os.RemoteException; |
| import android.view.View; |
| import android.webkit.ValueCallback; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.fragment.app.Fragment; |
| |
| import org.chromium.weblayer_private.interfaces.APICallException; |
| import org.chromium.weblayer_private.interfaces.IBrowser; |
| import org.chromium.weblayer_private.interfaces.IBrowserClient; |
| import org.chromium.weblayer_private.interfaces.ITab; |
| import org.chromium.weblayer_private.interfaces.ObjectWrapper; |
| import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; |
| |
| import java.util.Set; |
| |
| /** |
| * Browser contains any number of Tabs, with one active Tab. The active Tab is visible to the user, |
| * all other Tabs are hidden. |
| * |
| * By default Browser has a single active Tab. |
| */ |
| public class Browser { |
| private final IBrowser mImpl; |
| private final ObserverList<TabListCallback> mTabListCallbacks; |
| private final UrlBarController mUrlBarController; |
| |
| // Constructor for test mocking. |
| protected Browser() { |
| mImpl = null; |
| mTabListCallbacks = null; |
| mUrlBarController = null; |
| } |
| |
| Browser(IBrowser impl) { |
| mImpl = impl; |
| mTabListCallbacks = new ObserverList<TabListCallback>(); |
| |
| try { |
| mImpl.setClient(new BrowserClientImpl()); |
| if (WebLayer.getSupportedMajorVersionInternal() >= 82) { |
| mUrlBarController = new UrlBarController(mImpl.getUrlBarController()); |
| } else { |
| mUrlBarController = null; |
| } |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| if (WebLayer.getSupportedMajorVersionInternal() < 82) { |
| // On WebLayer versions < 82 the tabs are internally created before the client is set, |
| // so it doesn't receive the onTabAdded() callbacks; hence the client-side Tab |
| // objects need to be manually created to mirror the implementation-side objects. |
| try { |
| for (Object tab : impl.getTabs()) { |
| // getTabs() returns List<TabImpl>, which isn't accessible from the client |
| // library. |
| ITab iTab = ITab.Stub.asInterface((android.os.IBinder) tab); |
| // Tab's constructor calls registerTab(). |
| new Tab(iTab, this); |
| } |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| } |
| |
| /** |
| * Returns the Browser for the supplied Fragment; null if |
| * {@link fragment} was not created by WebLayer. |
| * |
| * @return the Browser |
| */ |
| @Nullable |
| public static Browser fromFragment(@Nullable Fragment fragment) { |
| return fragment instanceof BrowserFragment ? ((BrowserFragment) fragment).getBrowser() |
| : null; |
| } |
| |
| // Called prior to notifying IBrowser of destroy(). |
| void prepareForDestroy() { |
| // See comment in Tab$TabClientImpl.onTabDestroyed for details on this. |
| if (WebLayer.getSupportedMajorVersionInternal() >= 87) return; |
| |
| for (Tab tab : getTabs()) { |
| Tab.unregisterTab(tab); |
| } |
| } |
| |
| /** |
| * Sets the active (visible) Tab. Only one Tab is visible at a time. |
| * |
| * @param tab The Tab to make active. |
| * |
| * @throws IllegalStateException if {@link tab} was not added to this |
| * Browser. |
| * |
| * @see #addTab() |
| */ |
| public void setActiveTab(@NonNull Tab tab) { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| if (getActiveTab() != tab && !mImpl.setActiveTab(tab.getITab())) { |
| throw new IllegalStateException("attachTab() must be called before " |
| + "setActiveTab"); |
| } |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Adds a tab to this Browser. If {link tab} is the active Tab of another Browser, then the |
| * other Browser's active tab is set to null. This does nothing if {@link tab} is already |
| * contained in this Browser. |
| * |
| * @param tab The Tab to add. |
| */ |
| public void addTab(@NonNull Tab tab) { |
| ThreadCheck.ensureOnUiThread(); |
| if (tab.getBrowser() == this) return; |
| try { |
| mImpl.addTab(tab.getITab()); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Returns the active (visible) Tab associated with this |
| * Browser. |
| * |
| * @return The Tab. |
| */ |
| @Nullable |
| public Tab getActiveTab() { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| Tab tab = Tab.getTabById(mImpl.getActiveTabId()); |
| assert tab == null || tab.getBrowser() == this; |
| return tab; |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Returns the set of Tabs contained in this Browser. |
| * |
| * @return The Tabs |
| */ |
| @NonNull |
| public Set<Tab> getTabs() { |
| ThreadCheck.ensureOnUiThread(); |
| return Tab.getTabsInBrowser(this); |
| } |
| |
| /** |
| * Disposes a Tab. If {@link tab} is the active Tab, no Tab is made active. After this call |
| * {@link tab} should not be used. |
| * |
| * Note this will skip any beforeunload handlers. To run those first, use |
| * {@link Tab#dispatchBeforeUnloadAndClose} instead. |
| * |
| * @param tab The Tab to dispose. |
| * |
| * @throws IllegalStateException is {@link tab} is not in this Browser. |
| */ |
| public void destroyTab(@NonNull Tab tab) { |
| ThreadCheck.ensureOnUiThread(); |
| if (tab.getBrowser() != this) { |
| throw new IllegalStateException("destroyTab() must be called on a Tab in the Browser"); |
| } |
| try { |
| mImpl.destroyTab(tab.getITab()); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Adds a TabListCallback. |
| * |
| * @param callback The TabListCallback. |
| */ |
| public void registerTabListCallback(@NonNull TabListCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| mTabListCallbacks.addObserver(callback); |
| } |
| |
| /** |
| * Removes a TabListCallback. |
| * |
| * @param callback The TabListCallback. |
| */ |
| public void unregisterTabListCallback(@NonNull TabListCallback callback) { |
| ThreadCheck.ensureOnUiThread(); |
| mTabListCallbacks.removeObserver(callback); |
| } |
| |
| /** |
| * Sets the View shown at the top of the browser. A value of null removes the view. The |
| * top-view is typically used to show the uri. The top-view scrolls with the page. |
| * |
| * @param view The new top-view. |
| */ |
| public void setTopView(@Nullable View view) { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| mImpl.setTopView(ObjectWrapper.wrap(view)); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Sets the View shown at the top of the browser. The top-view is typically used to show the |
| * uri. This method also allows you to control the scrolling behavior of the top-view by setting |
| * a minimum height it will scroll to, and pinning the top-view to the top of the web contents. |
| * |
| * @param view The new top-view, or null to remove the view. |
| * @param minHeight The minimum height in pixels that the top controls can scoll up to. A value |
| * of 0 means the top-view should scroll entirely off screen. |
| * @param onlyExpandControlsAtPageTop Whether the top-view should only be expanded when the web |
| * content is scrolled to the top. A true value makes the top-view behave as though it |
| * were inserted into the top of the page content. If true, the top-view should NOT be |
| * used to display the URL, as this will prevent it from expanding in security-sensitive |
| * contexts where the URL should be visible to the user. |
| * @param animate Whether or not any height/visibility changes that result from this call |
| * should be animated. |
| * |
| * @since 86 |
| */ |
| public void setTopView(@Nullable View view, int minHeight, boolean onlyExpandControlsAtPageTop, |
| boolean animate) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 86) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.setTopViewAndScrollingBehavior( |
| ObjectWrapper.wrap(view), minHeight, onlyExpandControlsAtPageTop, animate); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Sets the View shown at the bottom of the browser. A value of null removes the view. |
| * |
| * @param view The new bottom-view. |
| * |
| * @since 84 |
| */ |
| public void setBottomView(@Nullable View view) { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 84) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| mImpl.setBottomView(ObjectWrapper.wrap(view)); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Creates a new tab attached to this browser. This will call {@link TabListCallback#onTabAdded} |
| * with the new tab. |
| * |
| * @since 85 |
| */ |
| public @NonNull Tab createTab() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 85) { |
| throw new UnsupportedOperationException(); |
| } |
| try { |
| ITab iTab = mImpl.createTab(); |
| Tab tab = Tab.getTabById(iTab.getId()); |
| assert tab != null; |
| return tab; |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Control support for embedding use cases such as animations. This should be enabled when the |
| * container view of the fragment is animated in any way, needs to be rotated or blended, or |
| * need to control z-order with other views or other BrowserFragmentImpls. Note embedder should |
| * keep WebLayer in the default non-embedding mode when user is interacting with the web |
| * content. Embedding mode does not support encrypted video. |
| * |
| * @param enable Whether to support embedding |
| * @param callback {@link Callback} to be called with a boolean indicating whether request |
| * succeeded. A request might fail if it is subsumed by a subsequent request, or if this object |
| * is destroyed. |
| */ |
| public void setSupportsEmbedding(boolean enable, @NonNull Callback<Boolean> callback) { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| mImpl.setSupportsEmbedding( |
| enable, ObjectWrapper.wrap((ValueCallback<Boolean>) callback::onResult)); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Returns {@link Profile} associated with this Browser Fragment. Multiple fragments can share |
| * the same Profile. |
| */ |
| @NonNull |
| public Profile getProfile() { |
| ThreadCheck.ensureOnUiThread(); |
| try { |
| return Profile.of(mImpl.getProfile()); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| } |
| |
| /** |
| * Returns the UrlBarController. |
| * @since 82 |
| */ |
| @NonNull |
| public UrlBarController getUrlBarController() { |
| ThreadCheck.ensureOnUiThread(); |
| if (WebLayer.getSupportedMajorVersionInternal() < 82) { |
| throw new UnsupportedOperationException(); |
| } |
| return mUrlBarController; |
| } |
| |
| private final class BrowserClientImpl extends IBrowserClient.Stub { |
| @Override |
| public void onActiveTabChanged(int activeTabId) { |
| StrictModeWorkaround.apply(); |
| Tab tab = Tab.getTabById(activeTabId); |
| for (TabListCallback callback : mTabListCallbacks) { |
| callback.onActiveTabChanged(tab); |
| } |
| } |
| |
| @Override |
| public void onTabAdded(ITab iTab) { |
| StrictModeWorkaround.apply(); |
| int id = 0; |
| try { |
| id = iTab.getId(); |
| } catch (RemoteException e) { |
| throw new APICallException(e); |
| } |
| Tab tab = Tab.getTabById(id); |
| if (tab == null) { |
| tab = new Tab(iTab, Browser.this); |
| } else { |
| tab.setBrowser(Browser.this); |
| } |
| for (TabListCallback callback : mTabListCallbacks) { |
| callback.onTabAdded(tab); |
| } |
| } |
| |
| @Override |
| public void onTabRemoved(int tabId) { |
| StrictModeWorkaround.apply(); |
| Tab tab = Tab.getTabById(tabId); |
| // This should only be called with a previously created tab. |
| assert tab != null; |
| // And this should only be called for tabs attached to this browser. |
| assert tab.getBrowser() == Browser.this; |
| |
| tab.setBrowser(null); |
| for (TabListCallback callback : mTabListCallbacks) { |
| callback.onTabRemoved(tab); |
| } |
| } |
| } |
| } |