IntentSanitizer.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.content;

import static androidx.core.util.Preconditions.checkNotNull;

import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.strictmode.UnsafeIntentLaunchViolation;
import android.provider.MediaStore;

import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.util.Consumer;
import androidx.core.util.Predicate;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * This class is used to make a sanitized copy of an {@link Intent}. This could be used when
 * {@link UnsafeIntentLaunchViolation} is detected.
 * This class is thread safe and the object created is safe to be reused.
 * Typical usage of the class:
 * <pre>
 * {@code
 * Intent intent = new  IntentSanitizer.Builder()
 *      .allowComponent(“com.example.ActivityA”)
 *      .allowData(“com.example”)
 *      .allowType(“text/plain”)
 *      .build()
 *      .sanitizeByThrowing(intent);
 * }
 * </pre>
 *
 * At least one of the allowPackage, allowComponent must be called unless implicit intent is
 * allowed. In which case, allowAnyComponent must be called and caution has to be taken to
 * protect your private data.
 */
public class IntentSanitizer {
    private static final String TAG = "IntentSanitizer";

    private int mAllowedFlags;
    private Predicate<String> mAllowedActions;
    private Predicate<Uri> mAllowedData;
    private Predicate<String> mAllowedTypes;
    private Predicate<String> mAllowedCategories;
    private Predicate<String> mAllowedPackages;
    private Predicate<ComponentName> mAllowedComponents;
    private boolean mAllowAnyComponent;
    private Map<String, Predicate<Object>> mAllowedExtras;
    private boolean mAllowClipDataText;
    private Predicate<Uri> mAllowedClipDataUri;
    private Predicate<ClipData> mAllowedClipData;
    private boolean mAllowIdentifier;
    private boolean mAllowSelector;
    private boolean mAllowSourceBounds;

    private IntentSanitizer() {
    }

    /**
     * Convenient method for filtering unwanted members from the input intent and log it.
     *
     * @param in input intent
     * @return a copy of the input intent after filtering out unwanted members.
     */
    @NonNull
    public Intent sanitizeByFiltering(@NonNull Intent in) {
        return sanitize(in, msg -> {});
    }

    /**
     * Convenient method for throwing a SecurityException when unwanted members of the input
     * intent is encountered.
     *
     * @param in input intent
     * @return a copy of the input intent if the input intent does not contain any unwanted members.
     * @throws SecurityException if the input intent contains any unwanted members.
     */
    @NonNull
    public Intent sanitizeByThrowing(@NonNull Intent in) {
        return sanitize(in, msg -> {
            throw new SecurityException(msg);
        });
    }

