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