Add UserHandle and BinderChannelCredentials to BinderChannelBuilder to support cross-user communication through OnDeviceServer. (#10197)

Add UserHandle and BinderChannelCredentials to BinderChannelBuilder to support cross-user ondevice server.
This commit is contained in:
wwtbuaa01 2023-06-29 19:01:13 +08:00 committed by GitHub
parent 4d2c3aac0e
commit 5799febde0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 375 additions and 12 deletions

View File

@ -35,6 +35,7 @@ import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.BindServiceFlags;
import io.grpc.binder.BinderChannelCredentials;
import io.grpc.binder.BinderServerBuilder;
import io.grpc.binder.HostServices;
import io.grpc.binder.InboundParcelablePolicy;
@ -146,7 +147,9 @@ public final class BinderClientTransportTest {
public BinderTransport.BinderClientTransport build() {
return new BinderTransport.BinderClientTransport(
appContext,
BinderChannelCredentials.forDefault(),
serverAddress,
null,
BindServiceFlags.DEFAULTS,
ContextCompat.getMainExecutor(appContext),
executorServicePool,

View File

@ -24,6 +24,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.ServerStreamTracer;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.BindServiceFlags;
import io.grpc.binder.BinderChannelCredentials;
import io.grpc.binder.HostServices;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicies;
@ -95,7 +96,9 @@ public final class BinderTransportTest extends AbstractTransportTest {
AndroidComponentAddress addr = (AndroidComponentAddress) server.getListenSocketAddress();
return new BinderTransport.BinderClientTransport(
appContext,
BinderChannelCredentials.forDefault(),
addr,
null,
BindServiceFlags.DEFAULTS,
ContextCompat.getMainExecutor(appContext),
executorServicePool,

View File

@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.content.Context;
import android.os.UserHandle;
import androidx.core.content.ContextCompat;
import com.google.errorprone.annotations.DoNotCall;
import io.grpc.ChannelCredentials;
@ -71,7 +72,37 @@ public final class BinderChannelBuilder
public static BinderChannelBuilder forAddress(
AndroidComponentAddress directAddress, Context sourceContext) {
return new BinderChannelBuilder(
checkNotNull(directAddress, "directAddress"), null, sourceContext);
checkNotNull(directAddress, "directAddress"),
null,
sourceContext,
BinderChannelCredentials.forDefault());
}
/**
* Creates a channel builder that will bind to a remote Android service with provided
* BinderChannelCredentials.
*
* <p>The underlying Android binding will be torn down when the channel becomes idle. This happens
* after 30 minutes without use by default but can be configured via {@link
* ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link
* ManagedChannel#enterIdle()}.
*
* <p>You the caller are responsible for managing the lifecycle of any channels built by the
* resulting builder. They will not be shut down automatically.
*
* @param directAddress the {@link AndroidComponentAddress} referencing the service to bind to.
* @param sourceContext the context to bind from (e.g. The current Activity or Application).
* @param channelCredentials the arbitrary binder specific channel credentials to be used to
* establish a binder connection.
* @return a new builder
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
public static BinderChannelBuilder forAddress(
AndroidComponentAddress directAddress,
Context sourceContext,
BinderChannelCredentials channelCredentials) {
return new BinderChannelBuilder(
checkNotNull(directAddress, "directAddress"), null, sourceContext, channelCredentials);
}
/**
@ -92,7 +123,37 @@ public final class BinderChannelBuilder
* @return a new builder
*/
public static BinderChannelBuilder forTarget(String target, Context sourceContext) {
return new BinderChannelBuilder(null, checkNotNull(target, "target"), sourceContext);
return new BinderChannelBuilder(
null,
checkNotNull(target, "target"),
sourceContext,
BinderChannelCredentials.forDefault());
}
/**
* Creates a channel builder that will bind to a remote Android service, via a string target name
* which will be resolved.
*
* <p>The underlying Android binding will be torn down when the channel becomes idle. This happens
* after 30 minutes without use by default but can be configured via {@link
* ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link
* ManagedChannel#enterIdle()}.
*
* <p>You the caller are responsible for managing the lifecycle of any channels built by the
* resulting builder. They will not be shut down automatically.
*
* @param target A target uri which should resolve into an {@link AndroidComponentAddress}
* referencing the service to bind to.
* @param sourceContext the context to bind from (e.g. The current Activity or Application).
* @param channelCredentials the arbitrary binder specific channel credentials to be used to
* establish a binder connection.
* @return a new builder
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
public static BinderChannelBuilder forTarget(
String target, Context sourceContext, BinderChannelCredentials channelCredentials) {
return new BinderChannelBuilder(
null, checkNotNull(target, "target"), sourceContext, channelCredentials);
}
/**
@ -121,12 +182,14 @@ public final class BinderChannelBuilder
private SecurityPolicy securityPolicy;
private InboundParcelablePolicy inboundParcelablePolicy;
private BindServiceFlags bindServiceFlags;
@Nullable private UserHandle targetUserHandle;
private boolean strictLifecycleManagement;
private BinderChannelBuilder(
@Nullable AndroidComponentAddress directAddress,
@Nullable String target,
Context sourceContext) {
Context sourceContext,
BinderChannelCredentials channelCredentials) {
mainThreadExecutor =
ContextCompat.getMainExecutor(checkNotNull(sourceContext, "sourceContext"));
securityPolicy = SecurityPolicies.internalOnly();
@ -139,10 +202,12 @@ public final class BinderChannelBuilder
public ClientTransportFactory buildClientTransportFactory() {
return new TransportFactory(
sourceContext,
channelCredentials,
mainThreadExecutor,
schedulerPool,
managedChannelImplBuilder.getOffloadExecutorPool(),
securityPolicy,
targetUserHandle,
bindServiceFlags,
inboundParcelablePolicy);
}
@ -216,6 +281,23 @@ public final class BinderChannelBuilder
return this;
}
/**
* Provides the target {@UserHandle} of the remote Android service.
*
* <p>When targetUserHandle is set, Context.bindServiceAsUser will used and additional Android
* permissions will be required. If your usage does not require cross-user communications, please
* do not set this field. It is the caller's responsibility to make sure that it holds the
* corresponding permissions.
*
* @param targetUserHandle the target user to bind into.
* @return this
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
public BinderChannelBuilder bindAsUser(UserHandle targetUserHandle) {
this.targetUserHandle = targetUserHandle;
return this;
}
/** Sets the policy for inbound parcelable objects. */
public BinderChannelBuilder inboundParcelablePolicy(
InboundParcelablePolicy inboundParcelablePolicy) {
@ -245,12 +327,14 @@ public final class BinderChannelBuilder
/** Creates new binder transports. */
private static final class TransportFactory implements ClientTransportFactory {
private final Context sourceContext;
private final BinderChannelCredentials channelCredentials;
private final Executor mainThreadExecutor;
private final ObjectPool<ScheduledExecutorService> scheduledExecutorPool;
private final ObjectPool<? extends Executor> offloadExecutorPool;
private final SecurityPolicy securityPolicy;
private final InboundParcelablePolicy inboundParcelablePolicy;
@Nullable private final UserHandle targetUserHandle;
private final BindServiceFlags bindServiceFlags;
private final InboundParcelablePolicy inboundParcelablePolicy;
private ScheduledExecutorService executorService;
private Executor offloadExecutor;
@ -258,17 +342,21 @@ public final class BinderChannelBuilder
TransportFactory(
Context sourceContext,
BinderChannelCredentials channelCredentials,
Executor mainThreadExecutor,
ObjectPool<ScheduledExecutorService> scheduledExecutorPool,
ObjectPool<? extends Executor> offloadExecutorPool,
SecurityPolicy securityPolicy,
@Nullable UserHandle targetUserHandle,
BindServiceFlags bindServiceFlags,
InboundParcelablePolicy inboundParcelablePolicy) {
this.sourceContext = sourceContext;
this.channelCredentials = channelCredentials;
this.mainThreadExecutor = mainThreadExecutor;
this.scheduledExecutorPool = scheduledExecutorPool;
this.offloadExecutorPool = offloadExecutorPool;
this.securityPolicy = securityPolicy;
this.targetUserHandle = targetUserHandle;
this.bindServiceFlags = bindServiceFlags;
this.inboundParcelablePolicy = inboundParcelablePolicy;
@ -284,7 +372,9 @@ public final class BinderChannelBuilder
}
return new BinderTransport.BinderClientTransport(
sourceContext,
channelCredentials,
(AndroidComponentAddress) addr,
targetUserHandle,
bindServiceFlags,
mainThreadExecutor,
scheduledExecutorPool,

View File

@ -0,0 +1,70 @@
/*
* Copyright 2022 The gRPC Authors
*
* 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 io.grpc.binder;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.ComponentName;
import io.grpc.ChannelCredentials;
import io.grpc.ExperimentalApi;
import javax.annotation.Nullable;
/** Additional arbitrary arguments to establish a Android binder connection channel. */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
public final class BinderChannelCredentials extends ChannelCredentials {
/**
* Creates the default BinderChannelCredentials.
*
* @return a BinderChannelCredentials
*/
public static BinderChannelCredentials forDefault() {
return new BinderChannelCredentials(null);
}
/**
* Creates a BinderChannelCredentials to be used with DevicePolicyManager API.
*
* @param devicePolicyAdminComponentName the admin component to be specified with
* DevicePolicyManager.bindDeviceAdminServiceAsUser API.
* @return a BinderChannelCredentials
*/
public static BinderChannelCredentials forDevicePolicyAdmin(
ComponentName devicePolicyAdminComponentName) {
return new BinderChannelCredentials(devicePolicyAdminComponentName);
}
@Nullable private final ComponentName devicePolicyAdminComponentName;
private BinderChannelCredentials(@Nullable ComponentName devicePolicyAdminComponentName) {
this.devicePolicyAdminComponentName = devicePolicyAdminComponentName;
}
@Override
public ChannelCredentials withoutBearerTokens() {
return this;
}
/**
* Returns the admin component to be specified with DevicePolicyManager
* bindDeviceAdminServiceAsUser API.
*/
@Nullable
public ComponentName getDevicePolicyAdminComponentName() {
return devicePolicyAdminComponentName;
}
}

View File

@ -28,6 +28,7 @@ import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import android.os.UserHandle;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.util.concurrent.ListenableFuture;
@ -46,6 +47,7 @@ import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.BindServiceFlags;
import io.grpc.binder.BinderChannelCredentials;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
@ -568,7 +570,9 @@ public abstract class BinderTransport
public BinderClientTransport(
Context sourceContext,
BinderChannelCredentials channelCredentials,
AndroidComponentAddress targetAddress,
@Nullable UserHandle targetUserHandle,
BindServiceFlags bindServiceFlags,
Executor mainThreadExecutor,
ObjectPool<ScheduledExecutorService> executorServicePool,
@ -590,7 +594,9 @@ public abstract class BinderTransport
new ServiceBinding(
mainThreadExecutor,
sourceContext,
channelCredentials,
targetAddress.asBindIntent(),
targetUserHandle,
bindServiceFlags.toInteger(),
this);
}

View File

@ -16,15 +16,20 @@
package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkState;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.UserHandle;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.Status;
import io.grpc.binder.BinderChannelCredentials;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -56,7 +61,26 @@ final class ServiceBinding implements Bindable, ServiceConnection {
UNBOUND,
}
// Type of the method used when binding the service.
private enum BindMethodType {
BIND_SERVICE("bindService"),
BIND_SERVICE_AS_USER("bindServiceAsUser"),
DEVICE_POLICY_BIND_SEVICE_ADMIN("DevicePolicyManager.bindDeviceAdminServiceAsUser");
private final String methodName;
BindMethodType(String methodName) {
this.methodName = methodName;
}
public String methodName() {
return methodName;
}
}
private final BinderChannelCredentials channelCredentials;
private final Intent bindIntent;
@Nullable private final UserHandle targetUserHandle;
private final int bindFlags;
private final Observer observer;
private final Executor mainThreadExecutor;
@ -76,7 +100,9 @@ final class ServiceBinding implements Bindable, ServiceConnection {
ServiceBinding(
Executor mainThreadExecutor,
Context sourceContext,
BinderChannelCredentials channelCredentials,
Intent bindIntent,
@Nullable UserHandle targetUserHandle,
int bindFlags,
Observer observer) {
// We need to synchronize here ensure other threads see all
@ -87,6 +113,8 @@ final class ServiceBinding implements Bindable, ServiceConnection {
this.observer = observer;
this.sourceContext = sourceContext;
this.mainThreadExecutor = mainThreadExecutor;
this.channelCredentials = channelCredentials;
this.targetUserHandle = targetUserHandle;
state = State.NOT_BINDING;
reportedState = State.NOT_BINDING;
}
@ -117,7 +145,14 @@ final class ServiceBinding implements Bindable, ServiceConnection {
public synchronized void bind() {
if (state == State.NOT_BINDING) {
state = State.BINDING;
Status bindResult = bindInternal(sourceContext, bindIntent, this, bindFlags);
Status bindResult =
bindInternal(
sourceContext,
bindIntent,
this,
bindFlags,
channelCredentials,
targetUserHandle);
if (!bindResult.isOk()) {
handleBindServiceFailure(sourceContext, this);
state = State.UNBOUND;
@ -127,19 +162,57 @@ final class ServiceBinding implements Bindable, ServiceConnection {
}
private static Status bindInternal(
Context context, Intent bindIntent, ServiceConnection conn, int flags) {
Context context,
Intent bindIntent,
ServiceConnection conn,
int flags,
BinderChannelCredentials channelCredentials,
@Nullable UserHandle targetUserHandle) {
BindMethodType bindMethodType = BindMethodType.BIND_SERVICE;
try {
if (!context.bindService(bindIntent, conn, flags)) {
if (targetUserHandle == null) {
checkState(
channelCredentials.getDevicePolicyAdminComponentName() == null,
"BindingChannelCredentials is expected to have null devicePolicyAdmin when"
+ " targetUserHandle is not set");
} else {
if (channelCredentials.getDevicePolicyAdminComponentName() != null) {
bindMethodType = BindMethodType.DEVICE_POLICY_BIND_SEVICE_ADMIN;
} else {
bindMethodType = BindMethodType.BIND_SERVICE_AS_USER;
}
}
boolean bindResult = false;
switch (bindMethodType) {
case BIND_SERVICE:
bindResult = context.bindService(bindIntent, conn, flags);
break;
case BIND_SERVICE_AS_USER:
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle);
break;
case DEVICE_POLICY_BIND_SEVICE_ADMIN:
DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
bindResult = devicePolicyManager.bindDeviceAdminServiceAsUser(
channelCredentials.getDevicePolicyAdminComponentName(),
bindIntent,
conn,
flags,
targetUserHandle);
break;
}
if (!bindResult) {
return Status.UNIMPLEMENTED.withDescription(
"bindService(" + bindIntent + ") returned false");
bindMethodType.methodName() + "(" + bindIntent + ") returned false");
}
return Status.OK;
} catch (SecurityException e) {
return Status.PERMISSION_DENIED.withCause(e).withDescription(
"SecurityException from bindService");
return Status.PERMISSION_DENIED
.withCause(e)
.withDescription("SecurityException from " + bindMethodType.methodName());
} catch (RuntimeException e) {
return Status.INTERNAL.withCause(e).withDescription(
"RuntimeException from bindService");
"RuntimeException from " + bindMethodType.methodName());
}
}

View File

@ -0,0 +1,32 @@
package io.grpc.binder;
import static com.google.common.truth.Truth.assertThat;
import android.content.ComponentName;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class BinderChannelCredentialsTest {
private final Context appContext = ApplicationProvider.getApplicationContext();
@Test
public void defaultBinderChannelCredentials() {
BinderChannelCredentials channelCredentials = BinderChannelCredentials.forDefault();
assertThat(channelCredentials.getDevicePolicyAdminComponentName()).isNull();
}
@Test
public void binderChannelCredentialsForDevicePolicyAdmin() {
String deviceAdminClassName = "DevicePolicyAdmin";
BinderChannelCredentials channelCredentials =
BinderChannelCredentials.forDevicePolicyAdmin(
new ComponentName(appContext, deviceAdminClassName));
assertThat(channelCredentials.getDevicePolicyAdminComponentName()).isNotNull();
assertThat(channelCredentials.getDevicePolicyAdminComponentName().getClassName())
.isEqualTo(deviceAdminClassName);
}
}

View File

@ -24,15 +24,21 @@ import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import android.app.Application;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.Parcel;
import android.os.UserHandle;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.binder.BinderChannelCredentials;
import io.grpc.binder.internal.Bindable.Observer;
import java.util.Arrays;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -44,6 +50,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowDevicePolicyManager;
@LooperMode(PAUSED)
@RunWith(RobolectricTestRunner.class)
@ -256,6 +263,48 @@ public final class ServiceBindingTest {
shadowOf(getMainLooper()).idle();
}
@Test
@Config(sdk = 30)
public void testBindWithTargetUserHandle() throws Exception {
binding =
newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build();
shadowOf(getMainLooper()).idle();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty();
assertThat(observer.gotBoundEvent).isTrue();
assertThat(observer.binder).isSameInstanceAs(mockBinder);
assertThat(observer.gotUnboundEvent).isFalse();
assertThat(binding.isSourceContextCleared()).isFalse();
}
@Test
@Config(sdk = 30)
public void testBindWithDeviceAdmin() throws Exception {
String deviceAdminClassName = "DevicePolicyAdmin";
ComponentName adminComponent = new ComponentName(appContext, deviceAdminClassName);
allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0);
binding =
newBuilder()
.setTargetUserHandle(UserHandle.getUserHandleForUid(/* userId= */ 0))
.setTargetUserHandle(generateUserHandle(/* userId= */ 0))
.setChannelCredentials(
BinderChannelCredentials.forDevicePolicyAdmin(adminComponent))
.build();
shadowOf(getMainLooper()).idle();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty();
assertThat(observer.gotBoundEvent).isTrue();
assertThat(observer.binder).isSameInstanceAs(mockBinder);
assertThat(observer.gotUnboundEvent).isFalse();
assertThat(binding.isSourceContextCleared()).isFalse();
}
private void assertNoLockHeld() {
try {
binding.wait(1);
@ -268,6 +317,28 @@ public final class ServiceBindingTest {
}
}
private static void allowBindDeviceAdminForUser(Context context, ComponentName admin, int userId) {
ShadowDevicePolicyManager devicePolicyManager =
shadowOf(context.getSystemService(DevicePolicyManager.class));
devicePolicyManager.setDeviceOwner(admin);
devicePolicyManager.setBindDeviceAdminTargetUsers(
Arrays.asList(UserHandle.getUserHandleForUid(userId)));
shadowOf((DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE));
devicePolicyManager.setDeviceOwner(admin);
devicePolicyManager.setBindDeviceAdminTargetUsers(
Arrays.asList(generateUserHandle(userId)));
}
/** Generate UserHandles the hard way. */
private static UserHandle generateUserHandle(int userId) {
Parcel userParcel = Parcel.obtain();
userParcel.writeInt(userId);
userParcel.setDataPosition(0);
UserHandle userHandle = new UserHandle(userParcel);
userParcel.recycle();
return userHandle;
}
private class TestObserver implements Bindable.Observer {
public boolean gotBoundEvent;
@ -298,9 +369,11 @@ public final class ServiceBindingTest {
private Observer observer;
private Intent bindIntent = new Intent();
private int bindServiceFlags;
@Nullable private UserHandle targetUserHandle = null;
private BinderChannelCredentials channelCredentials = BinderChannelCredentials.forDefault();
public ServiceBindingBuilder setSourceContext(Context sourceContext) {
this.sourceContext = sourceContext;
this.sourceContext = sourceContext;
return this;
}
@ -324,11 +397,24 @@ public final class ServiceBindingTest {
return this;
}
public ServiceBindingBuilder setTargetUserHandle(UserHandle targetUserHandle) {
this.targetUserHandle = targetUserHandle;
return this;
}
public ServiceBindingBuilder setChannelCredentials(
BinderChannelCredentials channelCredentials) {
this.channelCredentials = channelCredentials;
return this;
}
public ServiceBinding build() {
return new ServiceBinding(
ContextCompat.getMainExecutor(sourceContext),
sourceContext,
channelCredentials,
bindIntent,
targetUserHandle,
bindServiceFlags,
observer);
}