diff --git a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java index dd9cf26bd3d..41ea76146de 100644 --- a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java @@ -36,11 +36,13 @@ import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.NameResolverRegistry; import io.grpc.Server; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptors; import io.grpc.ServerServiceDefinition; import io.grpc.internal.GrpcUtil; +import io.grpc.internal.testing.FakeNameResolverProvider; import io.grpc.stub.ClientCalls; import io.grpc.stub.ServerCalls; import io.grpc.stub.StreamObserver; @@ -66,6 +68,7 @@ public final class BinderChannelSmokeTest { private static final int SLIGHTLY_MORE_THAN_ONE_BLOCK = 16 * 1024 + 100; private static final String MSG = "Some text which will be repeated many many times"; + private static final String SERVER_TARGET_URI = "fake://server"; final MethodDescriptor method = MethodDescriptor.newBuilder(StringMarshaller.INSTANCE, StringMarshaller.INSTANCE) @@ -85,7 +88,7 @@ public final class BinderChannelSmokeTest { .setType(MethodDescriptor.MethodType.BIDI_STREAMING) .build(); - AndroidComponentAddress serverAddress; + FakeNameResolverProvider fakeNameResolverProvider; ManagedChannel channel; AtomicReference headersCapture = new AtomicReference<>(); @@ -118,6 +121,8 @@ public void setUp() throws Exception { TestUtils.recordRequestHeadersInterceptor(headersCapture)); AndroidComponentAddress serverAddress = HostServices.allocateService(appContext); + fakeNameResolverProvider = new FakeNameResolverProvider(SERVER_TARGET_URI, serverAddress); + NameResolverRegistry.getDefaultRegistry().register(fakeNameResolverProvider); HostServices.configureService(serverAddress, HostServices.serviceParamsBuilder() .setServerFactory((service, receiver) -> @@ -132,6 +137,7 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { channel.shutdownNow(); + NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverProvider); HostServices.awaitServiceShutdown(); } @@ -192,6 +198,12 @@ public void testStreamingCallOptionHeaders() throws Exception { assertThat(headersCapture.get().get(GrpcUtil.TIMEOUT_KEY)).isGreaterThan(0); } + @Test + public void testConnectViaTargetUri() throws Exception { + channel = BinderChannelBuilder.forTarget(SERVER_TARGET_URI, appContext).build(); + assertThat(doCall("Hello").get()).isEqualTo("Hello"); + } + private static String createLargeString(int size) { StringBuilder sb = new StringBuilder(); while (sb.length() < size) { diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 99191cfad3c..91e4e8f1c76 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -67,13 +67,35 @@ public final class BinderChannelBuilder *

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 targetAddress the {@link AndroidComponentAddress} referencing the service to bind to. + * @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). * @return a new builder */ public static BinderChannelBuilder forAddress( - AndroidComponentAddress targetAddress, Context sourceContext) { - return new BinderChannelBuilder(targetAddress, sourceContext); + AndroidComponentAddress directAddress, Context sourceContext) { + return new BinderChannelBuilder( + checkNotNull(directAddress, "directAddress"), null, sourceContext); + } + + /** + * Creates a channel builder that will bind to a remote Android service, via a string + * target name which will be resolved. + * + *

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()}. + * + *

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). + * @return a new builder + */ + public static BinderChannelBuilder forTarget(String target, Context sourceContext) { + return new BinderChannelBuilder(null, checkNotNull(target, "target"), sourceContext); } /** @@ -88,7 +110,7 @@ public static BinderChannelBuilder forAddress(String name, int port) { /** * Always fails. Call {@link #forAddress(AndroidComponentAddress, Context)} instead. */ - @DoNotCall("Unsupported. Use forAddress(AndroidComponentAddress, Context) instead") + @DoNotCall("Unsupported. Use forTarget(String, Context) instead") public static BinderChannelBuilder forTarget(String target) { throw new UnsupportedOperationException( "call forAddress(AndroidComponentAddress, Context) instead"); @@ -104,9 +126,11 @@ public static BinderChannelBuilder forTarget(String target) { private BindServiceFlags bindServiceFlags; private BinderChannelBuilder( - AndroidComponentAddress targetAddress, + @Nullable AndroidComponentAddress directAddress, + @Nullable String target, Context sourceContext) { - mainThreadExecutor = ContextCompat.getMainExecutor(sourceContext); + mainThreadExecutor = + ContextCompat.getMainExecutor(checkNotNull(sourceContext, "sourceContext")); securityPolicy = SecurityPolicies.internalOnly(); inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT; bindServiceFlags = BindServiceFlags.DEFAULTS; @@ -126,12 +150,20 @@ public ClientTransportFactory buildClientTransportFactory() { } } - managedChannelImplBuilder = - new ManagedChannelImplBuilder( - targetAddress, - targetAddress.getAuthority(), - new BinderChannelTransportFactoryBuilder(), - null); + if (directAddress != null) { + managedChannelImplBuilder = + new ManagedChannelImplBuilder( + directAddress, + directAddress.getAuthority(), + new BinderChannelTransportFactoryBuilder(), + null); + } else { + managedChannelImplBuilder = + new ManagedChannelImplBuilder( + target, + new BinderChannelTransportFactoryBuilder(), + null); + } } @Override diff --git a/testing/src/main/java/io/grpc/internal/testing/FakeNameResolverProvider.java b/testing/src/main/java/io/grpc/internal/testing/FakeNameResolverProvider.java new file mode 100644 index 00000000000..d056707b719 --- /dev/null +++ b/testing/src/main/java/io/grpc/internal/testing/FakeNameResolverProvider.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021 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.internal.testing; + +import com.google.common.collect.ImmutableList; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; +import io.grpc.Status; +import java.net.SocketAddress; +import java.net.URI; + +/** A name resolver to always resolve the given URI into the given address. */ +public final class FakeNameResolverProvider extends NameResolverProvider { + + private final URI targetUri; + private final SocketAddress address; + + public FakeNameResolverProvider(String targetUri, SocketAddress address) { + this.targetUri = URI.create(targetUri); + this.address = address; + } + + @Override + public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) { + if (targetUri.equals(this.targetUri)) { + return new FakeNameResolver(address); + } + return null; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 5; // Default + } + + @Override + public String getDefaultScheme() { + return targetUri.getScheme(); + } + + /** A single name resolver. */ + private static final class FakeNameResolver extends NameResolver { + private static final String AUTHORITY = "fake-authority"; + + private final SocketAddress address; + private volatile boolean shutdown; + + private FakeNameResolver(SocketAddress address) { + this.address = address; + } + + @Override + public void start(Listener2 listener) { + if (shutdown) { + listener.onError(Status.FAILED_PRECONDITION.withDescription("Resolver is shutdown")); + } else { + listener.onResult( + ResolutionResult.newBuilder() + .setAddresses(ImmutableList.of(new EquivalentAddressGroup(address))) + .build()); + } + } + + @Override + public String getServiceAuthority() { + return AUTHORITY; + } + + @Override + public void shutdown() { + shutdown = true; + } + } +}