    /**
     * This method sanitizes the given intent. If dirty members are found, the errors are consumed
     * by the penalty object. The penalty action could be called multiple times if multiple
     * issues exist.
     *
     * @param in      the given intent.
     * @param penalty consumer of the error message if dirty members are found.
     * @return a sanitized copy of the given intent.
     */
    @NonNull
    public Intent sanitize(@NonNull Intent in,
            @NonNull Consumer<String> penalty) {
        Intent intent = new Intent();

        ComponentName componentName = in.getComponent();
        if ((mAllowAnyComponent && componentName == null)
                || mAllowedComponents.test(componentName)) {
            intent.setComponent(componentName);
        } else {
            penalty.accept("Component is not allowed: " + componentName);
            intent.setComponent(new ComponentName("android", "java.lang.Void"));
        }

        String packageName = in.getPackage();
        if (packageName == null || mAllowedPackages.test(packageName)) {
            intent.setPackage(packageName);
        } else {
            penalty.accept(("Package is not allowed: " + packageName));
        }

        if ((mAllowedFlags | in.getFlags()) == mAllowedFlags) {
            intent.setFlags(in.getFlags());
        } else {
            intent.setFlags(mAllowedFlags & in.getFlags());
            penalty.accept("The intent contains flags that are not allowed: "
                    + "0x" + Integer.toHexString(in.getFlags() & ~mAllowedFlags));
        }

        String action = in.getAction();
        if (action == null || mAllowedActions.test(action)) {
            intent.setAction(action);
        } else {
            penalty.accept("Action is not allowed: " + action);
        }

        Uri data = in.getData();
        if (data == null || mAllowedData.test(data)) {
            intent.setData(data);
        } else {
            penalty.accept("Data is not allowed: " + data);
        }

        String type = in.getType();
        if (type == null || mAllowedTypes.test(type)) {
            intent.setDataAndType(intent.getData(), type);
        } else {
            penalty.accept("Type is not allowed: " + type);
        }

        Set<String> categories = in.getCategories();
        if (categories != null) {
            for (String category : categories) {
                if (mAllowedCategories.test(category)) {
                    intent.addCategory(category);
                } else {
                    penalty.accept("Category is not allowed: " + category);
                }
            }
        }

        Bundle extras = in.getExtras();
        if (extras != null) {
            for (String key : extras.keySet()) {
                if (key.equals(Intent.EXTRA_STREAM)
                        && (mAllowedFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) == 0) {
                    penalty.accept(
                            "Allowing Extra Stream requires also allowing at least "
                                    + " FLAG_GRANT_READ_URI_PERMISSION Flag.");
                    continue;
                }
                if (key.equals(MediaStore.EXTRA_OUTPUT)
                        && (~mAllowedFlags
                        & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) != 0) {
                    penalty.accept("Allowing Extra Output requires also allowing "
                            + "FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION"
                            + " Flags.");
                    continue;
                }
                Object value = extras.get(key);
                Predicate<Object> test = mAllowedExtras.get(key);
                if (test != null && test.test(value)) {
                    putExtra(intent, key, value);
                } else {
                    penalty.accept("Extra is not allowed. Key: " + key + ". Value: " + value);
                }
            }
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            Api16Impl.sanitizeClipData(in, intent, mAllowedClipData, mAllowClipDataText,
                    mAllowedClipDataUri, penalty);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            if (mAllowIdentifier) {
                Api29Impl.setIdentifier(intent, Api29Impl.getIdentifier(in));
            } else if (Api29Impl.getIdentifier(in) != null) {
                penalty.accept("Identifier is not allowed: " + Api29Impl.getIdentifier(in));
            }
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
            if (mAllowSelector) {
                Api15Impl.setSelector(intent, Api15Impl.getSelector(in));
            } else if (Api15Impl.getSelector(in) != null) {
                penalty.accept("Selector is not allowed: " + Api15Impl.getSelector(in));
            }
        }

        if (mAllowSourceBounds) {
            intent.setSourceBounds(in.getSourceBounds());
        } else if (in.getSourceBounds() != null) {
            penalty.accept("SourceBounds is not allowed: " + in.getSourceBounds());
        }

        return intent;
    }

    private void putExtra(Intent intent, String key, Object value) {
        if (value == null) {
            intent.getExtras().putString(key, null);
        } else if (value instanceof Parcelable) {
            intent.putExtra(key, (Parcelable) value);
        } else if (value instanceof Parcelable[]) {
            intent.putExtra(key, (Parcelable[]) value);
        } else if (value instanceof Serializable) {
            intent.putExtra(key, (Serializable) value);
        } else {
            throw new IllegalArgumentException("Unsupported type " + value.getClass());
        }
    }

    /**
     * General strategy of building is to only offer additive “or” operations that are chained
     * together. Any more complex operations can be performed by the developer providing their
     * own custom Predicate.
     */
    public static final class Builder {
        private static final int HISTORY_STACK_FLAGS =
                Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
                        | Intent.FLAG_ACTIVITY_CLEAR_TASK
                        | Intent.FLAG_ACTIVITY_CLEAR_TOP
                        | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
                        | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
                        | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
                        | Intent.FLAG_ACTIVITY_MULTIPLE_TASK
                        | Intent.FLAG_ACTIVITY_NEW_DOCUMENT
                        | Intent.FLAG_ACTIVITY_NEW_TASK
                        | Intent.FLAG_ACTIVITY_NO_ANIMATION
                        | Intent.FLAG_ACTIVITY_NO_HISTORY
                        | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP
                        | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
                        | Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP
                        | Intent.FLAG_ACTIVITY_TASK_ON_HOME;

        private static final int RECEIVER_FLAGS =
                Intent.FLAG_RECEIVER_FOREGROUND
                        | Intent.FLAG_RECEIVER_NO_ABORT
                        | Intent.FLAG_RECEIVER_REGISTERED_ONLY
                        | Intent.FLAG_RECEIVER_REPLACE_PENDING
                        | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS;

