556 lines
21 KiB
Java
556 lines
21 KiB
Java
// Copyright 2015 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
package org.chromium.base;
|
|
|
|
import android.app.PendingIntent;
|
|
import android.content.ActivityNotFoundException;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.os.BadParcelableException;
|
|
import android.os.Binder;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.IBinder;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import android.os.TransactionTooLargeException;
|
|
import android.text.TextUtils;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import org.chromium.base.compat.ApiHelperForM;
|
|
import org.chromium.base.compat.ApiHelperForS;
|
|
|
|
import java.io.Serializable;
|
|
import java.util.ArrayList;
|
|
|
|
/** Utilities dealing with extracting information from intents and creating common intents. */
|
|
public class IntentUtils {
|
|
private static final String TAG = "IntentUtils";
|
|
|
|
/** The scheme for referrer coming from an application. */
|
|
public static final String ANDROID_APP_REFERRER_SCHEME = "android-app";
|
|
|
|
/** Intent extra used to identify the sending application. */
|
|
public static final String TRUSTED_APPLICATION_CODE_EXTRA = "trusted_application_code_extra";
|
|
|
|
/** Fake ComponentName used in constructing TRUSTED_APPLICATION_CODE_EXTRA. */
|
|
private static ComponentName sFakeComponentName;
|
|
|
|
private static final Object COMPONENT_NAME_LOCK = new Object();
|
|
|
|
private static boolean sForceTrustedIntentForTesting;
|
|
|
|
/** Just like {@link Intent#hasExtra(String)} but doesn't throw exceptions. */
|
|
public static boolean safeHasExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.hasExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "hasExtra failed on intent " + intent);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#removeExtra(String)} but doesn't throw exceptions. */
|
|
public static void safeRemoveExtra(Intent intent, String name) {
|
|
try {
|
|
intent.removeExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "removeExtra failed on intent " + intent);
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getBooleanExtra(String, boolean)} but doesn't throw exceptions. */
|
|
public static boolean safeGetBooleanExtra(Intent intent, String name, boolean defaultValue) {
|
|
try {
|
|
return intent.getBooleanExtra(name, defaultValue);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getBooleanExtra failed on intent " + intent);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Bundle#getBoolean(String, boolean)} but doesn't throw exceptions. */
|
|
public static boolean safeGetBoolean(Bundle bundle, String name, boolean defaultValue) {
|
|
try {
|
|
return bundle.getBoolean(name, defaultValue);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getBoolean failed on bundle " + bundle);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getIntExtra(String, int)} but doesn't throw exceptions. */
|
|
public static int safeGetIntExtra(Intent intent, String name, int defaultValue) {
|
|
try {
|
|
return intent.getIntExtra(name, defaultValue);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getIntExtra failed on intent " + intent);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Bundle#getInt(String, int)} but doesn't throw exceptions. */
|
|
public static int safeGetInt(Bundle bundle, String name, int defaultValue) {
|
|
try {
|
|
return bundle.getInt(name, defaultValue);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getInt failed on bundle " + bundle);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getIntArrayExtra(String)} but doesn't throw exceptions. */
|
|
public static int[] safeGetIntArrayExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.getIntArrayExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getIntArrayExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Bundle#getIntArray(String)} but doesn't throw exceptions. */
|
|
public static int[] safeGetIntArray(Bundle bundle, String name) {
|
|
try {
|
|
return bundle.getIntArray(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getIntArray failed on bundle " + bundle);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Bundle#getFloatArray(String)} but doesn't throw exceptions. */
|
|
public static float[] safeGetFloatArray(Bundle bundle, String name) {
|
|
try {
|
|
return bundle.getFloatArray(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getFloatArray failed on bundle " + bundle);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getLongExtra(String, long)} but doesn't throw exceptions. */
|
|
public static long safeGetLongExtra(Intent intent, String name, long defaultValue) {
|
|
try {
|
|
return intent.getLongExtra(name, defaultValue);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getLongExtra failed on intent " + intent);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getStringExtra(String)} but doesn't throw exceptions. */
|
|
public static String safeGetStringExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.getStringExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getStringExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Bundle#getString(String)} but doesn't throw exceptions. */
|
|
public static String safeGetString(Bundle bundle, String name) {
|
|
try {
|
|
return bundle.getString(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getString failed on bundle " + bundle);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getBundleExtra(String)} but doesn't throw exceptions. */
|
|
public static Bundle safeGetBundleExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.getBundleExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getBundleExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Bundle#getBundle(String)} but doesn't throw exceptions. */
|
|
public static Bundle safeGetBundle(Bundle bundle, String name) {
|
|
try {
|
|
return bundle.getBundle(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getBundle failed on bundle " + bundle);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Bundle#getParcelable(String)} but doesn't throw exceptions. */
|
|
public static <T extends Parcelable> T safeGetParcelable(Bundle bundle, String name) {
|
|
try {
|
|
return bundle.getParcelable(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getParcelable failed on bundle " + bundle);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getParcelableExtra(String)} but doesn't throw exceptions. */
|
|
public static <T extends Parcelable> T safeGetParcelableExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.getParcelableExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getParcelableExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Just link {@link Intent#getParcelableArrayListExtra(String)} but doesn't throw exceptions.
|
|
*/
|
|
public static <T extends Parcelable> ArrayList<T> getParcelableArrayListExtra(
|
|
Intent intent, String name) {
|
|
try {
|
|
return intent.getParcelableArrayListExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getParcelableArrayListExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just link {@link Bundle#getParcelableArrayList(String)} but doesn't throw exceptions. */
|
|
public static <T extends Parcelable> ArrayList<T> safeGetParcelableArrayList(
|
|
Bundle bundle, String name) {
|
|
try {
|
|
return bundle.getParcelableArrayList(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getParcelableArrayList failed on bundle " + bundle);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getParcelableArrayExtra(String)} but doesn't throw exceptions. */
|
|
public static Parcelable[] safeGetParcelableArrayExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.getParcelableArrayExtra(name);
|
|
} catch (Throwable t) {
|
|
Log.e(TAG, "getParcelableArrayExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getStringArrayListExtra(String)} but doesn't throw exceptions. */
|
|
public static ArrayList<String> safeGetStringArrayListExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.getStringArrayListExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getStringArrayListExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getByteArrayExtra(String)} but doesn't throw exceptions. */
|
|
public static byte[] safeGetByteArrayExtra(Intent intent, String name) {
|
|
try {
|
|
return intent.getByteArrayExtra(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getByteArrayExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Just like {@link Intent#getSerializableExtra(String)} but doesn't throw exceptions. */
|
|
@SuppressWarnings("unchecked")
|
|
public static <T extends Serializable> T safeGetSerializableExtra(Intent intent, String name) {
|
|
try {
|
|
return (T) intent.getSerializableExtra(name);
|
|
} catch (ClassCastException ex) {
|
|
Log.e(TAG, "Invalide class for Serializable: " + name, ex);
|
|
return null;
|
|
} catch (Throwable t) {
|
|
// Catches un-serializable exceptions.
|
|
Log.e(TAG, "getSerializableExtra failed on intent " + intent);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the value associated with the given name, or null if no mapping of the desired type
|
|
* exists for the given name or a null value is explicitly associated with the name.
|
|
*
|
|
* @param name a key string
|
|
* @return an IBinder value, or null
|
|
*/
|
|
public static IBinder safeGetBinder(Bundle bundle, String name) {
|
|
if (bundle == null) return null;
|
|
try {
|
|
return bundle.getBinder(name);
|
|
} catch (Throwable t) {
|
|
// Catches un-parceling exceptions.
|
|
Log.e(TAG, "getBinder failed on bundle " + bundle);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return a Binder from an Intent, or null.
|
|
*
|
|
* Creates a temporary copy of the extra Bundle, which is required as
|
|
* Intent#getBinderExtra() doesn't exist, but Bundle.getBinder() does.
|
|
*/
|
|
public static IBinder safeGetBinderExtra(Intent intent, String name) {
|
|
if (!intent.hasExtra(name)) return null;
|
|
Bundle extras = intent.getExtras();
|
|
return safeGetBinder(extras, name);
|
|
}
|
|
|
|
/**
|
|
* Inserts a {@link Binder} value into an Intent as an extra.
|
|
*
|
|
* @param intent Intent to put the binder into.
|
|
* @param name Key.
|
|
* @param binder Binder object.
|
|
*/
|
|
public static void safePutBinderExtra(Intent intent, String name, IBinder binder) {
|
|
if (intent == null) return;
|
|
Bundle bundle = new Bundle();
|
|
try {
|
|
bundle.putBinder(name, binder);
|
|
} catch (Throwable t) {
|
|
// Catches parceling exceptions.
|
|
Log.e(TAG, "putBinder failed on bundle " + bundle);
|
|
}
|
|
intent.putExtras(bundle);
|
|
}
|
|
|
|
/** See {@link #safeStartActivity(Context, Intent, Bundle)}. */
|
|
public static boolean safeStartActivity(Context context, Intent intent) {
|
|
return safeStartActivity(context, intent, null);
|
|
}
|
|
|
|
/**
|
|
* Catches any failures to start an Activity.
|
|
* @param context Context to use when starting the Activity.
|
|
* @param intent Intent to fire.
|
|
* @param bundle Bundle of launch options.
|
|
* @return Whether or not Android accepted the Intent.
|
|
*/
|
|
public static boolean safeStartActivity(
|
|
Context context, Intent intent, @Nullable Bundle bundle) {
|
|
try {
|
|
context.startActivity(intent, bundle);
|
|
return true;
|
|
} catch (ActivityNotFoundException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Returns whether the intent starts an activity in a new task or a new document. */
|
|
public static boolean isIntentForNewTaskOrNewDocument(Intent intent) {
|
|
int testFlags = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
|
|
return (intent.getFlags() & testFlags) != 0;
|
|
}
|
|
|
|
/**
|
|
* Returns how large the Intent will be in Parcel form, which is helpful for gauging whether
|
|
* Android will deliver the Intent instead of throwing a TransactionTooLargeException.
|
|
*
|
|
* @param intent Intent to get the size of.
|
|
* @return Number of bytes required to parcel the Intent.
|
|
*/
|
|
public static int getParceledIntentSize(Intent intent) {
|
|
Parcel parcel = Parcel.obtain();
|
|
intent.writeToParcel(parcel, 0);
|
|
return parcel.dataSize();
|
|
}
|
|
|
|
/**
|
|
* Given an exception, check whether it wrapped a {@link TransactionTooLargeException}. If it
|
|
* does, then log the underlying error. If not, throw the original exception again.
|
|
*
|
|
* @param e The caught RuntimeException.
|
|
* @param intent The intent that triggered the RuntimeException to be thrown.
|
|
*/
|
|
public static void logTransactionTooLargeOrRethrow(RuntimeException e, Intent intent) {
|
|
// See http://crbug.com/369574.
|
|
if (e.getCause() instanceof TransactionTooLargeException) {
|
|
Log.e(TAG, "Could not resolve Activity for intent " + intent.toString(), e);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private static Intent logInvalidIntent(Intent intent, Exception e) {
|
|
Log.e(TAG, "Invalid incoming intent.", e);
|
|
return intent.replaceExtras((Bundle) null);
|
|
}
|
|
|
|
/**
|
|
* Sanitizes an intent. In case the intent cannot be unparcelled, all extras will be removed to
|
|
* make it safe to use.
|
|
* @return A safe to use version of this intent.
|
|
*/
|
|
public static Intent sanitizeIntent(final Intent incomingIntent) {
|
|
// On Android T+, items are only deserialized when the items themselves are queried, so the
|
|
// code below is a no-op.
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return incomingIntent;
|
|
if (incomingIntent == null) return null;
|
|
try {
|
|
incomingIntent.getBooleanExtra("TriggerUnparcel", false);
|
|
return incomingIntent;
|
|
} catch (BadParcelableException e) {
|
|
return logInvalidIntent(incomingIntent, e);
|
|
} catch (RuntimeException e) {
|
|
if (e.getCause() instanceof ClassNotFoundException) {
|
|
return logInvalidIntent(incomingIntent, e);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return True if the intent is a MAIN intent a launcher would send.
|
|
*/
|
|
public static boolean isMainIntentFromLauncher(Intent intent) {
|
|
return intent != null
|
|
&& TextUtils.equals(intent.getAction(), Intent.ACTION_MAIN)
|
|
&& intent.hasCategory(Intent.CATEGORY_LAUNCHER)
|
|
&& 0 == (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
|
|
}
|
|
|
|
/**
|
|
* Gets the PendingIntent flag for the specified mutability.
|
|
* PendingIntent.FLAG_IMMUTABLE was added in API level 23 (M), and FLAG_MUTABLE was added in
|
|
* Android S.
|
|
*
|
|
* Unless mutability is required, PendingIntents should always be marked as Immutable as this
|
|
* is the more secure default.
|
|
*/
|
|
public static int getPendingIntentMutabilityFlag(boolean mutable) {
|
|
if (!mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
return ApiHelperForM.getPendingIntentImmutableFlag();
|
|
} else if (mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
return ApiHelperForS.getPendingIntentMutableFlag();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Determines whether this app is the only possible handler for this Intent.
|
|
*
|
|
* @param context Any context for this app.
|
|
* @param intent The intent to check.
|
|
* @return True if the intent targets this app.
|
|
*/
|
|
public static boolean intentTargetsSelf(Context context, Intent intent) {
|
|
boolean hasPackage = !TextUtils.isEmpty(intent.getPackage());
|
|
boolean matchesPackage = hasPackage && context.getPackageName().equals(intent.getPackage());
|
|
boolean hasComponent = intent.getComponent() != null;
|
|
boolean matchesComponent =
|
|
hasComponent
|
|
&& context.getPackageName().equals(intent.getComponent().getPackageName());
|
|
|
|
// Component takes precedence over PackageName when routing Intents if both are set, but to
|
|
// be on the safe side, ensure that if we have both package and component set, that they
|
|
// agree.
|
|
if (matchesComponent) {
|
|
if (hasPackage) {
|
|
// We should not create intents that disagree on package/component, but for security
|
|
// purposes we should handle this case.
|
|
assert matchesPackage;
|
|
return matchesPackage;
|
|
}
|
|
return true;
|
|
}
|
|
if (matchesPackage) {
|
|
assert !hasComponent;
|
|
return !hasComponent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static ComponentName getFakeComponentName(String packageName) {
|
|
synchronized (COMPONENT_NAME_LOCK) {
|
|
if (sFakeComponentName == null) {
|
|
sFakeComponentName = new ComponentName(packageName, "FakeClass");
|
|
}
|
|
}
|
|
|
|
return sFakeComponentName;
|
|
}
|
|
|
|
private static PendingIntent getAuthenticationToken() {
|
|
Intent fakeIntent = new Intent();
|
|
Context appContext = ContextUtils.getApplicationContext();
|
|
fakeIntent.setComponent(getFakeComponentName(appContext.getPackageName()));
|
|
return PendingIntent.getActivity(
|
|
appContext, 0, fakeIntent, getPendingIntentMutabilityFlag(false));
|
|
}
|
|
|
|
/**
|
|
* Sets TRUSTED_APPLICATION_CODE_EXTRA on the provided intent to identify it as coming from
|
|
* a trusted source.
|
|
*
|
|
* @param intent An Intent that targets either current package, or explicitly targets a
|
|
* component of the current package.
|
|
*/
|
|
public static void addTrustedIntentExtras(Intent intent) {
|
|
// It is crucial that we never leak the authentication token to other packages, because
|
|
// then the other package could be used to impersonate us/do things as us.
|
|
boolean toSelf =
|
|
IntentUtils.intentTargetsSelf(ContextUtils.getApplicationContext(), intent);
|
|
assert toSelf;
|
|
// For security reasons we have to check the asserted condition anyways.
|
|
if (!toSelf) return;
|
|
|
|
// The PendingIntent functions as an authentication token --- it could only have come
|
|
// from us. Stash it in the real Intent as an extra we can validate upon receiving it.
|
|
intent.putExtra(TRUSTED_APPLICATION_CODE_EXTRA, getAuthenticationToken());
|
|
}
|
|
|
|
/**
|
|
* @param intent An Intent to be checked.
|
|
* @return Whether an intent originates from the current app.
|
|
*/
|
|
public static boolean isTrustedIntentFromSelf(@Nullable Intent intent) {
|
|
if (intent == null) return false;
|
|
|
|
if (sForceTrustedIntentForTesting) return true;
|
|
|
|
// Fetch the authentication token (a PendingIntent) created by
|
|
// addTrustedIntentExtras, if any. If anything goes wrong trying to retrieve the
|
|
// token (examples include BadParcelableException or ClassNotFoundException), fail closed.
|
|
PendingIntent token =
|
|
IntentUtils.safeGetParcelableExtra(intent, TRUSTED_APPLICATION_CODE_EXTRA);
|
|
if (token == null) return false;
|
|
|
|
// Fetch what should be a matching token. If the PendingIntents are equal, we know that the
|
|
// sender was us.
|
|
PendingIntent pending = getAuthenticationToken();
|
|
return pending.equals(token);
|
|
}
|
|
|
|
public static void setForceIsTrustedIntentForTesting(boolean isTrusted) {
|
|
sForceTrustedIntentForTesting = isTrusted;
|
|
ResettersForTesting.register(() -> sForceTrustedIntentForTesting = false);
|
|
}
|
|
}
|