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