        private int mAllowedFlags;
        private Predicate<String> mAllowedActions = v -> false;
        private Predicate<Uri> mAllowedData = v -> false;
        private Predicate<String> mAllowedTypes = v -> false;
        private Predicate<String> mAllowedCategories = v -> false;
        private Predicate<String> mAllowedPackages = v -> false;
        private Predicate<ComponentName> mAllowedComponents = v -> false;
        private boolean mAllowAnyComponent;
        private boolean mAllowSomeComponents;
        private Map<String, Predicate<Object>> mAllowedExtras = new HashMap<>();
        private boolean mAllowClipDataText = false;
        private Predicate<Uri> mAllowedClipDataUri = v -> false;
        private Predicate<ClipData> mAllowedClipData = v -> false;
        private boolean mAllowIdentifier;
        private boolean mAllowSelector;
        private boolean mAllowSourceBounds;

        /**
         * Sets allowed flags.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         * In most cases following grant URI permission related flags should
         * <b>not</b> be allowed:
         * <ul>
         * <li>FLAG_GRANT_PERSISTABLE_URI_PERMISSION</li>
         * <li>FLAG_GRANT_PREFIX_URI_PERMISSION</li>
         * <li>FLAG_GRANT_READ_URI_PERMISSION</li>
         * <li>FLAG_GRANT_WRITE_URI_PERMISSION</li>
         * </ul>
         * Setting these flags would allow others to access URIs only your
         * app has permission to access. These URIs could be set in intent's data, clipData
         * and/or, in certain circumstances, extras with key of {@link Intent#EXTRA_STREAM} or
         * {@link MediaStore#EXTRA_OUTPUT}.
         * When these flags are allowed, you should sanitize URIs. See
         * {@link #allowDataWithAuthority(String)},
         * {@link #allowData(Predicate)}, {@link #allowClipDataUriWithAuthority(String)},
         * {@link #allowClipDataUri(Predicate)}, {@link #allowExtraStreamUriWithAuthority(String)},
         * {@link #allowExtraStream(Predicate)}, {@link #allowExtraOutput(String)},
         * {@link #allowExtraOutput(Predicate)}
         *
         * @param flags allowed flags.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowFlags(int flags) {
            mAllowedFlags |= flags;
            return this;
        }

        /**
         * Adds all history stack flags into the allowed flags set. They are:
         * <ul>
         * <li>FLAG_ACTIVITY_BROUGHT_TO_FRONT
         * <li>FLAG_ACTIVITY_CLEAR_TASK
         * <li>FLAG_ACTIVITY_CLEAR_TOP
         * <li>FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
         * <li>FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
         * <li>FLAG_ACTIVITY_LAUNCH_ADJACENT
         * <li>FLAG_ACTIVITY_MULTIPLE_TASK
         * <li>FLAG_ACTIVITY_NEW_DOCUMENT
         * <li>FLAG_ACTIVITY_NEW_TASK
         * <li>FLAG_ACTIVITY_NO_ANIMATION
         * <li>FLAG_ACTIVITY_NO_HISTORY
         * <li>FLAG_ACTIVITY_PREVIOUS_IS_TOP
         * <li>FLAG_ACTIVITY_REORDER_TO_FRONT
         * <li>FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
         * <li>FLAG_ACTIVITY_RETAIN_IN_RECENTS
         * <li>FLAG_ACTIVITY_SINGLE_TOP
         * <li>FLAG_ACTIVITY_TASK_ON_HOME
         * </ul>
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowHistoryStackFlags() {
            mAllowedFlags |= HISTORY_STACK_FLAGS;
            return this;
        }

        /**
         * Adds all receiver flags into the allowed flags set. They are
         * <ul>
         * <li>FLAG_RECEIVER_FOREGROUND
         * <li>FLAG_RECEIVER_NO_ABORT
         * <li>FLAG_RECEIVER_REGISTERED_ONLY
         * <li>FLAG_RECEIVER_REPLACE_PENDING
         * <li>FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS
         * </ul>
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowReceiverFlags() {
            mAllowedFlags |= RECEIVER_FLAGS;
            return this;
        }

        /**
         * Add an action to the list of allowed actions.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param action the name of an action.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowAction(@NonNull String action) {
            checkNotNull(action);
            allowAction(action::equals);
            return this;
        }

        /**
         * Add a filter for allowed actions.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter a filter that tests if an action is allowed.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowAction(@NonNull Predicate<String> filter) {
            checkNotNull(filter);
            mAllowedActions = mAllowedActions.or(filter);
            return this;
        }

        /**
         * Convenient method to allow all data whose URI authority equals to the given.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param authority the URI's authority.
         * @return this builder
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowDataWithAuthority(@NonNull String authority) {
            checkNotNull(authority);
            allowData(v -> authority.equals(v.getAuthority()));
            return this;
        }

        /**
         * Allow data that passes the filter test.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter data filter.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowData(@NonNull Predicate<Uri> filter) {
            checkNotNull(filter);
            mAllowedData = mAllowedData.or(filter);
            return this;
        }

        /**
         * Add a data type to the allowed type list. Exact match is used to check the allowed
         * types. For example, if you pass in "image/*" here, it won't allow an intent with type of
         * "image/png".
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param type the data type that is allowed
         * @return this builder
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowType(@NonNull String type) {
            checkNotNull(type);
            return allowType(type::equals);
        }

        /**
         * Add a filter for allowed data types.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the data type filter.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowType(@NonNull Predicate<String> filter) {
            checkNotNull(filter);
            mAllowedTypes = mAllowedTypes.or(filter);
            return this;
        }

        /**
         * Add a category to the allowed category list.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param category the allowed category.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowCategory(@NonNull String category) {
            checkNotNull(category);
            return allowCategory(category::equals);
        }

        /**
         * Add a filter for allowed categories.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the category filter.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowCategory(@NonNull Predicate<String> filter) {
            checkNotNull(filter);
            mAllowedCategories = mAllowedCategories.or(filter);
            return this;
        }

        /**
         * Add a package to the allowed packages.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowPackage(@NonNull String packageName) {
            checkNotNull(packageName);
            return allowPackage(packageName::equals);
        }

        /**
         * Add a filter for allowed packages.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the package name filter.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowPackage(@NonNull Predicate<String> filter) {
            checkNotNull(filter);
            mAllowedPackages = mAllowedPackages.or(filter);
            return this;
        }

        /**
         * Add a component to the allowed components list.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param component the allowed component.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowComponent(@NonNull ComponentName component) {
            checkNotNull(component);
            return allowComponent(component::equals);
        }

        /**
         * Add a filter for allowed components.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the component filter.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowComponent(@NonNull Predicate<ComponentName> filter) {
            checkNotNull(filter);
            mAllowSomeComponents = true;
            mAllowedComponents = mAllowedComponents.or(filter);
            return this;
        }

        /**
         * Add a package to the allowed package list. Any component under this package is allowed.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowComponentWithPackage(@NonNull String packageName) {
            checkNotNull(packageName);
            return allowComponent(v -> packageName.equals(v.getPackageName()));
        }

        /**
         * Allow any components. Be cautious to call this method. When this method is called, you
         * should definitely disallow the 4 grant URI permission flags.
         * This method is useful in case the redirected intent is designed to support implicit
         * intent. This method is made mutually exclusive to the 4 methods that allow components
         * or packages.
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowAnyComponent() {
            mAllowAnyComponent = true;
            mAllowedComponents = v -> true;
            return this;
        }

        /**
         * Allows clipData that contains text.
         * overwrite each other.
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowClipDataText() {
            mAllowClipDataText = true;
            return this;
        }

        /**
         * Allows clipData whose items URIs authorities match the given authority.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param authority the given authority.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowClipDataUriWithAuthority(@NonNull String authority) {
            checkNotNull(authority);
            return allowClipDataUri(v -> authority.equals(v.getAuthority()));
        }


        /**
         * Allows clipData whose items URIs pass the given URI filter.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the given URI filter.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowClipDataUri(@NonNull Predicate<Uri> filter) {
            checkNotNull(filter);
            mAllowedClipDataUri = mAllowedClipDataUri.or(filter);
            return this;
        }


        /**
         * Allows clipData that passes the given filter.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the given clipData filter.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowClipData(@NonNull Predicate<ClipData> filter) {
            checkNotNull(filter);
            mAllowedClipData = mAllowedClipData.or(filter);
            return this;
        }

        /**
         * Allows an extra member whose key and type of value matches the given.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param key   the given extra key.
         * @param clazz the given class of the extra value.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowExtra(@NonNull String key, @NonNull Class<?> clazz) {
            return allowExtra(key, clazz, (v) -> true);
        }


        /**
         * Allows an extra member whose key matches the given key and whose value is of the type of
         * the given clazz and passes the value filter.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param key         given extra key.
         * @param clazz       given type of the extra value.
         * @param valueFilter the extra value filter.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public <T> Builder allowExtra(@NonNull String key, @NonNull Class<T> clazz,
                @NonNull Predicate<T> valueFilter) {
            checkNotNull(key);
            checkNotNull(clazz);
            checkNotNull(valueFilter);
            return allowExtra(key, v -> clazz.isInstance(v) && valueFilter.test(clazz.cast(v)));
        }

        /**
         * Allows an extra member whose key matches the given key and whose value passes the
         * filter test.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param key    the extra key.
         * @param filter the filter for the extra value.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowExtra(@NonNull String key, @NonNull Predicate<Object> filter) {
            checkNotNull(key);
            checkNotNull(filter);
            Predicate<Object> allowedExtra = mAllowedExtras.get(key);
            if (allowedExtra == null) allowedExtra = v -> false;
            allowedExtra = allowedExtra.or(filter);
            mAllowedExtras.put(key, allowedExtra);
            return this;
        }

        /**
         * Allows an extra member with the key Intent.EXTRA_STREAM. The value type has to be URI
         * and the authority matches the given parameter.
         * In order to use this method, user has to be explicitly allow the
         * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} flag. Otherwise, it will trigger penalty
         * during sanitization.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param uriAuthority the given URI authority.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowExtraStreamUriWithAuthority(@NonNull String uriAuthority) {
            checkNotNull(uriAuthority);
            allowExtra(Intent.EXTRA_STREAM, Uri.class,
                    (v) -> uriAuthority.equals(v.getAuthority()));
            return this;
        }

        /**
         * Allows an extra member with the key Intent.EXTRA_STREAM. The value type has to be URI
         * and the value also passes the given filter test.
         * In order to use this method, user has to be explicitly allow the
         * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} flag. Otherwise, it will trigger penalty
         * during sanitization.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the given URI authority.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowExtraStream(@NonNull Predicate<Uri> filter) {
            allowExtra(Intent.EXTRA_STREAM, Uri.class, filter);
            return this;
        }

        /**
         * Allows an extra member with the key MediaStore.EXTRA_OUTPUT. The value type has to be URI
         * and the authority matches the given parameter.
         * In order to use this method, user has to be explicitly allow the
         * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
         * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} flags. Otherwise, it will trigger penalty
         * during sanitization.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param uriAuthority the given URI authority.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowExtraOutput(@NonNull String uriAuthority) {
            allowExtra(MediaStore.EXTRA_OUTPUT, Uri.class,
                    (v) -> uriAuthority.equals(v.getAuthority()));
            return this;
        }

        /**
         * Allows an extra member with the key MediaStore.EXTRA_OUTPUT. The value type has to be URI
         * and the value also passes the given filter test.
         * In order to use this method, user has to be explicitly allow the
         * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
         * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} flags. Otherwise, it will trigger penalty
         * during sanitization.
         * This method can be called multiple times and the result is additive. They will not
         * overwrite each other.
         *
         * @param filter the given URI authority.
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowExtraOutput(@NonNull Predicate<Uri> filter) {
            allowExtra(MediaStore.EXTRA_OUTPUT, Uri.class, filter);
            return this;
        }

        /**
         * Allows any identifier.
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowIdentifier() {
            mAllowIdentifier = true;
            return this;
        }

        /**
         * Allow any selector.
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowSelector() {
            mAllowSelector = true;
            return this;
        }

        /**
         * Allow any source bounds.
         *
         * @return this builder.
         */
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder allowSourceBounds() {
            mAllowSourceBounds = true;
            return this;
        }

