// 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.StrictMode;
import android.os.UserManager;
import android.speech.RecognizerIntent;
import android.text.TextUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.FieldTrialList;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.download.DownloadUtils;
import org.chromium.chrome.browser.instantapps.InstantAppsHandler;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.tabmodel.DocumentModeAssassin;
import org.chromium.chrome.browser.webapps.ChromeWebApkHost;
import org.chromium.components.signin.AccountManagerHelper;
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.
*/
public class FeatureUtilities {
private static final String TAG = "FeatureUtilities";
private static final String HERB_EXPERIMENT_NAME = "TabManagementExperiment";
private static final String HERB_EXPERIMENT_FLAVOR_PARAM = "type";
private static Boolean sHasGoogleAccountAuthenticator;
private static Boolean sHasRecognitionIntentHandler;
private static Boolean sDocumentModeDisabled;
private static String sCachedHerbFlavor;
private static boolean sIsHerbFlavorCached;
/** Used to track if cached command line flags should be refreshed. */
private static CommandLine.ResetListener sResetListener = null;
/**
* 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) {
AccountManagerHelper accountHelper = AccountManagerHelper.get(context);
sHasGoogleAccountAuthenticator = accountHelper.hasGoogleAccountAuthenticator();
}
return sHasGoogleAccountAuthenticator;
}
@VisibleForTesting
static boolean hasGoogleAccounts(Context context) {
return AccountManagerHelper.get(context).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(context);
}
/**
* 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);
}
private static boolean isHerbDisallowed(Context context) {
return isDocumentMode(context);
}
/**
* @return Which flavor of Herb is being tested.
* See {@link ChromeSwitches#HERB_FLAVOR_ELDERBERRY} and its related switches.
*/
public static String getHerbFlavor() {
Context context = ContextUtils.getApplicationContext();
if (isHerbDisallowed(context)) return ChromeSwitches.HERB_FLAVOR_DISABLED;
if (!sIsHerbFlavorCached) {
sCachedHerbFlavor = null;
// Allowing disk access for preferences while prototyping.
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
sCachedHerbFlavor =
ChromePreferenceManager.getInstance(context).getCachedHerbFlavor();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
sIsHerbFlavorCached = true;
Log.d(TAG, "Retrieved cached Herb flavor: " + sCachedHerbFlavor);
}
return sCachedHerbFlavor;
}
/**
* Caches flags that must take effect on startup but are set via native code.
*/
public static void cacheNativeFlags() {
cacheHerbFlavor();
DownloadUtils.cacheIsDownloadHomeEnabled();
InstantAppsHandler.getInstance().cacheInstantAppsEnabled();
ChromeWebApkHost.cacheEnabledStateForNextLaunch();
}
/**
* Caches which flavor of Herb the user prefers from native.
*/
private static void cacheHerbFlavor() {
Context context = ContextUtils.getApplicationContext();
if (isHerbDisallowed(context)) return;
String oldFlavor = getHerbFlavor();
// Check the experiment value before the command line to put the user in the correct group.
// The first clause does the null checks so so we can freely use the startsWith() function.
String newFlavor = FieldTrialList.findFullName(HERB_EXPERIMENT_NAME);
Log.d(TAG, "Experiment flavor: " + newFlavor);
if (!TextUtils.isEmpty(newFlavor)
&& newFlavor.startsWith(ChromeSwitches.HERB_FLAVOR_ELDERBERRY)) {
newFlavor = ChromeSwitches.HERB_FLAVOR_ELDERBERRY;
} else {
newFlavor = ChromeSwitches.HERB_FLAVOR_DISABLED;
}
CommandLine instance = CommandLine.getInstance();
if (instance.hasSwitch(ChromeSwitches.HERB_FLAVOR_DISABLED_SWITCH)) {
newFlavor = ChromeSwitches.HERB_FLAVOR_DISABLED;
} else if (instance.hasSwitch(ChromeSwitches.HERB_FLAVOR_ELDERBERRY_SWITCH)) {
newFlavor = ChromeSwitches.HERB_FLAVOR_ELDERBERRY;
}
Log.d(TAG, "Caching flavor: " + newFlavor);
sCachedHerbFlavor = newFlavor;
if (!TextUtils.equals(oldFlavor, newFlavor)) {
ChromePreferenceManager.getInstance(context).setCachedHerbFlavor(newFlavor);
}
}
/**
* @return True if tab model merging for Android N+ is enabled.
*/
public static boolean isTabModelMergingEnabled() {
return Build.VERSION.SDK_INT > Build.VERSION_CODES.M;
}
private static native void nativeSetCustomTabVisible(boolean visible);
private static native void nativeSetIsInMultiWindowMode(boolean isInMultiWindowMode);
}