Compare commits

..

15 Commits

Author SHA1 Message Date
Kannan J c7202c0db5
Bump readme (#12305) 2025-08-24 18:21:52 +05:30
Eric Anderson 028afbe352
xds: Implement equals in WRRLBConfig
Just an is a8de9f0, lack of equals causes cluster_resolver to consider every update a different configuration and restart itself.

Handling NaN should really be prevented with validation, but it looks like that
would lead to yak shaving at the moment.

b/435208946
2025-08-22 08:07:51 -07:00
John Cormie afdbecb235
binder: Move BinderTransport's inner classes to the top level (#12303)
BinderTransport.java was getting too long and deeply nested.

This is a pure refactor with no behavior changes.
2025-08-21 16:08:58 -07:00
MV Shiva 2039266ebc
xds: xdsClient caches transient error for new watchers (#12262) 2025-08-19 21:41:52 +05:30
Jiri Kaplan 43bef65cf9 netty: Support BCJSSE provider in GrpcSslContexts 2025-08-19 07:17:57 -07:00
Eric Anderson 437e03dc98
xds: Avoid PriorityLb re-enabling timer on duplicate CONNECTING (#12289)
Since c4256add4 we no longer fabricate a TRANSIENT_FAILURE update from
children. However, previously that would have set
seenReadyOrIdleSinceTransientFailure = false and prevented future timer
creation. If a LB policy gives extraneous updates with state CONNECTING,
then it was possible to re-create failOverTimer which would then wait
the 10 seconds for the child to finish CONNECTING. We only want to give
the child one opportunity after transitioning out of READY/IDLE.

https://github.com/grpc/proposal/pull/509
2025-08-19 12:53:47 +05:30
Eric Anderson 6462ef9a11 netty: Count sent RST_STREAMs against limit
Http2RstCounterEncoder has to be constructed before
NettyServerHandler/Http2ConnectionHandler so it must be static. Thus the
code/counters were moved into RstStreamCounter which then can be
constructed earlier and shared.

This depends on Netty 4.1.124 for a bug fix to actually call the
encoder:
be53dc3c9a
2025-08-18 07:23:34 -07:00
Eric Anderson 95d16d85c8 Upgrade to Netty 4.1.124.Final
This implicitly disables NettyAdaptiveCumulator (#11284), which can have a
performance impact. We delayed upgrading Netty to give time to rework
the optimization, but we've gone too long already without upgrading
which causes problems for vulnerability tracking.
2025-08-13 14:23:14 -07:00
Sangamesh f50726d32e
android: Clean up android lint and other warnings (#12143)
Worked on clearing the lint warnings (OldTargetApi, ObsoleteSdkInt,
InlinedApi, NewApi)

Fixes #12142
2025-08-11 15:18:01 -07:00
Eric Anderson 06707f7c38 xds: Use a different log name for XdsClientImpl and ControlPlaneClient
Seems like a good time to stop hating ourselves, as that seems to be the
only reason to use the same string.
2025-08-08 14:23:43 -07:00
John Cormie efcdebb904
Introduce a NameResolver for Android's `intent:` URIs (#12248)
Let grpc-binder clients find on-device services by [implicit Intent](https://developer.android.com/guide/components/intents-filters#Types) target URI, lifting the need to hard code a server's package name.
2025-08-07 08:38:44 -07:00
Eric Anderson f30964ab82
Bump versions of dependencies (#12252)
Notably, protobuf to 3.25.8, opentelemetry to 1.52.0. Protobuf in Bazel
has 25.5 in the BCR and it seems better to align the WORKSPACE
with that version. But we can't actually use 25.5 in BCR because it is
incompatible with Bazel 7.
2025-08-06 11:01:45 -07:00
MV Shiva 7040417eee
stub: use the closedTrailers in StatusException (#12259) 2025-08-06 12:24:33 +05:30
camel a40c8cf5a4
binder: Let apps call SecurityPolicy.checkAuthorization() by PeerUid (#12257)
This allows a server with access to PeerUid to check additional application-layer security policy *after* the call itself is authorized by the transport layer. Cross cutting application-layer checks could be done from a ServerInterceptor (RPC method level policy, say). Checks based on the substance of a request message could be done by the individual RPC method implementations themselves.
2025-08-05 16:47:45 -07:00
Kannan J 8b46ad58c3
Start 1.76.0 development cycle (#12258) 2025-08-05 22:00:39 +05:30
69 changed files with 1973 additions and 632 deletions

View File

@ -2,7 +2,7 @@ module(
name = "grpc-java",
compatibility_level = 0,
repo_name = "io_grpc_grpc_java",
version = "1.75.0", # CURRENT_GRPC_VERSION
version = "1.76.0-SNAPSHOT", # CURRENT_GRPC_VERSION
)
# GRPC_DEPS_START

View File

@ -14,7 +14,7 @@ android {
}
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
minSdkVersion 22
targetSdkVersion 33
versionCode 1
versionName "1.0"

View File

@ -217,7 +217,6 @@ public final class AndroidChannelBuilder extends ForwardingChannelBuilder<Androi
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
unregisterRunnable =
new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
@ -231,7 +230,6 @@ public final class AndroidChannelBuilder extends ForwardingChannelBuilder<Androi
context.registerReceiver(networkReceiver, networkIntentFilter);
unregisterRunnable =
new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
context.unregisterReceiver(networkReceiver);

View File

@ -166,6 +166,11 @@ public final class NameResolverRegistry {
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find DNS NameResolver", e);
}
try {
list.add(Class.forName("io.grpc.binder.internal.IntentNameResolverProvider"));
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find IntentNameResolverProvider", e);
}
return Collections.unmodifiableList(list);
}

View File

@ -13,7 +13,7 @@ android {
targetCompatibility 1.8
}
defaultConfig {
minSdkVersion 21
minSdkVersion 22
targetSdkVersion 33
versionCode 1
versionName "1.0"

View File

@ -11,11 +11,13 @@
<service android:name="io.grpc.binder.HostServices$HostService1" android:exported="false">
<intent-filter>
<action android:name="action1"/>
<data android:scheme="scheme" android:host="authority" android:path="/path"/>
</intent-filter>
</service>
<service android:name="io.grpc.binder.HostServices$HostService2" android:exported="false">
<intent-filter>
<action android:name="action2"/>
<data android:scheme="scheme" android:host="authority" android:path="/path"/>
</intent-filter>
</service>
</application>

View File

@ -23,6 +23,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.test.core.app.ApplicationProvider;
@ -39,7 +40,6 @@ import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.NameResolverRegistry;
import io.grpc.ServerCall;
import io.grpc.ServerCall.Listener;
import io.grpc.ServerCallHandler;
@ -49,7 +49,6 @@ import io.grpc.ServerServiceDefinition;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.testing.FakeNameResolverProvider;
import io.grpc.stub.ClientCalls;
import io.grpc.stub.MetadataUtils;
import io.grpc.stub.ServerCalls;
@ -77,7 +76,6 @@ 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";
private static final Metadata.Key<PoisonParcelable> POISON_KEY =
ParcelableUtils.metadataKey("poison-bin", PoisonParcelable.CREATOR);
@ -99,7 +97,6 @@ public final class BinderChannelSmokeTest {
.setType(MethodDescriptor.MethodType.BIDI_STREAMING)
.build();
FakeNameResolverProvider fakeNameResolverProvider;
ManagedChannel channel;
AtomicReference<Metadata> headersCapture = new AtomicReference<>();
AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
@ -138,8 +135,6 @@ public final class BinderChannelSmokeTest {
PeerUids.newPeerIdentifyingServerInterceptor());
AndroidComponentAddress serverAddress = HostServices.allocateService(appContext);
fakeNameResolverProvider = new FakeNameResolverProvider(SERVER_TARGET_URI, serverAddress);
NameResolverRegistry.getDefaultRegistry().register(fakeNameResolverProvider);
HostServices.configureService(
serverAddress,
HostServices.serviceParamsBuilder()
@ -166,7 +161,6 @@ public final class BinderChannelSmokeTest {
@After
public void tearDown() throws Exception {
channel.shutdownNow();
NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverProvider);
HostServices.awaitServiceShutdown();
}
@ -235,7 +229,11 @@ public final class BinderChannelSmokeTest {
@Test
public void testConnectViaTargetUri() throws Exception {
channel = BinderChannelBuilder.forTarget(SERVER_TARGET_URI, appContext).build();
// Compare with the <intent-filter> mapping in AndroidManifest.xml.
channel =
BinderChannelBuilder.forTarget(
"intent://authority/path#Intent;action=action1;scheme=scheme;end;", appContext)
.build();
assertThat(doCall("Hello").get()).isEqualTo("Hello");
}
@ -245,7 +243,10 @@ public final class BinderChannelSmokeTest {
channel =
BinderChannelBuilder.forAddress(
AndroidComponentAddress.forBindIntent(
new Intent().setAction("action1").setPackage(appContext.getPackageName())),
new Intent()
.setAction("action1")
.setData(Uri.parse("scheme://authority/path"))
.setPackage(appContext.getPackageName())),
appContext)
.build();
assertThat(doCall("Hello").get()).isEqualTo("Hello");

View File

@ -100,7 +100,7 @@ public final class BinderClientTransportTest {
.build();
AndroidComponentAddress serverAddress;
BinderTransport.BinderClientTransport transport;
BinderClientTransport transport;
BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy();
private final ObjectPool<ScheduledExecutorService> executorServicePool =
@ -178,7 +178,7 @@ public final class BinderClientTransportTest {
return this;
}
public BinderTransport.BinderClientTransport build() {
public BinderClientTransport build() {
return factoryBuilder
.buildClientTransportFactory()
.newClientTransport(serverAddress, new ClientTransportOptions(), null);
@ -502,8 +502,7 @@ public final class BinderClientTransportTest {
}
private static void startAndAwaitReady(
BinderTransport.BinderClientTransport transport, TestTransportListener transportListener)
throws Exception {
BinderClientTransport transport, TestTransportListener transportListener) throws Exception {
transport.start(transportListener).run();
transportListener.awaitReady();
}

View File

@ -106,8 +106,7 @@ public final class BinderTransportTest extends AbstractTransportTest {
options.setEagAttributes(eagAttrs());
options.setChannelLogger(transportLogger());
return new BinderTransport.BinderClientTransport(
builder.buildClientTransportFactory(), addr, options);
return new BinderClientTransport(builder.buildClientTransportFactory(), addr, options);
}
@Test

View File

@ -34,6 +34,18 @@ public final class ApiConstants {
*/
public static final String ACTION_BIND = "grpc.io.action.BIND";
/**
* Gives a {@link NameResolver} access to its Channel's "source" {@link android.content.Context},
* the entry point to almost every other Android API.
*
* <p>This argument is set automatically by {@link BinderChannelBuilder}. Any value passed to
* {@link io.grpc.ManagedChannelBuilder#setNameResolverArg} will be ignored.
*
* <p>See {@link BinderChannelBuilder#forTarget(String, android.content.Context)} for more.
*/
public static final NameResolver.Args.Key<android.content.Context> SOURCE_ANDROID_CONTEXT =
NameResolver.Args.Key.create("source-android-context");
/**
* Specifies the Android user in which target URIs should be resolved.
*

View File

@ -67,4 +67,25 @@ public abstract class AsyncSecurityPolicy extends SecurityPolicy {
* authorized.
*/
public abstract ListenableFuture<Status> checkAuthorizationAsync(int uid);
/**
* Decides whether the given Android UID is authorized, without providing its raw integer value.
*
* <p>Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except
* the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the
* transport). This allows a server to check additional application-layer security policy for
* itself *after* the call itself is authorized by the transport layer. Cross cutting application-
* layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the
* substance of a request message could be done by the individual RPC method implementations
* themselves.
*
* <p>See #checkAuthorizationAsync(int) for details on the semantics. See {@link
* PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}.
*
* @param uid The Android UID to authenticate.
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public final ListenableFuture<Status> checkAuthorizationAsync(PeerUid uid) {
return checkAuthorizationAsync(uid.getUid());
}
}

View File

@ -321,6 +321,8 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
public ManagedChannel build() {
transportFactoryBuilder.setOffloadExecutorPool(
managedChannelImplBuilder.getOffloadExecutorPool());
setNameResolverArg(
ApiConstants.SOURCE_ANDROID_CONTEXT, transportFactoryBuilder.getSourceContext());
return super.build();
}
}

View File

@ -184,7 +184,6 @@ public final class SecurityPolicies {
* Creates {@link SecurityPolicy} which checks if the app is a device owner app. See {@link
* DevicePolicyManager}.
*/
@RequiresApi(18)
public static io.grpc.binder.SecurityPolicy isDeviceOwner(Context applicationContext) {
DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
@ -199,7 +198,6 @@ public final class SecurityPolicies {
* Creates {@link SecurityPolicy} which checks if the app is a profile owner app. See {@link
* DevicePolicyManager}.
*/
@RequiresApi(21)
public static SecurityPolicy isProfileOwner(Context applicationContext) {
DevicePolicyManager devicePolicyManager =
(DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE);

View File

@ -53,4 +53,25 @@ public abstract class SecurityPolicy {
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public abstract Status checkAuthorization(int uid);
/**
* Decides whether the given Android UID is authorized, without providing its raw integer value.
*
* <p>Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except
* the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the
* transport). This allows a server to check additional application-layer security policy for
* itself *after* the call itself is authorized by the transport layer. Cross cutting application-
* layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the
* substance of a request message could be done by the individual RPC method implementations
* themselves.
*
* <p>See #checkAuthorizationAsync(int) for details on the semantics. See {@link
* PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}.
*
* @param uid The Android UID to authenticate.
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public final Status checkAuthorization(PeerUid uid) {
return checkAuthorization(uid.getUid());
}
}

View File

@ -11,8 +11,8 @@ import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
/**
* Tracks which {@link BinderTransport.BinderServerTransport} are currently active and allows
* invoking a {@link Runnable} only once all transports are terminated.
* Tracks which {@link BinderServerTransport} are currently active and allows invoking a {@link
* Runnable} only once all transports are terminated.
*/
final class ActiveTransportTracker implements ServerListener {
private final ServerListener delegate;

View File

@ -0,0 +1,441 @@
/*
* Copyright 2020 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.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import com.google.common.base.Ticker;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ClientStreamTracer;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FailingClientStream;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.StatsTraceContext;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
/** Concrete client-side transport implementation. */
@ThreadSafe
@Internal
public final class BinderClientTransport extends BinderTransport
implements ConnectionClientTransport, Bindable.Observer {
private final ObjectPool<? extends Executor> offloadExecutorPool;
private final Executor offloadExecutor;
private final SecurityPolicy securityPolicy;
private final Bindable serviceBinding;
/** Number of ongoing calls which keep this transport "in-use". */
private final AtomicInteger numInUseStreams;
private final long readyTimeoutMillis;
private final PingTracker pingTracker;
private final boolean preAuthorizeServer;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@GuardedBy("this")
private int latestCallId = FIRST_CALL_ID;
@GuardedBy("this")
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> authResultFuture; // null before we check auth.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> preAuthResultFuture; // null before we pre-auth.
/**
* Constructs a new transport instance.
*
* @param factory parameters common to all a Channel's transports
* @param targetAddress the fully resolved and load-balanced server address
* @param options other parameters that can vary as transports come and go within a Channel
*/
public BinderClientTransport(
BinderClientTransportFactory factory,
AndroidComponentAddress targetAddress,
ClientTransportOptions options) {
super(
factory.scheduledExecutorPool,
buildClientAttributes(
options.getEagAttributes(),
factory.sourceContext,
targetAddress,
factory.inboundParcelablePolicy),
factory.binderDecorator,
buildLogId(factory.sourceContext, targetAddress));
this.offloadExecutorPool = factory.offloadExecutorPool;
this.securityPolicy = factory.securityPolicy;
this.offloadExecutor = offloadExecutorPool.getObject();
this.readyTimeoutMillis = factory.readyTimeoutMillis;
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
this.preAuthorizeServer =
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
serviceBinding =
new ServiceBinding(
factory.mainThreadExecutor,
factory.sourceContext,
factory.channelCredentials,
targetAddress.asBindIntent(),
targetAddress.getTargetUser() != null
? targetAddress.getTargetUser()
: factory.defaultTargetUserHandle,
factory.bindServiceFlags.toInteger(),
this);
}
@Override
void releaseExecutors() {
super.releaseExecutors();
offloadExecutorPool.returnObject(offloadExecutor);
}
@Override
public synchronized void onBound(IBinder binder) {
sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)));
}
@Override
public synchronized void onUnbound(Status reason) {
shutdownInternal(reason, true);
}
@CheckReturnValue
@Override
public synchronized Runnable start(Listener clientTransportListener) {
this.clientTransportListener = checkNotNull(clientTransportListener);
return () -> {
synchronized (BinderClientTransport.this) {
if (inState(TransportState.NOT_STARTED)) {
setState(TransportState.SETUP);
try {
if (preAuthorizeServer) {
preAuthorize(serviceBinding.resolve());
} else {
serviceBinding.bind();
}
} catch (StatusException e) {
shutdownInternal(e.getStatus(), true);
return;
}
if (readyTimeoutMillis >= 0) {
readyTimeoutFuture =
getScheduledExecutorService()
.schedule(
BinderClientTransport.this::onReadyTimeout,
readyTimeoutMillis,
MILLISECONDS);
}
}
}
};
}
@GuardedBy("this")
private void preAuthorize(ServiceInfo serviceInfo) {
// It's unlikely, but the identity/existence of this Service could change by the time we
// actually connect. It doesn't matter though, because:
// - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer
// will eventually retry using a new transport instance that will see the Service's new state.
// - If pre-auth succeeds (but would fail against the server's new state), we might give an
// unauthorized server a chance to run, but the connection will still fail by SecurityPolicy
// check later in handshake. Pre-auth remains effective at mitigating abuse because malware
// can't typically control the exact timing of its installation.
preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid);
Futures.addCallback(
preAuthResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handlePreAuthResult(result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
private synchronized void handlePreAuthResult(Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else {
serviceBinding.bind();
}
}
}
private synchronized void onReadyTimeout() {
if (inState(TransportState.SETUP)) {
readyTimeoutFuture = null;
shutdownInternal(
Status.DEADLINE_EXCEEDED.withDescription(
"Connect timeout " + readyTimeoutMillis + "ms lapsed"),
true);
}
}
@Override
public synchronized ClientStream newStream(
final MethodDescriptor<?, ?> method,
final Metadata headers,
final CallOptions callOptions,
ClientStreamTracer[] tracers) {
if (!inState(TransportState.READY)) {
return newFailingClientStream(
isShutdown()
? shutdownStatus
: Status.INTERNAL.withDescription("newStream() before transportReady()"),
attributes,
headers,
tracers);
}
int callId = latestCallId++;
if (latestCallId == LAST_CALL_ID) {
latestCallId = FIRST_CALL_ID;
}
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
Inbound.ClientInbound inbound =
new Inbound.ClientInbound(
this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions));
if (ongoingCalls.putIfAbsent(callId, inbound) != null) {
Status failure = Status.INTERNAL.withDescription("Clashing call IDs");
shutdownInternal(failure, true);
return newFailingClientStream(failure, attributes, headers, tracers);
} else {
if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) {
clientTransportListener.transportInUse(true);
}
Outbound.ClientOutbound outbound =
new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext);
if (method.getType().clientSendsOneMessage()) {
return new SingleMessageClientStream(inbound, outbound, attributes);
} else {
return new MultiMessageClientStream(inbound, outbound, attributes);
}
}
}
@Override
protected void unregisterInbound(Inbound<?> inbound) {
if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) {
clientTransportListener.transportInUse(false);
}
super.unregisterInbound(inbound);
}
@Override
public void ping(final PingCallback callback, Executor executor) {
pingTracker.startPing(callback, executor);
}
@Override
public synchronized void shutdown(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, true);
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
clientTransportListener.transportShutdown(status);
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (numInUseStreams.getAndSet(0) > 0) {
clientTransportListener.transportInUse(false);
}
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
if (preAuthResultFuture != null) {
preAuthResultFuture.cancel(false); // No effect if already complete.
}
if (authResultFuture != null) {
authResultFuture.cancel(false); // No effect if already complete.
}
serviceBinding.unbind();
clientTransportListener.transportTerminated();
}
@Override
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {
int remoteUid = Binder.getCallingUid();
attributes = setSecurityAttrs(attributes, remoteUid);
if (inState(TransportState.SETUP)) {
int version = parcel.readInt();
IBinder binder = parcel.readStrongBinder();
if (version != WIRE_FORMAT_VERSION) {
shutdownInternal(Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true);
} else if (binder == null) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
} else {
authResultFuture = checkServerAuthorizationAsync(remoteUid);
Futures.addCallback(
authResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handleAuthResult(binder, result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
}
}
private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
return (securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
}
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))) {
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;
}
}
}
}
}
private synchronized void handleAuthResult(Throwable t) {
shutdownInternal(
Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true);
}
@GuardedBy("this")
@Override
protected void handlePingResponse(Parcel parcel) {
pingTracker.onPingResponse(parcel.readInt());
}
private static ClientStream newFailingClientStream(
Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) {
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
statsTraceContext.clientOutboundHeaders();
return new FailingClientStream(failure, tracers);
}
private static InternalLogId buildLogId(
Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate(
BinderClientTransport.class,
sourceContext.getClass().getSimpleName() + "->" + targetAddress);
}
private static Attributes buildClientAttributes(
Attributes eagAttrs,
Context sourceContext,
AndroidComponentAddress targetAddress,
InboundParcelablePolicy inboundParcelablePolicy) {
return Attributes.newBuilder()
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now.
.set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs)
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext))
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress)
.set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy)
.build();
}
private static Attributes setSecurityAttrs(Attributes attributes, int uid) {
return attributes.toBuilder()
.set(REMOTE_UID, uid)
.set(
GrpcAttributes.ATTR_SECURITY_LEVEL,
uid == Process.myUid()
? SecurityLevel.PRIVACY_AND_INTEGRITY
: SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this.
.build();
}
}

View File

@ -83,12 +83,12 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
}
@Override
public BinderTransport.BinderClientTransport newClientTransport(
public BinderClientTransport newClientTransport(
SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) {
if (closed) {
throw new IllegalStateException("The transport factory is closed.");
}
return new BinderTransport.BinderClientTransport(this, (AndroidComponentAddress) addr, options);
return new BinderClientTransport(this, (AndroidComponentAddress) addr, options);
}
@Override
@ -142,6 +142,10 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
return this;
}
public Context getSourceContext() {
return sourceContext;
}
public Builder setOffloadExecutorPool(ObjectPool<? extends Executor> offloadExecutorPool) {
this.offloadExecutorPool = checkNotNull(offloadExecutorPool, "offloadExecutorPool");
return this;

View File

@ -178,8 +178,8 @@ public final class BinderServer implements InternalServer, LeakSafeOneWayBinder.
serverPolicyChecker,
checkNotNull(executor, "Not started?"));
// Create a new transport and let our listener know about it.
BinderTransport.BinderServerTransport transport =
new BinderTransport.BinderServerTransport(
BinderServerTransport transport =
new BinderServerTransport(
executorServicePool,
attrsBuilder.build(),
streamTracerFactories,

View File

@ -0,0 +1,126 @@
/*
* Copyright 2020 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.internal;
import android.os.IBinder;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.StatsTraceContext;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
/** Concrete server-side transport implementation. */
@Internal
public final class BinderServerTransport extends BinderTransport implements ServerTransport {
private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate 'callbackBinder', for fault injection.
*/
public BinderServerTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories,
OneWayBinderProxy.Decorator binderDecorator,
IBinder callbackBinder) {
super(executorServicePool, attributes, binderDecorator, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories;
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));
}
public synchronized void setServerTransportListener(
ServerTransportListener serverTransportListener) {
this.serverTransportListener = serverTransportListener;
if (isShutdown()) {
setState(TransportState.SHUTDOWN_TERMINATED);
notifyTerminated();
releaseExecutors();
} else {
sendSetupTransaction();
// Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback
// it triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = serverTransportListener.transportReady(attributes);
}
}
}
StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) {
return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers);
}
synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) {
if (isShutdown()) {
return Status.UNAVAILABLE.withDescription("transport is shutdown");
} else {
serverTransportListener.streamCreated(stream, methodName, headers);
return Status.OK;
}
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
// Nothing to do.
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (serverTransportListener != null) {
serverTransportListener.transportTerminated();
}
}
@Override
public synchronized void shutdown() {
shutdownInternal(Status.OK, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
shutdownInternal(reason, true);
}
@Override
@Nullable
@GuardedBy("this")
protected Inbound<?> createInbound(int callId) {
return new Inbound.ServerInbound(this, attributes, callId);
}
private static InternalLogId buildLogId(Attributes attributes) {
return InternalLogId.allocate(
BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
}
}

View File

@ -19,67 +19,32 @@ package io.grpc.binder.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import androidx.annotation.BinderThread;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.base.Verify;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ClientStreamTracer;
import io.grpc.Grpc;
import io.grpc.Internal;
import io.grpc.InternalChannelz.SocketStats;
import io.grpc.InternalLogId;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.AsyncSecurityPolicy;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.FailingClientStream;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ManagedClientTransport;
import io.grpc.internal.ObjectPool;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.StatsTraceContext;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@ -169,10 +134,10 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
private static final int RESERVED_TRANSACTIONS = 1000;
/** The first call ID we can use. */
private static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS;
static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS;
/** The last call ID we can use. */
private static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION;
static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION;
/** The states of this transport. */
protected enum TransportState {
@ -218,7 +183,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
// Only read/written on @BinderThread.
private long acknowledgedIncomingBytes;
private BinderTransport(
protected BinderTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
OneWayBinderProxy.Decorator binderDecorator,
@ -559,478 +524,6 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
}
}
/** Concrete client-side transport implementation. */
@ThreadSafe
@Internal
public static final class BinderClientTransport extends BinderTransport
implements ConnectionClientTransport, Bindable.Observer {
private final ObjectPool<? extends Executor> offloadExecutorPool;
private final Executor offloadExecutor;
private final SecurityPolicy securityPolicy;
private final Bindable serviceBinding;
/** Number of ongoing calls which keep this transport "in-use". */
private final AtomicInteger numInUseStreams;
private final long readyTimeoutMillis;
private final PingTracker pingTracker;
private final boolean preAuthorizeServer;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@GuardedBy("this")
private int latestCallId = FIRST_CALL_ID;
@GuardedBy("this")
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
@GuardedBy("this")
@Nullable private ListenableFuture<Status> authResultFuture; // null before we check auth.
@GuardedBy("this")
@Nullable
private ListenableFuture<Status> preAuthResultFuture; // null before we pre-auth.
/**
* Constructs a new transport instance.
*
* @param factory parameters common to all a Channel's transports
* @param targetAddress the fully resolved and load-balanced server address
* @param options other parameters that can vary as transports come and go within a Channel
*/
public BinderClientTransport(
BinderClientTransportFactory factory,
AndroidComponentAddress targetAddress,
ClientTransportOptions options) {
super(
factory.scheduledExecutorPool,
buildClientAttributes(
options.getEagAttributes(),
factory.sourceContext,
targetAddress,
factory.inboundParcelablePolicy),
factory.binderDecorator,
buildLogId(factory.sourceContext, targetAddress));
this.offloadExecutorPool = factory.offloadExecutorPool;
this.securityPolicy = factory.securityPolicy;
this.offloadExecutor = offloadExecutorPool.getObject();
this.readyTimeoutMillis = factory.readyTimeoutMillis;
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
this.preAuthorizeServer =
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
serviceBinding =
new ServiceBinding(
factory.mainThreadExecutor,
factory.sourceContext,
factory.channelCredentials,
targetAddress.asBindIntent(),
targetAddress.getTargetUser() != null
? targetAddress.getTargetUser()
: factory.defaultTargetUserHandle,
factory.bindServiceFlags.toInteger(),
this);
}
@Override
void releaseExecutors() {
super.releaseExecutors();
offloadExecutorPool.returnObject(offloadExecutor);
}
@Override
public synchronized void onBound(IBinder binder) {
sendSetupTransaction(
binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)));
}
@Override
public synchronized void onUnbound(Status reason) {
shutdownInternal(reason, true);
}
@CheckReturnValue
@Override
public synchronized Runnable start(ManagedClientTransport.Listener clientTransportListener) {
this.clientTransportListener = checkNotNull(clientTransportListener);
return () -> {
synchronized (BinderClientTransport.this) {
if (inState(TransportState.NOT_STARTED)) {
setState(TransportState.SETUP);
try {
if (preAuthorizeServer) {
preAuthorize(serviceBinding.resolve());
} else {
serviceBinding.bind();
}
} catch (StatusException e) {
shutdownInternal(e.getStatus(), true);
return;
}
if (readyTimeoutMillis >= 0) {
readyTimeoutFuture =
getScheduledExecutorService()
.schedule(
BinderClientTransport.this::onReadyTimeout,
readyTimeoutMillis,
MILLISECONDS);
}
}
}
};
}
@GuardedBy("this")
private void preAuthorize(ServiceInfo serviceInfo) {
// It's unlikely, but the identity/existence of this Service could change by the time we
// actually connect. It doesn't matter though, because:
// - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer
// will eventually retry using a new transport instance that will see the Service's new state.
// - If pre-auth succeeds (but would fail against the server's new state), we might give an
// unauthorized server a chance to run, but the connection will still fail by SecurityPolicy
// check later in handshake. Pre-auth remains effective at mitigating abuse because malware
// can't typically control the exact timing of its installation.
preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid);
Futures.addCallback(
preAuthResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handlePreAuthResult(result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
private synchronized void handlePreAuthResult(Status authorization) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else {
serviceBinding.bind();
}
}
}
private synchronized void onReadyTimeout() {
if (inState(TransportState.SETUP)) {
readyTimeoutFuture = null;
shutdownInternal(
Status.DEADLINE_EXCEEDED.withDescription(
"Connect timeout " + readyTimeoutMillis + "ms lapsed"),
true);
}
}
@Override
public synchronized ClientStream newStream(
final MethodDescriptor<?, ?> method,
final Metadata headers,
final CallOptions callOptions,
ClientStreamTracer[] tracers) {
if (!inState(TransportState.READY)) {
return newFailingClientStream(
isShutdown()
? shutdownStatus
: Status.INTERNAL.withDescription("newStream() before transportReady()"),
attributes,
headers,
tracers);
}
int callId = latestCallId++;
if (latestCallId == LAST_CALL_ID) {
latestCallId = FIRST_CALL_ID;
}
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
Inbound.ClientInbound inbound =
new Inbound.ClientInbound(
this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions));
if (ongoingCalls.putIfAbsent(callId, inbound) != null) {
Status failure = Status.INTERNAL.withDescription("Clashing call IDs");
shutdownInternal(failure, true);
return newFailingClientStream(failure, attributes, headers, tracers);
} else {
if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) {
clientTransportListener.transportInUse(true);
}
Outbound.ClientOutbound outbound =
new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext);
if (method.getType().clientSendsOneMessage()) {
return new SingleMessageClientStream(inbound, outbound, attributes);
} else {
return new MultiMessageClientStream(inbound, outbound, attributes);
}
}
}
@Override
protected void unregisterInbound(Inbound<?> inbound) {
if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) {
clientTransportListener.transportInUse(false);
}
super.unregisterInbound(inbound);
}
@Override
public void ping(final PingCallback callback, Executor executor) {
pingTracker.startPing(callback, executor);
}
@Override
public synchronized void shutdown(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
checkNotNull(reason, "reason");
shutdownInternal(reason, true);
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
clientTransportListener.transportShutdown(status);
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (numInUseStreams.getAndSet(0) > 0) {
clientTransportListener.transportInUse(false);
}
if (readyTimeoutFuture != null) {
readyTimeoutFuture.cancel(false);
readyTimeoutFuture = null;
}
if (preAuthResultFuture != null) {
preAuthResultFuture.cancel(false); // No effect if already complete.
}
if (authResultFuture != null) {
authResultFuture.cancel(false); // No effect if already complete.
}
serviceBinding.unbind();
clientTransportListener.transportTerminated();
}
@Override
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {
int remoteUid = Binder.getCallingUid();
attributes = setSecurityAttrs(attributes, remoteUid);
if (inState(TransportState.SETUP)) {
int version = parcel.readInt();
IBinder binder = parcel.readStrongBinder();
if (version != WIRE_FORMAT_VERSION) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true);
} else if (binder == null) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
} else {
authResultFuture = checkServerAuthorizationAsync(remoteUid);
Futures.addCallback(
authResultFuture,
new FutureCallback<Status>() {
@Override
public void onSuccess(Status result) {
handleAuthResult(binder, result);
}
@Override
public void onFailure(Throwable t) {
handleAuthResult(t);
}
},
offloadExecutor);
}
}
}
private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
return (securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
}
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))) {
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;
}
}
}
}
}
private synchronized void handleAuthResult(Throwable t) {
shutdownInternal(
Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true);
}
@GuardedBy("this")
@Override
protected void handlePingResponse(Parcel parcel) {
pingTracker.onPingResponse(parcel.readInt());
}
private static ClientStream newFailingClientStream(
Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) {
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(tracers, attributes, headers);
statsTraceContext.clientOutboundHeaders();
return new FailingClientStream(failure, tracers);
}
private static InternalLogId buildLogId(
Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate(
BinderClientTransport.class,
sourceContext.getClass().getSimpleName() + "->" + targetAddress);
}
private static Attributes buildClientAttributes(
Attributes eagAttrs,
Context sourceContext,
AndroidComponentAddress targetAddress,
InboundParcelablePolicy inboundParcelablePolicy) {
return Attributes.newBuilder()
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now.
.set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs)
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext))
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress)
.set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy)
.build();
}
private static Attributes setSecurityAttrs(Attributes attributes, int uid) {
return attributes.toBuilder()
.set(REMOTE_UID, uid)
.set(
GrpcAttributes.ATTR_SECURITY_LEVEL,
uid == Process.myUid()
? SecurityLevel.PRIVACY_AND_INTEGRITY
: SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this.
.build();
}
}
/** Concrete server-side transport implementation. */
@Internal
public static final class BinderServerTransport extends BinderTransport
implements ServerTransport {
private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener;
/**
* Constructs a new transport instance.
*
* @param binderDecorator used to decorate 'callbackBinder', for fault injection.
*/
public BinderServerTransport(
ObjectPool<ScheduledExecutorService> executorServicePool,
Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories,
OneWayBinderProxy.Decorator binderDecorator,
IBinder callbackBinder) {
super(executorServicePool, attributes, binderDecorator, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories;
// TODO(jdcormie): Plumb in the Server's executor() and use it here instead.
setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService()));
}
public synchronized void setServerTransportListener(
ServerTransportListener serverTransportListener) {
this.serverTransportListener = serverTransportListener;
if (isShutdown()) {
setState(TransportState.SHUTDOWN_TERMINATED);
notifyTerminated();
releaseExecutors();
} else {
sendSetupTransaction();
// Check we're not shutdown again, since a failure inside sendSetupTransaction (or a
// callback it triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
attributes = serverTransportListener.transportReady(attributes);
}
}
}
StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) {
return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers);
}
synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) {
if (isShutdown()) {
return Status.UNAVAILABLE.withDescription("transport is shutdown");
} else {
serverTransportListener.streamCreated(stream, methodName, headers);
return Status.OK;
}
}
@Override
@GuardedBy("this")
void notifyShutdown(Status status) {
// Nothing to do.
}
@Override
@GuardedBy("this")
void notifyTerminated() {
if (serverTransportListener != null) {
serverTransportListener.transportTerminated();
}
}
@Override
public synchronized void shutdown() {
shutdownInternal(Status.OK, false);
}
@Override
public synchronized void shutdownNow(Status reason) {
shutdownInternal(reason, true);
}
@Override
@Nullable
@GuardedBy("this")
protected Inbound<?> createInbound(int callId) {
return new Inbound.ServerInbound(this, attributes, callId);
}
private static InternalLogId buildLogId(Attributes attributes) {
return InternalLogId.allocate(
BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR));
}
}
private static void checkTransition(TransportState current, TransportState next) {
switch (next) {
case SETUP:

View File

@ -610,10 +610,9 @@ abstract class Inbound<L extends StreamListener> implements StreamListener.Messa
// Server-side inbound transactions.
static final class ServerInbound extends Inbound<ServerStreamListener> {
private final BinderTransport.BinderServerTransport serverTransport;
private final BinderServerTransport serverTransport;
ServerInbound(
BinderTransport.BinderServerTransport transport, Attributes attributes, int callId) {
ServerInbound(BinderServerTransport transport, Attributes attributes, int callId) {
super(transport, attributes, callId);
this.serverTransport = transport;
}

View File

@ -0,0 +1,299 @@
/*
* Copyright 2025 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.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static io.grpc.binder.internal.SystemApis.createContextAsUser;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.UserHandle;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/**
* A {@link NameResolver} that resolves Android-standard "intent:" target URIs to the list of {@link
* AndroidComponentAddress} that match it by manifest intent filter.
*/
final class IntentNameResolver extends NameResolver {
private final Intent targetIntent; // Never mutated.
@Nullable private final UserHandle targetUser; // null means same user that hosts this process.
private final Context targetUserContext;
private final Executor offloadExecutor;
private final Executor sequentialExecutor;
private final SynchronizationContext syncContext;
private final ServiceConfigParser serviceConfigParser;
// Accessed only on `sequentialExecutor`
@Nullable private PackageChangeReceiver receiver; // != null when registered
// Accessed only on 'syncContext'.
private boolean shutdown;
private boolean queryNeeded;
@Nullable private Listener2 listener; // != null after start().
@Nullable private ListenableFuture<ResolutionResult> queryResultFuture; // != null when querying.
@EquivalentAddressGroup.Attr
private static final Attributes CONSTANT_EAG_ATTRS =
Attributes.newBuilder()
// Servers discovered in PackageManager are especially untrusted. After all, any app can
// declare any intent filter it wants! Require pre-authorization so that unauthorized apps
// don't even get a chance to run onCreate()/onBind().
.set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true)
.build();
IntentNameResolver(Intent targetIntent, Args args) {
this.targetIntent = targetIntent;
this.targetUser = args.getArg(ApiConstants.TARGET_ANDROID_USER);
Context context =
checkNotNull(args.getArg(ApiConstants.SOURCE_ANDROID_CONTEXT), "SOURCE_ANDROID_CONTEXT")
.getApplicationContext();
this.targetUserContext =
targetUser != null ? createContextForTargetUserOrThrow(context, targetUser) : context;
// This Executor is nominally optional but all grpc-java Channels provide it since 1.25.
this.offloadExecutor =
checkNotNull(args.getOffloadExecutor(), "NameResolver.Args.getOffloadExecutor()");
// Ensures start()'s work runs before resolve()'s' work, and both run before shutdown()'s.
this.sequentialExecutor = MoreExecutors.newSequentialExecutor(offloadExecutor);
this.syncContext = args.getSynchronizationContext();
this.serviceConfigParser = args.getServiceConfigParser();
}
private static Context createContextForTargetUserOrThrow(Context context, UserHandle targetUser) {
try {
return createContextAsUser(context, targetUser, /* flags= */ 0); // @SystemApi since R.
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"TARGET_ANDROID_USER NameResolver.Arg requires SDK_INT >= R and @SystemApi visibility");
}
}
@Override
public void start(Listener2 listener) {
checkState(this.listener == null, "Already started!");
checkState(!shutdown, "Resolver is shutdown");
this.listener = checkNotNull(listener);
sequentialExecutor.execute(this::registerReceiver);
resolve();
}
@Override
public void refresh() {
checkState(listener != null, "Not started!");
resolve();
}
private void resolve() {
syncContext.throwIfNotInThisSynchronizationContext();
if (shutdown) {
return;
}
// We can't block here in 'syncContext' so we offload PackageManager queries to an Executor.
// But offloading complicates things a bit because other calls can arrive while we wait for the
// results. We keep 'listener' up-to-date with the latest state in PackageManager by doing:
// 1. Only one query-and-report-to-listener operation at a time.
// 2. At least one query-and-report-to-listener AFTER every PackageManager state change.
if (queryResultFuture == null) {
queryResultFuture = Futures.submit(this::queryPackageManager, sequentialExecutor);
queryResultFuture.addListener(this::onQueryComplete, syncContext);
} else {
// There's already a query in-flight but (2) says we need at least one more. Our sequential
// Executor would be enough to ensure (1) but we also don't want a backlog of work to build up
// if things change rapidly. Just make a note to start a new query when this one finishes.
queryNeeded = true;
}
}
private void onQueryComplete() {
syncContext.throwIfNotInThisSynchronizationContext();
checkState(queryResultFuture != null);
checkState(queryResultFuture.isDone());
// Capture non-final `listener` here while we're on 'syncContext'.
Listener2 listener = checkNotNull(this.listener);
Futures.addCallback(
queryResultFuture, // Already isDone() so this execute()s immediately.
new FutureCallback<ResolutionResult>() {
@Override
public void onSuccess(ResolutionResult result) {
listener.onResult2(result);
}
@Override
public void onFailure(Throwable t) {
listener.onResult2(
ResolutionResult.newBuilder()
.setAddressesOrError(StatusOr.fromStatus(Status.fromThrowable(t)))
.build());
}
},
syncContext); // Already on 'syncContext' but addCallback() is faster than try/get/catch.
queryResultFuture = null;
if (queryNeeded) {
// One or more resolve() requests arrived while we were working on the last one. Just one
// follow-on query can subsume all of them.
queryNeeded = false;
resolve();
}
}
@Override
public String getServiceAuthority() {
return "localhost";
}
@Override
public void shutdown() {
syncContext.throwIfNotInThisSynchronizationContext();
if (!shutdown) {
shutdown = true;
sequentialExecutor.execute(this::maybeUnregisterReceiver);
}
}
private ResolutionResult queryPackageManager() throws StatusException {
List<ResolveInfo> queryResults = queryIntentServices(targetIntent);
// Avoid a spurious UnsafeIntentLaunchViolation later. Since S, Android's StrictMode is very
// conservative, marking any Intent parsed from a string as suspicious and complaining when you
// bind to it. But all this is pointless with grpc-binder, which already goes even further by
// not trusting addresses at all! Instead, we rely on SecurityPolicy, which won't allow a
// connection to an unauthorized server UID no matter how you got there.
Intent prototypeBindIntent = sanitize(targetIntent);
// Model each matching android.app.Service as an EAG (server) with a single address.
List<EquivalentAddressGroup> addresses = new ArrayList<>();
for (ResolveInfo resolveInfo : queryResults) {
prototypeBindIntent.setComponent(
new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name));
addresses.add(
new EquivalentAddressGroup(
AndroidComponentAddress.newBuilder()
.setBindIntent(prototypeBindIntent) // Makes a copy.
.setTargetUser(targetUser)
.build(),
CONSTANT_EAG_ATTRS));
}
return ResolutionResult.newBuilder()
.setAddressesOrError(StatusOr.fromValue(addresses))
// Empty service config means we get the default 'pick_first' load balancing policy.
.setServiceConfig(serviceConfigParser.parseServiceConfig(ImmutableMap.of()))
.build();
}
private List<ResolveInfo> queryIntentServices(Intent intent) throws StatusException {
int flags = 0;
if (Build.VERSION.SDK_INT >= 29) {
// Don't match direct-boot-unaware Services that can't presently be created. We'll query again
// after the user is unlocked. The MATCH_DIRECT_BOOT_AUTO behavior is actually the default but
// being explicit here avoids an android.os.strictmode.ImplicitDirectBootViolation.
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
}
List<ResolveInfo> intentServices =
targetUserContext.getPackageManager().queryIntentServices(intent, flags);
if (intentServices == null || intentServices.isEmpty()) {
// Must be the same as when ServiceBinding's call to bindService() returns false.
throw Status.UNIMPLEMENTED
.withDescription("Service not found for intent " + intent)
.asException();
}
return intentServices;
}
// Returns a new Intent with the same action, data and categories as 'input'.
private static Intent sanitize(Intent input) {
Intent output = new Intent();
output.setAction(input.getAction());
output.setData(input.getData());
Set<String> categories = input.getCategories();
if (categories != null) {
for (String category : categories) {
output.addCategory(category);
}
}
// Don't bother copying extras and flags since AndroidComponentAddress (rightly) ignores them.
// Don't bother copying package or ComponentName either, since we're about to set that.
return output;
}
final class PackageChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Get off the main thread and into the correct SynchronizationContext.
syncContext.executeLater(IntentNameResolver.this::resolve);
offloadExecutor.execute(syncContext::drain);
}
}
@SuppressLint("UnprotectedReceiver") // All of these are protected system broadcasts.
private void registerReceiver() {
checkState(receiver == null, "Already registered!");
receiver = new PackageChangeReceiver();
IntentFilter filter = new IntentFilter();
filter.addDataScheme("package");
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
targetUserContext.registerReceiver(receiver, filter);
if (Build.VERSION.SDK_INT >= 24) {
// Clients running in direct boot mode must refresh() when the user is unlocked because
// that's when `directBootAware=false` services become visible in queryIntentServices()
// results. ACTION_BOOT_COMPLETED would work too but it's delivered with lower priority.
targetUserContext.registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
}
}
private void maybeUnregisterReceiver() {
if (receiver != null) { // NameResolver API contract appears to allow shutdown without start().
targetUserContext.unregisterReceiver(receiver);
receiver = null;
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2025 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.internal;
import static android.content.Intent.URI_INTENT_SCHEME;
import android.content.Intent;
import com.google.common.collect.ImmutableSet;
import io.grpc.NameResolver;
import io.grpc.NameResolver.Args;
import io.grpc.NameResolverProvider;
import io.grpc.binder.AndroidComponentAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* A {@link NameResolverProvider} that handles Android-standard "intent:" target URIs, resolving
* them to the list of {@link AndroidComponentAddress} that match by manifest intent filter.
*/
public final class IntentNameResolverProvider extends NameResolverProvider {
static final String ANDROID_INTENT_SCHEME = "intent";
@Override
public String getDefaultScheme() {
return ANDROID_INTENT_SCHEME;
}
@Nullable
@Override
public NameResolver newNameResolver(URI targetUri, final Args args) {
if (Objects.equals(targetUri.getScheme(), ANDROID_INTENT_SCHEME)) {
return new IntentNameResolver(parseUriArg(targetUri), args);
} else {
return null;
}
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public int priority() {
return 3; // Lower than DNS so we don't accidentally become the default scheme for a registry.
}
@Override
public ImmutableSet<Class<? extends SocketAddress>> getProducedSocketAddressTypes() {
return ImmutableSet.of(AndroidComponentAddress.class);
}
private static Intent parseUriArg(URI targetUri) {
try {
return Intent.parseUri(targetUri.toString(), URI_INTENT_SCHEME);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@ -193,18 +193,27 @@ final class ServiceBinding implements Bindable, ServiceConnection {
bindResult = context.bindService(bindIntent, conn, flags);
break;
case BIND_SERVICE_AS_USER:
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle);
} else {
return Status.INTERNAL.withDescription("Cross user Channel requires Android R+");
}
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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bindResult =
devicePolicyManager.bindDeviceAdminServiceAsUser(
channelCredentials.getDevicePolicyAdminComponentName(),
bindIntent,
conn,
flags,
targetUserHandle);
} else {
return Status.INTERNAL.withDescription(
"Device policy admin binding requires Android R+");
}
break;
}
if (!bindResult) {

View File

@ -0,0 +1,60 @@
/*
* Copyright 2025 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.internal;
import android.content.Context;
import android.os.UserHandle;
import java.lang.reflect.Method;
/**
* A collection of static methods that wrap hidden Android "System APIs."
*
* <p>grpc-java can't call Android methods marked @SystemApi directly, even though many of our users
* are "system apps" entitled to do so. Being a library built outside the Android source tree, these
* "non-SDK" elements simply don't exist from our compiler's perspective. Instead we resort to
* reflection but use the static wrappers found here to keep call sites readable and type safe.
*
* <p>Modern Android's JRE also limits the visibility of these methods at *runtime*. Only certain
* privileged apps installed on the system image app can call them, even using reflection, and this
* wrapper doesn't change that. Callers are responsible for ensuring that the host app actually has
* the ability to call @SystemApis and all methods throw {@link ReflectiveOperationException} as a
* reminder to do that. See
* https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces for more.
*/
final class SystemApis {
private static volatile Method createContextAsUserMethod;
// Not to be instantiated.
private SystemApis() {}
/**
* Returns a new Context object whose methods act as if they were running in the given user.
*
* @throws ReflectiveOperationException if SDK_INT < R or host app lacks @SystemApi visibility
*/
public static Context createContextAsUser(Context context, UserHandle userHandle, int flags)
throws ReflectiveOperationException {
if (createContextAsUserMethod == null) {
synchronized (SystemApis.class) {
if (createContextAsUserMethod == null) {
createContextAsUserMethod =
Context.class.getMethod("createContextAsUser", UserHandle.class, int.class);
}
}
}
return (Context) createContextAsUserMethod.invoke(context, userHandle, flags);
}
}

View File

@ -56,12 +56,12 @@ public final class BinderServerTransportTest {
@Mock IBinder mockBinder;
BinderTransport.BinderServerTransport transport;
BinderServerTransport transport;
@Before
public void setUp() throws Exception {
transport =
new BinderTransport.BinderServerTransport(
new BinderServerTransport(
new FixedObjectPool<>(executorService),
Attributes.EMPTY,
ImmutableList.of(),

View File

@ -0,0 +1,115 @@
/*
* Copyright 2025 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.internal;
import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.NameResolverProvider;
import io.grpc.SynchronizationContext;
import io.grpc.binder.ApiConstants;
import java.net.URI;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoTestRule;
import org.robolectric.RobolectricTestRunner;
/** A test for IntentNameResolverProvider. */
@RunWith(RobolectricTestRunner.class)
public final class IntentNameResolverProviderTest {
private final Application appContext = ApplicationProvider.getApplicationContext();
private final SynchronizationContext syncContext = newSynchronizationContext();
private final NameResolver.Args args = newNameResolverArgs();
private NameResolverProvider provider;
@Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this);
@Mock public NameResolver.Listener2 mockListener;
@Captor public ArgumentCaptor<ResolutionResult> resultCaptor;
@Before
public void setUp() {
provider = new IntentNameResolverProvider();
}
@Test
public void testProviderScheme_returnsIntentScheme() throws Exception {
assertThat(provider.getDefaultScheme())
.isEqualTo(IntentNameResolverProvider.ANDROID_INTENT_SCHEME);
}
@Test
public void testNoResolverForUnknownScheme_returnsNull() throws Exception {
assertThat(provider.newNameResolver(new URI("random://uri"), args)).isNull();
}
@Test
public void testResolutionWithBadUri_throwsIllegalArg() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> provider.newNameResolver(new URI("intent:xxx#Intent;e.x=1;end;"), args));
}
@Test
public void testResolverForIntentScheme_returnsResolver() throws Exception {
URI uri = new URI("intent://authority/path#Intent;action=action;scheme=scheme;end");
NameResolver resolver = provider.newNameResolver(uri, args);
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("localhost");
syncContext.execute(() -> resolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError()).isNotNull();
syncContext.execute(resolver::shutdown);
shadowOf(getMainLooper()).idle();
}
/** Returns a new test-specific {@link NameResolver.Args} instance. */
private NameResolver.Args newNameResolverArgs() {
return NameResolver.Args.newBuilder()
.setDefaultPort(-1)
.setProxyDetector((target) -> null) // No proxies here.
.setSynchronizationContext(syncContext)
.setOffloadExecutor(ContextCompat.getMainExecutor(appContext))
.setServiceConfigParser(mock(ServiceConfigParser.class))
.setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext)
.build();
}
private static SynchronizationContext newSynchronizationContext() {
return new SynchronizationContext(
(thread, exception) -> {
throw new AssertionError(exception);
});
}
}

View File

@ -0,0 +1,531 @@
/*
* Copyright 2025 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.internal;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REPLACED;
import static android.os.Looper.getMainLooper;
import static android.os.Process.myUserHandle;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.Status;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.ApiConstants;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoTestRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowPackageManager;
/** A test for IntentNameResolverProvider. */
@RunWith(RobolectricTestRunner.class)
public final class IntentNameResolverTest {
private static final ComponentName SOME_COMPONENT_NAME =
new ComponentName("com.foo.bar", "SomeComponent");
private static final ComponentName ANOTHER_COMPONENT_NAME =
new ComponentName("org.blah", "AnotherComponent");
private final Application appContext = ApplicationProvider.getApplicationContext();
private final SynchronizationContext syncContext = newSynchronizationContext();
private final NameResolver.Args args = newNameResolverArgs().build();
private final ShadowPackageManager shadowPackageManager =
shadowOf(appContext.getPackageManager());
@Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this);
@Mock public NameResolver.Listener2 mockListener;
@Captor public ArgumentCaptor<ResolutionResult> resultCaptor;
@Test
public void testResolverForIntentScheme_returnsResolverWithLocalHostAuthority() throws Exception {
NameResolver resolver = newNameResolver(newIntent());
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("localhost");
}
@Test
public void testResolutionWithoutServicesAvailable_returnsUnimplemented() throws Exception {
NameResolver nameResolver = newNameResolver(newIntent());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode())
.isEqualTo(Status.UNIMPLEMENTED.getCode());
}
@Test
public void testResolutionWithMultipleServicesAvailable_returnsAndroidComponentAddresses()
throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
// Adds another valid Service
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testExplicitResolutionByComponent_returnsRestrictedResults() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver =
newNameResolver(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME));
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testExplicitResolutionByPackage_returnsRestrictedResults() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver =
newNameResolver(intent.cloneFilter().setPackage(ANOTHER_COMPONENT_NAME.getPackageName()));
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testResolution_setsPreAuthEagAttribute() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
assertThat(
getEagsOrThrow(resultCaptor.getValue()).stream()
.map(EquivalentAddressGroup::getAttributes)
.collect(toImmutableList())
.get(0)
.get(ApiConstants.PRE_AUTH_SERVER_OVERRIDE))
.isTrue();
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
@Test
public void testServiceRemoved_pushesUpdatedAndroidComponentAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
shadowPackageManager.removeService(ANOTHER_COMPONENT_NAME);
broadcastPackageChange(ACTION_PACKAGE_REPLACED, ANOTHER_COMPONENT_NAME.getPackageName());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
@Config(sdk = 30)
public void testTargetAndroidUser_pushesUpdatedAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
NameResolver nameResolver =
newNameResolver(
intent,
newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode())
.isEqualTo(Status.UNIMPLEMENTED.getCode());
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
broadcastPackageChange(ACTION_PACKAGE_ADDED, SOME_COMPONENT_NAME.getPackageName());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
ImmutableList.of(
AndroidComponentAddress.newBuilder()
.setTargetUser(myUserHandle())
.setBindIntent(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME))
.build()));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
@Config(sdk = 29)
public void testTargetAndroidUser_notSupported_throwsWithHelpfulMessage() throws Exception {
NameResolver.Args args =
newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build();
IllegalArgumentException iae =
assertThrows(IllegalArgumentException.class, () -> newNameResolver(newIntent(), args));
assertThat(iae.getMessage()).contains("TARGET_ANDROID_USER");
assertThat(iae.getMessage()).contains("SDK_INT >= R");
}
@Test
@Config(sdk = 29)
public void testServiceAppearsUponBootComplete_pushesUpdatedAndroidComponentAddresses()
throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
// Suppose this directBootAware=true Service appears in PackageManager before a user unlock.
shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(false);
ServiceInfo someServiceInfo = shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
someServiceInfo.directBootAware = true;
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
// TODO(b/331618070): Robolectric doesn't yet support ServiceInfo.directBootAware filtering.
// Simulate support by waiting for a user unlock to add this !directBootAware Service.
ServiceInfo anotherServiceInfo =
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
anotherServiceInfo.directBootAware = false;
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(true);
broadcastUserUnlocked(myUserHandle());
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
verifyNoMoreInteractions(mockListener);
}
@Test
public void testRefresh_returnsSameAndroidComponentAddresses() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::refresh);
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(
toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)),
toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty();
}
@Test
public void testRefresh_collapsesMultipleRequestsIntoOneLookup() throws Exception {
Intent intent = newIntent();
IntentFilter serviceIntentFilter = newFilterMatching(intent);
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter);
NameResolver nameResolver = newNameResolver(intent);
syncContext.execute(() -> nameResolver.start(mockListener)); // Should kick off the 1st lookup.
syncContext.execute(nameResolver::refresh); // Should queue a lookup to run when 1st finishes.
syncContext.execute(nameResolver::refresh); // Should be ignored since a lookup is already Q'd.
syncContext.execute(nameResolver::refresh); // Also ignored.
shadowOf(getMainLooper()).idle();
verify(mockListener, never()).onError(any());
verify(mockListener, times(2)).onResult2(resultCaptor.capture());
assertThat(getAddressesOrThrow(resultCaptor.getValue()))
.containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)));
syncContext.execute(nameResolver::shutdown);
shadowOf(getMainLooper()).idle();
}
private void broadcastPackageChange(String action, String pkgName) {
Intent broadcast = new Intent();
broadcast.setAction(action);
broadcast.setData(Uri.parse("package:" + pkgName));
appContext.sendBroadcast(broadcast);
}
private void broadcastUserUnlocked(UserHandle userHandle) {
Intent unlockedBroadcast = new Intent(Intent.ACTION_USER_UNLOCKED);
unlockedBroadcast.putExtra(Intent.EXTRA_USER, userHandle);
appContext.sendBroadcast(unlockedBroadcast);
}
@Test
public void testResolutionOnResultThrows_onErrorNotCalled() throws Exception {
RetainingUncaughtExceptionHandler exceptionHandler = new RetainingUncaughtExceptionHandler();
SynchronizationContext syncContext = new SynchronizationContext(exceptionHandler);
Intent intent = newIntent();
shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME);
shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, newFilterMatching(intent));
@SuppressWarnings("serial")
class SomeRuntimeException extends RuntimeException {}
doThrow(SomeRuntimeException.class).when(mockListener).onResult2(any());
NameResolver nameResolver =
newNameResolver(
intent, newNameResolverArgs().setSynchronizationContext(syncContext).build());
syncContext.execute(() -> nameResolver.start(mockListener));
shadowOf(getMainLooper()).idle();
verify(mockListener).onResult2(any());
verify(mockListener, never()).onError(any());
assertThat(exceptionHandler.uncaught).hasSize(1);
assertThat(exceptionHandler.uncaught.get(0)).isInstanceOf(SomeRuntimeException.class);
}
private static Intent newIntent() {
Intent intent = new Intent();
intent.setAction("test.action");
intent.setData(Uri.parse("grpc:ServiceName"));
return intent;
}
private static IntentFilter newFilterMatching(Intent intent) {
IntentFilter filter = new IntentFilter();
if (intent.getAction() != null) {
filter.addAction(intent.getAction());
}
Uri data = intent.getData();
if (data != null) {
if (data.getScheme() != null) {
filter.addDataScheme(data.getScheme());
}
if (data.getSchemeSpecificPart() != null) {
filter.addDataSchemeSpecificPart(data.getSchemeSpecificPart(), 0);
}
}
Set<String> categories = intent.getCategories();
if (categories != null) {
for (String category : categories) {
filter.addCategory(category);
}
}
return filter;
}
private static List<EquivalentAddressGroup> getEagsOrThrow(ResolutionResult result) {
StatusOr<List<EquivalentAddressGroup>> eags = result.getAddressesOrError();
if (!eags.hasValue()) {
throw eags.getStatus().asRuntimeException();
}
return eags.getValue();
}
// Extracts just the addresses from 'result's EquivalentAddressGroups.
private static ImmutableList<List<SocketAddress>> getAddressesOrThrow(ResolutionResult result) {
return getEagsOrThrow(result).stream()
.map(EquivalentAddressGroup::getAddresses)
.collect(toImmutableList());
}
// Converts given Intents to a list of ACAs, for convenient comparison with getAddressesOrThrow().
private static ImmutableList<AndroidComponentAddress> toAddressList(Intent... bindIntents) {
ImmutableList.Builder<AndroidComponentAddress> builder = ImmutableList.builder();
for (Intent bindIntent : bindIntents) {
builder.add(AndroidComponentAddress.forBindIntent(bindIntent));
}
return builder.build();
}
private NameResolver newNameResolver(Intent targetIntent) {
return newNameResolver(targetIntent, args);
}
private NameResolver newNameResolver(Intent targetIntent, NameResolver.Args args) {
return new IntentNameResolver(targetIntent, args);
}
/** Returns a new test-specific {@link NameResolver.Args} instance. */
private NameResolver.Args.Builder newNameResolverArgs() {
return NameResolver.Args.newBuilder()
.setDefaultPort(-1)
.setProxyDetector((target) -> null) // No proxies here.
.setSynchronizationContext(syncContext)
.setOffloadExecutor(ContextCompat.getMainExecutor(appContext))
.setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext)
.setServiceConfigParser(mock(ServiceConfigParser.class));
}
/**
* Returns a test {@link SynchronizationContext}.
*
* <p>Exceptions will cause the test to fail with {@link AssertionError}.
*/
private static SynchronizationContext newSynchronizationContext() {
return new SynchronizationContext(
(thread, exception) -> {
throw new AssertionError(exception);
});
}
static final class RetainingUncaughtExceptionHandler implements UncaughtExceptionHandler {
final ArrayList<Throwable> uncaught = new ArrayList<>();
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
uncaught.add(e);
}
}
}

