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