ContentUriValidator.java

package androidx.wear.protolayout.renderer.inflater;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Process;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

class ContentUriValidator {
    @NonNull final Context mAppContext;
    @NonNull private final PackageManager mPackageManager;
    @NonNull private final String mAllowedPackageName;
    @NonNull private final UriPermissionValidator mUriPermissionValidator;

    public ContentUriValidator(@NonNull Context appContext, @NonNull String allowedPackageName) {
        this.mAppContext = appContext;
        this.mPackageManager = appContext.getPackageManager();
        this.mAllowedPackageName = allowedPackageName;
        this.mUriPermissionValidator = new UriPermissionValidator();
    }

    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    ContentUriValidator(
            @NonNull Context appContext,
            @NonNull String allowedPackageName,
            @NonNull UriPermissionValidator uriPermissionValidator) {
        this.mAppContext = appContext;
        this.mPackageManager = appContext.getPackageManager();
        this.mAllowedPackageName = allowedPackageName;
        this.mUriPermissionValidator = uriPermissionValidator;
    }

    /**
     * Validate that a content URI can be used by the package name passed to this validator.
     *
     * <p>This will check multiple things: the content provider for the given authority must:
     *
     * <ul>
     *   <li>Exist and be resolvable.
     *   <li>Be exported.
     *   <li>Belong to the same app as the package name passed to this validator's constructor.
     *   <li>Have explicitly granted us read permission to its content URI (using {@link
     *       Context#grantUriPermission})
     * </ul>
     */
    @SuppressWarnings("deprecation")
    // PackageManager#resolveContentProvider(String,int) is deprecated. Though, we can't use the
    // replacement method as it was introduced in API level 33.
    public boolean validateUri(@NonNull Uri uri) {
        // First ensure that it's a content:// URI
        String scheme = uri.getScheme();
        String authority = uri.getAuthority();

        if (scheme == null || !scheme.equals("content")) {
            return false;
        }

        if (authority == null) {
            return false;
        }

        // Ensure that the authority belongs to the allowed package.
        ProviderInfo providerInfo =
                mPackageManager.resolveContentProvider(authority, /* flags= */ 0);

        if (providerInfo == null) {
            // Android does support authorities of the form <user_id>@authority. If so, try querying
            // for that one.
            int authorityIndex = authority.lastIndexOf("@");
            if (authorityIndex != -1) {
                String actualAuthority = authority.substring(authorityIndex + 1);
                providerInfo =
                        mPackageManager.resolveContentProvider(actualAuthority, /* flags= */ 0);
            }

            if (providerInfo == null) {
                return false;
            }
        }

        // Provider must be exported.
        if (!providerInfo.exported) {
            return false;
        }

        if (!mUriPermissionValidator.canAccessUri(uri)) {
            return false;
        }

        // Otherwise, only allow content from the same package that provided the layout.
        return providerInfo.packageName.equals(mAllowedPackageName);
    }

    /**
     * Utility class to check that this process has access to a given URI.
     *
     * <p>This only exists for testing; Robolectric doesn't provider a way to fake out
     * checkUriPermission, and stubbing out Context is generally frowned upon.
     */
    class UriPermissionValidator {
        public boolean canAccessUri(@NonNull Uri uri) {
            int pid = Process.myPid();
            int uid = Process.myUid();

            // Ensure that this process has been granted access to the content URI, *explicitly*.
            return mAppContext.checkUriPermission(
                            uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    == PackageManager.PERMISSION_GRANTED;
        }
    }
}