Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 20 additions & 50 deletions binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.grpc.binder.internal;

import static com.google.common.base.Preconditions.checkState;
import static io.grpc.binder.internal.SystemApis.createContextAsUser;

import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
Expand All @@ -32,13 +33,10 @@
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.VerifyException;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.BinderChannelCredentials;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -93,8 +91,6 @@ public String methodName() {
private final Observer observer;
private final Executor mainThreadExecutor;

private static volatile Method queryIntentServicesAsUserMethod;

@GuardedBy("this")
private State state;

Expand Down Expand Up @@ -194,8 +190,10 @@ private static Status bindInternal(
break;
case BIND_SERVICE_AS_USER:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// We don't need SystemApis because bindServiceAsUser() is simply public in R+.
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle);
} else {
// TODO(jdcormie): Use SystemApis to make this work pre-R.
return Status.INTERNAL.withDescription("Cross user Channel requires Android R+");
}
break;
Expand Down Expand Up @@ -266,51 +264,9 @@ void unbindInternal(Status reason) {
}
}

// Sadly the PackageManager#resolveServiceAsUser() API we need isn't part of the SDK or even a
// @SystemApi as of this writing. Modern Android prevents even system apps from calling it, by any
// means (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces).
// So instead we call queryIntentServicesAsUser(), which does more than we need but *is* a
// @SystemApi in all the SDK versions where we support cross-user Channels.
@Nullable
private static ResolveInfo resolveServiceAsUser(
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
List<ResolveInfo> results =
queryIntentServicesAsUser(packageManager, intent, flags, targetUserHandle);
// The first query result is "what would be returned by resolveService", per the javadoc.
return (results != null && !results.isEmpty()) ? results.get(0) : null;
}

// The cross-user Channel feature requires the client to be a system app so we assume @SystemApi
// queryIntentServicesAsUser() is visible to us at runtime. It would be visible at build time too,
// if our host system app were written to call it directly. We only have to use reflection here
// because grpc-java is a library built outside the Android source tree where the compiler can't
// see the "non-SDK" @SystemApis that we need.
@Nullable
@SuppressWarnings("unchecked") // Safe by PackageManager#queryIntentServicesAsUser spec in AOSP.
private static List<ResolveInfo> queryIntentServicesAsUser(
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
try {
if (queryIntentServicesAsUserMethod == null) {
synchronized (ServiceBinding.class) {
if (queryIntentServicesAsUserMethod == null) {
queryIntentServicesAsUserMethod =
PackageManager.class.getMethod(
"queryIntentServicesAsUser", Intent.class, int.class, UserHandle.class);
}
}
}
return (List<ResolveInfo>)
queryIntentServicesAsUserMethod.invoke(packageManager, intent, flags, targetUserHandle);
} catch (ReflectiveOperationException e) {
throw new VerifyException(e);
}
}

@AnyThread
@Override
public ServiceInfo resolve() throws StatusException {
checkState(sourceContext != null);
PackageManager packageManager = sourceContext.getPackageManager();
int flags = 0;
if (Build.VERSION.SDK_INT >= 29) {
// Filter out non-'directBootAware' <service>s when 'targetUserHandle' is locked. Here's why:
Expand All @@ -319,10 +275,16 @@ public ServiceInfo resolve() throws StatusException {
// 'directBootAware'-ness. This flag explicitly tells resolveService() to act the same way.
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
}
Context targetUserContext;
try {
targetUserContext = getContextForTargetUser();
} catch (ReflectiveOperationException e) {
throw Status.INTERNAL
.withDescription("Cross-user pre-auth requires SDK_INT >= R and @SystemApi visibility")
.asException();
}
ResolveInfo resolveInfo =
targetUserHandle != null
? resolveServiceAsUser(packageManager, bindIntent, flags, targetUserHandle)
: packageManager.resolveService(bindIntent, flags);
targetUserContext.getPackageManager().resolveService(bindIntent, flags);
if (resolveInfo == null) {
throw Status.UNIMPLEMENTED // Same status code as when bindService() returns false.
.withDescription("resolveService(" + bindIntent + " / " + targetUserHandle + ") was null")
Expand All @@ -331,6 +293,14 @@ public ServiceInfo resolve() throws StatusException {
return resolveInfo.serviceInfo;
}

private Context getContextForTargetUser() throws ReflectiveOperationException {
checkState(sourceContext != null);
return targetUserHandle == null
? sourceContext
: createContextAsUser(
sourceContext, targetUserHandle, /* flags= */ 0); // @SystemApi since R.
}

@MainThread
private void clearReferences() {
sourceContext = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,15 @@ public void testResolveWithTargetUserHandle() throws Exception {
assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName);
}

@Test
@Config(sdk = 29)
public void testResolveWithUnsupportedTargetUserHandle() throws Exception {
binding = newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build();
StatusException statusException = assertThrows(StatusException.class, binding::resolve);
assertThat(statusException.getStatus().getCode()).isEqualTo(Code.INTERNAL);
assertThat(statusException.getStatus().getDescription()).contains("SDK_INT >= R");
}

@Test
public void testResolveNonExistentServiceThrows() throws Exception {
ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService");
Expand Down
Loading