1 // Copyright 2020 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.toolbar; 6 7 import android.content.Context; 8 import android.content.res.Configuration; 9 import android.graphics.drawable.Drawable; 10 import android.view.View.OnClickListener; 11 12 import org.chromium.base.FeatureList; 13 import org.chromium.base.ObserverList; 14 import org.chromium.base.metrics.RecordUserAction; 15 import org.chromium.base.supplier.Supplier; 16 import org.chromium.chrome.R; 17 import org.chromium.chrome.browser.flags.ChromeFeatureList; 18 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher; 19 import org.chromium.chrome.browser.lifecycle.ConfigurationChangedObserver; 20 import org.chromium.chrome.browser.tab.Tab; 21 import org.chromium.components.embedder_support.util.UrlUtilities; 22 import org.chromium.ui.modaldialog.ModalDialogManager; 23 import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogManagerObserver; 24 import org.chromium.ui.modelutil.PropertyModel; 25 26 /** 27 * Handles displaying the voice search button on toolbar depending on several conditions (e.g. 28 * device width, whether NTP is shown, whether voice is enabled). 29 * 30 * TODO(crbug.com/1144976): Move this to ../voice/ along with VoiceRecognitionHandler and the 31 * assistant support. 32 */ 33 public class VoiceToolbarButtonController 34 implements ButtonDataProvider, ConfigurationChangedObserver { 35 /** 36 * Default minimum width to show the voice search button. 37 */ 38 public static final int DEFAULT_MIN_WIDTH_DP = 360; 39 40 private final Supplier<Tab> mActiveTabSupplier; 41 private final ActivityLifecycleDispatcher mActivityLifecycleDispatcher; 42 43 private final ModalDialogManager mModalDialogManager; 44 private final ModalDialogManagerObserver mModalDialogManagerObserver; 45 46 private final VoiceSearchDelegate mVoiceSearchDelegate; 47 48 private final ButtonData mButtonData; 49 private final ObserverList<ButtonDataObserver> mObservers = new ObserverList<>(); 50 51 private Integer mMinimumWidthDp; 52 private int mScreenWidthDp; 53 54 /** 55 * Delegate interface for interacting with voice search. 56 */ 57 public interface VoiceSearchDelegate { 58 /** 59 * @return True if voice search is enabled for the current session. 60 */ isVoiceSearchEnabled()61 boolean isVoiceSearchEnabled(); 62 63 /** 64 * Starts a voice search interaction. 65 */ startVoiceRecognition()66 void startVoiceRecognition(); 67 } 68 69 /** 70 * Creates a VoiceToolbarButtonController object. 71 * @param context The Context for retrieving resources, etc. 72 * @param activeTabSupplier Provides the currently displayed {@link Tab}. 73 * @param activityLifecycleDispatcher Dispatcher for activity lifecycle events, e.g. 74 * configuration changes. 75 * @param modalDialogManager Dispatcher for modal lifecycle events 76 * @param voiceSearchDelegate Provides interaction with voice search. 77 */ VoiceToolbarButtonController(Context context, Drawable buttonDrawable, Supplier<Tab> activeTabSupplier, ActivityLifecycleDispatcher activityLifecycleDispatcher, ModalDialogManager modalDialogManager, VoiceSearchDelegate voiceSearchDelegate)78 public VoiceToolbarButtonController(Context context, Drawable buttonDrawable, 79 Supplier<Tab> activeTabSupplier, 80 ActivityLifecycleDispatcher activityLifecycleDispatcher, 81 ModalDialogManager modalDialogManager, VoiceSearchDelegate voiceSearchDelegate) { 82 mActiveTabSupplier = activeTabSupplier; 83 84 // Register for onConfigurationChanged events, which notify on changes to screen width. 85 mActivityLifecycleDispatcher = activityLifecycleDispatcher; 86 mActivityLifecycleDispatcher.register(this); 87 88 mModalDialogManagerObserver = new ModalDialogManagerObserver() { 89 @Override 90 public void onDialogAdded(PropertyModel model) { 91 mButtonData.isEnabled = false; 92 notifyObservers(mButtonData.canShow); 93 } 94 95 @Override 96 public void onLastDialogDismissed() { 97 mButtonData.isEnabled = true; 98 notifyObservers(mButtonData.canShow); 99 } 100 }; 101 mModalDialogManager = modalDialogManager; 102 mModalDialogManager.addObserver(mModalDialogManagerObserver); 103 104 mVoiceSearchDelegate = voiceSearchDelegate; 105 106 OnClickListener onClickListener = (view) -> { 107 RecordUserAction.record("MobileTopToolbarVoiceButton"); 108 mVoiceSearchDelegate.startVoiceRecognition(); 109 }; 110 111 mButtonData = new ButtonData(/*canShow=*/false, buttonDrawable, onClickListener, 112 R.string.accessibility_toolbar_btn_mic, 113 /*supportsTinting=*/true, /*iphCommandBuilder=*/null, /*isEnabled=*/true); 114 115 mScreenWidthDp = context.getResources().getConfiguration().screenWidthDp; 116 } 117 118 @Override onConfigurationChanged(Configuration configuration)119 public void onConfigurationChanged(Configuration configuration) { 120 if (mScreenWidthDp == configuration.screenWidthDp) { 121 return; 122 } 123 mScreenWidthDp = configuration.screenWidthDp; 124 mButtonData.canShow = shouldShowVoiceButton(mActiveTabSupplier.get()); 125 notifyObservers(mButtonData.canShow); 126 } 127 128 @Override destroy()129 public void destroy() { 130 mActivityLifecycleDispatcher.unregister(this); 131 mModalDialogManager.removeObserver(mModalDialogManagerObserver); 132 mObservers.clear(); 133 } 134 135 @Override addObserver(ButtonDataObserver obs)136 public void addObserver(ButtonDataObserver obs) { 137 mObservers.addObserver(obs); 138 } 139 140 @Override removeObserver(ButtonDataObserver obs)141 public void removeObserver(ButtonDataObserver obs) { 142 mObservers.removeObserver(obs); 143 } 144 145 @Override get(Tab tab)146 public ButtonData get(Tab tab) { 147 mButtonData.canShow = shouldShowVoiceButton(tab); 148 return mButtonData; 149 } 150 shouldShowVoiceButton(Tab tab)151 private boolean shouldShowVoiceButton(Tab tab) { 152 if (!FeatureList.isInitialized() 153 || !ChromeFeatureList.isEnabled(ChromeFeatureList.VOICE_BUTTON_IN_TOP_TOOLBAR) 154 || tab == null || tab.isIncognito() 155 || !mVoiceSearchDelegate.isVoiceSearchEnabled()) { 156 return false; 157 } 158 159 if (mMinimumWidthDp == null) { 160 mMinimumWidthDp = ChromeFeatureList.getFieldTrialParamByFeatureAsInt( 161 ChromeFeatureList.VOICE_BUTTON_IN_TOP_TOOLBAR, "minimum_width_dp", 162 DEFAULT_MIN_WIDTH_DP); 163 } 164 165 boolean isDeviceWideEnough = mScreenWidthDp >= mMinimumWidthDp; 166 if (!isDeviceWideEnough) return false; 167 168 return UrlUtilities.isHttpOrHttps(tab.getUrl()); 169 } 170 notifyObservers(boolean hint)171 private void notifyObservers(boolean hint) { 172 for (ButtonDataObserver observer : mObservers) { 173 observer.buttonDataChanged(hint); 174 } 175 } 176 } 177