diff --git a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java index e3a8c58bf88..4e3cfcf0d05 100644 --- a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java @@ -97,6 +97,7 @@ public final class BinderChannelSmokeTest { .setType(MethodDescriptor.MethodType.BIDI_STREAMING) .build(); + AndroidComponentAddress serverAddress; ManagedChannel channel; AtomicReference headersCapture = new AtomicReference<>(); AtomicReference clientUidCapture = new AtomicReference<>(); @@ -134,7 +135,7 @@ public void setUp() throws Exception { TestUtils.recordRequestHeadersInterceptor(headersCapture), PeerUids.newPeerIdentifyingServerInterceptor()); - AndroidComponentAddress serverAddress = HostServices.allocateService(appContext); + serverAddress = HostServices.allocateService(appContext); HostServices.configureService( serverAddress, HostServices.serviceParamsBuilder() @@ -149,13 +150,15 @@ public void setUp() throws Exception { .build()) .build()); - channel = - BinderChannelBuilder.forAddress(serverAddress, appContext) + channel = newBinderChannelBuilder().build(); + } + + BinderChannelBuilder newBinderChannelBuilder() { + return BinderChannelBuilder.forAddress(serverAddress, appContext) .inboundParcelablePolicy( - InboundParcelablePolicy.newBuilder() - .setAcceptParcelableMetadataValues(true) - .build()) - .build(); + InboundParcelablePolicy.newBuilder() + .setAcceptParcelableMetadataValues(true) + .build()); } @After @@ -185,6 +188,18 @@ public void testBasicCall() throws Exception { assertThat(doCall("Hello").get()).isEqualTo("Hello"); } + @Test + public void testBasicCallWithLegacyAuthStrategy() throws Exception { + channel = newBinderChannelBuilder().useLegacyAuthStrategy().build(); + assertThat(doCall("Hello").get()).isEqualTo("Hello"); + } + + @Test + public void testBasicCallWithV2AuthStrategy() throws Exception { + channel = newBinderChannelBuilder().useV2AuthStrategy().build(); + assertThat(doCall("Hello").get()).isEqualTo("Hello"); + } + @Test public void testPeerUidIsRecorded() throws Exception { assertThat(doCall("Hello").get()).isEqualTo("Hello"); diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 18928339fbd..1a68725d823 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -308,6 +308,62 @@ public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) { return this; } + /** + * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}. + * + *

This method selects the original "legacy" authorization strategy, which is no longer + * preferred for two reasons. First, the legacy strategy considers the UID of the server *process* + * we connect to. This is problematic for services using the `android:isolatedProcess` attribute, + * which runs them under a different UID and without any of the privileges of the hosting app. + * Second, the legacy authorization strategy performs SecurityPolicy checks later in the + * handshake, which means the calling UID must be rechecked on every subsequent transaction. For + * these reasons, prefer {@link #useV2AuthStrategy()} instead. + * + *

The server does not know which authorization strategy a client is using. Both strategies + * work with all versions of the grpc-binder server. + * + *

The default authorization strategy is unspecified. Clients that require the legacy strategy + * should configure it explicitly using this method. Eventually support for the legacy strategy + * will be removed. + * + * @return this + */ + public BinderChannelBuilder useLegacyAuthStrategy() { + transportFactoryBuilder.setUseLegacyAuthStrategy(true); + return this; + } + + /** + * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}. + * + *

This method selects the v2 authorization strategy. It improves on {@link + * #useLegacyAuthStrategy()}, by considering the UID of the server *app* we connect to, rather + * than the server *process*. This allows clients to connect to services using the + * `android:isolatedProcess` attribute, which runs them under a different ephemeral UID and + * without any of the privileges of the hosting app. + * + *

Furthermore, the v2 authorization strategy performs SecurityPolicy checks earlier the + * handshake, which allows subsequent transactions over the connection to proceed securely without + * further UID checks. For these reasons, clients should prefer the v2 strategy. + * + *

The server does not know which authorization strategy a client is using. Both strategies + * work with all versions of the grpc-binder server. + * + *

The default authorization strategy is unspecified. Clients that require the v2 strategy + * should configure it explicitly using this method. Eventually support for the legacy strategy + * will be removed. + * + *

If moving to the new authorization strategy causes a robolectric test to fail, ensure your + * fake Service component is registered with `ShadowPackageManager` using `addOrUpdateService()`. + * + * @return this + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12397") + public BinderChannelBuilder useV2AuthStrategy() { + transportFactoryBuilder.setUseLegacyAuthStrategy(false); + return this; + } + @Override public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) { checkState( diff --git a/binder/src/main/java/io/grpc/binder/internal/Bindable.java b/binder/src/main/java/io/grpc/binder/internal/Bindable.java index ae0c7284faf..59a2502de2b 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Bindable.java +++ b/binder/src/main/java/io/grpc/binder/internal/Bindable.java @@ -54,8 +54,11 @@ interface Observer { * before giving them a chance to run. However, note that the identity/existence of the resolved * Service can change between the time this method returns and the time you actually bind/connect * to it. For example, suppose the target package gets uninstalled or upgraded right after this - * method returns. In {@link Observer#onBound}, you should verify that the server you resolved is - * the same one you connected to. + * method returns. + * + *

Compare with {@link #getConnectedServiceInfo()}, which can only be called after {@link + * Observer#onBound(IBinder)} but can be used to learn about the service you actually connected + * to. */ @AnyThread ServiceInfo resolve() throws StatusException; @@ -68,6 +71,21 @@ interface Observer { @AnyThread void bind(); + /** + * Asks PackageManager for details about the remote Service we *actually* connected to. + * + *

Can only be called after {@link Observer#onBound}. + * + *

Compare with {@link #resolve()}, which reports which service would be selected as of now but + * *without* connecting. + * + * @throws StatusException UNIMPLEMENTED if the connected service isn't found (an {@link + * Observer#onUnbound} callback has likely already happened or is on its way!) + * @throws IllegalStateException if {@link Observer#onBound} has not "happened-before" this call + */ + @AnyThread + ServiceInfo getConnectedServiceInfo() throws StatusException; + /** * Unbind from the remote service if connected. * diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 144ad56eec3..6ed00104c8d 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -25,6 +25,8 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Process; +import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; import com.google.common.base.Ticker; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -72,6 +74,9 @@ public final class BinderClientTransport extends BinderTransport private final SecurityPolicy securityPolicy; private final Bindable serviceBinding; + @GuardedBy("this") + private final ClientHandshake handshake; + /** Number of ongoing calls which keep this transport "in-use". */ private final AtomicInteger numInUseStreams; @@ -122,9 +127,10 @@ public BinderClientTransport( Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); this.preAuthorizeServer = preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; + this.handshake = + factory.useLegacyAuthStrategy ? new LegacyClientHandshake() : new NewClientHandshake(); numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); - serviceBinding = new ServiceBinding( factory.mainThreadExecutor, @@ -146,7 +152,9 @@ void releaseExecutors() { @Override public synchronized void onBound(IBinder binder) { - sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + binderProxy = binderDecorator.decorate(binderProxy); + handshake.onBound(binderProxy); } @Override @@ -329,7 +337,6 @@ void notifyTerminated() { @Override @GuardedBy("this") protected void handleSetupTransport(Parcel parcel) { - int remoteUid = Binder.getCallingUid(); if (inState(TransportState.SETUP)) { int version = parcel.readInt(); IBinder binder = parcel.readStrongBinder(); @@ -339,56 +346,154 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - restrictIncomingBinderToCallsFrom(remoteUid); - attributes = setSecurityAttrs(attributes, remoteUid); - authResultFuture = checkServerAuthorizationAsync(remoteUid); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + handshake.handleSetupTransport(binderProxy); + } + } + } + + private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { + return (securityPolicy instanceof AsyncSecurityPolicy) + ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) + : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + } + + private final class LegacyClientHandshake implements ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. + public void onBound(OneWayBinderProxy binder) { + sendSetupTransaction(binder); + } + + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. + public void handleSetupTransport(OneWayBinderProxy binder) { + int remoteUid = Binder.getCallingUid(); + restrictIncomingBinderToCallsFrom(remoteUid); + attributes = setSecurityAttrs(attributes, remoteUid); + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + synchronized (BinderClientTransport.this) { + handleAuthResult(binder, result); + } + } + + @Override + public void onFailure(Throwable t) { + BinderClientTransport.this.handleAuthResult(t); + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(OneWayBinderProxy binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(binder)) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + onHandshakeComplete(); + } + } + } + } + } + + private class NewClientHandshake implements ClientHandshake { + @Override + @GuardedBy("BinderClientTransport.this") + public void onBound(OneWayBinderProxy endpointBinder) { + Futures.addCallback( + Futures.submit(serviceBinding::getConnectedServiceInfo, offloadExecutor), + new FutureCallback() { + @Override + public void onSuccess(ServiceInfo result) { + synchronized (BinderClientTransport.this) { + onConnectedServiceInfo(endpointBinder, result); + } + } + + @Override + public void onFailure(Throwable t) { + synchronized (BinderClientTransport.this) { + shutdownInternal(Status.fromThrowable(t), true); + } + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void onConnectedServiceInfo(OneWayBinderProxy endpointBinder, ServiceInfo serviceInfo) { + if (inState(TransportState.SETUP)) { + attributes = setSecurityAttrs(attributes, serviceInfo.applicationInfo.uid); + authResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); + Futures.addCallback( authResultFuture, new FutureCallback() { @Override public void onSuccess(Status result) { - handleAuthResult(binder, result); + synchronized (BinderClientTransport.this) { + handleAuthResult(endpointBinder, result); + } } @Override public void onFailure(Throwable t) { - handleAuthResult(t); + BinderClientTransport.this.handleAuthResult(t); } }, offloadExecutor); } } - } - private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { - return (securityPolicy instanceof AsyncSecurityPolicy) - ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) - : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); - } + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(OneWayBinderProxy endpointBinder, Status authResult) { + if (inState(TransportState.SETUP)) { + if (!authResult.isOk()) { + shutdownInternal(authResult, true); + } else { + sendSetupTransaction(endpointBinder); + } + } + } - private synchronized void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + @Override + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(OneWayBinderProxy serverBinder) { + if (!setOutgoingBinder(serverBinder)) { shutdownInternal( Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } - } + onHandshakeComplete(); } } } + @GuardedBy("this") + private void onHandshakeComplete() { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } + private synchronized void handleAuthResult(Throwable t) { shutdownInternal( Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); @@ -400,6 +505,27 @@ protected void handlePingResponse(Parcel parcel) { pingTracker.onPingResponse(parcel.readInt()); } + /** + * A base for all implementations of the client handshake. + * + *

Supports a clean migration away from the legacy approach, one client at a time. + */ + private interface ClientHandshake { + /** + * Notifies the implementation that the binding has succeeded and we are now connected to the + * server's "endpoint" which can be reached at 'endpointBinder'. + */ + @MainThread + void onBound(OneWayBinderProxy endpointBinder); + + /** + * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a + * server that can be reached at 'serverBinder'. + */ + @BinderThread + void handleSetupTransport(OneWayBinderProxy serverBinder); + } + private static ClientStream newFailingClientStream( Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { StatsTraceContext statsTraceContext = diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 3f51452c90c..a22453fd601 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -56,6 +56,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor final OneWayBinderProxy.Decorator binderDecorator; final long readyTimeoutMillis; final boolean preAuthorizeServers; // TODO(jdcormie): Default to true. + final boolean useLegacyAuthStrategy; ScheduledExecutorService executorService; Executor offloadExecutor; @@ -77,6 +78,7 @@ private BinderClientTransportFactory(Builder builder) { binderDecorator = checkNotNull(builder.binderDecorator); readyTimeoutMillis = builder.readyTimeoutMillis; preAuthorizeServers = builder.preAuthorizeServers; + useLegacyAuthStrategy = builder.useLegacyAuthStrategy; executorService = scheduledExecutorPool.getObject(); offloadExecutor = offloadExecutorPool.getObject(); @@ -131,6 +133,7 @@ public static final class Builder implements ClientTransportFactoryBuilder { OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR; long readyTimeoutMillis = 60_000; boolean preAuthorizeServers; + boolean useLegacyAuthStrategy = true; // TODO(jdcormie): Default to false. @Override public BinderClientTransportFactory buildClientTransportFactory() { @@ -229,5 +232,11 @@ public Builder setPreAuthorizeServers(boolean preAuthorizeServers) { this.preAuthorizeServers = preAuthorizeServers; return this; } + + /** Specifies which version of the client handshake to use. */ + public Builder setUseLegacyAuthStrategy(boolean useLegacyAuthStrategy) { + this.useLegacyAuthStrategy = useLegacyAuthStrategy; + return this; + } } } diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 3351736108e..4b6bf7d06fb 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -102,6 +102,9 @@ public String methodName() { private State reportedState; // Only used on the main thread. + @GuardedBy("this") + private ComponentName connectedServiceName; + @AnyThread ServiceBinding( Executor mainThreadExecutor, @@ -305,6 +308,26 @@ private void clearReferences() { sourceContext = null; } + @AnyThread + @Override + public ServiceInfo getConnectedServiceInfo() throws StatusException { + try { + return getContextForTargetUser("cross-user v2 handshake") + .getPackageManager() + .getServiceInfo(getConnectedServiceName(), /* flags= */ 0); + } catch (PackageManager.NameNotFoundException e) { + throw Status.UNIMPLEMENTED + .withCause(e) + .withDescription("connected remote service was uninstalled/disabled during handshake") + .asException(); + } + } + + private synchronized ComponentName getConnectedServiceName() { + checkState(connectedServiceName != null, "onBound() not yet called!"); + return connectedServiceName; + } + @Override @MainThread public void onServiceConnected(ComponentName className, IBinder binder) { @@ -312,6 +335,7 @@ public void onServiceConnected(ComponentName className, IBinder binder) { synchronized (this) { if (state == State.BINDING) { state = State.BOUND; + connectedServiceName = className; bound = true; } } diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index d3d73f0e9eb..dce1d74d0de 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -25,6 +25,7 @@ import static io.grpc.binder.internal.BinderTransport.SHUTDOWN_TRANSPORT; import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -119,11 +120,19 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest private int nextServerAddress; - @Parameter public boolean preAuthServersParam; + @Parameter(value = 0) + public boolean preAuthServersParam; - @Parameters(name = "preAuthServersParam={0}") - public static ImmutableList data() { - return ImmutableList.of(true, false); + @Parameter(value = 1) + public boolean useLegacyAuthStrategy; + + @Parameters(name = "preAuthServersParam={0};useLegacyAuthStrategy={1}") + public static ImmutableList data() { + return ImmutableList.of( + new Object[] {false, false}, + new Object[] {false, true}, + new Object[] {true, false}, + new Object[] {true, true}); } @Override @@ -189,6 +198,7 @@ protected InternalServer newServer( BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() { return new BinderClientTransportFactory.Builder() .setPreAuthorizeServers(preAuthServersParam) + .setUseLegacyAuthStrategy(useLegacyAuthStrategy) .setSourceContext(application) .setScheduledExecutorPool(executorServicePool) .setOffloadExecutorPool(offloadExecutorPool); @@ -249,7 +259,11 @@ public void clientAuthorizesServerUidsInOrder() throws Exception { } AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); - assertThat(authRequest.uid).isEqualTo(11111); + if (useLegacyAuthStrategy) { + assertThat(authRequest.uid).isEqualTo(11111); + } else { + assertThat(authRequest.uid).isEqualTo(22222); + } verify(mockClientTransportListener, never()).transportReady(); authRequest.setResult(Status.OK); @@ -320,6 +334,10 @@ public void clientIgnoresDuplicateSetupTransaction() throws Exception { @Test public void clientIgnoresTransactionFromNonServerUids() throws Exception { server.start(serverListener); + + // This test is not applicable to the new auth strategy which keeps the client Binder a secret. + assumeTrue(useLegacyAuthStrategy); + client = newClientTransport(server); startTransport(client, mockClientTransportListener); diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index 0875881dcc5..b9a745e1521 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -116,6 +116,32 @@ public void testBind() throws Exception { assertThat(binding.isSourceContextCleared()).isFalse(); } + @Test + public void testGetConnectedServiceInfo() throws Exception { + binding = newBuilder().setTargetComponent(serviceComponent).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.gotBoundEvent).isTrue(); + + ServiceInfo serviceInfo = binding.getConnectedServiceInfo(); + assertThat(serviceInfo.name).isEqualTo(serviceComponent.getClassName()); + assertThat(serviceInfo.packageName).isEqualTo(serviceComponent.getPackageName()); + } + + @Test + public void testGetConnectedServiceInfoThrows() throws Exception { + binding = newBuilder().setTargetComponent(serviceComponent).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.gotBoundEvent).isTrue(); + shadowOf(appContext.getPackageManager()).removeService(serviceComponent); + + StatusException se = assertThrows(StatusException.class, binding::getConnectedServiceInfo); + assertThat(se.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED); + } + @Test public void testBindingIntent() throws Exception { shadowApplication.setComponentNameAndServiceForBindService(null, null); @@ -391,6 +417,7 @@ public void testBindWithDeviceAdmin() throws Exception { newBuilder() .setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0)) .setTargetUserHandle(generateUserHandle(/* userId= */ 0)) + .setTargetComponent(serviceComponent) .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) .build(); shadowOf(getMainLooper()).idle(); @@ -403,6 +430,10 @@ public void testBindWithDeviceAdmin() throws Exception { assertThat(observer.binder).isSameInstanceAs(mockBinder); assertThat(observer.gotUnboundEvent).isFalse(); assertThat(binding.isSourceContextCleared()).isFalse(); + + ServiceInfo serviceInfo = binding.getConnectedServiceInfo(); + assertThat(serviceInfo.name).isEqualTo(serviceComponent.getClassName()); + assertThat(serviceInfo.packageName).isEqualTo(serviceComponent.getPackageName()); } private void assertNoLockHeld() {