// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs.features.toolbar;
import static org.chromium.chrome.browser.dependency_injection.ChromeCommonQualifiers.APP_CONTEXT;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsIntent;
import dagger.Lazy;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsVisibilityManager;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.CustomButtonParams;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.customtabs.CloseButtonVisibilityManager;
import org.chromium.chrome.browser.customtabs.CustomTabCompositorContentInitializer;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabController;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.share.ShareDelegateSupplier;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ToolbarManager;
import org.chromium.components.browser_ui.share.ShareHelper;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.util.TokenHolder;
import org.chromium.url.GURL;
import javax.inject.Inject;
import javax.inject.Named;
/**
* Works with the toolbar in a Custom Tab. Encapsulates interactions with Chrome's toolbar-related
* classes such as {@link ToolbarManager} and {@link BrowserControlsVisibilityManager}.
*
* TODO(pshmakov):
* 1. Reduce the coupling between Custom Tab toolbar and Chrome's common code. In particular,
* ToolbarLayout has Custom Tab specific methods that throw unless we're in a Custom Tab - we need a
* better design.
* 2. Make toolbar lazy. E.g. in Trusted Web Activities we always start without toolbar - delay
* executing any initialization code and creating {@link ToolbarManager} until the toolbar needs
* to appear.
* 3. Refactor to MVC.
*/
@ActivityScope
public class CustomTabToolbarCoordinator {
private final BrowserServicesIntentDataProvider mIntentDataProvider;
private final CustomTabActivityTabProvider mTabProvider;
private final CustomTabsConnection mConnection;
private final Activity mActivity;
private final ActivityWindowAndroid mWindowAndroid;
private final Context mAppContext;
private final CustomTabActivityTabController mTabController;
private final Lazy<BrowserControlsVisibilityManager> mBrowserControlsVisibilityManager;
private final CustomTabActivityNavigationController mNavigationController;
private final CloseButtonVisibilityManager mCloseButtonVisibilityManager;
private final CustomTabBrowserControlsVisibilityDelegate mVisibilityDelegate;
private final CustomTabToolbarColorController mToolbarColorController;
@Nullable private ToolbarManager mToolbarManager;
private int mControlsHidingToken = TokenHolder.INVALID_TOKEN;
private boolean mInitializedToolbarWithNative;
private PendingIntent.OnFinished mButtonClickOnFinishedForTesting;
private static final String TAG = "CustomTabToolbarCoor";
@Inject
public CustomTabToolbarCoordinator(
BrowserServicesIntentDataProvider intentDataProvider,
CustomTabActivityTabProvider tabProvider,
CustomTabsConnection connection,
Activity activity,
ActivityWindowAndroid windowAndroid,
@Named(APP_CONTEXT) Context appContext,
CustomTabActivityTabController tabController,
Lazy<BrowserControlsVisibilityManager> controlsVisiblityManager,
CustomTabActivityNavigationController navigationController,
CloseButtonVisibilityManager closeButtonVisibilityManager,
CustomTabBrowserControlsVisibilityDelegate visibilityDelegate,
CustomTabCompositorContentInitializer compositorContentInitializer,
CustomTabToolbarColorController toolbarColorController) {
mIntentDataProvider = intentDataProvider;
mTabProvider = tabProvider;
mConnection = connection;
mActivity = activity;
mWindowAndroid = windowAndroid;
mAppContext = appContext;
mTabController = tabController;
mBrowserControlsVisibilityManager = controlsVisiblityManager;
mNavigationController = navigationController;
mCloseButtonVisibilityManager = closeButtonVisibilityManager;
mVisibilityDelegate = visibilityDelegate;
mToolbarColorController = toolbarColorController;
compositorContentInitializer.addCallback(this::onCompositorContentInitialized);
}
/**
* Notifies the navigation controller that the ToolbarManager has been created and is ready for
* use. ToolbarManager isn't passed directly to the constructor because it's not guaranteed to
* be initialized yet.
*/
public void onToolbarInitialized(ToolbarManager manager) {
assert manager != null : "Toolbar manager not initialized";
mToolbarManager = manager;
mToolbarColorController.onToolbarInitialized(manager);
mCloseButtonVisibilityManager.onToolbarInitialized(manager);
manager.setShowTitle(
mConnection.getTitleVisibilityState(mIntentDataProvider)
== CustomTabsIntent.SHOW_PAGE_TITLE);
if (mConnection.shouldHideDomainForSession(mIntentDataProvider.getSession())) {
manager.setUrlBarHidden(true);
}
if (mIntentDataProvider.isMediaViewer()) {
manager.setToolbarShadowVisibility(View.GONE);
}
showCustomButtonsOnToolbar();
}
/**
* Configures the custom button on toolbar. Does nothing if invalid data is provided by clients.
*/
private void showCustomButtonsOnToolbar() {
for (CustomButtonParams params : mIntentDataProvider.getCustomButtonsOnToolbar()) {
View.OnClickListener onClickListener = v -> onCustomButtonClick(params);
mToolbarManager.addCustomActionButton(
params.getIcon(mActivity), params.getDescription(), onClickListener);
}
}
@VisibleForTesting
void onCustomButtonClick(CustomButtonParams params) {
Tab tab = mTabProvider.getTab();
if (tab == null) return;
// The share button from CCT should have custom actions, however if the
// ShareDelegateSupplier is null, we should fallback to the default share action without
// custom buttons.
Supplier<ShareDelegate> supplier = ShareDelegateSupplier.from(mWindowAndroid);
if (ChromeFeatureList.isEnabled(ChromeFeatureList.SHARE_CUSTOM_ACTIONS_IN_CCT)
&& params.getType() == CustomButtonParams.ButtonType.CCT_SHARE_BUTTON
&& supplier != null
&& supplier.get() != null) {
supplier.get()
.share(
tab,
/* shareDirectly= */ false,
ShareDelegate.ShareOrigin.CUSTOM_TAB_SHARE_BUTTON);
} else if (params.getType() == CustomButtonParams.ButtonType.CCT_OPEN_IN_BROWSER_BUTTON) {
if (mNavigationController.openCurrentUrlInBrowser()) {
WebContents webContents = tab == null ? null : tab.getWebContents();
mConnection.notifyOpenInBrowser(mIntentDataProvider.getSession(), webContents);
}
} else {
sendButtonPendingIntentWithUrlAndTitle(params, tab.getOriginalUrl(), tab.getTitle());
}
RecordUserAction.record("CustomTabsCustomActionButtonClick");
Resources resources = mActivity.getResources();
if (mIntentDataProvider.shouldEnableEmbeddedMediaExperience()
&& TextUtils.equals(params.getDescription(), resources.getString(R.string.share))) {
ShareHelper.recordShareSource(ShareHelper.ShareSourceAndroid.ANDROID_SHARE_SHEET);
RecordUserAction.record("CustomTabsCustomActionButtonClick.DownloadsUI.Share");
}
}
/**
* Sends the pending intent for the custom button on the toolbar with the given {@code params},
* with the given {@code url} as data.
* @param params The parameters for the custom button.
* @param url The URL to attach as additional data to the {@link PendingIntent}.
* @param title The title to attach as additional data to the {@link PendingIntent}.
*/
private void sendButtonPendingIntentWithUrlAndTitle(
CustomButtonParams params, GURL url, String title) {
Intent addedIntent = new Intent();
addedIntent.setData(Uri.parse(url.getSpec()));
addedIntent.putExtra(Intent.EXTRA_SUBJECT, title);
try {
ActivityOptions options = ActivityOptions.makeBasic();
ApiCompatibilityUtils.setActivityOptionsBackgroundActivityStartMode(options);
params.getPendingIntent()
.send(
mAppContext,
0,
addedIntent,
mButtonClickOnFinishedForTesting,
null,
null,
options.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "CanceledException while sending pending intent in custom tab");
}
}
private void onCompositorContentInitialized(LayoutManagerImpl layoutDriver) {
mToolbarManager.initializeWithNative(
layoutDriver,
/* stripLayoutHelperManager= */ null,
/* newTabClickHandler= */ null,
/* bookmarkClickHandler= */ null,
/* customTabsBackClickHandler= */ v -> onCloseButtonClick(),
/* archivedTabCountSupplier= */ null);
mInitializedToolbarWithNative = true;
}
private void onCloseButtonClick() {
RecordUserAction.record("CustomTabs.CloseButtonClicked");
if (mIntentDataProvider.shouldEnableEmbeddedMediaExperience()) {
RecordUserAction.record("CustomTabs.CloseButtonClicked.DownloadsUI");
}
mNavigationController.navigateOnClose();
}
public void setBrowserControlsState(@BrowserControlsState int controlsState) {
mVisibilityDelegate.setControlsState(controlsState);
if (controlsState == BrowserControlsState.HIDDEN) {
mControlsHidingToken =
mBrowserControlsVisibilityManager
.get()
.hideAndroidControlsAndClearOldToken(mControlsHidingToken);
} else {
mBrowserControlsVisibilityManager
.get()
.releaseAndroidControlsHidingToken(mControlsHidingToken);
}
}
/** Shows toolbar temporarily, for a few seconds. */
public void showToolbarTemporarily() {
mBrowserControlsVisibilityManager
.get()
.getBrowserVisibilityDelegate()
.showControlsTransient();
}
/**
* Updates a custom button with a new icon and description. Provided {@link CustomButtonParams}
* must have the updated data.
* Returns whether has succeeded.
*/
public boolean updateCustomButton(CustomButtonParams params) {
if (!params.doesIconFitToolbar(mActivity)) {
return false;
}
int index = mIntentDataProvider.getCustomToolbarButtonIndexForId(params.getId());
if (index == -1) {
assert false;
return false;
}
if (mToolbarManager == null) {
return false;
}
mToolbarManager.updateCustomActionButton(
index, params.getIcon(mActivity), params.getDescription());
return true;
}
/**
* Returns whether the toolbar has been fully initialized
* ({@link ToolbarManager#initializeWithNative}).
*/
public boolean toolbarIsInitialized() {
return mInitializedToolbarWithNative;
}
/**
* Set the callback object for the {@link PendingIntent} which is sent by the custom buttons.
*/
public void setCustomButtonPendingIntentOnFinishedForTesting(
PendingIntent.OnFinished onFinished) {
mButtonClickOnFinishedForTesting = onFinished;
ResettersForTesting.register(() -> mButtonClickOnFinishedForTesting = null);
}
}