// 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 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 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 ArrayList 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 ArrayList 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 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 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); } }