        /**
         * Build the IntentSanitizer.
         *
         * @return the IntentSanitizer
         */
        @SuppressLint("SyntheticAccessor")
        @NonNull
        public IntentSanitizer build() {
            if ((mAllowAnyComponent && mAllowSomeComponents)
                    || (!mAllowAnyComponent && !mAllowSomeComponents)) {
                throw new SecurityException(
                        "You must call either allowAnyComponent or one or more "
                                + "of the allowComponent methods; but not both.");
            }

            IntentSanitizer sanitizer = new IntentSanitizer();
            sanitizer.mAllowedFlags = this.mAllowedFlags;
            sanitizer.mAllowedActions = this.mAllowedActions;
            sanitizer.mAllowedData = this.mAllowedData;
            sanitizer.mAllowedTypes = this.mAllowedTypes;
            sanitizer.mAllowedCategories = this.mAllowedCategories;
            sanitizer.mAllowedPackages = this.mAllowedPackages;
            sanitizer.mAllowAnyComponent = this.mAllowAnyComponent;
            sanitizer.mAllowedComponents = this.mAllowedComponents;
            sanitizer.mAllowedExtras = this.mAllowedExtras;
            sanitizer.mAllowClipDataText = this.mAllowClipDataText;
            sanitizer.mAllowedClipDataUri = this.mAllowedClipDataUri;
            sanitizer.mAllowedClipData = this.mAllowedClipData;
            sanitizer.mAllowIdentifier = this.mAllowIdentifier;
            sanitizer.mAllowSelector = this.mAllowSelector;
            sanitizer.mAllowSourceBounds = this.mAllowSourceBounds;
            return sanitizer;
        }
    }

