blob: 66dbb8ad9b47ddbfd46ce15f4ed2bef7d1811e72 [file] [log] [blame]
// Copyright 2021 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.chrome.browser.toolbar.adaptive;
import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS;
import android.content.Context;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Callback;
import org.chromium.base.FeatureList;
import org.chromium.base.ObserverList;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.NativeInitObserver;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ButtonData;
import org.chromium.chrome.browser.toolbar.ButtonData.ButtonSpec;
import org.chromium.chrome.browser.toolbar.ButtonDataImpl;
import org.chromium.chrome.browser.toolbar.ButtonDataProvider;
import org.chromium.chrome.browser.toolbar.ButtonDataProvider.ButtonDataObserver;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures.AdaptiveToolbarButtonVariant;
import org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarPreferenceFragment;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/** Meta {@link ButtonDataProvider} which chooses the optional button variant that will be shown. */
public class AdaptiveToolbarButtonController implements ButtonDataProvider, ButtonDataObserver,
NativeInitObserver,
SharedPreferencesManager.Observer {
private ObserverList<ButtonDataObserver> mObservers = new ObserverList<>();
@Nullable
private ButtonDataProvider mSingleProvider;
// Maps from {@link AdaptiveToolbarButtonVariant} to {@link ButtonDataProvider}.
private Map<Integer, ButtonDataProvider> mButtonDataProviderMap = new HashMap<>();
/**
* {@link ButtonData} instance returned by {@link AdaptiveToolbarButtonController#get(Tab)}
* when wrapping {@code mOriginalButtonSpec}.
*/
private final ButtonDataImpl mButtonData = new ButtonDataImpl();
/** The last received {@link ButtonSpec}. */
@Nullable
private ButtonSpec mOriginalButtonSpec;
/** {@code true} if the SessionVariant histogram value was already recorded. */
private boolean mIsSessionVariantRecorded;
private final ActivityLifecycleDispatcher mLifecycleDispatcher;
private final SharedPreferencesManager mSharedPreferencesManager;
@Nullable
private View.OnLongClickListener mMenuHandler;
private final Callback<Integer> mMenuClickListener;
private final AdaptiveButtonActionMenuCoordinator mMenuCoordinator;
/**
* Constructs the {@link AdaptiveToolbarButtonController}.
*
* @param context used in {@link SettingsLauncher}
* @param settingsLauncher opens adaptive button settings
* @param lifecycleDispatcher notifies about native initialization
*/
public AdaptiveToolbarButtonController(Context context, SettingsLauncher settingsLauncher,
ActivityLifecycleDispatcher lifecycleDispatcher,
AdaptiveButtonActionMenuCoordinator menuCoordinator,
SharedPreferencesManager sharedPreferencesManager) {
mMenuClickListener = id -> {
if (id == R.id.customize_adaptive_button_menu_id) {
RecordUserAction.record("MobileAdaptiveMenuCustomize");
settingsLauncher.launchSettingsActivity(
context, AdaptiveToolbarPreferenceFragment.class);
return;
}
assert false : "unknown adaptive button menu id: " + id;
};
mLifecycleDispatcher = lifecycleDispatcher;
mLifecycleDispatcher.register(this);
mMenuCoordinator = menuCoordinator;
mSharedPreferencesManager = sharedPreferencesManager;
mSharedPreferencesManager.addObserver(this);
}
/**
* Adds an instance of a button variant to the collection of buttons managed by {@code
* AdaptiveToolbarButtonController}.
*
* @param variant The button variant of {@code buttonProvider}.
* @param buttonProvider The provider implementing the button variant. {@code
* AdaptiveToolbarButtonController} takes ownership of the provider and will {@link
* #destroy()} it, once the provider is no longer needed.
*/
public void addButtonVariant(
@AdaptiveToolbarButtonVariant int variant, ButtonDataProvider buttonProvider) {
assert variant >= 0
&& variant < AdaptiveToolbarButtonVariant.NUM_ENTRIES
: "invalid adaptive button variant: "
+ variant;
assert variant
!= AdaptiveToolbarButtonVariant.UNKNOWN
: "must not provide UNKNOWN button provider";
assert variant
!= AdaptiveToolbarButtonVariant.NONE : "must not provide NONE button provider";
mButtonDataProviderMap.put(variant, buttonProvider);
}
@Override
public void destroy() {
setSingleProvider(null);
mObservers.clear();
mSharedPreferencesManager.removeObserver(this);
mLifecycleDispatcher.unregister(this);
Iterator<Map.Entry<Integer, ButtonDataProvider>> it =
mButtonDataProviderMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, ButtonDataProvider> entry = it.next();
entry.getValue().destroy();
it.remove();
}
}
private void setSingleProvider(@Nullable ButtonDataProvider buttonProvider) {
if (mSingleProvider != null) {
mSingleProvider.removeObserver(this);
}
mSingleProvider = buttonProvider;
if (mSingleProvider != null) {
mSingleProvider.addObserver(this);
}
}
@Override
public void addObserver(ButtonDataObserver obs) {
mObservers.addObserver(obs);
}
@Override
public void removeObserver(ButtonDataObserver obs) {
mObservers.removeObserver(obs);
}
@Override
public ButtonData get(@Nullable Tab tab) {
if (mSingleProvider == null) return null;
final ButtonData receivedButtonData = mSingleProvider.get(tab);
if (receivedButtonData == null) return null;
if (!mIsSessionVariantRecorded && receivedButtonData.canShow()
&& receivedButtonData.isEnabled()) {
mIsSessionVariantRecorded = true;
RecordHistogram.recordEnumeratedHistogram(
"Android.AdaptiveToolbarButton.SessionVariant",
receivedButtonData.getButtonSpec().getButtonVariant(),
AdaptiveToolbarButtonVariant.NUM_ENTRIES);
}
mButtonData.setCanShow(receivedButtonData.canShow());
mButtonData.setEnabled(receivedButtonData.isEnabled());
final ButtonSpec receivedButtonSpec = receivedButtonData.getButtonSpec();
// ButtonSpec is immutable, so we keep the previous value when noting changes.
if (receivedButtonSpec != mOriginalButtonSpec) {
assert receivedButtonSpec.getOnLongClickListener()
== null
: "adaptive button variants are expected to not set a long click listener";
if (mMenuHandler == null) mMenuHandler = createMenuHandler();
mOriginalButtonSpec = receivedButtonSpec;
mButtonData.setButtonSpec(new ButtonSpec(receivedButtonSpec.getDrawable(),
wrapClickListener(receivedButtonSpec.getOnClickListener(),
receivedButtonSpec.getButtonVariant()),
mMenuHandler, receivedButtonSpec.getContentDescriptionResId(),
receivedButtonSpec.getSupportsTinting(),
receivedButtonSpec.getIPHCommandBuilder(),
receivedButtonSpec.getButtonVariant()));
}
return mButtonData;
}
private static View.OnClickListener wrapClickListener(View.OnClickListener receivedListener,
@AdaptiveToolbarButtonVariant int buttonVariant) {
return view -> {
RecordHistogram.recordEnumeratedHistogram("Android.AdaptiveToolbarButton.Clicked",
buttonVariant, AdaptiveToolbarButtonVariant.NUM_ENTRIES);
receivedListener.onClick(view);
};
}
@Nullable
private View.OnLongClickListener createMenuHandler() {
if (!FeatureList.isInitialized()) return null;
return mMenuCoordinator.createOnLongClickListener(mMenuClickListener);
}
@Override
public void buttonDataChanged(boolean canShowHint) {
notifyObservers(canShowHint);
}
@Override
public void onFinishNativeInitialization() {
if (AdaptiveToolbarFeatures.isSingleVariantModeEnabled()) {
@AdaptiveToolbarButtonVariant
int variant = AdaptiveToolbarFeatures.getSingleVariantMode();
setSingleProvider(mButtonDataProviderMap.get(variant));
} else if (AdaptiveToolbarFeatures.isCustomizationEnabled()) {
new AdaptiveToolbarStatePredictor().recomputeUiState(uiState -> {
setSingleProvider(uiState.canShowUi
? mButtonDataProviderMap.get(uiState.toolbarButtonState)
: null);
notifyObservers(uiState.canShowUi);
});
// We need the menu handler only if the customization feature is on.
if (mMenuHandler != null) return;
mMenuHandler = createMenuHandler();
if (mMenuHandler == null) return;
} else {
return;
}
// Clearing mOriginalButtonSpec forces a refresh of mButtonData on the next get()
mOriginalButtonSpec = null;
notifyObservers(mButtonData.canShow());
}
private void notifyObservers(boolean canShowHint) {
for (ButtonDataObserver observer : mObservers) {
observer.buttonDataChanged(canShowHint);
}
}
/** Returns the {@link ButtonDataProvider} used in a single-variant mode. */
@Nullable
@VisibleForTesting
public ButtonDataProvider getSingleProviderForTesting() {
return mSingleProvider;
}
@Override
public void onPreferenceChanged(String key) {
if (ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS.equals(key)) {
assert AdaptiveToolbarFeatures.isCustomizationEnabled();
new AdaptiveToolbarStatePredictor().recomputeUiState(uiState -> {
setSingleProvider(uiState.canShowUi
? mButtonDataProviderMap.get(uiState.toolbarButtonState)
: null);
notifyObservers(uiState.canShowUi);
});
}
}
}