View File

@ -29,6 +29,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.os.Parcel;
import android.os.UserHandle;
@ -327,6 +328,50 @@ public final class ServiceBindingTest {
assertThat(statusException.getStatus().getDescription()).contains("12345");
}
@Test
@Config(sdk = 30)
public void testBindService_doesNotThrowInternalErrorWhenSdkAtLeastR() {
UserHandle userHandle = generateUserHandle(/* userId= */ 12345);
binding = newBuilder().setTargetUserHandle(userHandle).build();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(Build.VERSION.SDK_INT).isEqualTo(Build.VERSION_CODES.R);
assertThat(observer.unboundReason).isNull();
}
@Test
@Config(sdk = 28)
public void testBindServiceAsUser_returnsErrorWhenSdkBelowR() {
UserHandle userHandle = generateUserHandle(/* userId= */ 12345);
binding = newBuilder().setTargetUserHandle(userHandle).build();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.INTERNAL);
assertThat(observer.unboundReason.getDescription())
.isEqualTo("Cross user Channel requires Android R+");
}
@Test
@Config(sdk = 28)
public void testDevicePolicyBlind_returnsErrorWhenSdkBelowR() {
String deviceAdminClassName = "DevicePolicyAdmin";
ComponentName adminComponent = new ComponentName(appContext, deviceAdminClassName);
allowBindDeviceAdminForUser(appContext, adminComponent, 10);
binding =
newBuilder()
.setTargetUserHandle(UserHandle.getUserHandleForUid(10))
.setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent))
.build();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.INTERNAL);
assertThat(observer.unboundReason.getDescription())
.isEqualTo("Device policy admin binding requires Android R+");
}
@Test
@Config(sdk = 30)
public void testBindWithDeviceAdmin() throws Exception {

View File

@ -24,8 +24,8 @@ import io.grpc.internal.TestUtils.NoopChannelLogger;
import java.net.SocketAddress;
/**
* Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to
* mention irrelevant details (go/tott/719).
* Helps unit tests create {@link BinderClientTransport} instances without having to mention
* irrelevant details (go/tott/719).
*/
public class BinderClientTransportBuilder {
private BinderClientTransportFactory factory;
@ -54,7 +54,7 @@ public class BinderClientTransportBuilder {
return this;
}
public BinderTransport.BinderClientTransport build() {
public BinderClientTransport build() {
return factory.newClientTransport(
checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger));
}

View File

@ -21,7 +21,7 @@ subprojects {
apply plugin: "net.ltgt.errorprone"
group = "io.grpc"
version = "1.75.0" // CURRENT_GRPC_VERSION
version = "1.76.0-SNAPSHOT" // CURRENT_GRPC_VERSION
repositories {
maven { // The google mirror is less flaky than mavenCentral()

View File

@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler (version 1.75.0)",
value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)",
comments = "Source: grpc/testing/compiler/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
@java.lang.Deprecated

View File

@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName;
* </pre>
*/
@javax.annotation.Generated(
value = "by gRPC proto compiler (version 1.75.0)",
value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)",
comments = "Source: grpc/testing/compiler/test.proto")
@io.grpc.stub.annotations.GrpcGenerated
public final class TestServiceGrpc {

View File

@ -219,7 +219,7 @@ public final class GrpcUtil {
public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults();
public static final String IMPLEMENTATION_VERSION = "1.75.0"; // CURRENT_GRPC_VERSION
public static final String IMPLEMENTATION_VERSION = "1.76.0-SNAPSHOT"; // CURRENT_GRPC_VERSION
/**
* The default timeout in nanos for a keepalive ping request.

View File

@ -14,7 +14,7 @@ android {
namespace = 'io.grpc.cronet'
compileSdkVersion 33
defaultConfig {
minSdkVersion 21
minSdkVersion 22
targetSdkVersion 33
versionCode 1
versionName "1.0"

View File

@ -1,4 +1,4 @@
bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.75.0") # CURRENT_GRPC_VERSION
bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.76.0-SNAPSHOT") # CURRENT_GRPC_VERSION
bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58")
bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1")
bazel_dep(name = "rules_jvm_external", version = "6.0")

View File

@ -34,7 +34,7 @@ android {
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' // CURRENT_GRPC_VERSION
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
}
}
generateProtoTasks {
@ -54,12 +54,12 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.google.truth:truth:1.1.5'
testImplementation 'io.grpc:grpc-testing:1.75.0' // CURRENT_GRPC_VERSION
testImplementation 'io.grpc:grpc-testing:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
}

View File

@ -32,7 +32,7 @@ android {
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' // CURRENT_GRPC_VERSION
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
}
}
generateProtoTasks {
@ -52,8 +52,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53'
}

View File

@ -32,7 +32,7 @@ android {
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' // CURRENT_GRPC_VERSION
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
}
}
generateProtoTasks {
@ -52,8 +52,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53'
}

View File

@ -33,7 +33,7 @@ android {
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.1' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' // CURRENT_GRPC_VERSION
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
}
}
generateProtoTasks {
@ -53,8 +53,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0'
// You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.75.0' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53'
}

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.8'
def protocVersion = protobufVersion

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
dependencies {

View File

@ -23,7 +23,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.8'
dependencies {

View File

@ -6,13 +6,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-debug</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source>

View File

@ -23,7 +23,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.8'
dependencies {

View File

@ -6,13 +6,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-dualstack</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source>

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.8'
def protocVersion = protobufVersion

View File

@ -6,13 +6,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-gauth</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.8</protobuf.version>
<!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source>

View File

@ -22,7 +22,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
def openTelemetryVersion = '1.52.0'
def openTelemetryPrometheusVersion = '1.52.0-alpha'

View File

@ -22,7 +22,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
dependencies {

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.8'
dependencies {

View File

@ -6,13 +6,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-hostname</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source>

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.8'
def protocVersion = protobufVersion

View File

@ -7,13 +7,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-jwt-auth</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.8</protobuf.version>
<protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 -->

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protobufVersion = '3.25.8'
def protocVersion = protobufVersion

View File

@ -7,13 +7,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-oauth</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.8</protobuf.version>
<protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 -->

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
def openTelemetryVersion = '1.52.0'
def openTelemetryPrometheusVersion = '1.52.0-alpha'

View File

@ -16,7 +16,7 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
dependencies {

View File

@ -16,7 +16,7 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
dependencies {

View File

@ -15,7 +15,7 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
dependencies {

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
dependencies {

View File

@ -6,13 +6,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>example-tls</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protoc.version>3.25.8</protoc.version>
<!-- required for jdk9 -->
<maven.compiler.source>1.8</maven.compiler.source>

View File

@ -21,7 +21,7 @@ java {
// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.75.0' // CURRENT_GRPC_VERSION
def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
dependencies {

View File

@ -6,13 +6,13 @@
<packaging>jar</packaging>
<!-- Feel free to delete the comment at the end of these lines. It is just
for safely updating the version in our release process. -->
<version>1.75.0</version><!-- CURRENT_GRPC_VERSION -->
<version>1.76.0-SNAPSHOT</version><!-- CURRENT_GRPC_VERSION -->
<name>examples</name>
<url>https://github.com/grpc/grpc-java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.75.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<grpc.version>1.76.0-SNAPSHOT</grpc.version><!-- CURRENT_GRPC_VERSION -->
<protobuf.version>3.25.8</protobuf.version>
<protoc.version>3.25.8</protoc.version>
<!-- required for JDK 8 -->

8
lint.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!--
Suppress temporarily due to AAPT2 failures with SDK 35/36 on AGP 7.x.
Remove after AGP upgrade.
-->
<issue id="OldTargetApi" severity="ignore" />
</lint>

View File

@ -84,6 +84,7 @@ public class GrpcSslContexts {
private static final String SUN_PROVIDER_NAME = "SunJSSE";
private static final String IBM_PROVIDER_NAME = "IBMJSSE2";
private static final String OPENJSSE_PROVIDER_NAME = "OpenJSSE";
private static final String BCJSSE_PROVIDER_NAME = "BCJSSE";
/**
* Creates an SslContextBuilder with ciphers and APN appropriate for gRPC.
@ -199,7 +200,8 @@ public class GrpcSslContexts {
jdkProvider.getName() + " selected, but Java 9+ and Jetty NPN/ALPN unavailable");
}
} else if (IBM_PROVIDER_NAME.equals(jdkProvider.getName())
|| OPENJSSE_PROVIDER_NAME.equals(jdkProvider.getName())) {
|| OPENJSSE_PROVIDER_NAME.equals(jdkProvider.getName())
|| BCJSSE_PROVIDER_NAME.equals(jdkProvider.getName())) {
if (JettyTlsUtil.isJava9AlpnAvailable()) {
apc = ALPN;
} else {
@ -255,7 +257,8 @@ public class GrpcSslContexts {
return provider;
}
} else if (IBM_PROVIDER_NAME.equals(provider.getName())
|| OPENJSSE_PROVIDER_NAME.equals(provider.getName())) {
|| OPENJSSE_PROVIDER_NAME.equals(provider.getName())
|| BCJSSE_PROVIDER_NAME.equals(provider.getName())) {
if (JettyTlsUtil.isJava9AlpnAvailable()) {
return provider;
}

View File

@ -29,6 +29,7 @@ import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -69,7 +70,7 @@ public final class BlockingClientCall<ReqT, RespT> {
private final ThreadSafeThreadlessExecutor executor;
private boolean writeClosed;
private volatile Status closedStatus; // null if not closed
private AtomicReference<CloseState> closeState = new AtomicReference<>();
BlockingClientCall(ClientCall<ReqT, RespT> call, ThreadSafeThreadlessExecutor executor) {
this.call = call;
@ -120,22 +121,22 @@ public final class BlockingClientCall<ReqT, RespT> {
logger.finer("Client Blocking read had value: " + bufferedValue);
}
Status currentClosedStatus;
CloseState currentCloseState;
if (bufferedValue != null) {
call.request(1);
return bufferedValue;
} else if ((currentClosedStatus = closedStatus) == null) {
} else if ((currentCloseState = closeState.get()) == null) {
throw new IllegalStateException(
"The message disappeared... are you reading from multiple threads?");
} else if (!currentClosedStatus.isOk()) {
throw currentClosedStatus.asException();
} else if (!currentCloseState.status.isOk()) {
throw currentCloseState.status.asException(currentCloseState.trailers);
} else {
return null;
}
}
boolean skipWaitingForRead() {
return closedStatus != null || !buffer.isEmpty();
return closeState.get() != null || !buffer.isEmpty();
}
/**
@ -148,11 +149,11 @@ public final class BlockingClientCall<ReqT, RespT> {
* @throws StatusException If the stream was closed in an error state
*/
public boolean hasNext() throws InterruptedException, StatusException {
executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closedStatus != null, this);
executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closeState.get() != null, this);
Status currentClosedStatus = closedStatus;
if (currentClosedStatus != null && !currentClosedStatus.isOk()) {
throw currentClosedStatus.asException();
CloseState currentCloseState = closeState.get();
if (currentCloseState != null && !currentCloseState.status.isOk()) {
throw currentCloseState.status.asException(currentCloseState.trailers);
}
return !buffer.isEmpty();
@ -221,17 +222,16 @@ public final class BlockingClientCall<ReqT, RespT> {
}
Predicate<BlockingClientCall<ReqT, RespT>> predicate =
(x) -> x.call.isReady() || x.closedStatus != null;
(x) -> x.call.isReady() || x.closeState.get() != null;
executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this);
Status savedClosedStatus = closedStatus;
if (savedClosedStatus == null) {
CloseState savedCloseState = closeState.get();
if (savedCloseState == null || savedCloseState.status == null) {
call.sendMessage(request);
return true;
} else if (savedClosedStatus.isOk()) {
} else if (savedCloseState.status.isOk()) {
return false;
} else {
// Propagate any errors returned from the server
throw savedClosedStatus.asException();
throw savedCloseState.status.asException(savedCloseState.trailers);
}
}
@ -274,7 +274,8 @@ public final class BlockingClientCall<ReqT, RespT> {
@VisibleForTesting
Status getClosedStatus() {
drainQuietly();
return closedStatus;
CloseState state = closeState.get();
return (state == null) ? null : state.status;
}
/**
@ -317,7 +318,7 @@ public final class BlockingClientCall<ReqT, RespT> {
* @return True if writes haven't been closed and the server hasn't closed the stream
*/
private boolean isWriteLegal() {
return !writeClosed && closedStatus == null;
return !writeClosed && closeState.get() == null;
}
ClientCall.Listener<RespT> getListener() {
@ -335,15 +336,25 @@ public final class BlockingClientCall<ReqT, RespT> {
private final class QueuingListener extends ClientCall.Listener<RespT> {
@Override
public void onMessage(RespT value) {
Preconditions.checkState(closedStatus == null, "ClientCall already closed");
Preconditions.checkState(closeState.get() == null, "ClientCall already closed");
buffer.add(value);
}
@Override
public void onClose(Status status, Metadata trailers) {
Preconditions.checkState(closedStatus == null, "ClientCall already closed");
closedStatus = status;
CloseState newCloseState = new CloseState(status, trailers);
boolean wasSet = closeState.compareAndSet(null, newCloseState);
Preconditions.checkState(wasSet, "ClientCall already closed");
}
}
private static final class CloseState {
final Status status;
final Metadata trailers;
CloseState(Status status, Metadata trailers) {
this.status = Preconditions.checkNotNull(status, "status");
this.trailers = trailers;
}
}
}

View File

@ -48,6 +48,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
@ -738,6 +739,32 @@ final class WeightedRoundRobinLoadBalancer extends MultiChildLoadBalancer {
this.errorUtilizationPenalty = errorUtilizationPenalty;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof WeightedRoundRobinLoadBalancerConfig)) {
return false;
}
WeightedRoundRobinLoadBalancerConfig that = (WeightedRoundRobinLoadBalancerConfig) o;
return this.blackoutPeriodNanos == that.blackoutPeriodNanos
&& this.weightExpirationPeriodNanos == that.weightExpirationPeriodNanos
&& this.enableOobLoadReport == that.enableOobLoadReport
&& this.oobReportingPeriodNanos == that.oobReportingPeriodNanos
&& this.weightUpdatePeriodNanos == that.weightUpdatePeriodNanos
// Float.compare considers NaNs equal
&& Float.compare(this.errorUtilizationPenalty, that.errorUtilizationPenalty) == 0;
}
@Override
public int hashCode() {
return Objects.hash(
blackoutPeriodNanos,
weightExpirationPeriodNanos,
enableOobLoadReport,
oobReportingPeriodNanos,
weightUpdatePeriodNanos,
errorUtilizationPenalty);
}
static final class Builder {
long blackoutPeriodNanos = 10_000_000_000L; // 10s
long weightExpirationPeriodNanos = 180_000_000_000L; //3min

View File

@ -35,6 +35,7 @@ import com.github.xds.service.orca.v3.OrcaLoadReportRequest;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.testing.EqualsTester;
import com.google.protobuf.Duration;
import io.grpc.Attributes;
import io.grpc.CallOptions;
@ -215,6 +216,40 @@ public class WeightedRoundRobinLoadBalancerTest {
weightedPicker.pickSubchannel(mockArgs);
}
@Test
public void config_equalsTester() {
WeightedRoundRobinLoadBalancerConfig defaults =
WeightedRoundRobinLoadBalancerConfig.newBuilder().build();
new EqualsTester()
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder().build(),
WeightedRoundRobinLoadBalancerConfig.newBuilder().build(),
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setBlackoutPeriodNanos(defaults.blackoutPeriodNanos).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setBlackoutPeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setWeightExpirationPeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setEnableOobLoadReport(true).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setOobReportingPeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setWeightUpdatePeriodNanos(5).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setErrorUtilizationPenalty(0.5F).build())
.addEqualityGroup(
WeightedRoundRobinLoadBalancerConfig.newBuilder()
.setErrorUtilizationPenalty(Float.NaN).build())
.testEquals();
}
@Test
public void wrrLifeCycle() {
syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder()