blob: 66536a05c0b4008f351dd11d6005bd8fc96404c3 [file] [log] [blame]
// Copyright 2016 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.locale;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.StrictMode;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import org.chromium.base.ActivityState;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.PreferencesLauncher;
import org.chromium.chrome.browser.preferences.SearchEnginePreference;
import org.chromium.chrome.browser.search_engines.TemplateUrl;
import org.chromium.chrome.browser.search_engines.TemplateUrlService;
import org.chromium.chrome.browser.snackbar.Snackbar;
import org.chromium.chrome.browser.snackbar.SnackbarManager;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.browser.vr.OnExitVrRequestListener;
import org.chromium.chrome.browser.vr.VrIntentUtils;
import org.chromium.chrome.browser.vr.VrShellDelegate;
import org.chromium.chrome.browser.widget.PromoDialog;
import org.chromium.ui.base.PageTransition;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.Callable;
/**
* Manager for some locale specific logics.
*/
public class LocaleManager {
public static final String PREF_AUTO_SWITCH = "LocaleManager_PREF_AUTO_SWITCH";
public static final String PREF_PROMO_SHOWN = "LocaleManager_PREF_PROMO_SHOWN";
public static final String PREF_WAS_IN_SPECIAL_LOCALE = "LocaleManager_WAS_IN_SPECIAL_LOCALE";
public static final String SPECIAL_LOCALE_ID = "US";
/** The current state regarding search engine promo dialogs. */
@IntDef({SEARCH_ENGINE_PROMO_SHOULD_CHECK, SEARCH_ENGINE_PROMO_CHECKED_NOT_SHOWN,
SEARCH_ENGINE_PROMO_CHECKED_AND_SHOWN})
@Retention(RetentionPolicy.SOURCE)
public @interface SearchEnginePromoState {}
public static final int SEARCH_ENGINE_PROMO_SHOULD_CHECK = -1;
public static final int SEARCH_ENGINE_PROMO_CHECKED_NOT_SHOWN = 0;
public static final int SEARCH_ENGINE_PROMO_CHECKED_AND_SHOWN = 1;
/** The different types of search engine promo dialogs. */
@IntDef({SEARCH_ENGINE_PROMO_DONT_SHOW, SEARCH_ENGINE_PROMO_SHOW_SOGOU,
SEARCH_ENGINE_PROMO_SHOW_EXISTING, SEARCH_ENGINE_PROMO_SHOW_NEW})
@Retention(RetentionPolicy.SOURCE)
public @interface SearchEnginePromoType {}
public static final int SEARCH_ENGINE_PROMO_DONT_SHOW = -1;
public static final int SEARCH_ENGINE_PROMO_SHOW_SOGOU = 0;
public static final int SEARCH_ENGINE_PROMO_SHOW_EXISTING = 1;
public static final int SEARCH_ENGINE_PROMO_SHOW_NEW = 2;
protected static final String KEY_SEARCH_ENGINE_PROMO_SHOW_STATE =
"com.android.chrome.SEARCH_ENGINE_PROMO_SHOWN";
private static final int SNACKBAR_DURATION_MS = 6000;
private static LocaleManager sInstance;
private boolean mSearchEnginePromoCompleted;
private boolean mSearchEnginePromoShownThisSession;
private boolean mSearchEnginePromoCheckedThisSession;
// LocaleManager is a singleton and it should not have strong reference to UI objects.
// SnackbarManager is owned by ChromeActivity and is not null as long as the activity is alive.
private WeakReference<SnackbarManager> mSnackbarManager = new WeakReference<>(null);
private LocaleTemplateUrlLoader mLocaleTemplateUrlLoader;
private SnackbarController mSnackbarController = new SnackbarController() {
@Override
public void onDismissNoAction(Object actionData) { }
@Override
public void onAction(Object actionData) {
Context context = ContextUtils.getApplicationContext();
Intent intent = PreferencesLauncher.createIntentForSettingsPage(context,
SearchEnginePreference.class.getName());
context.startActivity(intent);
}
};
/**
* @return An instance of the {@link LocaleManager}. This should only be called on UI thread.
*/
@CalledByNative
public static LocaleManager getInstance() {
assert ThreadUtils.runningOnUiThread();
if (sInstance == null) {
sInstance = AppHooks.get().createLocaleManager();
}
return sInstance;
}
/**
* Default constructor.
*/
public LocaleManager() {
int state = SEARCH_ENGINE_PROMO_SHOULD_CHECK;
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
state = ContextUtils.getAppSharedPreferences().getInt(
KEY_SEARCH_ENGINE_PROMO_SHOW_STATE, SEARCH_ENGINE_PROMO_SHOULD_CHECK);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
mSearchEnginePromoCompleted = state == SEARCH_ENGINE_PROMO_CHECKED_AND_SHOWN;
}
/**
* Starts listening to state changes of the phone.
*/
public void startObservingPhoneChanges() {
maybeAutoSwitchSearchEngine();
}
/**
* Stops listening to state changes of the phone.
*/
public void stopObservingPhoneChanges() {}
/**
* Starts recording metrics in deferred startup.
*/
public void recordStartupMetrics() {}
/**
* @return Whether the Chrome instance is running in a special locale.
*/
public boolean isSpecialLocaleEnabled() {
// If there is a kill switch sent from the server, disable the feature.
if (!ChromeFeatureList.isEnabled("SpecialLocaleWrapper")) {
return false;
}
boolean inSpecialLocale = ChromeFeatureList.isEnabled("SpecialLocale");
inSpecialLocale = isReallyInSpecialLocale(inSpecialLocale);
return inSpecialLocale;
}
/**
* @return The country id of the special locale.
*/
public String getSpecialLocaleId() {
return SPECIAL_LOCALE_ID;
}
/**
* Adds local search engines for special locale.
*/
public void addSpecialSearchEngines() {
if (!isSpecialLocaleEnabled()) return;
getLocaleTemplateUrlLoader().loadTemplateUrls();
}
/**
* Removes local search engines for special locale.
*/
public void removeSpecialSearchEngines() {
if (isSpecialLocaleEnabled()) return;
getLocaleTemplateUrlLoader().removeTemplateUrls();
}
/**
* Overrides the default search engine to a different search engine we designate. This is a
* no-op if the user has manually changed DSP settings.
*/
public void overrideDefaultSearchEngine() {
if (!isSearchEngineAutoSwitchEnabled() || !isSpecialLocaleEnabled()) return;
getLocaleTemplateUrlLoader().overrideDefaultSearchProvider();
showSnackbar(ContextUtils.getApplicationContext().getString(R.string.using_sogou));
}
/**
* Reverts the temporary change made in {@link #overrideDefaultSearchEngine()}. This is a no-op
* if the user has manually changed DSP settings.
*/
public void revertDefaultSearchEngineOverride() {
if (!isSearchEngineAutoSwitchEnabled() || isSpecialLocaleEnabled()) return;
getLocaleTemplateUrlLoader().setGoogleAsDefaultSearch();
showSnackbar(ContextUtils.getApplicationContext().getString(R.string.using_google));
}
/**
* Switches the default search engine based on the current locale, if the user has delegated
* Chrome to do so. This method also adds some special engines to user's search engine list, as
* long as the user is in this locale.
*/
protected void maybeAutoSwitchSearchEngine() {
SharedPreferences preferences = ContextUtils.getAppSharedPreferences();
boolean wasInSpecialLocale = preferences.getBoolean(PREF_WAS_IN_SPECIAL_LOCALE, false);
boolean isInSpecialLocale = isSpecialLocaleEnabled();
if (wasInSpecialLocale && !isInSpecialLocale) {
revertDefaultSearchEngineOverride();
removeSpecialSearchEngines();
} else if (isInSpecialLocale && !wasInSpecialLocale) {
addSpecialSearchEngines();
overrideDefaultSearchEngine();
} else if (isInSpecialLocale) {
// As long as the user is in the special locale, special engines should be in the list.
addSpecialSearchEngines();
}
preferences.edit().putBoolean(PREF_WAS_IN_SPECIAL_LOCALE, isInSpecialLocale).apply();
}
/**
* Shows a promotion dialog about search engines depending on Locale and other conditions.
* See {@link LocaleManager#getSearchEnginePromoShowType()} for possible types and logic.
*
* @param activity Activity showing the dialog.
* @param onSearchEngineFinalized Notified when the search engine has been finalized. This can
* either mean no dialog is needed, or the dialog was needed and
* the user completed the dialog with a valid selection.
*/
public void showSearchEnginePromoIfNeeded(
final Activity activity, final @Nullable Callback<Boolean> onSearchEngineFinalized) {
assert LibraryLoader.getInstance().isInitialized();
TemplateUrlService.getInstance().runWhenLoaded(new Runnable() {
@Override
public void run() {
handleSearchEnginePromoWithTemplateUrlsLoaded(activity, onSearchEngineFinalized);
}
});
}
private void handleSearchEnginePromoWithTemplateUrlsLoaded(
final Activity activity, final @Nullable Callback<Boolean> onSearchEngineFinalized) {
assert TemplateUrlService.getInstance().isLoaded();
final Callback<Boolean> finalizeInternalCallback = new Callback<Boolean>() {
@Override
public void onResult(Boolean result) {
if (result != null && result) {
mSearchEnginePromoCheckedThisSession = true;
} else {
@SearchEnginePromoType
int promoType = getSearchEnginePromoShowType();
if (promoType == SEARCH_ENGINE_PROMO_SHOW_EXISTING
|| promoType == SEARCH_ENGINE_PROMO_SHOW_NEW) {
onUserLeavePromoDialogWithNoConfirmedChoice(promoType);
}
}
if (onSearchEngineFinalized != null) onSearchEngineFinalized.onResult(result);
}
};
if (TemplateUrlService.getInstance().isDefaultSearchManaged()
|| ApiCompatibilityUtils.isDemoUser(activity)) {
finalizeInternalCallback.onResult(true);
return;
}
final int shouldShow = getSearchEnginePromoShowType();
Callable<PromoDialog> dialogCreator;
switch (shouldShow) {
case SEARCH_ENGINE_PROMO_DONT_SHOW:
finalizeInternalCallback.onResult(true);
return;
case SEARCH_ENGINE_PROMO_SHOW_SOGOU:
dialogCreator = new Callable<PromoDialog>() {
@Override
public PromoDialog call() throws Exception {
return new SogouPromoDialog(
activity, LocaleManager.this, finalizeInternalCallback);
}
};
break;
case SEARCH_ENGINE_PROMO_SHOW_EXISTING:
case SEARCH_ENGINE_PROMO_SHOW_NEW:
dialogCreator = new Callable<PromoDialog>() {
@Override
public PromoDialog call() throws Exception {
return new DefaultSearchEnginePromoDialog(
activity, shouldShow, finalizeInternalCallback);
}
};
break;
default:
assert false;
finalizeInternalCallback.onResult(true);
return;
}
// If the activity has been destroyed by the time the TemplateUrlService has
// loaded, then do not attempt to show the dialog.
if (ApplicationStatus.getStateForActivity(activity) == ActivityState.DESTROYED) {
finalizeInternalCallback.onResult(false);
return;
}
if (VrIntentUtils.isLaunchingIntoVr(activity, activity.getIntent())
|| VrShellDelegate.isInVr()) {
showPromoDialogForVr(dialogCreator, activity);
} else {
showPromoDialog(dialogCreator);
}
mSearchEnginePromoShownThisSession = true;
}
private void showPromoDialogForVr(Callable<PromoDialog> dialogCreator, Activity activity) {
VrShellDelegate.requestToExitVrForSearchEnginePromoDialog(new OnExitVrRequestListener() {
@Override
public void onSucceeded() {
showPromoDialog(dialogCreator);
}
@Override
public void onDenied() {
// We need to make sure that the dialog shows up even if user denied to
// leave VR.
VrShellDelegate.forceExitVrImmediately();
showPromoDialog(dialogCreator);
}
}, activity);
}
private void showPromoDialog(Callable<PromoDialog> dialogCreator) {
try {
dialogCreator.call().show();
} catch (Exception e) {
// Exception is caught purely because Callable states it can be thrown. This is never
// expected to be hit.
throw new RuntimeException(e);
}
}
/**
* @return Whether auto switch for search engine is enabled.
*/
public boolean isSearchEngineAutoSwitchEnabled() {
return ContextUtils.getAppSharedPreferences().getBoolean(PREF_AUTO_SWITCH, false);
}
/**
* Sets whether auto switch for search engine is enabled.
*/
public void setSearchEngineAutoSwitch(boolean isEnabled) {
ContextUtils.getAppSharedPreferences().edit().putBoolean(PREF_AUTO_SWITCH, isEnabled)
.apply();
}
/**
* Sets the {@link SnackbarManager} used by this instance.
*/
public void setSnackbarManager(SnackbarManager manager) {
mSnackbarManager = new WeakReference<SnackbarManager>(manager);
}
private void showSnackbar(CharSequence title) {
SnackbarManager manager = mSnackbarManager.get();
if (manager == null) return;
Context context = ContextUtils.getApplicationContext();
Snackbar snackbar = Snackbar.make(title, mSnackbarController, Snackbar.TYPE_NOTIFICATION,
Snackbar.UMA_SPECIAL_LOCALE);
snackbar.setDuration(SNACKBAR_DURATION_MS);
snackbar.setAction(context.getString(R.string.preferences), null);
manager.showSnackbar(snackbar);
}
/**
* Does some extra checking about whether the user is in special locale.
* @param inSpecialLocale Whether the variation service thinks the client is in special locale.
* @return The result after extra confirmation.
*/
protected boolean isReallyInSpecialLocale(boolean inSpecialLocale) {
return inSpecialLocale;
}
/**
* @return Whether and which search engine promo should be shown.
*/
@SearchEnginePromoType
public int getSearchEnginePromoShowType() {
if (!isSpecialLocaleEnabled()) return SEARCH_ENGINE_PROMO_DONT_SHOW;
SharedPreferences preferences = ContextUtils.getAppSharedPreferences();
if (preferences.getBoolean(PREF_PROMO_SHOWN, false)) {
return SEARCH_ENGINE_PROMO_DONT_SHOW;
}
return SEARCH_ENGINE_PROMO_SHOW_SOGOU;
}
/**
* @return The referral ID to be passed when searching with Yandex as the DSE.
*/
@CalledByNative
protected String getYandexReferralId() {
return "";
}
/**
* @return The referral ID to be passed when searching with Mail.RU as the DSE.
*/
@CalledByNative
protected String getMailRUReferralId() {
return "";
}
/**
* To be called after the user has made a selection from a search engine promo dialog.
* @param type The type of search engine promo dialog that was shown.
* @param keywords The keywords for all search engines listed in the order shown to the user.
* @param keyword The keyword for the search engine chosen.
*/
protected void onUserSearchEngineChoiceFromPromoDialog(
@SearchEnginePromoType int type, List<String> keywords, String keyword) {
TemplateUrlService.getInstance().setSearchEngine(keyword);
ContextUtils.getAppSharedPreferences()
.edit()
.putInt(KEY_SEARCH_ENGINE_PROMO_SHOW_STATE, SEARCH_ENGINE_PROMO_CHECKED_AND_SHOWN)
.apply();
mSearchEnginePromoCompleted = true;
}
/**
* To be called when the search engine promo dialog is dismissed without the user confirming
* a valid search engine selection.
*/
protected void onUserLeavePromoDialogWithNoConfirmedChoice(@SearchEnginePromoType int type) {}
private LocaleTemplateUrlLoader getLocaleTemplateUrlLoader() {
if (mLocaleTemplateUrlLoader == null) {
mLocaleTemplateUrlLoader = new LocaleTemplateUrlLoader(getSpecialLocaleId());
}
return mLocaleTemplateUrlLoader;
}
/**
* Get the list of search engines that a user may choose between.
* @param promoType Which search engine list to show.
* @return List of engines to show.
*/
public List<TemplateUrl> getSearchEnginesForPromoDialog(@SearchEnginePromoType int promoType) {
throw new IllegalStateException(
"Not applicable unless existing or new promos are required");
}
/** Set a LocaleManager to be used for testing. */
@VisibleForTesting
public static void setInstanceForTest(LocaleManager instance) {
sInstance = instance;
}
/**
* Record any locale based metrics related with the search widget. Recorded on initialization
* only.
* @param widgetPresent Whether there is at least one search widget on home screen.
*/
public void recordLocaleBasedSearchWidgetMetrics(boolean widgetPresent) {}
/**
* @return Whether the search engine promo has been shown and the user selected a valid option
* and successfully completed the promo.
*/
public boolean hasCompletedSearchEnginePromo() {
return mSearchEnginePromoCompleted;
}
/**
* @return Whether the search engine promo has been shown in this session.
*/
public boolean hasShownSearchEnginePromoThisSession() {
return mSearchEnginePromoShownThisSession;
}
/**
* @return Whether we still have to check for whether search engine dialog is necessary.
*/
public boolean needToCheckForSearchEnginePromo() {
if (ChromeFeatureList.isInitialized()
&& !ChromeFeatureList.isEnabled(
ChromeFeatureList.SEARCH_ENGINE_PROMO_EXISTING_DEVICE)) {
return false;
}
int state = SEARCH_ENGINE_PROMO_SHOULD_CHECK;
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
state = ContextUtils.getAppSharedPreferences().getInt(
KEY_SEARCH_ENGINE_PROMO_SHOW_STATE, SEARCH_ENGINE_PROMO_SHOULD_CHECK);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
return !mSearchEnginePromoCheckedThisSession && state == SEARCH_ENGINE_PROMO_SHOULD_CHECK;
}
/**
* Record any locale based metrics related with search. Recorded per search.
* @param isFromSearchWidget Whether the search was performed from the search widget.
* @param url Url for the search made.
* @param transition The transition type for the navigation.
*/
public void recordLocaleBasedSearchMetrics(
boolean isFromSearchWidget, String url, @PageTransition int transition) {}
}