1 // Copyright 2016 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.locale;
6 
7 import android.app.Activity;
8 import android.content.Context;
9 
10 import androidx.annotation.IntDef;
11 import androidx.annotation.Nullable;
12 import androidx.annotation.VisibleForTesting;
13 
14 import org.chromium.base.ActivityState;
15 import org.chromium.base.ApiCompatibilityUtils;
16 import org.chromium.base.ApplicationStatus;
17 import org.chromium.base.Callback;
18 import org.chromium.base.CommandLine;
19 import org.chromium.base.ContextUtils;
20 import org.chromium.base.ThreadUtils;
21 import org.chromium.base.annotations.CalledByNative;
22 import org.chromium.base.library_loader.LibraryLoader;
23 import org.chromium.chrome.R;
24 import org.chromium.chrome.browser.AppHooks;
25 import org.chromium.chrome.browser.flags.ChromeFeatureList;
26 import org.chromium.chrome.browser.flags.ChromeSwitches;
27 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
28 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
29 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
30 import org.chromium.chrome.browser.search_engines.settings.SearchEngineSettings;
31 import org.chromium.chrome.browser.settings.SettingsLauncher;
32 import org.chromium.chrome.browser.settings.SettingsLauncherImpl;
33 import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
34 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
35 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController;
36 import org.chromium.chrome.browser.vr.OnExitVrRequestListener;
37 import org.chromium.chrome.browser.vr.VrModuleProvider;
38 import org.chromium.components.browser_ui.widget.PromoDialog;
39 import org.chromium.components.search_engines.TemplateUrl;
40 import org.chromium.ui.base.PageTransition;
41 
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 import java.lang.ref.WeakReference;
45 import java.util.List;
46 import java.util.concurrent.Callable;
47 
48 /**
49  * Manager for some locale specific logics.
50  */
51 public class LocaleManager {
52     public static final String SPECIAL_LOCALE_ID = "US";
53 
54     /** The current state regarding search engine promo dialogs. */
55     @IntDef({SearchEnginePromoState.SHOULD_CHECK, SearchEnginePromoState.CHECKED_NOT_SHOWN,
56             SearchEnginePromoState.CHECKED_AND_SHOWN})
57     @Retention(RetentionPolicy.SOURCE)
58     public @interface SearchEnginePromoState {
59         int SHOULD_CHECK = -1;
60         int CHECKED_NOT_SHOWN = 0;
61         int CHECKED_AND_SHOWN = 1;
62     }
63 
64     /** The different types of search engine promo dialogs. */
65     @IntDef({SearchEnginePromoType.DONT_SHOW, SearchEnginePromoType.SHOW_SOGOU,
66             SearchEnginePromoType.SHOW_EXISTING, SearchEnginePromoType.SHOW_NEW})
67     @Retention(RetentionPolicy.SOURCE)
68     public @interface SearchEnginePromoType {
69         int DONT_SHOW = -1;
70         int SHOW_SOGOU = 0;
71         int SHOW_EXISTING = 1;
72         int SHOW_NEW = 2;
73     }
74 
75     // TODO(crbug.com/1022108): Remove this when downstream uses the replacement:
76     // {@link ChromePreferenceKeys#LOCALE_MANAGER_SEARCH_ENGINE_PROMO_SHOW_STATE}.
77     protected static final String KEY_SEARCH_ENGINE_PROMO_SHOW_STATE =
78             "com.android.chrome.SEARCH_ENGINE_PROMO_SHOWN";
79 
80     private static final int SNACKBAR_DURATION_MS = 6000;
81 
82     private static LocaleManager sInstance;
83 
84     private boolean mSearchEnginePromoCompleted;
85     private boolean mSearchEnginePromoShownThisSession;
86     private boolean mSearchEnginePromoCheckedThisSession;
87 
88     // LocaleManager is a singleton and it should not have strong reference to UI objects.
89     // SnackbarManager is owned by ChromeActivity and is not null as long as the activity is alive.
90     private WeakReference<SnackbarManager> mSnackbarManager = new WeakReference<>(null);
91     private LocaleTemplateUrlLoader mLocaleTemplateUrlLoader;
92 
93     private SnackbarController mSnackbarController = new SnackbarController() {
94         @Override
95         public void onDismissNoAction(Object actionData) { }
96 
97         @Override
98         public void onAction(Object actionData) {
99             Context context = ContextUtils.getApplicationContext();
100             SettingsLauncher settingsLauncher = new SettingsLauncherImpl();
101             settingsLauncher.launchSettingsActivity(context, SearchEngineSettings.class);
102         }
103     };
104 
105     /**
106      * @return An instance of the {@link LocaleManager}. This should only be called on UI thread.
107      */
108     @CalledByNative
getInstance()109     public static LocaleManager getInstance() {
110         assert ThreadUtils.runningOnUiThread();
111         if (sInstance == null) {
112             sInstance = AppHooks.get().createLocaleManager();
113         }
114         return sInstance;
115     }
116 
117     /**
118      * Default constructor.
119      */
LocaleManager()120     public LocaleManager() {
121         @SearchEnginePromoState
122         int state = SharedPreferencesManager.getInstance().readInt(
123                 ChromePreferenceKeys.LOCALE_MANAGER_SEARCH_ENGINE_PROMO_SHOW_STATE,
124                 SearchEnginePromoState.SHOULD_CHECK);
125         mSearchEnginePromoCompleted = state == SearchEnginePromoState.CHECKED_AND_SHOWN;
126     }
127 
128     /**
129      * Starts listening to state changes of the phone.
130      */
startObservingPhoneChanges()131     public void startObservingPhoneChanges() {
132         maybeAutoSwitchSearchEngine();
133     }
134 
135     /**
136      * Stops listening to state changes of the phone.
137      */
stopObservingPhoneChanges()138     public void stopObservingPhoneChanges() {}
139 
140     /**
141      * Starts recording metrics in deferred startup.
142      */
recordStartupMetrics()143     public void recordStartupMetrics() {}
144 
145     /**
146      * @return Whether the Chrome instance is running in a special locale.
147      */
isSpecialLocaleEnabled()148     public boolean isSpecialLocaleEnabled() {
149         return false;
150     }
151 
152     /**
153      * @return The country id of the special locale.
154      */
getSpecialLocaleId()155     public String getSpecialLocaleId() {
156         return SPECIAL_LOCALE_ID;
157     }
158 
159     /**
160      * Adds local search engines for special locale.
161      */
addSpecialSearchEngines()162     public void addSpecialSearchEngines() {
163         if (!isSpecialLocaleEnabled()) return;
164         getLocaleTemplateUrlLoader().loadTemplateUrls();
165     }
166 
167     /**
168      * Removes local search engines for special locale.
169      */
removeSpecialSearchEngines()170     public void removeSpecialSearchEngines() {
171         if (isSpecialLocaleEnabled()) return;
172         getLocaleTemplateUrlLoader().removeTemplateUrls();
173     }
174 
175     /**
176      * Overrides the default search engine to a different search engine we designate. This is a
177      * no-op if the user has manually changed DSP settings.
178      */
overrideDefaultSearchEngine()179     public void overrideDefaultSearchEngine() {
180         if (!isSearchEngineAutoSwitchEnabled() || !isSpecialLocaleEnabled()) return;
181         getLocaleTemplateUrlLoader().overrideDefaultSearchProvider();
182         showSnackbar(ContextUtils.getApplicationContext().getString(R.string.using_sogou));
183     }
184 
185     /**
186      * Reverts the temporary change made in {@link #overrideDefaultSearchEngine()}. This is a no-op
187      * if the user has manually changed DSP settings.
188      */
revertDefaultSearchEngineOverride()189     public void revertDefaultSearchEngineOverride() {
190         if (!isSearchEngineAutoSwitchEnabled() || isSpecialLocaleEnabled()) return;
191         getLocaleTemplateUrlLoader().setGoogleAsDefaultSearch();
192         showSnackbar(ContextUtils.getApplicationContext().getString(R.string.using_google));
193     }
194 
195     /**
196      * Switches the default search engine based on the current locale, if the user has delegated
197      * Chrome to do so. This method also adds some special engines to user's search engine list, as
198      * long as the user is in this locale.
199      */
maybeAutoSwitchSearchEngine()200     protected void maybeAutoSwitchSearchEngine() {
201         SharedPreferencesManager preferences = SharedPreferencesManager.getInstance();
202         boolean wasInSpecialLocale = preferences.readBoolean(
203                 ChromePreferenceKeys.LOCALE_MANAGER_WAS_IN_SPECIAL_LOCALE, false);
204         boolean isInSpecialLocale = isSpecialLocaleEnabled();
205         if (wasInSpecialLocale && !isInSpecialLocale) {
206             revertDefaultSearchEngineOverride();
207             removeSpecialSearchEngines();
208         } else if (isInSpecialLocale && !wasInSpecialLocale) {
209             addSpecialSearchEngines();
210             overrideDefaultSearchEngine();
211         } else if (isInSpecialLocale) {
212             // As long as the user is in the special locale, special engines should be in the list.
213             addSpecialSearchEngines();
214         }
215         preferences.writeBoolean(
216                 ChromePreferenceKeys.LOCALE_MANAGER_WAS_IN_SPECIAL_LOCALE, isInSpecialLocale);
217     }
218 
219     /**
220      * Shows a promotion dialog about search engines depending on Locale and other conditions.
221      * See {@link LocaleManager#getSearchEnginePromoShowType()} for possible types and logic.
222      *
223      * @param activity    Activity showing the dialog.
224      * @param onSearchEngineFinalized Notified when the search engine has been finalized.  This can
225      *                                either mean no dialog is needed, or the dialog was needed and
226      *                                the user completed the dialog with a valid selection.
227      */
showSearchEnginePromoIfNeeded( final Activity activity, final @Nullable Callback<Boolean> onSearchEngineFinalized)228     public void showSearchEnginePromoIfNeeded(
229             final Activity activity, final @Nullable Callback<Boolean> onSearchEngineFinalized) {
230         assert LibraryLoader.getInstance().isInitialized();
231         TemplateUrlServiceFactory.get().runWhenLoaded(new Runnable() {
232             @Override
233             public void run() {
234                 handleSearchEnginePromoWithTemplateUrlsLoaded(activity, onSearchEngineFinalized);
235             }
236         });
237     }
238 
handleSearchEnginePromoWithTemplateUrlsLoaded( final Activity activity, final @Nullable Callback<Boolean> onSearchEngineFinalized)239     private void handleSearchEnginePromoWithTemplateUrlsLoaded(
240             final Activity activity, final @Nullable Callback<Boolean> onSearchEngineFinalized) {
241         assert TemplateUrlServiceFactory.get().isLoaded();
242 
243         final Callback<Boolean> finalizeInternalCallback = new Callback<Boolean>() {
244             @Override
245             public void onResult(Boolean result) {
246                 if (result != null && result) {
247                     mSearchEnginePromoCheckedThisSession = true;
248                 } else {
249                     @SearchEnginePromoType
250                     int promoType = getSearchEnginePromoShowType();
251                     if (promoType == SearchEnginePromoType.SHOW_EXISTING
252                             || promoType == SearchEnginePromoType.SHOW_NEW) {
253                         onUserLeavePromoDialogWithNoConfirmedChoice(promoType);
254                     }
255                 }
256                 if (onSearchEngineFinalized != null) onSearchEngineFinalized.onResult(result);
257             }
258         };
259         if (TemplateUrlServiceFactory.get().isDefaultSearchManaged()
260                 || ApiCompatibilityUtils.isDemoUser()) {
261             finalizeInternalCallback.onResult(true);
262             return;
263         }
264 
265         @SearchEnginePromoType
266         final int shouldShow = getSearchEnginePromoShowType();
267         Callable<PromoDialog> dialogCreator;
268         switch (shouldShow) {
269             case SearchEnginePromoType.DONT_SHOW:
270                 finalizeInternalCallback.onResult(true);
271                 return;
272             case SearchEnginePromoType.SHOW_SOGOU:
273                 dialogCreator = new Callable<PromoDialog>() {
274                     @Override
275                     public PromoDialog call() throws Exception {
276                         return new SogouPromoDialog(
277                                 activity, LocaleManager.this, finalizeInternalCallback);
278                     }
279                 };
280                 break;
281             case SearchEnginePromoType.SHOW_EXISTING:
282             case SearchEnginePromoType.SHOW_NEW:
283                 dialogCreator = new Callable<PromoDialog>() {
284                     @Override
285                     public PromoDialog call() throws Exception {
286                         return new DefaultSearchEnginePromoDialog(
287                                 activity, shouldShow, finalizeInternalCallback);
288                     }
289                 };
290                 break;
291             default:
292                 assert false;
293                 finalizeInternalCallback.onResult(true);
294                 return;
295         }
296 
297         // If the activity has been destroyed by the time the TemplateUrlService has
298         // loaded, then do not attempt to show the dialog.
299         if (ApplicationStatus.getStateForActivity(activity) == ActivityState.DESTROYED) {
300             finalizeInternalCallback.onResult(false);
301             return;
302         }
303 
304         if (VrModuleProvider.getIntentDelegate().isLaunchingIntoVr(activity, activity.getIntent())
305                 || VrModuleProvider.getDelegate().isInVr()) {
306             showPromoDialogForVr(dialogCreator, activity);
307         } else {
308             showPromoDialog(dialogCreator);
309         }
310         mSearchEnginePromoShownThisSession = true;
311     }
312 
showPromoDialogForVr(Callable<PromoDialog> dialogCreator, Activity activity)313     private void showPromoDialogForVr(Callable<PromoDialog> dialogCreator, Activity activity) {
314         VrModuleProvider.getDelegate().requestToExitVrForSearchEnginePromoDialog(
315                 new OnExitVrRequestListener() {
316                     @Override
317                     public void onSucceeded() {
318                         showPromoDialog(dialogCreator);
319                     }
320 
321                     @Override
322                     public void onDenied() {
323                         // We need to make sure that the dialog shows up even if user denied to
324                         // leave VR.
325                         VrModuleProvider.getDelegate().forceExitVrImmediately();
326                         showPromoDialog(dialogCreator);
327                     }
328                 },
329                 activity);
330     }
331 
showPromoDialog(Callable<PromoDialog> dialogCreator)332     private void showPromoDialog(Callable<PromoDialog> dialogCreator) {
333         try {
334             dialogCreator.call().show();
335         } catch (Exception e) {
336             // Exception is caught purely because Callable states it can be thrown.  This is never
337             // expected to be hit.
338             throw new RuntimeException(e);
339         }
340     }
341 
342     /**
343      * @return Whether auto switch for search engine is enabled.
344      */
isSearchEngineAutoSwitchEnabled()345     public boolean isSearchEngineAutoSwitchEnabled() {
346         return SharedPreferencesManager.getInstance().readBoolean(
347                 ChromePreferenceKeys.LOCALE_MANAGER_AUTO_SWITCH, false);
348     }
349 
350     /**
351      * Sets whether auto switch for search engine is enabled.
352      */
setSearchEngineAutoSwitch(boolean isEnabled)353     public void setSearchEngineAutoSwitch(boolean isEnabled) {
354         SharedPreferencesManager.getInstance().writeBoolean(
355                 ChromePreferenceKeys.LOCALE_MANAGER_AUTO_SWITCH, isEnabled);
356     }
357 
358     /**
359      * Sets the {@link SnackbarManager} used by this instance.
360      */
setSnackbarManager(SnackbarManager manager)361     public void setSnackbarManager(SnackbarManager manager) {
362         mSnackbarManager = new WeakReference<SnackbarManager>(manager);
363     }
364 
showSnackbar(CharSequence title)365     private void showSnackbar(CharSequence title) {
366         SnackbarManager manager = mSnackbarManager.get();
367         if (manager == null) return;
368 
369         Context context = ContextUtils.getApplicationContext();
370         Snackbar snackbar = Snackbar.make(title, mSnackbarController, Snackbar.TYPE_NOTIFICATION,
371                 Snackbar.UMA_SPECIAL_LOCALE);
372         snackbar.setDuration(SNACKBAR_DURATION_MS);
373         snackbar.setAction(context.getString(R.string.settings), null);
374         manager.showSnackbar(snackbar);
375     }
376 
377     /**
378      * @return Whether and which search engine promo should be shown.
379      */
380     @SearchEnginePromoType
getSearchEnginePromoShowType()381     public int getSearchEnginePromoShowType() {
382         if (!isSpecialLocaleEnabled()) return SearchEnginePromoType.DONT_SHOW;
383         SharedPreferencesManager preferences = SharedPreferencesManager.getInstance();
384         if (preferences.readBoolean(ChromePreferenceKeys.LOCALE_MANAGER_PROMO_SHOWN, false)) {
385             return SearchEnginePromoType.DONT_SHOW;
386         }
387         return SearchEnginePromoType.SHOW_SOGOU;
388     }
389 
390     /**
391      * @return The referral ID to be passed when searching with Yandex as the DSE.
392      */
393     @CalledByNative
getYandexReferralId()394     protected String getYandexReferralId() {
395         return "";
396     }
397 
398     /**
399      * @return The referral ID to be passed when searching with Mail.RU as the DSE.
400      */
401     @CalledByNative
getMailRUReferralId()402     protected String getMailRUReferralId() {
403         return "";
404     }
405 
406     /**
407      * To be called after the user has made a selection from a search engine promo dialog.
408      * @param type The type of search engine promo dialog that was shown.
409      * @param keywords The keywords for all search engines listed in the order shown to the user.
410      * @param keyword The keyword for the search engine chosen.
411      */
onUserSearchEngineChoiceFromPromoDialog( @earchEnginePromoType int type, List<String> keywords, String keyword)412     protected void onUserSearchEngineChoiceFromPromoDialog(
413             @SearchEnginePromoType int type, List<String> keywords, String keyword) {
414         TemplateUrlServiceFactory.get().setSearchEngine(keyword);
415         SharedPreferencesManager.getInstance().writeInt(
416                 ChromePreferenceKeys.LOCALE_MANAGER_SEARCH_ENGINE_PROMO_SHOW_STATE,
417                 SearchEnginePromoState.CHECKED_AND_SHOWN);
418         mSearchEnginePromoCompleted = true;
419     }
420 
421     /**
422      * To be called when the search engine promo dialog is dismissed without the user confirming
423      * a valid search engine selection.
424      */
onUserLeavePromoDialogWithNoConfirmedChoice(@earchEnginePromoType int type)425     protected void onUserLeavePromoDialogWithNoConfirmedChoice(@SearchEnginePromoType int type) {}
426 
getLocaleTemplateUrlLoader()427     private LocaleTemplateUrlLoader getLocaleTemplateUrlLoader() {
428         if (mLocaleTemplateUrlLoader == null) {
429             mLocaleTemplateUrlLoader = new LocaleTemplateUrlLoader(getSpecialLocaleId());
430         }
431         return mLocaleTemplateUrlLoader;
432     }
433 
434     /**
435      * Get the list of search engines that a user may choose between.
436      * @param promoType Which search engine list to show.
437      * @return List of engines to show.
438      */
getSearchEnginesForPromoDialog(@earchEnginePromoType int promoType)439     public List<TemplateUrl> getSearchEnginesForPromoDialog(@SearchEnginePromoType int promoType) {
440         throw new IllegalStateException(
441                 "Not applicable unless existing or new promos are required");
442     }
443 
444     /** Set a LocaleManager to be used for testing. */
445     @VisibleForTesting
setInstanceForTest(LocaleManager instance)446     public static void setInstanceForTest(LocaleManager instance) {
447         sInstance = instance;
448     }
449 
450     /**
451      * Record any locale based metrics related with the search widget. Recorded on initialization
452      * only.
453      * @param widgetPresent Whether there is at least one search widget on home screen.
454      */
recordLocaleBasedSearchWidgetMetrics(boolean widgetPresent)455     public void recordLocaleBasedSearchWidgetMetrics(boolean widgetPresent) {}
456 
457     /**
458      * @return Whether the search engine promo has been shown and the user selected a valid option
459      *         and successfully completed the promo.
460      */
hasCompletedSearchEnginePromo()461     public boolean hasCompletedSearchEnginePromo() {
462         return mSearchEnginePromoCompleted;
463     }
464 
465     /**
466      * @return Whether the search engine promo has been shown in this session.
467      */
hasShownSearchEnginePromoThisSession()468     public boolean hasShownSearchEnginePromoThisSession() {
469         return mSearchEnginePromoShownThisSession;
470     }
471 
472     /**
473      * @return Whether we still have to check for whether search engine dialog is necessary.
474      */
needToCheckForSearchEnginePromo()475     public boolean needToCheckForSearchEnginePromo() {
476         if (ChromeFeatureList.isInitialized()
477                 && !ChromeFeatureList.isEnabled(
478                            ChromeFeatureList.SEARCH_ENGINE_PROMO_EXISTING_DEVICE)) {
479             return false;
480         }
481         @SearchEnginePromoState
482         int state = SharedPreferencesManager.getInstance().readInt(
483                 ChromePreferenceKeys.LOCALE_MANAGER_SEARCH_ENGINE_PROMO_SHOW_STATE,
484                 SearchEnginePromoState.SHOULD_CHECK);
485         return !mSearchEnginePromoCheckedThisSession
486                 && state == SearchEnginePromoState.SHOULD_CHECK;
487     }
488 
489     /**
490      * Record any locale based metrics related with search. Recorded per search.
491      * @param isFromSearchWidget Whether the search was performed from the search widget.
492      * @param url Url for the search made.
493      * @param transition The transition type for the navigation.
494      */
recordLocaleBasedSearchMetrics( boolean isFromSearchWidget, String url, @PageTransition int transition)495     public void recordLocaleBasedSearchMetrics(
496             boolean isFromSearchWidget, String url, @PageTransition int transition) {}
497 
498     /**
499      * @return Whether the user requires special handling.
500      */
isSpecialUser()501     public boolean isSpecialUser() {
502         if (CommandLine.getInstance().hasSwitch(ChromeSwitches.FORCE_ENABLE_SPECIAL_USER)) {
503             return true;
504         }
505         return false;
506     }
507 
508     /**
509      * Record metrics related to user type.
510      */
511     @CalledByNative
recordUserTypeMetrics()512     public void recordUserTypeMetrics() {}
513 }
514