    @RequiresApi(15)
    private static class Api15Impl {
        private Api15Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static void setSelector(Intent intent, Intent selector) {
            intent.setSelector(selector);
        }

        @DoNotInline
        static Intent getSelector(Intent intent) {
            return intent.getSelector();
        }
    }

    @RequiresApi(16)
    private static class Api16Impl {
        private Api16Impl() {
        }

        @DoNotInline
        static void sanitizeClipData(@NonNull Intent in, Intent out,
                Predicate<ClipData> mAllowedClipData,
                boolean mAllowClipDataText,
                Predicate<Uri> mAllowedClipDataUri, Consumer<String> penalty) {
            ClipData clipData = in.getClipData();

            if (clipData == null) return;

            ClipData newClipData = null;
            if (mAllowedClipData != null && mAllowedClipData.test(clipData)) {
                out.setClipData(clipData);
            } else {
                for (int i = 0; i < clipData.getItemCount(); i++) {
                    ClipData.Item item = clipData.getItemAt(i);
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                        Api31Impl.checkOtherMembers(i, item, penalty);
                    } else {
                        checkOtherMembers(i, item, penalty);
                    }

                    CharSequence itemText = null;
                    if (mAllowClipDataText) {
                        itemText = item.getText();
                    } else {
                        if (item.getText() != null) {
                            penalty.accept(
                                    "Item text cannot contain value. Item position: " + i + "."
                                            + " Text: " + item.getText());
                        }
                    }

                    Uri itemUri = null;
                    if (mAllowedClipDataUri == null) {
                        if (item.getUri() != null) {
                            penalty.accept(
                                    "Item URI is not allowed. Item position: " + i + ". URI: "
                                            + item.getUri());
                        }
                    } else {
                        if (item.getUri() == null || mAllowedClipDataUri.test(item.getUri())) {
                            itemUri = item.getUri();
                        } else {
                            penalty.accept(
                                    "Item URI is not allowed. Item position: " + i + ". URI: "
                                            + item.getUri());
                        }
                    }

                    if (itemText != null || itemUri != null) {
                        if (newClipData == null) {
                            newClipData = new ClipData(clipData.getDescription(),
                                    new ClipData.Item(itemText, null, itemUri));
                        } else {
                            newClipData.addItem(new ClipData.Item(itemText, null, itemUri));
                        }
                    }
                }
                if (newClipData != null) {
                    out.setClipData(newClipData);
                }
            }
        }

        private static void checkOtherMembers(int i, ClipData.Item item, Consumer<String> penalty) {
            if (item.getHtmlText() != null || item.getIntent() != null) {
                penalty.accept("ClipData item at position " + i + " contains htmlText, "
                        + "textLinks or intent: " + item);
            }
        }

        @RequiresApi(31)
        private static class Api31Impl {
            private Api31Impl() {
            }

            @DoNotInline
            static void checkOtherMembers(int i, ClipData.Item item, Consumer<String> penalty) {
                if (item.getHtmlText() != null || item.getIntent() != null
                        || item.getTextLinks() != null) {
                    penalty.accept("ClipData item at position " + i + " contains htmlText, "
                            + "textLinks or intent: " + item);
                }
            }
        }
    }

    @RequiresApi(29)
    private static class Api29Impl {
        private Api29Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static Intent setIdentifier(Intent intent, String identifier) {
            return intent.setIdentifier(identifier);
        }

        @DoNotInline
        static String getIdentifier(Intent intent) {
            return intent.getIdentifier();
        }
    }
}