// Copyright 2015 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.externalauth;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Binder;
import android.os.StrictMode;
import android.os.SystemClock;
import android.text.TextUtils;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.metrics.LaunchMetrics.SparseHistogramSample;
import org.chromium.chrome.browser.metrics.LaunchMetrics.TimesHistogramSample;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* Utility class for external authentication tools.
*
* This class is safe to use on any thread.
*/
public class ExternalAuthUtils {
public static final int FLAG_SHOULD_BE_GOOGLE_SIGNED = 1 << 0;
public static final int FLAG_SHOULD_BE_SYSTEM = 1 << 1;
private static final String TAG = "ExternalAuthUtils";
// Use an AtomicReference since getInstance() can be called from multiple threads.
private static AtomicReference<ExternalAuthUtils> sInstance =
new AtomicReference<ExternalAuthUtils>();
private final SparseHistogramSample mConnectionResultHistogramSample =
new SparseHistogramSample("GooglePlayServices.ConnectionResult");
private final TimesHistogramSample mRegistrationTimeHistogramSample = new TimesHistogramSample(
"Android.StrictMode.CheckGooglePlayServicesTime", TimeUnit.MILLISECONDS);
/**
* Returns the singleton instance of ExternalAuthUtils, creating it if needed.
*/
public static ExternalAuthUtils getInstance() {
if (sInstance.get() == null) {
ChromeApplication application =
(ChromeApplication) ContextUtils.getApplicationContext();
sInstance.compareAndSet(null, application.createExternalAuthUtils());
}
return sInstance.get();
}
/**
* Gets the calling package names for the current transaction.
* @param context The context to use for accessing the package manager.
* @return The calling package names.
*/
private static String[] getCallingPackages(Context context) {
int callingUid = Binder.getCallingUid();
PackageManager pm = context.getApplicationContext().getPackageManager();
return pm.getPackagesForUid(callingUid);
}
/**
* Returns whether the caller application is a part of the system build.
* @param pm Package manager to use for getting package related info.
* @param packageName The package name to inquire about.
*/
@VisibleForTesting
// TODO(crbug.com/635567): Fix this properly.
@SuppressLint("WrongConstant")
public boolean isSystemBuild(PackageManager pm, String packageName) {
try {
ApplicationInfo info = pm.getApplicationInfo(packageName, ApplicationInfo.FLAG_SYSTEM);
if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) throw new SecurityException();
} catch (NameNotFoundException e) {
Log.e(TAG, "Package with name " + packageName + " not found");
return false;
} catch (SecurityException e) {
Log.e(TAG, "Caller with package name " + packageName + " is not in the system build");
return false;
}
return true;
}
/**
* Returns whether the current build of Chrome is a Google-signed package.
*
* @param context the current context.
* @return whether the currently running application is signed with Google keys.
*/
public boolean isChromeGoogleSigned(Context context) {
return isGoogleSigned(context, context.getPackageName());
}
/**
* Returns whether the call is originating from a Google-signed package.
* @param appContext the current context.
* @param packageName The package name to inquire about.
*/
public boolean isGoogleSigned(Context context, String packageName) {
// This is overridden in a subclass.
return false;
}
/**
* Returns whether the callers of the current transaction contains a package that matches
* the give authentication requirements.
* @param context The context to use for getting package information.
* @param authRequirements The requirements to be exercised on the caller.
* @param packageToMatch The package name to compare with the caller.
* @return Whether the caller meets the authentication requirements.
*/
private boolean isCallerValid(Context context, int authRequirements, String packageToMatch) {
boolean shouldBeGoogleSigned = (authRequirements & FLAG_SHOULD_BE_GOOGLE_SIGNED) != 0;
boolean shouldBeSystem = (authRequirements & FLAG_SHOULD_BE_SYSTEM) != 0;
String[] callingPackages = getCallingPackages(context);
PackageManager pm = context.getApplicationContext().getPackageManager();
boolean matchFound = false;
for (String packageName : callingPackages) {
if (!TextUtils.isEmpty(packageToMatch) && !packageName.equals(packageToMatch)) continue;
matchFound = true;
if ((shouldBeGoogleSigned && !isGoogleSigned(context, packageName))
|| (shouldBeSystem && !isSystemBuild(pm, packageName))) {
return false;
}
}
return matchFound;
}
/**
* Returns whether the callers of the current transaction contains a package that matches
* the give authentication requirements.
* @param context The context to use for getting package information.
* @param authRequirements The requirements to be exercised on the caller.
* @param packageToMatch The package name to compare with the caller. Should be non-empty.
* @return Whether the caller meets the authentication requirements.
*/
public boolean isCallerValidForPackage(
Context context, int authRequirements, String packageToMatch) {
assert !TextUtils.isEmpty(packageToMatch);
return isCallerValid(context, authRequirements, packageToMatch);
}
/**
* Returns whether the callers of the current transaction matches the given authentication
* requirements.
* @param context The context to use for getting package information.
* @param authRequirements The requirements to be exercised on the caller.
* @return Whether the caller meets the authentication requirements.
*/
public boolean isCallerValid(Context context, int authRequirements) {
return isCallerValid(context, authRequirements, "");
}
/**
* @return Whether the current device lacks proper Google Play Services. This will return true
* if the service is not authentic or it is totally missing. Return false otherwise.
* Note this method returns false if the service is only temporarily disabled, such as
* when it is updating.
*/
public boolean isGooglePlayServicesMissing(final Context context) {
final int resultCode = checkGooglePlayServicesAvailable(context);
if (resultCode == ConnectionResult.SERVICE_MISSING
|| resultCode == ConnectionResult.SERVICE_INVALID) {
return true;
}
return false;
}
/**
* Checks whether Google Play Services can be used, applying the specified error-handling
* policy if a user-recoverable error occurs. This method is threadsafe. If the specified
* error-handling policy requires UI interaction, it will be run on the UI thread.
* Subclasses should generally not override this method; instead, they should override the
* helper methods {@link #checkGooglePlayServicesAvailable(Context)},
* {@link #describeError(int)}, and {@link #isUserRecoverableError(int)} instead, which are
* called in that order (as necessary) by this method.
* @param context The current context.
* @param errorHandler How to handle user-recoverable errors; must be non-null.
* @return true if and only if Google Play Services can be used
*/
public boolean canUseGooglePlayServices(
final Context context, final UserRecoverableErrorHandler errorHandler) {
final int resultCode = checkGooglePlayServicesAvailable(context);
recordConnectionResult(resultCode);
if (resultCode == ConnectionResult.SUCCESS) {
return true; // Hooray!
}
// resultCode is some kind of error.
Log.v(TAG, "Unable to use Google Play Services: %s", describeError(resultCode));
if (isUserRecoverableError(resultCode)) {
Runnable errorHandlerTask = new Runnable() {
@Override
public void run() {
errorHandler.handleError(context, resultCode);
}
};
ThreadUtils.runOnUiThread(errorHandlerTask);
}
return false;
}
/**
* Same as {@link #canUseGooglePlayServices(Context, UserRecoverableErrorHandler)}
* but also with the constraint that first-party APIs must be available. This check is
* implemented by verifying that the package is Google-signed; if not, first-party APIs will
* be unavailable at runtime.
* Nuance: The check on whether or not the package is Google-signed itself requires access to
* Google Play Services, so this method first checks for "normal" (non-first-party) access and,
* if successful, makes a second call to Google Play Services to determine the state of the
* package signature. The failure handling policy only applies to the first check, since Google
* Play Services provides "canned" ways to deal with failures; there is no special handling of
* the case where the Google Play Services check succeeds and the Google-signed package check
* fails (the method will simply return false).
* @param context The current context.
* @param userRecoverableErrorHandler How to handle user-recoverable errors from Google
* Play Services; must be non-null.
* @return true if and only if first-party Google Play Services can be used
*/
public boolean canUseFirstPartyGooglePlayServices(
Context context, UserRecoverableErrorHandler userRecoverableErrorHandler) {
return canUseGooglePlayServices(context, userRecoverableErrorHandler)
&& isChromeGoogleSigned(context);
}
/**
* Record the result of a connection attempt. The default implementation records via a UMA
* histogram.
* @param resultCode the result from {@link #checkGooglePlayServicesAvailable(Context)}
*/
protected void recordConnectionResult(final int resultCode) {
mConnectionResultHistogramSample.record(resultCode);
}
/**
* Invokes whatever external code is necessary to check if Google Play Services is available
* and returns the code produced by the attempt. Subclasses can override to force the behavior
* one way or another, or to change the way that the check is performed.
* @param context The current context.
* @return The code produced by calling the external code
*/
protected int checkGooglePlayServicesAvailable(final Context context) {
// Temporarily allowing disk access. TODO: Fix. See http://crbug.com/577190
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
try {
long time = SystemClock.elapsedRealtime();
int isAvailable =
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
mRegistrationTimeHistogramSample.record(SystemClock.elapsedRealtime() - time);
return isAvailable;
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
/**
* Invokes whatever external code is necessary to check if the specified error code produced
* by {@link #checkGooglePlayServicesAvailable(Context)} represents a user-recoverable error.
* Subclasses can override to filter error codes as desired.
* @param errorCode The code to check
* @return true If the code represents a user-recoverable error
*/
protected boolean isUserRecoverableError(final int errorCode) {
return GoogleApiAvailability.getInstance().isUserResolvableError(errorCode);
}
/**
* Invokes whatever external code is necessary to obtain a textual description of an error
* code produced by {@link #checkGooglePlayServicesAvailable(Context)}.
* @param errorCode The code to check
* @return a textual description of the error code
*/
protected String describeError(final int errorCode) {
return GoogleApiAvailability.getInstance().getErrorString(errorCode);
}
}