blob: 11f6916b86bd8a3280743d9fceb32333e6b5f8d3 [file] [log] [blame]
// Copyright 2014 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.util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.UserManager;
import android.speech.RecognizerIntent;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.StrictModeContext;
import org.chromium.base.SysUtils;
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.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.firstrun.FirstRunUtils;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.tabmodel.DocumentModeAssassin;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.variations.VariationsAssociatedData;
import org.chromium.ui.base.DeviceFormFactor;
import java.util.List;
/**
* A utility {@code class} meant to help determine whether or not certain features are supported by
* this device.
*
* This utility class also contains support for cached feature flags that must take effect on
* startup before native is initialized but are set via native code. The caching is done in
* {@link android.content.SharedPreferences}, which is available in Java immediately.
*
* When adding a new cached flag, it is common practice to use a static Boolean in this file to
* track whether the feature is enabled. A static method that returns the static Boolean can
* then be added to this file allowing client code to query whether the feature is enabled. The
* first time the method is called, the static Boolean should be set to the corresponding shared
* preference. After native is initialized, the shared preference will be updated to reflect the
* native flag value (e.g. the actual experimental feature flag value).
*
* When using a cached flag, the static Boolean should be the source of truth for whether the
* feature is turned on for the current session. As such, always rely on the static Boolean
* when determining whether the corresponding experimental behavior should be enabled. When
* querying whether a cached feature is enabled from native, an @CalledByNative method can be
* exposed in this file to allow feature_utilities.cc to retrieve the cached value.
*
* For cached flags that are queried before native is initialized, when a new experiment
* configuration is received the metrics reporting system will record metrics as if the
* experiment is enabled despite the experimental behavior not yet taking effect. This will be
* remedied on the next process restart, when the static Boolean is reset to the newly cached
* value in shared preferences.
*/
public class FeatureUtilities {
private static final String TAG = "FeatureUtilities";
private static final Integer CONTEXTUAL_SUGGESTIONS_TOOLBAR_MIN_DP = 320;
private static Boolean sHasGoogleAccountAuthenticator;
private static Boolean sHasRecognitionIntentHandler;
private static Boolean sIsSoleEnabled;
private static Boolean sIsHomePageButtonForceEnabled;
private static Boolean sIsHomepageTileEnabled;
private static Boolean sIsNewTabPageButtonEnabled;
private static Boolean sIsBottomToolbarEnabled;
private static Boolean sShouldInflateToolbarOnBackgroundThread;
private static Boolean sIsNightModeAvailable;
private static Boolean sDownloadAutoResumptionEnabledInNative;
private static final String NTP_BUTTON_TRIAL_NAME = "NewTabPage";
private static final String NTP_BUTTON_VARIANT_PARAM_NAME = "variation";
/**
* Determines whether or not the {@link RecognizerIntent#ACTION_WEB_SEARCH} {@link Intent}
* is handled by any {@link android.app.Activity}s in the system. The result will be cached for
* future calls. Passing {@code false} to {@code useCachedValue} will force it to re-query any
* {@link android.app.Activity}s that can process the {@link Intent}.
* @param context The {@link Context} to use to check to see if the {@link Intent} will
* be handled.
* @param useCachedValue Whether or not to use the cached value from a previous result.
* @return {@code true} if recognition is supported. {@code false} otherwise.
*/
public static boolean isRecognitionIntentPresent(Context context, boolean useCachedValue) {
ThreadUtils.assertOnUiThread();
if (sHasRecognitionIntentHandler == null || !useCachedValue) {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> activities = pm.queryIntentActivities(
new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
sHasRecognitionIntentHandler = activities.size() > 0;
}
return sHasRecognitionIntentHandler;
}
/**
* Determines whether or not the user has a Google account (so we can sync) or can add one.
* @param context The {@link Context} that we should check accounts under.
* @return Whether or not sync is allowed on this device.
*/
public static boolean canAllowSync(Context context) {
return (hasGoogleAccountAuthenticator(context) && hasSyncPermissions(context))
|| hasGoogleAccounts(context);
}
@VisibleForTesting
static boolean hasGoogleAccountAuthenticator(Context context) {
if (sHasGoogleAccountAuthenticator == null) {
AccountManagerFacade accountHelper = AccountManagerFacade.get();
sHasGoogleAccountAuthenticator = accountHelper.hasGoogleAccountAuthenticator();
}
return sHasGoogleAccountAuthenticator;
}
@VisibleForTesting
static boolean hasGoogleAccounts(Context context) {
return AccountManagerFacade.get().hasGoogleAccounts();
}
@SuppressLint("InlinedApi")
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private static boolean hasSyncPermissions(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return true;
UserManager manager = (UserManager) context.getSystemService(Context.USER_SERVICE);
Bundle userRestrictions = manager.getUserRestrictions();
return !userRestrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false);
}
/**
* Check whether Chrome should be running on document mode.
* @param context The context to use for checking configuration.
* @return Whether Chrome should be running on document mode.
*/
public static boolean isDocumentMode(Context context) {
return isDocumentModeEligible(context) && !DocumentModeAssassin.isOptedOutOfDocumentMode();
}
/**
* Whether the device could possibly run in Document mode (may return true even if the document
* mode is turned off).
*
* This function can't be changed to return false (even if document mode is deleted) because we
* need to know whether a user needs to be migrated away.
*
* @param context The context to use for checking configuration.
* @return Whether the device could possibly run in Document mode.
*/
public static boolean isDocumentModeEligible(Context context) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& !DeviceFormFactor.isTablet();
}
/**
* Records the current custom tab visibility state with native-side feature utilities.
* @param visible Whether a custom tab is visible.
*/
public static void setCustomTabVisible(boolean visible) {
nativeSetCustomTabVisible(visible);
}
/**
* Records whether the activity is in multi-window mode with native-side feature utilities.
* @param isInMultiWindowMode Whether the activity is in Android N multi-window mode.
*/
public static void setIsInMultiWindowMode(boolean isInMultiWindowMode) {
nativeSetIsInMultiWindowMode(isInMultiWindowMode);
}
/**
* Caches flags that must take effect on startup but are set via native code.
*/
public static void cacheNativeFlags() {
cacheSoleEnabled();
cacheCommandLineOnNonRootedEnabled();
FirstRunUtils.cacheFirstRunPrefs();
cacheHomePageButtonForceEnabled();
cacheHomepageTileEnabled();
cacheNewTabPageButtonEnabled();
cacheBottomToolbarEnabled();
cacheInflateToolbarOnBackgroundThread();
cacheNightModeAvailable();
cacheDownloadAutoResumptionEnabledInNative();
// Propagate DONT_PREFETCH_LIBRARIES and REACHED_CODE_PROFILER feature values to
// LibraryLoader. This can't be done in LibraryLoader itself because it lives in //base and
// can't depend on ChromeFeatureList.
LibraryLoader.setDontPrefetchLibrariesOnNextRuns(
ChromeFeatureList.isEnabled(ChromeFeatureList.DONT_PREFETCH_LIBRARIES));
LibraryLoader.setReachedCodeProfilerEnabledOnNextRuns(
ChromeFeatureList.isEnabled(ChromeFeatureList.REACHED_CODE_PROFILER));
}
/**
* @return True if tab model merging for Android N+ is enabled.
*/
public static boolean isTabModelMergingEnabled() {
if (CommandLine.getInstance().hasSwitch(ChromeSwitches.DISABLE_TAB_MERGING_FOR_TESTING)) {
return false;
}
return Build.VERSION.SDK_INT > Build.VERSION_CODES.M;
}
/**
* Cache whether or not the home page button is force enabled so on next startup, the value can
* be made available immediately.
*/
public static void cacheHomePageButtonForceEnabled() {
if (PartnerBrowserCustomizations.isHomepageProviderAvailableAndEnabled()) return;
ChromePreferenceManager.getInstance().writeBoolean(
ChromePreferenceManager.HOME_PAGE_BUTTON_FORCE_ENABLED_KEY,
ChromeFeatureList.isEnabled(ChromeFeatureList.HOME_PAGE_BUTTON_FORCE_ENABLED));
}
/**
* @return Whether or not the home page button is force enabled.
*/
public static boolean isHomePageButtonForceEnabled() {
if (sIsHomePageButtonForceEnabled == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sIsHomePageButtonForceEnabled = prefManager.readBoolean(
ChromePreferenceManager.HOME_PAGE_BUTTON_FORCE_ENABLED_KEY, false);
}
}
return sIsHomePageButtonForceEnabled;
}
/**
* Resets whether the home page button is enabled for tests. After this is called, the next
* call to #isHomePageButtonForceEnabled() will retrieve the value from shared preferences.
*/
public static void resetHomePageButtonForceEnabledForTests() {
sIsHomePageButtonForceEnabled = null;
}
/**
* Cache whether or not the toolbar should be inflated on a background thread so on next
* startup, the value can be made available immediately.
*/
public static void cacheInflateToolbarOnBackgroundThread() {
boolean onBackgroundThread =
ChromeFeatureList.isEnabled(ChromeFeatureList.INFLATE_TOOLBAR_ON_BACKGROUND_THREAD);
ChromePreferenceManager.getInstance().writeBoolean(
ChromePreferenceManager.INFLATE_TOOLBAR_ON_BACKGROUND_THREAD_KEY,
onBackgroundThread);
}
public static boolean shouldInflateToolbarOnBackgroundThread() {
if (sShouldInflateToolbarOnBackgroundThread == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sShouldInflateToolbarOnBackgroundThread = prefManager.readBoolean(
ChromePreferenceManager.INFLATE_TOOLBAR_ON_BACKGROUND_THREAD_KEY, false);
}
}
return sShouldInflateToolbarOnBackgroundThread;
}
/**
* @return Whether or not the download auto-resumptions should be enabled in native.
*/
@CalledByNative
public static boolean isDownloadAutoResumptionEnabledInNative() {
if (sDownloadAutoResumptionEnabledInNative == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sDownloadAutoResumptionEnabledInNative = prefManager.readBoolean(
ChromePreferenceManager.DOWNLOAD_AUTO_RESUMPTION_IN_NATIVE_KEY, true);
}
}
return sDownloadAutoResumptionEnabledInNative;
}
/**
* Cache whether or not the new tab page button is enabled so on next startup, the value can
* be made available immediately.
*/
public static void cacheHomepageTileEnabled() {
ChromePreferenceManager.getInstance().writeBoolean(
ChromePreferenceManager.HOMEPAGE_TILE_ENABLED_KEY,
ChromeFeatureList.isEnabled(ChromeFeatureList.HOMEPAGE_TILE));
}
/**
* @return Whether or not the new tab page button is enabled.
*/
public static boolean isHomepageTileEnabled() {
if (sIsHomepageTileEnabled == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sIsHomepageTileEnabled = prefManager.readBoolean(
ChromePreferenceManager.HOMEPAGE_TILE_ENABLED_KEY, false);
}
}
return sIsHomepageTileEnabled;
}
/**
* Cache whether or not the new tab page button is enabled so that on next startup, it can be
* made available immediately.
*/
private static void cacheNewTabPageButtonEnabled() {
boolean isNTPButtonEnabled = ChromeFeatureList.isEnabled(ChromeFeatureList.NTP_BUTTON);
ChromePreferenceManager.getInstance().writeBoolean(
ChromePreferenceManager.NTP_BUTTON_ENABLED_KEY, isNTPButtonEnabled);
}
/**
* Gets the new tab page button variant from variations associated data.
* Native must be initialized before this method is called.
* @return The new tab page button variant.
*/
public static String getNTPButtonVariant() {
return VariationsAssociatedData.getVariationParamValue(
NTP_BUTTON_TRIAL_NAME, NTP_BUTTON_VARIANT_PARAM_NAME);
}
/**
* @return Whether or not the new tab page button is enabled.
*/
public static boolean isNewTabPageButtonEnabled() {
if (sIsNewTabPageButtonEnabled == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sIsNewTabPageButtonEnabled = prefManager.readBoolean(
ChromePreferenceManager.NTP_BUTTON_ENABLED_KEY, false);
}
}
return sIsNewTabPageButtonEnabled;
}
/**
* Cache whether or not the bottom toolbar is enabled so on next startup, the value can
* be made available immediately.
*/
public static void cacheBottomToolbarEnabled() {
ChromePreferenceManager.getInstance().writeBoolean(
ChromePreferenceManager.BOTTOM_TOOLBAR_ENABLED_KEY,
ChromeFeatureList.isEnabled(ChromeFeatureList.CHROME_DUET));
}
/**
* Cache whether or not download auto-resumptions are enabled in native so on next startup, the
* value can be made available immediately.
*/
private static void cacheDownloadAutoResumptionEnabledInNative() {
ChromePreferenceManager.getInstance().writeBoolean(
ChromePreferenceManager.DOWNLOAD_AUTO_RESUMPTION_IN_NATIVE_KEY,
ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE));
}
/**
* @return Whether or not the bottom toolbar is enabled.
*/
public static boolean isBottomToolbarEnabled() {
if (sIsBottomToolbarEnabled == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sIsBottomToolbarEnabled = prefManager.readBoolean(
ChromePreferenceManager.BOTTOM_TOOLBAR_ENABLED_KEY, false);
}
}
return sIsBottomToolbarEnabled
&& !DeviceFormFactor.isNonMultiDisplayContextOnTablet(
ContextUtils.getApplicationContext());
}
/**
* Cache whether or not night mode is available (i.e. night mode experiment is enabled) so on
* next startup, the value can be made available immediately.
*/
public static void cacheNightModeAvailable() {
ChromePreferenceManager.getInstance().writeBoolean(
ChromePreferenceManager.NIGHT_MODE_AVAILABLE_KEY,
ChromeFeatureList.isEnabled(ChromeFeatureList.ANDROID_NIGHT_MODE));
}
/**
* @return Whether or not night mode experiment is enabled (i.e. night mode experiment is
* enabled).
*/
public static boolean isNightModeAvailable() {
if (sIsNightModeAvailable == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sIsNightModeAvailable = prefManager.readBoolean(
ChromePreferenceManager.NIGHT_MODE_AVAILABLE_KEY, false);
}
}
return sIsNightModeAvailable;
}
/**
* Cache whether or not command line is enabled on non-rooted devices.
*/
private static void cacheCommandLineOnNonRootedEnabled() {
boolean isCommandLineOnNonRootedEnabled =
ChromeFeatureList.isEnabled(ChromeFeatureList.COMMAND_LINE_ON_NON_ROOTED);
ChromePreferenceManager manager = ChromePreferenceManager.getInstance();
manager.writeBoolean(ChromePreferenceManager.COMMAND_LINE_ON_NON_ROOTED_ENABLED_KEY,
isCommandLineOnNonRootedEnabled);
}
/**
* @return Whether or not the download progress infobar is enabled.
*/
public static boolean isDownloadProgressInfoBarEnabled() {
return ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_PROGRESS_INFOBAR);
}
/**
* Cache whether or not Sole integration is enabled.
*/
public static void cacheSoleEnabled() {
boolean featureEnabled = ChromeFeatureList.isEnabled(ChromeFeatureList.SOLE_INTEGRATION);
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
boolean prefEnabled =
prefManager.readBoolean(ChromePreferenceManager.SOLE_INTEGRATION_ENABLED_KEY, true);
if (featureEnabled == prefEnabled) return;
prefManager.writeBoolean(
ChromePreferenceManager.SOLE_INTEGRATION_ENABLED_KEY, featureEnabled);
}
/**
* @return Whether or not Sole integration is enabled.
*/
public static boolean isSoleEnabled() {
if (sIsSoleEnabled == null) {
ChromePreferenceManager prefManager = ChromePreferenceManager.getInstance();
// Allow disk access for preferences while Sole is in experimentation.
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
sIsSoleEnabled = prefManager.readBoolean(
ChromePreferenceManager.SOLE_INTEGRATION_ENABLED_KEY, true);
}
}
return sIsSoleEnabled;
}
/**
* @param activityContext The context for the containing activity.
* @return Whether contextual suggestions are enabled.
*/
public static boolean areContextualSuggestionsEnabled(Context activityContext) {
int smallestScreenWidth =
activityContext.getResources().getConfiguration().smallestScreenWidthDp;
return !DeviceFormFactor.isNonMultiDisplayContextOnTablet(activityContext)
&& !LocaleManager.getInstance().needToCheckForSearchEnginePromo()
&& (smallestScreenWidth >= CONTEXTUAL_SUGGESTIONS_TOOLBAR_MIN_DP
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON));
}
/**
* @return Whether this device is running Android Go. This is assumed when we're running Android
* O or later and we're on a low-end device.
*/
public static boolean isAndroidGo() {
return SysUtils.isLowEndDevice()
&& android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O;
}
/**
* @return Whether no-touch-mode is enabled.
*/
public static boolean isNoTouchModeEnabled() {
return CommandLine.getInstance().hasSwitch(ChromeSwitches.NO_TOUCH_MODE);
}
private static native void nativeSetCustomTabVisible(boolean visible);
private static native void nativeSetIsInMultiWindowMode(boolean isInMultiWindowMode);
}