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