• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2015 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base;
6 
7 import android.app.PendingIntent;
8 import android.content.ActivityNotFoundException;
9 import android.content.ComponentName;
10 import android.content.Context;
11 import android.content.Intent;
12 import android.os.BadParcelableException;
13 import android.os.Binder;
14 import android.os.Build;
15 import android.os.Bundle;
16 import android.os.IBinder;
17 import android.os.Parcel;
18 import android.os.Parcelable;
19 import android.os.TransactionTooLargeException;
20 import android.text.TextUtils;
21 
22 import androidx.annotation.Nullable;
23 
24 import org.chromium.base.compat.ApiHelperForM;
25 import org.chromium.base.compat.ApiHelperForS;
26 
27 import java.io.Serializable;
28 import java.util.ArrayList;
29 
30 /** Utilities dealing with extracting information from intents and creating common intents. */
31 public class IntentUtils {
32     private static final String TAG = "IntentUtils";
33 
34     /** The scheme for referrer coming from an application. */
35     public static final String ANDROID_APP_REFERRER_SCHEME = "android-app";
36 
37     /** Intent extra used to identify the sending application. */
38     public static final String TRUSTED_APPLICATION_CODE_EXTRA = "trusted_application_code_extra";
39 
40     /** Fake ComponentName used in constructing TRUSTED_APPLICATION_CODE_EXTRA. */
41     private static ComponentName sFakeComponentName;
42 
43     private static final Object COMPONENT_NAME_LOCK = new Object();
44 
45     private static boolean sForceTrustedIntentForTesting;
46 
47     /** Just like {@link Intent#hasExtra(String)} but doesn't throw exceptions. */
safeHasExtra(Intent intent, String name)48     public static boolean safeHasExtra(Intent intent, String name) {
49         try {
50             return intent.hasExtra(name);
51         } catch (Throwable t) {
52             // Catches un-parceling exceptions.
53             Log.e(TAG, "hasExtra failed on intent " + intent);
54             return false;
55         }
56     }
57 
58     /** Just like {@link Intent#removeExtra(String)} but doesn't throw exceptions. */
safeRemoveExtra(Intent intent, String name)59     public static void safeRemoveExtra(Intent intent, String name) {
60         try {
61             intent.removeExtra(name);
62         } catch (Throwable t) {
63             // Catches un-parceling exceptions.
64             Log.e(TAG, "removeExtra failed on intent " + intent);
65         }
66     }
67 
68     /** Just like {@link Intent#getBooleanExtra(String, boolean)} but doesn't throw exceptions. */
safeGetBooleanExtra(Intent intent, String name, boolean defaultValue)69     public static boolean safeGetBooleanExtra(Intent intent, String name, boolean defaultValue) {
70         try {
71             return intent.getBooleanExtra(name, defaultValue);
72         } catch (Throwable t) {
73             // Catches un-parceling exceptions.
74             Log.e(TAG, "getBooleanExtra failed on intent " + intent);
75             return defaultValue;
76         }
77     }
78 
79     /** Just like {@link Bundle#getBoolean(String, boolean)} but doesn't throw exceptions. */
safeGetBoolean(Bundle bundle, String name, boolean defaultValue)80     public static boolean safeGetBoolean(Bundle bundle, String name, boolean defaultValue) {
81         try {
82             return bundle.getBoolean(name, defaultValue);
83         } catch (Throwable t) {
84             // Catches un-parceling exceptions.
85             Log.e(TAG, "getBoolean failed on bundle " + bundle);
86             return defaultValue;
87         }
88     }
89 
90     /** Just like {@link Intent#getIntExtra(String, int)} but doesn't throw exceptions. */
safeGetIntExtra(Intent intent, String name, int defaultValue)91     public static int safeGetIntExtra(Intent intent, String name, int defaultValue) {
92         try {
93             return intent.getIntExtra(name, defaultValue);
94         } catch (Throwable t) {
95             // Catches un-parceling exceptions.
96             Log.e(TAG, "getIntExtra failed on intent " + intent);
97             return defaultValue;
98         }
99     }
100 
101     /** Just like {@link Bundle#getInt(String, int)} but doesn't throw exceptions. */
safeGetInt(Bundle bundle, String name, int defaultValue)102     public static int safeGetInt(Bundle bundle, String name, int defaultValue) {
103         try {
104             return bundle.getInt(name, defaultValue);
105         } catch (Throwable t) {
106             // Catches un-parceling exceptions.
107             Log.e(TAG, "getInt failed on bundle " + bundle);
108             return defaultValue;
109         }
110     }
111 
112     /** Just like {@link Intent#getIntArrayExtra(String)} but doesn't throw exceptions. */
safeGetIntArrayExtra(Intent intent, String name)113     public static int[] safeGetIntArrayExtra(Intent intent, String name) {
114         try {
115             return intent.getIntArrayExtra(name);
116         } catch (Throwable t) {
117             // Catches un-parceling exceptions.
118             Log.e(TAG, "getIntArrayExtra failed on intent " + intent);
119             return null;
120         }
121     }
122 
123     /** Just like {@link Bundle#getIntArray(String)} but doesn't throw exceptions. */
safeGetIntArray(Bundle bundle, String name)124     public static int[] safeGetIntArray(Bundle bundle, String name) {
125         try {
126             return bundle.getIntArray(name);
127         } catch (Throwable t) {
128             // Catches un-parceling exceptions.
129             Log.e(TAG, "getIntArray failed on bundle " + bundle);
130             return null;
131         }
132     }
133 
134     /** Just like {@link Bundle#getFloatArray(String)} but doesn't throw exceptions. */
safeGetFloatArray(Bundle bundle, String name)135     public static float[] safeGetFloatArray(Bundle bundle, String name) {
136         try {
137             return bundle.getFloatArray(name);
138         } catch (Throwable t) {
139             // Catches un-parceling exceptions.
140             Log.e(TAG, "getFloatArray failed on bundle " + bundle);
141             return null;
142         }
143     }
144 
145     /** Just like {@link Intent#getLongExtra(String, long)} but doesn't throw exceptions. */
safeGetLongExtra(Intent intent, String name, long defaultValue)146     public static long safeGetLongExtra(Intent intent, String name, long defaultValue) {
147         try {
148             return intent.getLongExtra(name, defaultValue);
149         } catch (Throwable t) {
150             // Catches un-parceling exceptions.
151             Log.e(TAG, "getLongExtra failed on intent " + intent);
152             return defaultValue;
153         }
154     }
155 
156     /** Just like {@link Intent#getStringExtra(String)} but doesn't throw exceptions. */
safeGetStringExtra(Intent intent, String name)157     public static String safeGetStringExtra(Intent intent, String name) {
158         try {
159             return intent.getStringExtra(name);
160         } catch (Throwable t) {
161             // Catches un-parceling exceptions.
162             Log.e(TAG, "getStringExtra failed on intent " + intent);
163             return null;
164         }
165     }
166 
167     /** Just like {@link Bundle#getString(String)} but doesn't throw exceptions. */
safeGetString(Bundle bundle, String name)168     public static String safeGetString(Bundle bundle, String name) {
169         try {
170             return bundle.getString(name);
171         } catch (Throwable t) {
172             // Catches un-parceling exceptions.
173             Log.e(TAG, "getString failed on bundle " + bundle);
174             return null;
175         }
176     }
177 
178     /** Just like {@link Intent#getBundleExtra(String)} but doesn't throw exceptions. */
safeGetBundleExtra(Intent intent, String name)179     public static Bundle safeGetBundleExtra(Intent intent, String name) {
180         try {
181             return intent.getBundleExtra(name);
182         } catch (Throwable t) {
183             // Catches un-parceling exceptions.
184             Log.e(TAG, "getBundleExtra failed on intent " + intent);
185             return null;
186         }
187     }
188 
189     /** Just like {@link Bundle#getBundle(String)} but doesn't throw exceptions. */
safeGetBundle(Bundle bundle, String name)190     public static Bundle safeGetBundle(Bundle bundle, String name) {
191         try {
192             return bundle.getBundle(name);
193         } catch (Throwable t) {
194             // Catches un-parceling exceptions.
195             Log.e(TAG, "getBundle failed on bundle " + bundle);
196             return null;
197         }
198     }
199 
200     /** Just like {@link Bundle#getParcelable(String)} but doesn't throw exceptions. */
safeGetParcelable(Bundle bundle, String name)201     public static <T extends Parcelable> T safeGetParcelable(Bundle bundle, String name) {
202         try {
203             return bundle.getParcelable(name);
204         } catch (Throwable t) {
205             // Catches un-parceling exceptions.
206             Log.e(TAG, "getParcelable failed on bundle " + bundle);
207             return null;
208         }
209     }
210 
211     /** Just like {@link Intent#getParcelableExtra(String)} but doesn't throw exceptions. */
safeGetParcelableExtra(Intent intent, String name)212     public static <T extends Parcelable> T safeGetParcelableExtra(Intent intent, String name) {
213         try {
214             return intent.getParcelableExtra(name);
215         } catch (Throwable t) {
216             // Catches un-parceling exceptions.
217             Log.e(TAG, "getParcelableExtra failed on intent " + intent);
218             return null;
219         }
220     }
221 
222     /**
223      * Just link {@link Intent#getParcelableArrayListExtra(String)} but doesn't throw exceptions.
224      */
getParcelableArrayListExtra( Intent intent, String name)225     public static <T extends Parcelable> ArrayList<T> getParcelableArrayListExtra(
226             Intent intent, String name) {
227         try {
228             return intent.getParcelableArrayListExtra(name);
229         } catch (Throwable t) {
230             // Catches un-parceling exceptions.
231             Log.e(TAG, "getParcelableArrayListExtra failed on intent " + intent);
232             return null;
233         }
234     }
235 
236     /** Just link {@link Bundle#getParcelableArrayList(String)} but doesn't throw exceptions. */
safeGetParcelableArrayList( Bundle bundle, String name)237     public static <T extends Parcelable> ArrayList<T> safeGetParcelableArrayList(
238             Bundle bundle, String name) {
239         try {
240             return bundle.getParcelableArrayList(name);
241         } catch (Throwable t) {
242             // Catches un-parceling exceptions.
243             Log.e(TAG, "getParcelableArrayList failed on bundle " + bundle);
244             return null;
245         }
246     }
247 
248     /** Just like {@link Intent#getParcelableArrayExtra(String)} but doesn't throw exceptions. */
safeGetParcelableArrayExtra(Intent intent, String name)249     public static Parcelable[] safeGetParcelableArrayExtra(Intent intent, String name) {
250         try {
251             return intent.getParcelableArrayExtra(name);
252         } catch (Throwable t) {
253             Log.e(TAG, "getParcelableArrayExtra failed on intent " + intent);
254             return null;
255         }
256     }
257 
258     /** Just like {@link Intent#getStringArrayListExtra(String)} but doesn't throw exceptions. */
safeGetStringArrayListExtra(Intent intent, String name)259     public static ArrayList<String> safeGetStringArrayListExtra(Intent intent, String name) {
260         try {
261             return intent.getStringArrayListExtra(name);
262         } catch (Throwable t) {
263             // Catches un-parceling exceptions.
264             Log.e(TAG, "getStringArrayListExtra failed on intent " + intent);
265             return null;
266         }
267     }
268 
269     /** Just like {@link Intent#getByteArrayExtra(String)} but doesn't throw exceptions. */
safeGetByteArrayExtra(Intent intent, String name)270     public static byte[] safeGetByteArrayExtra(Intent intent, String name) {
271         try {
272             return intent.getByteArrayExtra(name);
273         } catch (Throwable t) {
274             // Catches un-parceling exceptions.
275             Log.e(TAG, "getByteArrayExtra failed on intent " + intent);
276             return null;
277         }
278     }
279 
280     /** Just like {@link Intent#getSerializableExtra(String)} but doesn't throw exceptions. */
281     @SuppressWarnings("unchecked")
safeGetSerializableExtra(Intent intent, String name)282     public static <T extends Serializable> T safeGetSerializableExtra(Intent intent, String name) {
283         try {
284             return (T) intent.getSerializableExtra(name);
285         } catch (ClassCastException ex) {
286             Log.e(TAG, "Invalide class for Serializable: " + name, ex);
287             return null;
288         } catch (Throwable t) {
289             // Catches un-serializable exceptions.
290             Log.e(TAG, "getSerializableExtra failed on intent " + intent);
291             return null;
292         }
293     }
294 
295     /**
296      * Returns the value associated with the given name, or null if no mapping of the desired type
297      * exists for the given name or a null value is explicitly associated with the name.
298      *
299      * @param name a key string
300      * @return an IBinder value, or null
301      */
safeGetBinder(Bundle bundle, String name)302     public static IBinder safeGetBinder(Bundle bundle, String name) {
303         if (bundle == null) return null;
304         try {
305             return bundle.getBinder(name);
306         } catch (Throwable t) {
307             // Catches un-parceling exceptions.
308             Log.e(TAG, "getBinder failed on bundle " + bundle);
309             return null;
310         }
311     }
312 
313     /**
314      * @return a Binder from an Intent, or null.
315      *
316      * Creates a temporary copy of the extra Bundle, which is required as
317      * Intent#getBinderExtra() doesn't exist, but Bundle.getBinder() does.
318      */
safeGetBinderExtra(Intent intent, String name)319     public static IBinder safeGetBinderExtra(Intent intent, String name) {
320         if (!intent.hasExtra(name)) return null;
321         Bundle extras = intent.getExtras();
322         return safeGetBinder(extras, name);
323     }
324 
325     /**
326      * Inserts a {@link Binder} value into an Intent as an extra.
327      *
328      * @param intent Intent to put the binder into.
329      * @param name Key.
330      * @param binder Binder object.
331      */
safePutBinderExtra(Intent intent, String name, IBinder binder)332     public static void safePutBinderExtra(Intent intent, String name, IBinder binder) {
333         if (intent == null) return;
334         Bundle bundle = new Bundle();
335         try {
336             bundle.putBinder(name, binder);
337         } catch (Throwable t) {
338             // Catches parceling exceptions.
339             Log.e(TAG, "putBinder failed on bundle " + bundle);
340         }
341         intent.putExtras(bundle);
342     }
343 
344     /** See {@link #safeStartActivity(Context, Intent, Bundle)}. */
safeStartActivity(Context context, Intent intent)345     public static boolean safeStartActivity(Context context, Intent intent) {
346         return safeStartActivity(context, intent, null);
347     }
348 
349     /**
350      * Catches any failures to start an Activity.
351      * @param context Context to use when starting the Activity.
352      * @param intent  Intent to fire.
353      * @param bundle  Bundle of launch options.
354      * @return Whether or not Android accepted the Intent.
355      */
safeStartActivity( Context context, Intent intent, @Nullable Bundle bundle)356     public static boolean safeStartActivity(
357             Context context, Intent intent, @Nullable Bundle bundle) {
358         try {
359             context.startActivity(intent, bundle);
360             return true;
361         } catch (ActivityNotFoundException e) {
362             return false;
363         }
364     }
365 
366     /** Returns whether the intent starts an activity in a new task or a new document. */
isIntentForNewTaskOrNewDocument(Intent intent)367     public static boolean isIntentForNewTaskOrNewDocument(Intent intent) {
368         int testFlags = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
369         return (intent.getFlags() & testFlags) != 0;
370     }
371 
372     /**
373      * Returns how large the Intent will be in Parcel form, which is helpful for gauging whether
374      * Android will deliver the Intent instead of throwing a TransactionTooLargeException.
375      *
376      * @param intent Intent to get the size of.
377      * @return Number of bytes required to parcel the Intent.
378      */
getParceledIntentSize(Intent intent)379     public static int getParceledIntentSize(Intent intent) {
380         Parcel parcel = Parcel.obtain();
381         intent.writeToParcel(parcel, 0);
382         return parcel.dataSize();
383     }
384 
385     /**
386      * Given an exception, check whether it wrapped a {@link TransactionTooLargeException}.  If it
387      * does, then log the underlying error.  If not, throw the original exception again.
388      *
389      * @param e      The caught RuntimeException.
390      * @param intent The intent that triggered the RuntimeException to be thrown.
391      */
logTransactionTooLargeOrRethrow(RuntimeException e, Intent intent)392     public static void logTransactionTooLargeOrRethrow(RuntimeException e, Intent intent) {
393         // See http://crbug.com/369574.
394         if (e.getCause() instanceof TransactionTooLargeException) {
395             Log.e(TAG, "Could not resolve Activity for intent " + intent.toString(), e);
396         } else {
397             throw e;
398         }
399     }
400 
logInvalidIntent(Intent intent, Exception e)401     private static Intent logInvalidIntent(Intent intent, Exception e) {
402         Log.e(TAG, "Invalid incoming intent.", e);
403         return intent.replaceExtras((Bundle) null);
404     }
405 
406     /**
407      * Sanitizes an intent. In case the intent cannot be unparcelled, all extras will be removed to
408      * make it safe to use.
409      * @return A safe to use version of this intent.
410      */
sanitizeIntent(final Intent incomingIntent)411     public static Intent sanitizeIntent(final Intent incomingIntent) {
412         // On Android T+, items are only deserialized when the items themselves are queried, so the
413         // code below is a no-op.
414         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return incomingIntent;
415         if (incomingIntent == null) return null;
416         try {
417             incomingIntent.getBooleanExtra("TriggerUnparcel", false);
418             return incomingIntent;
419         } catch (BadParcelableException e) {
420             return logInvalidIntent(incomingIntent, e);
421         } catch (RuntimeException e) {
422             if (e.getCause() instanceof ClassNotFoundException) {
423                 return logInvalidIntent(incomingIntent, e);
424             }
425             throw e;
426         }
427     }
428 
429     /**
430      * @return True if the intent is a MAIN intent a launcher would send.
431      */
isMainIntentFromLauncher(Intent intent)432     public static boolean isMainIntentFromLauncher(Intent intent) {
433         return intent != null
434                 && TextUtils.equals(intent.getAction(), Intent.ACTION_MAIN)
435                 && intent.hasCategory(Intent.CATEGORY_LAUNCHER)
436                 && 0 == (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
437     }
438 
439     /**
440      * Gets the PendingIntent flag for the specified mutability.
441      * PendingIntent.FLAG_IMMUTABLE was added in API level 23 (M), and FLAG_MUTABLE was added in
442      * Android S.
443      *
444      * Unless mutability is required, PendingIntents should always be marked as Immutable as this
445      * is the more secure default.
446      */
getPendingIntentMutabilityFlag(boolean mutable)447     public static int getPendingIntentMutabilityFlag(boolean mutable) {
448         if (!mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
449             return ApiHelperForM.getPendingIntentImmutableFlag();
450         } else if (mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
451             return ApiHelperForS.getPendingIntentMutableFlag();
452         }
453         return 0;
454     }
455 
456     /**
457      * Determines whether this app is the only possible handler for this Intent.
458      *
459      * @param context Any context for this app.
460      * @param intent The intent to check.
461      * @return True if the intent targets this app.
462      */
intentTargetsSelf(Context context, Intent intent)463     public static boolean intentTargetsSelf(Context context, Intent intent) {
464         boolean hasPackage = !TextUtils.isEmpty(intent.getPackage());
465         boolean matchesPackage = hasPackage && context.getPackageName().equals(intent.getPackage());
466         boolean hasComponent = intent.getComponent() != null;
467         boolean matchesComponent =
468                 hasComponent
469                         && context.getPackageName().equals(intent.getComponent().getPackageName());
470 
471         // Component takes precedence over PackageName when routing Intents if both are set, but to
472         // be on the safe side, ensure that if we have both package and component set, that they
473         // agree.
474         if (matchesComponent) {
475             if (hasPackage) {
476                 // We should not create intents that disagree on package/component, but for security
477                 // purposes we should handle this case.
478                 assert matchesPackage;
479                 return matchesPackage;
480             }
481             return true;
482         }
483         if (matchesPackage) {
484             assert !hasComponent;
485             return !hasComponent;
486         }
487         return false;
488     }
489 
getFakeComponentName(String packageName)490     private static ComponentName getFakeComponentName(String packageName) {
491         synchronized (COMPONENT_NAME_LOCK) {
492             if (sFakeComponentName == null) {
493                 sFakeComponentName = new ComponentName(packageName, "FakeClass");
494             }
495         }
496 
497         return sFakeComponentName;
498     }
499 
getAuthenticationToken()500     private static PendingIntent getAuthenticationToken() {
501         Intent fakeIntent = new Intent();
502         Context appContext = ContextUtils.getApplicationContext();
503         fakeIntent.setComponent(getFakeComponentName(appContext.getPackageName()));
504         return PendingIntent.getActivity(
505                 appContext, 0, fakeIntent, getPendingIntentMutabilityFlag(false));
506     }
507 
508     /**
509      * Sets TRUSTED_APPLICATION_CODE_EXTRA on the provided intent to identify it as coming from
510      * a trusted source.
511      *
512      * @param intent An Intent that targets either current package, or explicitly targets a
513      *         component of the current package.
514      */
addTrustedIntentExtras(Intent intent)515     public static void addTrustedIntentExtras(Intent intent) {
516         // It is crucial that we never leak the authentication token to other packages, because
517         // then the other package could be used to impersonate us/do things as us.
518         boolean toSelf =
519                 IntentUtils.intentTargetsSelf(ContextUtils.getApplicationContext(), intent);
520         assert toSelf;
521         // For security reasons we have to check the asserted condition anyways.
522         if (!toSelf) return;
523 
524         // The PendingIntent functions as an authentication token --- it could only have come
525         // from us. Stash it in the real Intent as an extra we can validate upon receiving it.
526         intent.putExtra(TRUSTED_APPLICATION_CODE_EXTRA, getAuthenticationToken());
527     }
528 
529     /**
530      * @param intent An Intent to be checked.
531      * @return Whether an intent originates from the current app.
532      */
isTrustedIntentFromSelf(@ullable Intent intent)533     public static boolean isTrustedIntentFromSelf(@Nullable Intent intent) {
534         if (intent == null) return false;
535 
536         if (sForceTrustedIntentForTesting) return true;
537 
538         // Fetch the authentication token (a PendingIntent) created by
539         // addTrustedIntentExtras, if any. If anything goes wrong trying to retrieve the
540         // token (examples include BadParcelableException or ClassNotFoundException), fail closed.
541         PendingIntent token =
542                 IntentUtils.safeGetParcelableExtra(intent, TRUSTED_APPLICATION_CODE_EXTRA);
543         if (token == null) return false;
544 
545         // Fetch what should be a matching token. If the PendingIntents are equal, we know that the
546         // sender was us.
547         PendingIntent pending = getAuthenticationToken();
548         return pending.equals(token);
549     }
550 
setForceIsTrustedIntentForTesting(boolean isTrusted)551     public static void setForceIsTrustedIntentForTesting(boolean isTrusted) {
552         sForceTrustedIntentForTesting = isTrusted;
553         ResettersForTesting.register(() -> sForceTrustedIntentForTesting = false);
554     }
555 }
556