binder: BinderTransport implementation. (#8031)

This is the first major code drop for binderchannel, containing the transport class and its internals.
This commit is contained in:
markb74 2021-05-26 14:54:32 +02:00 committed by GitHub
parent 2239dd717c
commit 8e18c11bbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 5924 additions and 3 deletions

View File

@ -7,6 +7,22 @@ plugins {
description = 'gRPC BinderChannel'
android {
sourceSets {
test {
java {
srcDirs += "${projectDir}/../core/src/test/java/"
setIncludes(["io/grpc/internal/FakeClock.java",
"io/grpc/binder/**"])
}
}
androidTest {
java {
srcDirs += "${projectDir}/../core/src/test/java/"
setIncludes(["io/grpc/internal/AbstractTransportTest.java",
"io/grpc/binder/**"])
}
}
}
compileSdkVersion 29
compileOptions {
sourceCompatibility 1.8
@ -14,10 +30,11 @@ android {
}
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
lintOptions { abortOnError false }
}
@ -29,6 +46,7 @@ repositories {
dependencies {
api project(':grpc-core')
guavaDependency 'implementation'
implementation libraries.androidx_annotation
testImplementation libraries.androidx_core
@ -39,9 +57,33 @@ dependencies {
// Unreleased change: https://github.com/robolectric/robolectric/pull/5432
exclude group: 'com.google.auto.service', module: 'auto-service'
}
testImplementation (libraries.guava_testlib) {
exclude group: 'junit', module: 'junit'
}
testImplementation libraries.truth
androidTestAnnotationProcessor libraries.autovalue
androidTestImplementation project(':grpc-testing')
androidTestImplementation libraries.autovalue_annotation
androidTestImplementation libraries.junit
androidTestImplementation libraries.androidx_core
androidTestImplementation libraries.androidx_test
androidTestImplementation libraries.androidx_test_rules
androidTestImplementation libraries.androidx_test_ext_junit
androidTestImplementation libraries.truth
androidTestImplementation libraries.mockito_android
androidTestImplementation libraries.androidx_lifecycle_service
}
import net.ltgt.gradle.errorprone.CheckSeverity
tasks.withType(JavaCompile) {
options.compilerArgs += [
"-Xlint:-cast"
]
appendToProperty(it.options.errorprone.excludedPaths, ".*/R.java", "|")
// Reuses source code from grpc-core, which targets Java 7 (no method references)
options.errorprone.check("UnnecessaryAnonymousClass", CheckSeverity.OFF)
}
[publishMavenPublicationToMavenRepository]*.onlyIf { false }

View File

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.grpc.binder">
<application>
<service android:name="io.grpc.binder.HostServices$HostService1"/>
<service android:name="io.grpc.binder.HostServices$HostService2"/>
</application>
</manifest>

View File

@ -0,0 +1,261 @@
/*
* 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;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import androidx.lifecycle.LifecycleService;
import com.google.auto.value.AutoValue;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import io.grpc.NameResolver;
import io.grpc.ServerServiceDefinition;
import io.grpc.ServerStreamTracer;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.internal.InternalServer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* A test helper class for creating android services to host gRPC servers.
*
* <p>Currently only supports two servers at a time. If more are required, define a new class, add
* it to the manifest, and the hostServiceClasses array.
*/
public final class HostServices {
private static final Logger logger = Logger.getLogger(HostServices.class.getName());
private static final Class<?>[] hostServiceClasses =
new Class<?>[] {
HostService1.class, HostService2.class,
};
@AutoValue
public abstract static class ServiceParams {
@Nullable
abstract Executor transactionExecutor();
@Nullable
abstract Supplier<IBinder> rawBinderSupplier();
public abstract Builder toBuilder();
public static Builder builder() {
return new AutoValue_HostServices_ServiceParams.Builder();
}
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setRawBinderSupplier(Supplier<IBinder> binderSupplier);
/**
* If set, this executor will be used to pass any inbound transactions to the server. This can
* be used to simulate delayed, re-ordered, or dropped packets.
*/
public abstract Builder setTransactionExecutor(Executor transactionExecutor);
public abstract ServiceParams build();
}
}
@GuardedBy("HostServices.class")
private static final Map<Class<?>, AndroidComponentAddress> serviceAddresses = new HashMap<>();
@GuardedBy("HostServices.class")
private static final Map<Class<?>, ServiceParams> serviceParams = new HashMap<>();
@GuardedBy("HostServices.class")
private static final Map<Class<?>, HostService> activeServices = new HashMap<>();
@Nullable
@GuardedBy("HostServices.class")
private static CountDownLatch serviceShutdownLatch;
private HostServices() {}
/** Create a new {@link ServiceParams} builder. */
public static ServiceParams.Builder serviceParamsBuilder() {
return ServiceParams.builder();
}
/**
* Wait for all services to shutdown. This should be called from a test's tearDown method, to
* ensure the next test is able to use this class again (since Android itself is in control of the
* services).
*/
public static void awaitServiceShutdown() throws InterruptedException {
CountDownLatch latch = null;
synchronized (HostServices.class) {
if (serviceShutdownLatch == null && !activeServices.isEmpty()) {
latch = new CountDownLatch(activeServices.size());
serviceShutdownLatch = latch;
}
serviceParams.clear();
serviceAddresses.clear();
}
if (latch != null) {
if (!latch.await(10, SECONDS)) {
throw new AssertionError("Failed to shut down services");
}
}
synchronized (HostServices.class) {
checkState(activeServices.isEmpty());
checkState(serviceParams.isEmpty());
checkState(serviceAddresses.isEmpty());
serviceShutdownLatch = null;
}
}
/** Create the address for a host-service. */
private static AndroidComponentAddress hostServiceAddress(Context appContext, Class<?> cls) {
// NOTE: Even though we have a context object, we intentionally don't use a "local",
// address, since doing so would mark the address with our UID for security purposes,
// and that would limit the effectiveness of tests.
// Using this API forces us to rely on Binder.getCallingUid.
return AndroidComponentAddress.forRemoteComponent(appContext.getPackageName(), cls.getName());
}
/**
* Allocate a new host service.
*
* @param appContext The application context.
* @return The AndroidComponentAddress of the service.
*/
public static synchronized AndroidComponentAddress allocateService(Context appContext) {
for (Class<?> cls : hostServiceClasses) {
if (!serviceAddresses.containsKey(cls)) {
AndroidComponentAddress address = hostServiceAddress(appContext, cls);
serviceAddresses.put(cls, address);
return address;
}
}
throw new AssertionError("This test helper only supports two services at a time.");
}
/**
* Configure an allocated hosting service.
*
* @param androidComponentAddress The address of the service.
* @param params The parameters used to build the service.
*/
public static synchronized void configureService(
AndroidComponentAddress androidComponentAddress, ServiceParams params) {
for (Class<?> cls : hostServiceClasses) {
if (serviceAddresses.get(cls).equals(androidComponentAddress)) {
checkState(!serviceParams.containsKey(cls));
serviceParams.put(cls, params);
return;
}
}
throw new AssertionError("Unable to find service for address " + androidComponentAddress);
}
/** An Android Service to host each gRPC server. */
private abstract static class HostService extends LifecycleService {
@Nullable private ServiceParams params;
@Nullable private Supplier<IBinder> binderSupplier;
@Override
public final void onCreate() {
super.onCreate();
Class<?> cls = getClass();
synchronized (HostServices.class) {
checkState(!activeServices.containsKey(cls));
activeServices.put(cls, this);
checkState(serviceParams.containsKey(cls));
params = serviceParams.get(cls);
binderSupplier = params.rawBinderSupplier();
}
}
@Override
public final IBinder onBind(Intent intent) {
// Calling super here is a little weird (it returns null), but there's a @CallSuper
// annotation.
super.onBind(intent);
synchronized (HostServices.class) {
Executor executor = params.transactionExecutor();
if (executor != null) {
return new ProxyBinder(binderSupplier.get(), executor);
} else {
return binderSupplier.get();
}
}
}
@Override
public final void onDestroy() {
synchronized (HostServices.class) {
HostService removed = activeServices.remove(getClass());
checkState(removed == this);
serviceAddresses.remove(getClass());
serviceParams.remove(getClass());
if (serviceShutdownLatch != null) {
serviceShutdownLatch.countDown();
}
}
super.onDestroy();
}
}
/** The first concrete host service */
public static final class HostService1 extends HostService {}
/** The second concrete host service */
public static final class HostService2 extends HostService {}
/** Wraps an IBinder to send incoming transactions to a different thread. */
private static class ProxyBinder extends Binder {
private final IBinder delegate;
private final Executor executor;
ProxyBinder(IBinder delegate, Executor executor) {
this.delegate = delegate;
this.executor = executor;
}
@Override
protected boolean onTransact(int code, Parcel parcel, Parcel reply, int flags) {
executor.execute(
() -> {
try {
delegate.transact(code, parcel, reply, flags);
} catch (RemoteException re) {
logger.log(Level.WARNING, "Exception in proxybinder", re);
}
});
return true;
}
}
}

View File

@ -0,0 +1,133 @@
/*
* 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.content.Context;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.ServerStreamTracer;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.BindServiceFlags;
import io.grpc.binder.HostServices;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicies;
import io.grpc.internal.AbstractTransportTest;
import io.grpc.internal.InternalServer;
import io.grpc.internal.ManagedClientTransport;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* A test for the Android binder based transport.
*
* <p>This class really just sets up the test environment. All of the actual tests are defined in
* AbstractTransportTest.
*/
@RunWith(AndroidJUnit4.class)
public final class BinderTransportTest extends AbstractTransportTest {
private final Context appContext = ApplicationProvider.getApplicationContext();
private final ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(2);
@Override
@After
public void tearDown() throws InterruptedException {
super.tearDown();
HostServices.awaitServiceShutdown();
}
@Override
protected InternalServer newServer(List<ServerStreamTracer.Factory> streamTracerFactories) {
AndroidComponentAddress addr = HostServices.allocateService(appContext);
BinderServer binderServer = new BinderServer(addr,
scheduledExecutorService,
streamTracerFactories,
SecurityPolicies.serverInternalOnly(),
InboundParcelablePolicy.DEFAULT);
HostServices.configureService(addr,
HostServices.serviceParamsBuilder()
.setRawBinderSupplier(() -> binderServer.getHostBinder())
.build());
return binderServer;
}
@Override
protected InternalServer newServer(
int port, List<ServerStreamTracer.Factory> streamTracerFactories) {
return newServer(streamTracerFactories);
}
@Override
protected String testAuthority(InternalServer server) {
return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority();
}
@Override
protected ManagedClientTransport newClientTransport(InternalServer server) {
AndroidComponentAddress addr = (AndroidComponentAddress) server.getListenSocketAddress();
return new BinderTransport.BinderClientTransport(
appContext,
addr,
BindServiceFlags.DEFAULTS,
ContextCompat.getMainExecutor(appContext),
scheduledExecutorService,
MoreExecutors.directExecutor(),
SecurityPolicies.internalOnly(),
InboundParcelablePolicy.DEFAULT,
eagAttrs());
}
@Test
@Ignore("BinderTransport doesn't report socket stats yet.")
@Override
public void socketStats() throws Exception {}
@Test
@Ignore("BinderTransport doesn't do message-level flow control yet.")
@Override
public void flowControlPushBack() throws Exception {}
@Test
@Ignore("This test isn't appropriate for BinderTransport.")
@Override
public void serverAlreadyListening() throws Exception {
// This test asserts that two Servers can't listen on the same SocketAddress. For a regular
// network server, that address refers to a network port, and for a BinderServer it
// refers to an Android Service class declared in an applications manifest.
//
// However, unlike a regular network server, which is responsible for listening on its port, a
// BinderServier is not responsible for the creation of its host Service. The opposite is
// the case, with the host Android Service (itself created by the Android platform in
// response to a connection) building the gRPC server.
//
// Passing this test would require us to manually check that two Server instances aren't,
// created with the same Android Service class, but due to the "inversion of control" described
// above, we would actually be testing (and making assumptions about) the precise lifecycle of
// Android Services, which is arguably not our concern.
}
}

View File

@ -0,0 +1,81 @@
/*
* 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;
import android.content.ComponentName;
import android.content.Context;
import io.grpc.ExperimentalApi;
import java.net.SocketAddress;
/** Custom SocketAddress class referencing an Android Component. */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class AndroidComponentAddress extends SocketAddress {
private static final long serialVersionUID = 0L;
private final ComponentName component;
private AndroidComponentAddress(ComponentName component) {
this.component = component;
}
/** Create an address for the given context instance. */
public static AndroidComponentAddress forContext(Context context) {
return forLocalComponent(context, context.getClass());
}
/** Create an address referencing a component within this application. */
public static AndroidComponentAddress forLocalComponent(Context context, Class<?> cls) {
return forComponent(new ComponentName(context, cls));
}
/** Create an address referencing a component (potentially) in another application. */
public static AndroidComponentAddress forRemoteComponent(String packageName, String className) {
return forComponent(new ComponentName(packageName, className));
}
public static AndroidComponentAddress forComponent(ComponentName component) {
return new AndroidComponentAddress(component);
}
public String getAuthority() {
return component.getPackageName();
}
public ComponentName getComponent() {
return component;
}
@Override
public int hashCode() {
return component.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AndroidComponentAddress) {
AndroidComponentAddress that = (AndroidComponentAddress) obj;
return component.equals(that.component);
}
return false;
}
@Override
public String toString() {
return "AndroidComponentAddress[" + component + "]";
}
}

View File

@ -0,0 +1,31 @@
/*
* 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;
import android.content.Intent;
import io.grpc.ExperimentalApi;
/** Constant parts of the gRPC binder transport public API. */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class ApiConstants {
private ApiConstants() {}
/**
* Service Action: Identifies gRPC clients in a {@link android.app.Service#onBind(Intent)} call.
*/
public static final String ACTION_BIND = "grpc.io.action.BIND";
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.grpc.binder.internal;
package io.grpc.binder;
import static android.content.Context.BIND_ABOVE_CLIENT;
import static android.content.Context.BIND_ADJUST_WITH_ACTIVITY;

View File

@ -0,0 +1,107 @@
/*
* 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;
import com.google.common.base.Preconditions;
import io.grpc.ExperimentalApi;
/**
* Contains the policy for accepting inbound parcelable objects.
*
* <p>Since parcelables are generally error prone and parsing a parcelable can have unspecified
* side-effects, their use is generally discouraged. Some use cases require them though (E.g. when
* dealing with some platform-defined objects), so this policy allows them to be supported.
*
* <p>Parcelables can arrive as RPC messages, or as metadata values (in headers or footers). The
* default is to reject both cases, failing the RPC with a PERMISSION_DENED status code. This policy
* can be updated to accept one or both cases.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class InboundParcelablePolicy {
/** The maximum allowed total size of Parcelables in metadata. */
public static final int MAX_PARCELABLE_METADATA_SIZE = 32 * 1024;
public static final InboundParcelablePolicy DEFAULT =
new InboundParcelablePolicy(false, false, MAX_PARCELABLE_METADATA_SIZE);
private final boolean acceptParcelableMetadataValues;
private final boolean acceptParcelableMessages;
private final int maxParcelableMetadataSize;
private InboundParcelablePolicy(
boolean acceptParcelableMetadataValues,
boolean acceptParcelableMessages,
int maxParcelableMetadataSize) {
this.acceptParcelableMetadataValues = acceptParcelableMetadataValues;
this.acceptParcelableMessages = acceptParcelableMessages;
this.maxParcelableMetadataSize = maxParcelableMetadataSize;
}
public boolean shouldAcceptParcelableMetadataValues() {
return acceptParcelableMetadataValues;
}
public boolean shouldAcceptParcelableMessages() {
return acceptParcelableMessages;
}
public int getMaxParcelableMetadataSize() {
return maxParcelableMetadataSize;
}
public static Builder newBuilder() {
return new Builder();
}
/** A builder for InboundParcelablePolicy. */
public static final class Builder {
private boolean acceptParcelableMetadataValues = DEFAULT.acceptParcelableMetadataValues;
private boolean acceptParcelableMessages = DEFAULT.acceptParcelableMessages;
private int maxParcelableMetadataSize = DEFAULT.maxParcelableMetadataSize;
/** Sets whether the policy should accept parcelable metadata values. */
public Builder setAcceptParcelableMetadataValues(boolean acceptParcelableMetadataValues) {
this.acceptParcelableMetadataValues = acceptParcelableMetadataValues;
return this;
}
/** Sets whether the policy should accept parcelable messages. */
public Builder setAcceptParcelableMessages(boolean acceptParcelableMessages) {
this.acceptParcelableMessages = acceptParcelableMessages;
return this;
}
/**
* Sets the maximum allowed total size of parcelables in metadata.
*
* @param maxParcelableMetadataSize must not exceed {@link #MAX_PARCELABLE_METADATA_SIZE}
*/
public Builder setMaxParcelableMetadataSize(int maxParcelableMetadataSize) {
Preconditions.checkArgument(
maxParcelableMetadataSize <= MAX_PARCELABLE_METADATA_SIZE,
"Parcelable metadata size can't exceed MAX_PARCELABLE_METADATA_SIZE.");
this.maxParcelableMetadataSize = maxParcelableMetadataSize;
return this;
}
public InboundParcelablePolicy build() {
return new InboundParcelablePolicy(
acceptParcelableMetadataValues, acceptParcelableMessages, maxParcelableMetadataSize);
}
}
}

View File

@ -0,0 +1,57 @@
/*
* 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;
import android.os.Parcelable;
import io.grpc.ExperimentalApi;
import io.grpc.Metadata;
import io.grpc.binder.internal.InternalMetadataHelper;
/**
* Utility methods for using Android Parcelable objects with gRPC.
*
* <p>This class models the same pattern as the {@code ProtoLiteUtils} class.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class ParcelableUtils {
private ParcelableUtils() {}
/**
* Create a {@link Metadata.Key} for passing a Parcelable object in the metadata of an RPC,
* treating instances as mutable.
*
* <p><b>Note:<b/>Parcelables can only be sent across in-process and binder channels.
*/
public static <P extends Parcelable> Metadata.Key<P> metadataKey(
String name, Parcelable.Creator<P> creator) {
return InternalMetadataHelper.createParcelableMetadataKey(name, creator, false);
}
/**
* Create a {@link Metadata.Key} for passing a Parcelable object in the metadata of an RPC,
* treating instances as immutable. Immutability may be used for optimization purposes (e.g. Not
* copying for in-process calls).
*
* <p><b>Note:<b/>Parcelables can only be sent across in-process and binder channels.
*/
public static <P extends Parcelable> Metadata.Key<P> metadataKeyForImmutableType(
String name, Parcelable.Creator<P> creator) {
return InternalMetadataHelper.createParcelableMetadataKey(name, creator, true);
}
}

View File

@ -0,0 +1,58 @@
/*
* 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;
import android.os.Process;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import javax.annotation.CheckReturnValue;
/** Static factory methods for creating standard security policies. */
@CheckReturnValue
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class SecurityPolicies {
private static final int MY_UID = Process.myUid();
private SecurityPolicies() {}
public static ServerSecurityPolicy serverInternalOnly() {
return new ServerSecurityPolicy();
}
public static SecurityPolicy internalOnly() {
return new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return uid == MY_UID
? Status.OK
: Status.PERMISSION_DENIED.withDescription(
"Rejected by (internal-only) security policy");
}
};
}
public static SecurityPolicy permissionDenied(String description) {
Status denied = Status.PERMISSION_DENIED.withDescription(description);
return new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return denied;
}
};
}
}

View File

@ -0,0 +1,57 @@
/*
* 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;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import javax.annotation.CheckReturnValue;
/**
* Decides whether a given Android UID is authorized to access some resource.
*
* <p><b>IMPORTANT</b> For any concrete extensions of this class, it's assumed that the
* authorization status of a given UID will <b>not</b> change as long as a process with that UID is
* alive.
*
* <p>In order words, we expect the security policy for a given transport to remain constant for the
* lifetime of that transport. This is considered acceptable because no transport will survive the
* re-installation of the applications involved.
*/
@CheckReturnValue
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public abstract class SecurityPolicy {
/**
* Package visible constructor because we want this package to retain control over any new
* policies for now.
*/
SecurityPolicy() {}
/**
* Decides whether the given Android UID is authorized. (Validity is implementation dependent).
*
* <p><b>IMPORTANT</b>: This method may block for extended periods of time.
*
* <p>As long as any given UID has active processes, this method should return the same value for
* that UID. In order words, policy changes which occur while a transport instance is active, will
* have no effect on that transport instance.
*
* @param uid The Android UID to authenticate.
* @return A gRPC {@link Status} object, with OK indicating authorized.
*/
public abstract Status checkAuthorization(int uid);
}

View File

@ -0,0 +1,86 @@
/*
* 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;
import com.google.common.collect.ImmutableMap;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.CheckReturnValue;
/**
* A security policy for a gRPC server.
*
* Contains a default policy, and optional policies for each server.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class ServerSecurityPolicy {
private final SecurityPolicy defaultPolicy;
private final ImmutableMap<String, SecurityPolicy> perServicePolicies;
ServerSecurityPolicy() {
this(ImmutableMap.of());
}
private ServerSecurityPolicy(ImmutableMap<String, SecurityPolicy> perServicePolicies) {
this.defaultPolicy = SecurityPolicies.internalOnly();
this.perServicePolicies = perServicePolicies;
}
/**
* Return whether the given Android UID is authorized to access a particular service.
*
* <b>IMPORTANT</b>: This method may block for extended periods of time.
*
* @param uid The Android UID to authenticate.
* @param serviceName The name of the gRPC service being called.
*/
@CheckReturnValue
public Status checkAuthorizationForService(int uid, String serviceName) {
return perServicePolicies.getOrDefault(serviceName, defaultPolicy).checkAuthorization(uid);
}
public static Builder newBuilder() {
return new Builder();
}
/** Builder for an AndroidServiceSecurityPolicy. */
public static final class Builder {
private final Map<String, SecurityPolicy> grpcServicePolicies;
private Builder() {
grpcServicePolicies = new HashMap<>();
}
/**
* Specify a policy specific to a particular gRPC service.
*
* @param serviceName The fully qualified name of the gRPC service (from the proto).
* @param policy The security policy to apply to the service.
*/
public Builder servicePolicy(String serviceName, SecurityPolicy policy) {
grpcServicePolicies.put(serviceName, policy);
return this;
}
public ServerSecurityPolicy build() {
return new ServerSecurityPolicy(ImmutableMap.copyOf(grpcServicePolicies));
}
}
}

View File

@ -0,0 +1,167 @@
/*
* 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 android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import com.google.common.collect.ImmutableList;
import io.grpc.Attributes;
import io.grpc.Grpc;
import io.grpc.InternalChannelz.SocketStats;
import io.grpc.InternalInstrumented;
import io.grpc.ServerStreamTracer;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.ServerSecurityPolicy;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.InternalServer;
import io.grpc.internal.ServerListener;
import io.grpc.internal.SharedResourceHolder;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
* A gRPC InternalServer which accepts connections via a host AndroidService.
*
* <p>Multiple incoming connections transports may be active at a time.
*
* <b>IMPORTANT</b>: This implementation must comply with this published wire format.
* https://github.com/grpc/proposal/blob/master/L73-java-binderchannel/wireformat.md
*/
@ThreadSafe
final class BinderServer implements InternalServer, LeakSafeOneWayBinder.TransactionHandler {
private final boolean useSharedTimer;
private final ScheduledExecutorService executorService;
private final ImmutableList<ServerStreamTracer.Factory> streamTracerFactories;
private final AndroidComponentAddress listenAddress;
private final LeakSafeOneWayBinder hostServiceBinder;
private final ServerSecurityPolicy serverSecurityPolicy;
private final InboundParcelablePolicy inboundParcelablePolicy;
@GuardedBy("this")
private ServerListener listener;
@GuardedBy("this")
private boolean shutdown;
BinderServer(
AndroidComponentAddress listenAddress,
@Nullable ScheduledExecutorService executorService,
List<? extends ServerStreamTracer.Factory> streamTracerFactories,
ServerSecurityPolicy serverSecurityPolicy,
InboundParcelablePolicy inboundParcelablePolicy) {
this.listenAddress = listenAddress;
useSharedTimer = executorService == null;
this.executorService =
useSharedTimer ? SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE) : executorService;
this.streamTracerFactories =
ImmutableList.copyOf(checkNotNull(streamTracerFactories, "streamTracerFactories"));
this.serverSecurityPolicy = checkNotNull(serverSecurityPolicy, "serverSecurityPolicy");
this.inboundParcelablePolicy = inboundParcelablePolicy;
hostServiceBinder = new LeakSafeOneWayBinder(this);
}
/** Return the binder we're listening on. */
public IBinder getHostBinder() {
return hostServiceBinder;
}
@Override
public synchronized void start(ServerListener serverListener) throws IOException {
this.listener = serverListener;
}
@Override
public SocketAddress getListenSocketAddress() {
return listenAddress;
}
@Override
public List<? extends SocketAddress> getListenSocketAddresses() {
return ImmutableList.of(listenAddress);
}
@Override
public InternalInstrumented<SocketStats> getListenSocketStats() {
return null;
}
@Override
@Nullable
public List<InternalInstrumented<SocketStats>> getListenSocketStatsList() {
return null;
}
@Override
public synchronized void shutdown() {
if (!shutdown) {
shutdown = true;
if (useSharedTimer) {
// TODO: Transports may still be using this resource. They should
// be managing its use as well.
SharedResourceHolder.release(GrpcUtil.TIMER_SERVICE, executorService);
}
// Break the connection to the binder. We'll receive no more transactions.
hostServiceBinder.detach();
listener.serverShutdown();
}
}
@Override
public String toString() {
return "BinderServer[" + listenAddress + "]";
}
@Override
public synchronized boolean handleTransaction(int code, Parcel parcel) {
if (code == BinderTransport.SETUP_TRANSPORT) {
int version = parcel.readInt();
// If the client-provided version is more recent, we accept the connection,
// but specify the older version which we support.
if (version >= BinderTransport.EARLIEST_SUPPORTED_WIRE_FORMAT_VERSION) {
IBinder callbackBinder = parcel.readStrongBinder();
if (callbackBinder != null) {
int callingUid = Binder.getCallingUid();
Attributes.Builder attrsBuilder =
Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, listenAddress)
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new BoundClientAddress(callingUid))
.set(BinderTransport.REMOTE_UID, callingUid)
.set(BinderTransport.SERVER_AUTHORITY, listenAddress.getAuthority())
.set(BinderTransport.INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy);
BinderTransportSecurity.attachAuthAttrs(attrsBuilder, callingUid, serverSecurityPolicy);
// Create a new transport and let our listener know about it.
BinderTransport.BinderServerTransport transport =
new BinderTransport.BinderServerTransport(
executorService, attrsBuilder.build(), streamTracerFactories, callbackBinder);
transport.setServerTransportListener(listener.transportCreated(transport));
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,903 @@
/*
* 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 com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import android.content.Context;
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 com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.Grpc;
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.ApiConstants;
import io.grpc.binder.BindServiceFlags;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicy;
import io.grpc.internal.ClientStream;
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.ServerStream;
import io.grpc.internal.ServerTransport;
import io.grpc.internal.ServerTransportListener;
import io.grpc.internal.StatsTraceContext;
import io.grpc.internal.TimeProvider;
import java.util.ArrayList;
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.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
* Base class for binder-based gRPC transport.
*
* <p>This is used on both the client and service sides of the transport.
*
* <p>A note on synchronization. The nature of this class's interaction with each stream
* (bi-directional communication between gRPC calls and binder transactions) means that acquiring
* multiple locks in two different orders can happen easily. E.g. binder transactions will arrive in
* this class, and need to passed to a stream instance, whereas gRPC calls on a stream instance will
* need to call into this class to send a transaction (possibly waiting for the transport to become
* ready).
*
* <p>The split between Outbound & Inbound helps reduce this risk, but not entirely remove it.
*
* <p>For this reason, while most state within this class is guarded by this instance, methods
* exposed to individual stream instances need to use atomic or volatile types, since those calls
* will already be synchronized on the individual RPC objects.
*
* <p><b>IMPORTANT</b>: This implementation must comply with this published wire format.
* https://github.com/grpc/proposal/blob/master/L73-java-binderchannel/wireformat.md
*/
@ThreadSafe
abstract class BinderTransport
implements LeakSafeOneWayBinder.TransactionHandler, IBinder.DeathRecipient {
private static final Logger logger = Logger.getLogger(BinderTransport.class.getName());
/**
* Attribute used to store the Android UID of the remote app. This is guaranteed to be set on any
* active transport.
*/
static final Attributes.Key<Integer> REMOTE_UID = Attributes.Key.create("remote-uid");
/** The authority of the server. */
static final Attributes.Key<String> SERVER_AUTHORITY = Attributes.Key.create("server-authority");
/** A transport attribute to hold the {@link InboundParcelablePolicy}. */
static final Attributes.Key<InboundParcelablePolicy> INBOUND_PARCELABLE_POLICY =
Attributes.Key.create("inbound-parcelable-policy");
/**
* Version code for this wire format.
*
* <p>Should this change, we should still endeavor to support earlier wire-format versions. If
* that's not possible, {@link EARLIEST_SUPPORTED_WIRE_FORMAT_VERSION} should be updated below.
*/
static final int WIRE_FORMAT_VERSION = 1;
/** The version code of the earliest wire format we support. */
static final int EARLIEST_SUPPORTED_WIRE_FORMAT_VERSION = 1;
/** The max number of "in-flight" bytes before we start buffering transactions. */
private static final int TRANSACTION_BYTES_WINDOW = 128 * 1024;
/** The number of in-flight bytes we should receive between sendings acks to our peer. */
private static final int TRANSACTION_BYTES_WINDOW_FORCE_ACK = 16 * 1024;
/**
* Sent from the client to host service binder to initiate a new transport, and from the host to
* the binder. and from the host s Followed by: int wire_protocol_version IBinder
* client_transports_callback_binder
*/
static final int SETUP_TRANSPORT = IBinder.FIRST_CALL_TRANSACTION;
/** Send to shutdown the transport from either end. */
static final int SHUTDOWN_TRANSPORT = IBinder.FIRST_CALL_TRANSACTION + 1;
/** Send to acknowledge receipt of rpc bytes, for flow control. */
static final int ACKNOWLEDGE_BYTES = IBinder.FIRST_CALL_TRANSACTION + 2;
/** A ping request. */
private static final int PING = IBinder.FIRST_CALL_TRANSACTION + 3;
/** A response to a ping. */
private static final int PING_RESPONSE = IBinder.FIRST_CALL_TRANSACTION + 4;
/** Reserved transaction IDs for any special events we might need. */
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;
/** The last call ID we can use. */
private static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION;
/** The states of this transport. */
protected enum TransportState {
NOT_STARTED, // We haven't been started yet.
SETUP, // We're setting up the connection.
READY, // The transport is ready.
SHUTDOWN, // We've been shutdown and won't accept any additional calls (thought existing calls
// may continue).
SHUTDOWN_TERMINATED // We've been shutdown completely (or we failed to start). We can't send or
// receive any data.
}
private final ScheduledExecutorService scheduledExecutorService;
private final InternalLogId logId;
private final LeakSafeOneWayBinder incomingBinder;
protected final ConcurrentHashMap<Integer, Inbound<?>> ongoingCalls;
@GuardedBy("this")
protected Attributes attributes;
@GuardedBy("this")
private TransportState transportState = TransportState.NOT_STARTED;
@GuardedBy("this")
@Nullable
protected Status shutdownStatus;
@Nullable private IBinder outgoingBinder;
/** The number of outgoing bytes we've transmitted. */
private final AtomicLong numOutgoingBytes;
/** The number of incoming bytes we've received. */
private final AtomicLong numIncomingBytes;
/** The number of our outgoing bytes our peer has told us it received. */
private long acknowledgedOutgoingBytes;
/** The number of incoming bytes we've told our peer we've received. */
private long acknowledgedIncomingBytes;
/**
* Whether there are too many unacknowledged outgoing bytes to allow more RPCs right now. This is
* volatile because it'll be read without holding the lock.
*/
private volatile boolean transmitWindowFull;
private BinderTransport(
ScheduledExecutorService scheduledExecutorService,
Attributes attributes,
InternalLogId logId) {
this.scheduledExecutorService = scheduledExecutorService;
this.attributes = attributes;
this.logId = logId;
incomingBinder = new LeakSafeOneWayBinder(this);
ongoingCalls = new ConcurrentHashMap<>();
numOutgoingBytes = new AtomicLong();
numIncomingBytes = new AtomicLong();
}
// Override in child class.
public final ScheduledExecutorService getScheduledExecutorService() {
return scheduledExecutorService;
}
// Override in child class.
public final ListenableFuture<SocketStats> getStats() {
return immediateFuture(null);
}
// Override in child class.
public final InternalLogId getLogId() {
return logId;
}
// Override in child class.
public final synchronized Attributes getAttributes() {
return attributes;
}
/**
* Returns whether this transport is able to send rpc transactions. Intentionally unsynchronized
* since this will be called while Outbound is held.
*/
final boolean isReady() {
return !transmitWindowFull;
}
abstract void notifyShutdown(Status shutdownStatus);
abstract void notifyTerminated();
@GuardedBy("this")
boolean inState(TransportState transportState) {
return this.transportState == transportState;
}
@GuardedBy("this")
boolean isShutdown() {
return inState(TransportState.SHUTDOWN) || inState(TransportState.SHUTDOWN_TERMINATED);
}
@GuardedBy("this")
final void setState(TransportState newState) {
checkTransition(transportState, newState);
transportState = newState;
}
@GuardedBy("this")
protected boolean setOutgoingBinder(IBinder binder) {
this.outgoingBinder = binder;
try {
binder.linkToDeath(this, 0);
return true;
} catch (RemoteException re) {
return false;
}
}
@Override
public synchronized void binderDied() {
shutdownInternal(Status.UNAVAILABLE.withDescription("binderDied"), true);
}
@GuardedBy("this")
final void shutdownInternal(Status shutdownStatus, boolean forceTerminate) {
if (!isShutdown()) {
this.shutdownStatus = shutdownStatus;
setState(TransportState.SHUTDOWN);
notifyShutdown(shutdownStatus);
}
if (!inState(TransportState.SHUTDOWN_TERMINATED)
&& (forceTerminate || ongoingCalls.isEmpty())) {
incomingBinder.detach();
setState(TransportState.SHUTDOWN_TERMINATED);
sendShutdownTransaction();
ArrayList<Inbound<?>> calls = new ArrayList<>(ongoingCalls.values());
ongoingCalls.clear();
scheduledExecutorService.execute(
() -> {
for (Inbound<?> inbound : calls) {
synchronized (inbound) {
inbound.closeAbnormal(shutdownStatus);
}
}
notifyTerminated();
});
}
}
@GuardedBy("this")
final void sendSetupTransaction() {
sendSetupTransaction(checkNotNull(outgoingBinder));
}
@GuardedBy("this")
final void sendSetupTransaction(IBinder iBinder) {
Parcel parcel = Parcel.obtain();
parcel.writeInt(WIRE_FORMAT_VERSION);
parcel.writeStrongBinder(incomingBinder);
try {
if (!iBinder.transact(SETUP_TRANSPORT, parcel, null, IBinder.FLAG_ONEWAY)) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Failed sending SETUP_TRANSPORT transaction"), true);
}
} catch (RemoteException re) {
shutdownInternal(statusFromRemoteException(re), true);
}
parcel.recycle();
}
@GuardedBy("this")
private final void sendShutdownTransaction() {
if (outgoingBinder != null) {
try {
outgoingBinder.unlinkToDeath(this, 0);
} catch (NoSuchElementException e) {
// Ignore.
}
Parcel parcel = Parcel.obtain();
try {
outgoingBinder.transact(SHUTDOWN_TRANSPORT, parcel, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException re) {
// Ignore.
}
parcel.recycle();
}
}
protected synchronized void sendPing(int id) throws StatusException {
if (inState(TransportState.SHUTDOWN_TERMINATED)) {
throw shutdownStatus.asException();
} else if (outgoingBinder == null) {
throw Status.FAILED_PRECONDITION.withDescription("Transport not ready.").asException();
} else {
Parcel parcel = Parcel.obtain();
parcel.writeInt(id);
try {
outgoingBinder.transact(PING, parcel, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException re) {
throw statusFromRemoteException(re).asException();
} finally {
parcel.recycle();
}
}
}
protected void unregisterInbound(Inbound<?> inbound) {
unregisterCall(inbound.callId);
}
final void unregisterCall(int callId) {
boolean removed = (ongoingCalls.remove(callId) != null);
if (removed && ongoingCalls.isEmpty()) {
// Possibly shutdown (not synchronously, since inbound is held).
scheduledExecutorService.execute(
() -> {
synchronized (this) {
if (inState(TransportState.SHUTDOWN)) {
// No more ongoing calls, and we're shutdown. Finish the shutdown.
shutdownInternal(shutdownStatus, true);
}
}
});
}
}
final void sendTransaction(int callId, Parcel parcel) throws StatusException {
int dataSize = parcel.dataSize();
try {
if (!outgoingBinder.transact(callId, parcel, null, IBinder.FLAG_ONEWAY)) {
throw Status.UNAVAILABLE.withDescription("Failed sending transaction").asException();
}
} catch (RemoteException re) {
throw statusFromRemoteException(re).asException();
}
long nob = numOutgoingBytes.addAndGet(dataSize);
if ((nob - acknowledgedOutgoingBytes) > TRANSACTION_BYTES_WINDOW) {
logger.log(Level.FINE, "transmist window full. Outgoing=" + nob + " Ack'd Outgoing=" +
acknowledgedOutgoingBytes + " " + this);
transmitWindowFull = true;
}
}
final void sendOutOfBandClose(int callId, Status status) {
Parcel parcel = Parcel.obtain();
parcel.writeInt(0); // Placeholder for flags. Will be filled in below.
int flags = TransactionUtils.writeStatus(parcel, status);
TransactionUtils.fillInFlags(parcel, flags | TransactionUtils.FLAG_OUT_OF_BAND_CLOSE);
try {
sendTransaction(callId, parcel);
} catch (StatusException e) {
logger.log(Level.WARNING, "Failed sending oob close transaction", e);
}
parcel.recycle();
}
@Override
public final boolean handleTransaction(int code, Parcel parcel) {
if (code < FIRST_CALL_ID) {
synchronized (this) {
switch (code) {
case ACKNOWLEDGE_BYTES:
handleAcknowledgedBytes(parcel.readLong());
break;
case SHUTDOWN_TRANSPORT:
shutdownInternal(
Status.UNAVAILABLE.withDescription("transport shutdown by peer"), true);
break;
case SETUP_TRANSPORT:
handleSetupTransport(parcel);
break;
case PING:
handlePing(parcel);
break;
case PING_RESPONSE:
handlePingResponse(parcel);
break;
default:
return false;
}
return true;
}
} else {
int size = parcel.dataSize();
Inbound<?> inbound = ongoingCalls.get(code);
if (inbound == null) {
synchronized (this) {
if (!isShutdown()) {
// Create a new inbound. Strictly speaking we could end up doing this twice on
// two threads, hence the need to use putIfAbsent, and check its result.
inbound = createInbound(code);
if (inbound != null) {
Inbound<?> inbound2 = ongoingCalls.putIfAbsent(code, inbound);
if (inbound2 != null) {
inbound = inbound2;
}
}
}
}
}
if (inbound != null) {
inbound.handleTransaction(parcel);
}
long nib = numIncomingBytes.addAndGet(size);
if ((nib - acknowledgedIncomingBytes) > TRANSACTION_BYTES_WINDOW_FORCE_ACK) {
synchronized (this) {
sendAcknowledgeBytes(checkNotNull(outgoingBinder));
}
}
return true;
}
}
@Nullable
@GuardedBy("this")
protected Inbound<?> createInbound(int callId) {
return null;
}
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {}
@GuardedBy("this")
private final void handlePing(Parcel parcel) {
if (transportState == TransportState.READY) {
try {
outgoingBinder.transact(PING_RESPONSE, parcel, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException re) {
// Ignore.
}
}
}
@GuardedBy("this")
protected void handlePingResponse(Parcel parcel) {}
@GuardedBy("this")
private void sendAcknowledgeBytes(IBinder iBinder) {
// Send a transaction to acknowledge reception of incoming data.
long n = numIncomingBytes.get();
acknowledgedIncomingBytes = n;
Parcel parcel = Parcel.obtain();
parcel.writeLong(n);
try {
if (!iBinder.transact(ACKNOWLEDGE_BYTES, parcel, null, IBinder.FLAG_ONEWAY)) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Failed sending ack bytes transaction"), true);
}
} catch (RemoteException re) {
shutdownInternal(statusFromRemoteException(re), true);
}
parcel.recycle();
}
@GuardedBy("this")
final void handleAcknowledgedBytes(long numBytes) {
// The remote side has acknowledged reception of rpc data.
// (update with Math.max in case transactions are delivered out of order).
acknowledgedOutgoingBytes = wrapAwareMax(acknowledgedOutgoingBytes, numBytes);
if ((numOutgoingBytes.get() - acknowledgedOutgoingBytes) < TRANSACTION_BYTES_WINDOW
&& transmitWindowFull) {
logger.log(Level.FINE,
"handleAcknowledgedBytes: Transmit Window No-Longer Full. Unblock calls: " + this);
// We're ready again, and need to poke any waiting transactions.
transmitWindowFull = false;
for (Inbound<?> inbound : ongoingCalls.values()) {
inbound.onTransportReady();
}
}
}
private static final long wrapAwareMax(long a, long b) {
return a - b < 0 ? b : a;
}
/** Concrete client-side transport implementation. */
@ThreadSafe
static final class BinderClientTransport extends BinderTransport
implements ConnectionClientTransport, Bindable.Observer {
private final Executor blockingExecutor;
private final SecurityPolicy securityPolicy;
private final Bindable serviceBinding;
/** Number of ongoing calls which keep this transport "in-use". */
private final AtomicInteger numInUseStreams;
private final PingTracker pingTracker;
@Nullable private ManagedClientTransport.Listener clientTransportListener;
@GuardedBy("this")
private int latestCallId = FIRST_CALL_ID;
BinderClientTransport(
Context sourceContext,
AndroidComponentAddress targetAddress,
BindServiceFlags bindServiceFlags,
Executor mainThreadExecutor,
ScheduledExecutorService scheduledExecutorService,
Executor blockingExecutor,
SecurityPolicy securityPolicy,
InboundParcelablePolicy inboundParcelablePolicy,
Attributes eagAttrs) {
super(
scheduledExecutorService,
buildClientAttributes(eagAttrs, sourceContext, targetAddress, inboundParcelablePolicy),
buildLogId(sourceContext, targetAddress));
this.blockingExecutor = blockingExecutor;
this.securityPolicy = securityPolicy;
numInUseStreams = new AtomicInteger();
pingTracker = new PingTracker(TimeProvider.SYSTEM_TIME_PROVIDER, (id) -> sendPing(id));
serviceBinding =
new ServiceBinding(
mainThreadExecutor,
sourceContext,
targetAddress.getComponent(),
ApiConstants.ACTION_BIND,
bindServiceFlags.toInteger(),
this);
}
@Override
public synchronized void onBound(IBinder binder) {
sendSetupTransaction(binder);
}
@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);
serviceBinding.bind();
}
}
};
}
@Override
public synchronized ClientStream newStream(
final MethodDescriptor<?, ?> method,
final Metadata headers,
final CallOptions callOptions) {
if (isShutdown()) {
return newFailingClientStream(shutdownStatus, callOptions, attributes, headers);
} else {
int callId = latestCallId++;
if (latestCallId == LAST_CALL_ID) {
latestCallId = FIRST_CALL_ID;
}
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, callOptions, attributes, headers);
} else {
if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) {
clientTransportListener.transportInUse(true);
}
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(callOptions, attributes, headers);
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")
public void notifyShutdown(Status status) {
clientTransportListener.transportShutdown(status);
}
@Override
@GuardedBy("this")
public void notifyTerminated() {
if (numInUseStreams.getAndSet(0) > 0) {
clientTransportListener.transportInUse(false);
}
serviceBinding.unbind();
clientTransportListener.transportTerminated();
}
@Override
@GuardedBy("this")
protected void handleSetupTransport(Parcel parcel) {
// Add the remote uid to our attributes.
attributes = setSecurityAttrs(attributes, Binder.getCallingUid());
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 {
blockingExecutor.execute(() -> checkSecurityPolicy(binder));
}
}
}
private void checkSecurityPolicy(IBinder binder) {
Status authorization;
Integer remoteUid;
synchronized (this) {
remoteUid = attributes.get(REMOTE_UID);
}
if (remoteUid == null) {
authorization = Status.UNAUTHENTICATED.withDescription("No remote UID available");
} else {
authorization = securityPolicy.checkAuthorization(remoteUid);
}
synchronized (this) {
if (inState(TransportState.SETUP)) {
if (!authorization.isOk()) {
shutdownInternal(authorization, true);
} else if (!setOutgoingBinder(binder)) {
shutdownInternal(
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
} else {
// Check state again, since a failure inside setOutgoingBinder (or a callback it
// triggers), could have shut us down.
if (!isShutdown()) {
setState(TransportState.READY);
clientTransportListener.transportReady();
}
}
}
}
}
@GuardedBy("this")
@Override
protected void handlePingResponse(Parcel parcel) {
pingTracker.onPingResponse(parcel.readInt());
}
private static ClientStream newFailingClientStream(
Status failure, CallOptions callOptions, Attributes attributes, Metadata headers) {
StatsTraceContext statsTraceContext =
StatsTraceContext.newClientContext(callOptions, attributes, headers);
statsTraceContext.clientOutboundHeaders();
statsTraceContext.streamClosed(failure);
return new FailingClientStream(failure);
}
private static InternalLogId buildLogId(
Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate(
BinderClientTransport.class,
sourceContext.getClass().getSimpleName()
+ "->"
+ targetAddress.getComponent().toShortString());
}
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. */
static final class BinderServerTransport extends BinderTransport implements ServerTransport {
private final List<ServerStreamTracer.Factory> streamTracerFactories;
@Nullable private ServerTransportListener serverTransportListener;
BinderServerTransport(
ScheduledExecutorService scheduledExecutorService,
Attributes attributes,
List<ServerStreamTracer.Factory> streamTracerFactories,
IBinder callbackBinder) {
super(scheduledExecutorService, attributes, buildLogId(attributes));
this.streamTracerFactories = streamTracerFactories;
setOutgoingBinder(callbackBinder);
}
synchronized void setServerTransportListener(ServerTransportListener serverTransportListener) {
this.serverTransportListener = serverTransportListener;
if (isShutdown()) {
setState(TransportState.SHUTDOWN_TERMINATED);
notifyTerminated();
} 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")
public void notifyShutdown(Status status) {
// Nothing to do.
}
@Override
@GuardedBy("this")
public 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:
checkState(current == TransportState.NOT_STARTED);
break;
case READY:
checkState(current == TransportState.NOT_STARTED || current == TransportState.SETUP);
break;
case SHUTDOWN:
checkState(
current == TransportState.NOT_STARTED
|| current == TransportState.SETUP
|| current == TransportState.READY);
break;
case SHUTDOWN_TERMINATED:
checkState(current == TransportState.SHUTDOWN);
break;
default:
throw new AssertionError();
}
}
@VisibleForTesting
Map<Integer, Inbound<?>> getOngoingCalls() {
return ongoingCalls;
}
private static Status statusFromRemoteException(RemoteException e) {
if (e instanceof DeadObjectException || e instanceof TransactionTooLargeException) {
// These are to be expected from time to time and can simply be retried.
return Status.UNAVAILABLE.withCause(e);
}
// Otherwise, this exception from transact is unexpected.
return Status.INTERNAL.withCause(e);
}
}

View File

@ -0,0 +1,130 @@
/*
* 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 io.grpc.Attributes;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel;
import io.grpc.ServerBuilder;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import io.grpc.binder.ServerSecurityPolicy;
import io.grpc.internal.GrpcAttributes;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.CheckReturnValue;
/**
* Manages security for an Android Service hosted gRPC server.
*
* <p>Attaches authorization state to a newly-created transport, and contains a
* ServerInterceptor which ensures calls are authorized before allowing them to proceed.
*/
final class BinderTransportSecurity {
private static final Attributes.Key<TransportAuthorizationState> TRANSPORT_AUTHORIZATION_STATE =
Attributes.Key.create("transport-authorization-state");
private BinderTransportSecurity() {}
/**
* Install a security policy on an about-to-be created server.
*
* @param serverBuilder The ServerBuilder being used to create the server.
*/
static void installAuthInterceptor(ServerBuilder<?> serverBuilder) {
serverBuilder.intercept(new ServerAuthInterceptor());
}
/**
* Attach the given security policy to the transport attributes being built. Will be used by the
* auth interceptor to confirm accept or reject calls.
*
* @param builder The {@link Attributes.Builder} for the transport being created.
* @param remoteUid The remote UID of the transport.
* @param securityPolicy The policy to enforce on this transport.
*/
static void attachAuthAttrs(
Attributes.Builder builder, int remoteUid, ServerSecurityPolicy securityPolicy) {
builder
.set(
TRANSPORT_AUTHORIZATION_STATE,
new TransportAuthorizationState(remoteUid, securityPolicy))
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY);
}
/**
* Intercepts server calls and ensures they're authorized before allowing them to proceed.
* Authentication state is fetched from the call attributes, inherited from the transport.
*/
private static final class ServerAuthInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
Status authStatus =
call.getAttributes()
.get(TRANSPORT_AUTHORIZATION_STATE)
.checkAuthorization(call.getMethodDescriptor());
if (authStatus.isOk()) {
return next.startCall(call, headers);
} else {
call.close(authStatus, new Metadata());
return new ServerCall.Listener<ReqT>() {};
}
}
}
/**
* Maintaines the authorization state for a single transport instance. This class lives for the
* lifetime of a single transport.
*/
private static final class TransportAuthorizationState {
private final int uid;
private final ServerSecurityPolicy policy;
private final ConcurrentHashMap<String, Status> serviceAuthorization;
TransportAuthorizationState(int uid, ServerSecurityPolicy policy) {
this.uid = uid;
this.policy = policy;
serviceAuthorization = new ConcurrentHashMap<>(8);
}
/** Get whether we're authorized to make this call. */
@CheckReturnValue
Status checkAuthorization(MethodDescriptor<?, ?> method) {
String serviceName = method.getServiceName();
// Only cache decisions if the method can be sampled for tracing,
// which is true for all generated methods. Otherwise, programatically
// created methods could casue this cahe to grow unbounded.
boolean useCache = method.isSampledToLocalTracing();
Status authorization;
if (useCache) {
authorization = serviceAuthorization.get(serviceName);
if (authorization != null) {
return authorization;
}
}
authorization = policy.checkAuthorizationForService(uid, serviceName);
if (useCache) {
serviceAuthorization.putIfAbsent(serviceName, authorization);
}
return authorization;
}
}
}

View File

@ -0,0 +1,159 @@
/*
* 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 com.google.common.primitives.Ints;
import io.grpc.Drainable;
import io.grpc.KnownLength;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
/**
* A simple InputStream from a 2-dimensional byte array.
*
* Used to provide message data from incoming blocks of data. It is assumed that
* all byte arrays passed in the constructor of this this class are owned by the new
* instance.
*
* This also assumes byte arrays are created by the BlockPool class, and should
* be returned to it when this class is closed.
*/
@NotThreadSafe
final class BlockInputStream extends InputStream implements KnownLength, Drainable {
@Nullable
private byte[][] blocks;
@Nullable
private byte[] currentBlock;
private int blockIndex;
private int blockOffset;
private int available;
private boolean closed;
/**
* Creates a new stream with a single block.
*
* @param block The single byte array block, ownership of which is
* passed to this instance.
*/
BlockInputStream(byte[] block) {
this.blocks = null;
currentBlock = block.length > 0 ? block : null;
available = block.length;
}
/**
* Creates a new stream from a sequence of blocks.
*
* @param blocks A two dimensional byte array containing the data. Ownership
* of all blocks is passed to this instance.
* @param available The number of bytes available in total. This may be
* less than (but never more than) the total size of all byte arrays in blocks.
*/
BlockInputStream(byte[][] blocks, int available) {
this.blocks = blocks;
this.available = available;
if (blocks.length > 0) {
currentBlock = blocks[0];
}
}
@Override
public int read() throws IOException {
if (currentBlock != null) {
int res = currentBlock[blockOffset++];
available -= 1;
if (blockOffset == currentBlock.length) {
nextBlock();
}
return res;
}
return -1;
}
@Override
public int read(byte[] data, int off, int len) throws IOException {
int stillToRead = len;
while (currentBlock != null) {
int n = Ints.min(stillToRead, currentBlock.length - blockOffset, available);
System.arraycopy(currentBlock, blockOffset, data, off, n);
off += n;
stillToRead -= n;
available -= n;
if (stillToRead == 0) {
blockOffset += n;
if (blockOffset == currentBlock.length) {
nextBlock();
}
break;
} else {
nextBlock();
}
}
int bytesRead = len - stillToRead;
if (bytesRead > 0 || available > 0) {
return bytesRead;
}
return -1;
}
private void nextBlock() {
blockIndex += 1;
blockOffset = 0;
if (blocks != null && blockIndex < blocks.length) {
currentBlock = blocks[blockIndex];
} else {
currentBlock = null;
}
}
@Override
public int available() {
return available;
}
@Override
public int drainTo(OutputStream output) throws IOException {
int res = available;
while (available > 0) {
int n = Math.min(currentBlock.length - blockOffset, available);
output.write(currentBlock, blockOffset, n);
available -= n;
nextBlock();
}
return res;
}
@Override
public void close() {
if (!closed) {
closed = true;
if (blocks != null) {
for (byte[] block : blocks) {
BlockPool.releaseBlock(block);
}
} else if (currentBlock != null) {
BlockPool.releaseBlock(currentBlock);
}
currentBlock = null;
blocks = null;
}
}
}

View File

@ -0,0 +1,80 @@
/*
* 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 io.grpc.internal.GrpcUtil;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Manages a pool of byte-array blocks.
*
* <p>Unfortunately, the Android Parcel api only allws us to read a block of N bytes when we have a
* byte array of size N. This means we can't simply read into a large block and be done with it, we
* need to allocate a new buffer specifically. Boo, Android.
*
* <p>When writing data though, we can use a fixed-size buffer, so when large messages are
* split into standard-sized blocks, we only need a byte array allocation to read the last
* block.
*
* <p>This class maintains a pool of blocks of standard size, but also provides smaller blocks when
* requested. Currently, blocks of standard size are retained in the pool, when released, but we
* could chose to change this strategy.
*/
final class BlockPool {
/**
* The size of each standard block. (Currently 16k)
* The block size must be at least as large as the maximum header list size.
*/
private static final int BLOCK_SIZE = Math.max(16 * 1024, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE);
/**
* Maximum number of blocks to keep around. (Max 128k). This limit is a judgement call. 128k is
* small enough that it shouldn't significantly affect the memory usage of a large app, but large
* enough that it should to reduce allocation churn while gRPC is in use.
*/
private static final int BLOCK_POOL_SIZE = 128 * 1024 / BLOCK_SIZE;
/** A pool of byte arrays of standard size. We don't use any blocking methods of this instance. */
private static final Queue<byte[]> blockPool = new LinkedBlockingQueue<>(BLOCK_POOL_SIZE);
private BlockPool() {}
/** Acquire a block of standard size. */
static byte[] acquireBlock() {
return acquireBlock(BLOCK_SIZE);
}
/** Acquire a block of the specified size. */
static byte[] acquireBlock(int size) {
if (size == BLOCK_SIZE) {
byte[] block = blockPool.poll();
if (block != null) {
return block;
}
}
return new byte[size];
}
/** Release a now-unused block. */
static void releaseBlock(byte[] block) {
if (block.length == BLOCK_SIZE) {
blockPool.offer(block);
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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 java.net.SocketAddress;
/** An address to represent a binding from a remote client. */
final class BoundClientAddress extends SocketAddress {
private static final long serialVersionUID = 0L;
/** The UID of the address. For incoming binder transactions, this is all the info we have. */
private final int uid;
BoundClientAddress(int uid) {
this.uid = uid;
}
@Override
public int hashCode() {
return uid;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BoundClientAddress) {
BoundClientAddress that = (BoundClientAddress) obj;
return uid == that.uid;
}
return false;
}
@Override
public String toString() {
return "BoundClientAddress[" + uid + "]";
}
}

View File

@ -0,0 +1,731 @@
/*
* 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 com.google.common.base.Preconditions.checkState;
import android.os.Parcel;
import io.grpc.Attributes;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.internal.ClientStreamListener;
import io.grpc.internal.ClientStreamListener.RpcProgress;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerStreamListener;
import io.grpc.internal.StatsTraceContext;
import io.grpc.internal.StreamListener;
import java.io.InputStream;
import java.util.ArrayList;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Handles incoming binder transactions for a single stream, turning those transactions into calls
* to the stream listener.
*
* <p>Out-of-order messages are reassembled into their correct order.
*/
abstract class Inbound<L extends StreamListener> implements StreamListener.MessageProducer {
protected final BinderTransport transport;
protected final Attributes attributes;
final int callId;
// ==========================
// Values set when we're initialized.
@Nullable
@GuardedBy("this")
protected Outbound outbound;
@Nullable
@GuardedBy("this")
protected StatsTraceContext statsTraceContext;
@Nullable
@GuardedBy("this")
protected L listener;
// ==========================
// State of inbound data.
@Nullable
@GuardedBy("this")
private InputStream firstMessage;
@GuardedBy("this")
private int firstQueuedTransactionIndex;
@GuardedBy("this")
private int nextCompleteMessageEnd;
@Nullable
@GuardedBy("this")
private ArrayList<TransactionData> queuedTransactionData;
@GuardedBy("this")
private boolean suffixAvailable;
@GuardedBy("this")
private int suffixTransactionIndex;
@GuardedBy("this")
private int inboundDataSize;
// ==========================
// State of what we've delivered to gRPC.
/**
* Each rpc transmits (or receives) a prefix (including headers and possibly a method name), the
* data of zero or more request (or response) messages, and a suffix (possibly including a close
* status and trailers).
*
* <p>This enum represents those stages, for both availability (what we've been given), and
* delivery what we've sent.
*/
enum State {
// We aren't yet connected to a BinderStream instance and listener. Due to potentially
// out-of-order messages, a server-side instance can remain in this state for multiple
// transactions.
UNINITIALIZED,
// We're attached to a BinderStream instance and we have a listener we can report to.
// On the client-side, this happens as soon as the start() method is called (almost
// immediately), and on the server side, this happens as soon as we receive the prefix
// (so we know which method is being called).
INITIALIZED,
// We've delivered the prefix data to the listener. On the client side, this means we've
// delivered the response headers, and on the server side this state is effectively the same
// as INITIALIZED (since we initialize only by delivering the prefix).
PREFIX_DELIVERED,
// All messages have been received, and delivered to the listener.
ALL_MESSAGES_DELIVERED,
// We've delivered the suffix.
SUFFIX_DELIVERED,
// The stream is closed.
CLOSED
}
/*
* Represents which data we've delivered to the gRPC listener.
*/
@GuardedBy("this")
private State deliveryState = State.UNINITIALIZED;
@GuardedBy("this")
private int numReceivedMessages;
@GuardedBy("this")
private int numRequestedMessages;
@GuardedBy("this")
private boolean delivering;
@GuardedBy("this")
private boolean producingMessages;
private Inbound(BinderTransport transport, Attributes attributes, int callId) {
this.transport = transport;
this.attributes = attributes;
this.callId = callId;
}
@GuardedBy("this")
final void init(Outbound outbound, L listener) {
this.outbound = outbound;
this.statsTraceContext = outbound.getStatsTraceContext();
this.listener = listener;
if (!isClosed()) {
onDeliveryState(State.INITIALIZED);
}
}
final void unregister() {
transport.unregisterInbound(this);
}
boolean countsForInUse() {
return false;
}
// =====================
// Updates to delivery.
@GuardedBy("this")
protected final void onDeliveryState(State deliveryState) {
checkTransition(this.deliveryState, deliveryState);
this.deliveryState = deliveryState;
}
@GuardedBy("this")
protected final boolean isClosed() {
return deliveryState == State.CLOSED;
}
@GuardedBy("this")
private final boolean messageAvailable() {
return firstMessage != null || nextCompleteMessageEnd > 0;
}
@GuardedBy("this")
private boolean receivedAllTransactions() {
return suffixAvailable && firstQueuedTransactionIndex >= suffixTransactionIndex;
}
// ===================
// Internals.
@GuardedBy("this")
final void deliver() {
if (delivering) {
// Don't re-enter.
return;
}
delivering = true;
while (canDeliver()) {
deliverInternal();
}
delivering = false;
}
@GuardedBy("this")
private final boolean canDeliver() {
switch (deliveryState) {
case PREFIX_DELIVERED:
if (listener != null) {
if (producingMessages) {
// We're waiting for the listener to consume messages. Nothing to do.
return false;
} else if (messageAvailable()) {
// There's a message. We can deliver if we've been asked for messages, and we haven't
// already given the listener a MessageProducer.
return numRequestedMessages != 0;
} else {
// There are no messages available. Return true if that's the last of them, because we
// can send the suffix.
return receivedAllTransactions();
}
}
return false;
case ALL_MESSAGES_DELIVERED:
return listener != null && suffixAvailable;
default:
return false;
}
}
@GuardedBy("this")
@SuppressWarnings("fallthrough")
private final void deliverInternal() {
switch (deliveryState) {
case PREFIX_DELIVERED:
if (producingMessages) {
break;
} else if (messageAvailable()) {
producingMessages = true;
listener.messagesAvailable(this);
break;
} else if (!suffixAvailable) {
break;
}
onDeliveryState(State.ALL_MESSAGES_DELIVERED);
// Fall-through.
case ALL_MESSAGES_DELIVERED:
if (suffixAvailable) {
onDeliveryState(State.SUFFIX_DELIVERED);
deliverSuffix();
}
break;
default:
throw new AssertionError();
}
}
/** Deliver the suffix to gRPC. */
protected abstract void deliverSuffix();
@GuardedBy("this")
final void closeOnCancel(Status status) {
closeAbnormal(Status.CANCELLED, status, false);
}
@GuardedBy("this")
private final void closeOutOfBand(Status status) {
closeAbnormal(status, status, true);
}
@GuardedBy("this")
final void closeAbnormal(Status status) {
closeAbnormal(status, status, false);
}
@GuardedBy("this")
private final void closeAbnormal(
Status outboundStatus, Status internalStatus, boolean isOobFromRemote) {
if (!isClosed()) {
boolean wasInitialized = (deliveryState != State.UNINITIALIZED);
onDeliveryState(State.CLOSED);
if (wasInitialized) {
statsTraceContext.streamClosed(internalStatus);
}
if (!isOobFromRemote) {
transport.sendOutOfBandClose(callId, outboundStatus);
}
if (wasInitialized) {
deliverCloseAbnormal(internalStatus);
}
unregister();
}
}
@GuardedBy("this")
protected abstract void deliverCloseAbnormal(Status status);
final void onTransportReady() {
// Report transport readiness to the listener, and the outbound data.
Outbound outbound = null;
StreamListener listener = null;
synchronized (this) {
outbound = this.outbound;
listener = this.listener;
}
if (listener != null) {
listener.onReady();
}
if (outbound != null) {
try {
synchronized (outbound) {
outbound.onTransportReady();
}
} catch (StatusException se) {
synchronized (this) {
closeAbnormal(se.getStatus());
}
}
}
}
@GuardedBy("this")
public void requestMessages(int num) {
numRequestedMessages += num;
deliver();
}
final synchronized void handleTransaction(Parcel parcel) {
if (isClosed()) {
return;
}
try {
int flags = parcel.readInt();
if (TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_OUT_OF_BAND_CLOSE)) {
closeOutOfBand(TransactionUtils.readStatus(flags, parcel));
return;
}
int index = parcel.readInt();
boolean hasPrefix = TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_PREFIX);
boolean hasMessageData =
(TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_MESSAGE_DATA));
boolean hasSuffix = (TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_SUFFIX));
if (hasPrefix) {
handlePrefix(flags, parcel);
onDeliveryState(State.PREFIX_DELIVERED);
}
if (hasMessageData) {
handleMessageData(flags, index, parcel);
}
if (hasSuffix) {
handleSuffix(flags, parcel);
suffixTransactionIndex = index;
suffixAvailable = true;
}
if (index == firstQueuedTransactionIndex) {
if (queuedTransactionData == null) {
// This message was in order, and we haven't needed to queue anything yet.
firstQueuedTransactionIndex += 1;
} else if (!hasMessageData && !hasSuffix) {
// The first transaction arrived, but it contained no message data.
queuedTransactionData.remove(0);
firstQueuedTransactionIndex += 1;
}
}
reportInboundSize(parcel.dataSize());
deliver();
} catch (StatusException se) {
closeAbnormal(se.getStatus());
}
}
@GuardedBy("this")
abstract void handlePrefix(int flags, Parcel parcel) throws StatusException;
@GuardedBy("this")
abstract void handleSuffix(int flags, Parcel parcel) throws StatusException;
@GuardedBy("this")
private void handleMessageData(int flags, int index, Parcel parcel) throws StatusException {
InputStream stream = null;
byte[] block = null;
boolean lastBlockOfMessage = true;
int numBytes = 0;
if ((flags & TransactionUtils.FLAG_MESSAGE_DATA_IS_PARCELABLE) != 0) {
InboundParcelablePolicy policy = attributes.get(BinderTransport.INBOUND_PARCELABLE_POLICY);
if (policy == null || !policy.shouldAcceptParcelableMessages()) {
throw Status.PERMISSION_DENIED
.withDescription("Parcelable messages not allowed")
.asException();
}
int startPos = parcel.dataPosition();
stream = ParcelableInputStream.readFromParcel(parcel, getClass().getClassLoader());
numBytes = parcel.dataPosition() - startPos;
} else {
numBytes = parcel.readInt();
block = BlockPool.acquireBlock(numBytes);
if (numBytes > 0) {
parcel.readByteArray(block);
}
if ((flags & TransactionUtils.FLAG_MESSAGE_DATA_IS_PARTIAL) != 0) {
// Partial message. Ensure we have a message assembler.
lastBlockOfMessage = false;
}
}
if (queuedTransactionData == null) {
if (numReceivedMessages == 0 && lastBlockOfMessage && index == firstQueuedTransactionIndex) {
// Shortcut for when we receive a single message in one transaction.
checkState(firstMessage == null);
firstMessage = (stream != null) ? stream : new BlockInputStream(block);
reportInboundMessage(numBytes);
return;
}
queuedTransactionData = new ArrayList<>(16);
}
enqueueTransactionData(index, new TransactionData(stream, block, numBytes, lastBlockOfMessage));
}
@GuardedBy("this")
private void enqueueTransactionData(int index, TransactionData data) {
int offset = index - firstQueuedTransactionIndex;
if (offset < queuedTransactionData.size()) {
queuedTransactionData.set(offset, data);
lookForCompleteMessage();
} else if (offset > queuedTransactionData.size()) {
do {
queuedTransactionData.add(null);
} while (offset > queuedTransactionData.size());
queuedTransactionData.add(data);
} else {
queuedTransactionData.add(data);
lookForCompleteMessage();
}
}
@GuardedBy("this")
private void lookForCompleteMessage() {
int numBytes = 0;
if (nextCompleteMessageEnd == 0) {
for (int i = 0; i < queuedTransactionData.size(); i++) {
TransactionData data = queuedTransactionData.get(i);
if (data == null) {
// Missing block.
return;
} else {
numBytes += data.numBytes;
if (data.lastBlockOfMessage) {
// Found a complete message.
nextCompleteMessageEnd = i + 1;
reportInboundMessage(numBytes);
return;
}
}
}
}
}
@Override
@Nullable
public final synchronized InputStream next() {
InputStream stream = null;
if (firstMessage != null) {
stream = firstMessage;
firstMessage = null;
} else if (messageAvailable()) {
stream = assembleNextMessage();
}
if (stream != null) {
numRequestedMessages -= 1;
} else {
producingMessages = false;
if (receivedAllTransactions()) {
// That's the last of the messages delivered.
if (!isClosed()) {
onDeliveryState(State.ALL_MESSAGES_DELIVERED);
deliver();
}
}
}
return stream;
}
@GuardedBy("this")
private InputStream assembleNextMessage() {
InputStream message;
int numBlocks = nextCompleteMessageEnd;
nextCompleteMessageEnd = 0;
int numBytes = 0;
if (numBlocks == 1) {
// Single block.
TransactionData data = queuedTransactionData.remove(0);
numBytes = data.numBytes;
if (data.stream != null) {
message = data.stream;
} else {
message = new BlockInputStream(data.block);
}
} else {
byte[][] blocks = new byte[numBlocks][];
for (int i = 0; i < numBlocks; i++) {
TransactionData data = queuedTransactionData.remove(0);
blocks[i] = checkNotNull(data.block);
numBytes += blocks[i].length;
}
message = new BlockInputStream(blocks, numBytes);
}
firstQueuedTransactionIndex += numBlocks;
lookForCompleteMessage();
return message;
}
// ------------------------------------
// stats collection.
@GuardedBy("this")
private void reportInboundSize(int size) {
inboundDataSize += size;
if (statsTraceContext != null && inboundDataSize != 0) {
statsTraceContext.inboundWireSize(inboundDataSize);
statsTraceContext.inboundUncompressedSize(inboundDataSize);
inboundDataSize = 0;
}
}
@GuardedBy("this")
private void reportInboundMessage(int numBytes) {
checkNotNull(statsTraceContext);
statsTraceContext.inboundMessage(numReceivedMessages);
statsTraceContext.inboundMessageRead(numReceivedMessages, numBytes, numBytes);
numReceivedMessages += 1;
}
@Override
public synchronized String toString() {
return getClass().getSimpleName()
+ "[SfxA="
+ suffixAvailable
+ "/De="
+ deliveryState
+ "/Msg="
+ messageAvailable()
+ "/Lis="
+ (listener != null)
+ "]";
}
// ======================================
// Client-side inbound transactions.
static final class ClientInbound extends Inbound<ClientStreamListener> {
private final boolean countsForInUse;
@Nullable
@GuardedBy("this")
private Status closeStatus;
@Nullable
@GuardedBy("this")
private Metadata trailers;
ClientInbound(
BinderTransport transport, Attributes attributes, int callId, boolean countsForInUse) {
super(transport, attributes, callId);
this.countsForInUse = countsForInUse;
}
@Override
boolean countsForInUse() {
return countsForInUse;
}
@Override
@GuardedBy("this")
protected void handlePrefix(int flags, Parcel parcel) throws StatusException {
Metadata headers = MetadataHelper.readMetadata(parcel, attributes);
statsTraceContext.clientInboundHeaders();
listener.headersRead(headers);
}
@Override
@GuardedBy("this")
protected void handleSuffix(int flags, Parcel parcel) throws StatusException {
closeStatus = TransactionUtils.readStatus(flags, parcel);
trailers = MetadataHelper.readMetadata(parcel, attributes);
}
@Override
@GuardedBy("this")
protected void deliverSuffix() {
statsTraceContext.clientInboundTrailers(trailers);
statsTraceContext.streamClosed(closeStatus);
onDeliveryState(State.CLOSED);
listener.closed(closeStatus, RpcProgress.PROCESSED, trailers);
unregister();
}
@Override
@GuardedBy("this")
protected void deliverCloseAbnormal(Status status) {
listener.closed(status, RpcProgress.PROCESSED, new Metadata());
}
}
// ======================================
// Server-side inbound transactions.
static final class ServerInbound extends Inbound<ServerStreamListener> {
private final BinderTransport.BinderServerTransport serverTransport;
ServerInbound(
BinderTransport.BinderServerTransport transport, Attributes attributes, int callId) {
super(transport, attributes, callId);
this.serverTransport = transport;
}
@GuardedBy("this")
@Override
protected void handlePrefix(int flags, Parcel parcel) throws StatusException {
String methodName = parcel.readString();
Metadata headers = MetadataHelper.readMetadata(parcel, attributes);
StatsTraceContext statsTraceContext =
serverTransport.createStatsTraceContext(methodName, headers);
Outbound.ServerOutbound outbound =
new Outbound.ServerOutbound(serverTransport, callId, statsTraceContext);
ServerStream stream;
if ((flags & TransactionUtils.FLAG_EXPECT_SINGLE_MESSAGE) != 0) {
stream = new SingleMessageServerStream(this, outbound, attributes);
} else {
stream = new MultiMessageServerStream(this, outbound, attributes);
}
Status status = serverTransport.startStream(stream, methodName, headers);
if (status.isOk()) {
checkNotNull(listener); // Is it ok to assume this will happen synchronously?
if (transport.isReady()) {
listener.onReady();
}
} else {
closeAbnormal(status);
}
}
@GuardedBy("this")
@Override
protected void handleSuffix(int flags, Parcel parcel) {
// Nothing to read.
}
@Override
@GuardedBy("this")
protected void deliverSuffix() {
listener.halfClosed();
}
@Override
@GuardedBy("this")
protected void deliverCloseAbnormal(Status status) {
listener.closed(status);
}
@GuardedBy("this")
void onCloseSent(Status status) {
if (!isClosed()) {
onDeliveryState(State.CLOSED);
statsTraceContext.streamClosed(status);
listener.closed(Status.OK);
}
}
}
// ======================================
// Helper methods.
private static void checkTransition(State current, State next) {
switch (next) {
case INITIALIZED:
checkState(current == State.UNINITIALIZED, "%s -> %s", current, next);
break;
case PREFIX_DELIVERED:
checkState(
current == State.INITIALIZED || current == State.UNINITIALIZED,
"%s -> %s",
current,
next);
break;
case ALL_MESSAGES_DELIVERED:
checkState(current == State.PREFIX_DELIVERED, "%s -> %s", current, next);
break;
case SUFFIX_DELIVERED:
checkState(current == State.ALL_MESSAGES_DELIVERED, "%s -> %s", current, next);
break;
case CLOSED:
break;
default:
throw new AssertionError();
}
}
// ======================================
// Message reassembly.
/** Part of an unconsumed message. */
private static final class TransactionData {
@Nullable final InputStream stream;
@Nullable final byte[] block;
final int numBytes;
final boolean lastBlockOfMessage;
TransactionData(InputStream stream, byte[] block, int numBytes, boolean lastBlockOfMessage) {
this.stream = stream;
this.block = block;
this.numBytes = numBytes;
this.lastBlockOfMessage = lastBlockOfMessage;
}
@Override
public String toString() {
return "TransactionData["
+ numBytes
+ "b "
+ (stream != null ? "stream" : "array")
+ (lastBlockOfMessage ? "(last)]" : "]");
}
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.Parcelable;
import io.grpc.Metadata;
/**
* Accessor class for using MetadataHelper outside this package.
*/
public final class InternalMetadataHelper {
private InternalMetadataHelper() {}
public static <P extends Parcelable> Metadata.Key<P> createParcelableMetadataKey(
String name, Parcelable.Creator<P> creator, boolean immutableType) {
return Metadata.Key.of(
name, new MetadataHelper.ParcelableMetadataMarshaller<P>(creator, immutableType));
}
}

View File

@ -0,0 +1,224 @@
/*
* 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.Parcel;
import android.os.Parcelable;
import android.util.AndroidRuntimeException;
import io.grpc.Attributes;
import io.grpc.InternalMetadata;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.internal.GrpcUtil;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
/**
* Helper class for reading & writing metadata to parcels.
*
* <p>Metadata is written to a parcel as a single int for the number of name/value pairs, followed
* by the following pattern for each pair.
*
* <ol>
* <li>name length (int)
* <li>name (byte[])
* <li>value length OR sentinel (int)
* <li>value (byte[] OR Parcelable)
* </ol>
*
* The sentinel int at the start of a value may indicate bad metadata. When this happens, no more
* data follows the sentinel.
*/
final class MetadataHelper {
/** The generic metadata marshaller we use for reading parcelables from the transport. */
private static final Metadata.BinaryStreamMarshaller<Parcelable> TRANSPORT_INBOUND_MARSHALLER =
new ParcelableMetadataMarshaller<>(null, true);
/** Indicates the following value is a parcelable. */
private static final int PARCELABLE_SENTINEL = -1;
private MetadataHelper() {}
/**
* Write a Metadata instance to a Parcel.
*
* @param parcel The {@link Parcel} to write to.
* @param metadata The {@link Metadata} to write.
*/
public static void writeMetadata(Parcel parcel, @Nullable Metadata metadata)
throws StatusException, IOException {
int n = metadata != null ? InternalMetadata.headerCount(metadata) : 0;
if (n == 0) {
parcel.writeInt(0);
return;
}
Object[] serialized = InternalMetadata.serializePartial(metadata);
parcel.writeInt(n);
for (int i = 0; i < n; i++) {
byte[] name = (byte[]) serialized[i * 2];
parcel.writeInt(name.length);
parcel.writeByteArray(name);
Object value = serialized[i * 2 + 1];
if (value instanceof byte[]) {
byte[] valueBytes = (byte[]) value;
parcel.writeInt(valueBytes.length);
parcel.writeByteArray(valueBytes);
} else if (value instanceof ParcelableInputStream) {
parcel.writeInt(PARCELABLE_SENTINEL);
((ParcelableInputStream) value).writeToParcel(parcel);
} else {
// An InputStream which wasn't created by ParcelableUtils, which means there's another use
// of Metadata.BinaryStreamMarshaller. Just read the bytes.
//
// We know that BlockPool will give us a buffer at least as large as the max space for all
// names and values so it'll certainly be large enough (and the limit is only 8k so this
// is fine).
byte[] buffer = BlockPool.acquireBlock();
try {
InputStream stream = (InputStream) value;
int total = 0;
while (total < buffer.length) {
int read = stream.read(buffer, total, buffer.length - total);
if (read == -1) {
break;
}
total += read;
}
if (total == buffer.length) {
throw Status.RESOURCE_EXHAUSTED.withDescription("Metadata value too large").asException();
}
parcel.writeInt(total);
if (total > 0) {
parcel.writeByteArray(buffer, 0, total);
}
} finally {
BlockPool.releaseBlock(buffer);
}
}
}
}
/**
* Read a Metadata instance from a Parcel.
*
* @param parcel The {@link Parcel} to read from.
*/
public static Metadata readMetadata(Parcel parcel, Attributes attributes) throws StatusException {
int n = parcel.readInt();
if (n == 0) {
return new Metadata();
}
// For enforcing the header-size limit. Doesn't include parcelable data.
int bytesRead = 0;
// For enforcing the maximum allowed parcelable data (see InboundParcelablePolicy).
int parcelableBytesRead = 0;
Object[] serialized = new Object[n * 2];
for (int i = 0; i < n; i++) {
int numNameBytes = parcel.readInt();
bytesRead += 4;
byte[] name = readBytesChecked(parcel, numNameBytes, bytesRead);
bytesRead += numNameBytes;
serialized[i * 2] = name;
int numValueBytes = parcel.readInt();
bytesRead += 4;
if (numValueBytes == PARCELABLE_SENTINEL) {
InboundParcelablePolicy policy = attributes.get(BinderTransport.INBOUND_PARCELABLE_POLICY);
if (!policy.shouldAcceptParcelableMetadataValues()) {
throw Status.PERMISSION_DENIED
.withDescription("Parcelable metadata values not allowed")
.asException();
}
int parcelableStartPos = parcel.dataPosition();
try {
Parcelable value = parcel.readParcelable(MetadataHelper.class.getClassLoader());
if (value == null) {
throw Status.INTERNAL.withDescription("Read null parcelable in metadata").asException();
}
serialized[i * 2 + 1] = InternalMetadata.parsedValue(TRANSPORT_INBOUND_MARSHALLER, value);
} catch (AndroidRuntimeException are) {
throw Status.INTERNAL
.withCause(are)
.withDescription("Failure reading parcelable in metadata")
.asException();
}
int parcelableSize = parcel.dataPosition() - parcelableStartPos;
parcelableBytesRead += parcelableSize;
if (parcelableBytesRead > policy.getMaxParcelableMetadataSize()) {
throw Status.RESOURCE_EXHAUSTED
.withDescription(
"Inbound Parcelables too large according to policy (see InboundParcelablePolicy)")
.asException();
}
} else if (numValueBytes < 0) {
throw Status.INTERNAL.withDescription("Unrecognized metadata sentinel").asException();
} else {
byte[] value = readBytesChecked(parcel, numValueBytes, bytesRead);
bytesRead += numValueBytes;
serialized[i * 2 + 1] = value;
}
}
return InternalMetadata.newMetadataWithParsedValues(n, serialized);
}
/** Read a byte array checking that we're not reading too much. */
private static byte[] readBytesChecked(
Parcel parcel,
int numBytes,
int bytesRead) throws StatusException {
if (bytesRead + numBytes > GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE) {
throw Status.RESOURCE_EXHAUSTED.withDescription("Metadata too large").asException();
}
byte[] res = new byte[numBytes];
if (numBytes > 0) {
parcel.readByteArray(res);
}
return res;
}
/** A marshaller for passing parcelables in gRPC {@link Metadata} */
static final class ParcelableMetadataMarshaller<P extends Parcelable>
implements Metadata.BinaryStreamMarshaller<P> {
@Nullable private final Parcelable.Creator<P> creator;
private final boolean immutableType;
ParcelableMetadataMarshaller(@Nullable Parcelable.Creator<P> creator, boolean immutableType) {
this.creator = creator;
this.immutableType = immutableType;
}
@Override
public InputStream toStream(P value) {
return new ParcelableInputStream<>(creator, value, immutableType);
}
@Override
@SuppressWarnings("unchecked")
public P parseStream(InputStream stream) {
if (stream instanceof ParcelableInputStream) {
return ((ParcelableInputStream<P>) stream).getParcelable();
} else {
throw new UnsupportedOperationException(
"Can't unmarshall a parcelable from a regular byte stream");
}
}
}
}

View File

@ -0,0 +1,192 @@
/*
* 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 io.grpc.Attributes;
import io.grpc.Compressor;
import io.grpc.Deadline;
import io.grpc.DecompressorRegistry;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientStreamListener;
import io.grpc.internal.InsightBuilder;
import java.io.InputStream;
import javax.annotation.Nonnull;
/**
* The client side of a single RPC, which sends a stream of request messages.
*
* <p>An instance of this class is effectively a go-between, receiving messages from the gRPC
* ClientCall instance (via calls on the ClientStream interface we implement), and sending them out
* on the transport, as well as receiving messages from the transport, and passing the resultant
* data back to the gRPC ClientCall instance (via calls on the ClientStreamListener instance we're
* given).
*
* <p>These two communication directions are largely independent of each other, with the {@link
* Outbound} handling the gRPC to transport direction, and the {@link Inbound} class handling
* transport to gRPC direction.
*
* <p>Since the Inbound and Outbound halves are largely independent, their state is also
* synchronized independently.
*/
final class MultiMessageClientStream implements ClientStream {
private final Inbound.ClientInbound inbound;
private final Outbound.ClientOutbound outbound;
private final Attributes attributes;
MultiMessageClientStream(
Inbound.ClientInbound inbound, Outbound.ClientOutbound outbound, Attributes attributes) {
this.inbound = inbound;
this.outbound = outbound;
this.attributes = attributes;
}
@Override
public void start(ClientStreamListener listener) {
synchronized (inbound) {
inbound.init(outbound, listener);
}
if (outbound.isReady()) {
listener.onReady();
try {
synchronized (outbound) {
outbound.send();
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
}
@Override
public void request(int numMessages) {
synchronized (inbound) {
inbound.requestMessages(numMessages);
}
}
@Override
public boolean isReady() {
return outbound.isReady();
}
@Override
public void writeMessage(InputStream message) {
try {
synchronized (outbound) {
outbound.addMessage(message);
outbound.send();
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
@Override
public void halfClose() {
try {
synchronized (outbound) {
outbound.sendHalfClose();
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
@Override
public void cancel(Status status) {
synchronized (inbound) {
inbound.closeOnCancel(status);
}
}
@Override
public Attributes getAttributes() {
return attributes;
}
@Override
public final String toString() {
return "MultiMessageClientStream[" + inbound + "/" + outbound + "]";
}
// =====================
// Misc stubbed & unsupported methods.
@Override
public final void flush() {
// Ignore.
}
@Override
public final void setCompressor(Compressor compressor) {
// Ignore.
}
@Override
public final void setMessageCompression(boolean enable) {
// Ignore.
}
@Override
public void setDeadline(@Nonnull Deadline deadline) {
// Ignore. (Deadlines should still work at a higher level).
}
@Override
public void setAuthority(String authority) {
// Ignore.
}
@Override
public void setMaxInboundMessageSize(int maxSize) {
// Ignore.
}
@Override
public void setMaxOutboundMessageSize(int maxSize) {
// Ignore.
}
@Override
public void appendTimeoutInsight(InsightBuilder insight) {
// Ignore
}
@Override
public void setFullStreamDecompression(boolean fullStreamDecompression) {
// Ignore.
}
@Override
public void setDecompressorRegistry(DecompressorRegistry decompressorRegistry) {
// Ignore.
}
@Override
public void optimizeForDirectExecutor() {
// Ignore.
}
}

View File

@ -0,0 +1,182 @@
/*
* 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 io.grpc.Attributes;
import io.grpc.Compressor;
import io.grpc.Decompressor;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerStreamListener;
import io.grpc.internal.StatsTraceContext;
import java.io.InputStream;
import javax.annotation.Nullable;
/**
* The server side of a single RPC, which sends a stream of response messages.
*
* <p>An instance of this class is effectively a go-between, receiving messages from the gRPC
* ServerCall instance (via calls on the ServerStream interface we implement), and sending them out
* on the transport, as well as receiving messages from the transport, and passing the resultant
* data back to the gRPC ServerCall instance (via calls on the ServerStreamListener instance we're
* given).
*
* <p>These two communication directions are largely independent of each other, with the {@link
* Outbound} handling the gRPC to transport direction, and the {@link Inbound} class handling
* transport to gRPC direction.
*
* <p>Since the Inbound and Outbound halves are largely independent, their state is also
* synchronized independently.
*/
final class MultiMessageServerStream implements ServerStream {
private final Inbound.ServerInbound inbound;
private final Outbound.ServerOutbound outbound;
private final Attributes attributes;
MultiMessageServerStream(
Inbound.ServerInbound inbound, Outbound.ServerOutbound outbound, Attributes attributes) {
this.inbound = inbound;
this.outbound = outbound;
this.attributes = attributes;
}
@Override
public void setListener(ServerStreamListener listener) {
synchronized (inbound) {
inbound.init(outbound, listener);
}
}
@Override
public boolean isReady() {
return outbound.isReady();
}
@Override
public void request(int numMessages) {
synchronized (inbound) {
inbound.requestMessages(numMessages);
}
}
@Override
public void writeHeaders(Metadata headers) {
try {
synchronized (outbound) {
outbound.sendHeaders(headers);
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
@Override
public void writeMessage(InputStream message) {
try {
synchronized (outbound) {
outbound.addMessage(message);
outbound.send();
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
@Override
public void close(Status status, Metadata trailers) {
try {
synchronized (outbound) {
outbound.sendClose(status, trailers);
}
synchronized (inbound) {
inbound.onCloseSent(status);
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
@Override
public void cancel(Status status) {
synchronized (inbound) {
inbound.closeOnCancel(status);
}
}
@Override
public StatsTraceContext statsTraceContext() {
return outbound.getStatsTraceContext();
}
@Override
public Attributes getAttributes() {
return attributes;
}
@Nullable
@Override
public String getAuthority() {
return attributes.get(BinderTransport.SERVER_AUTHORITY);
}
@Override
public String toString() {
return "MultiMessageServerStream[" + inbound + "/" + outbound + "]";
}
// =====================
// Misc stubbed & unsupported methods.
@Override
public final void flush() {
// Ignore.
}
@Override
public final void setCompressor(Compressor compressor) {
// Ignore.
}
@Override
public final void setMessageCompression(boolean enable) {
// Ignore.
}
@Override
public void setDecompressor(Decompressor decompressor) {
// Ignore.
}
@Override
public void optimizeForDirectExecutor() {
// Ignore.
}
@Override
public int streamId() {
return -1;
}
}

View File

@ -0,0 +1,495 @@
/*
* 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 com.google.common.base.Preconditions.checkState;
import android.os.Parcel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.StatsTraceContext;
import java.io.IOException;
import java.io.InputStream;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Sends the set of outbound transactions for a single BinderStream (rpc).
*
* <p>Handles buffering internally for flow control, and splitting large messages into multiple
* transactions where necessary.
*
* <p>Also handles reporting to the {@link StatsTraceContext}.
*
* <p>A note on threading: All calls into this class are expected to hold this object as a lock.
* However, since calls from gRPC are serialized already, the only reason we need to care about
* threading is the onTransportReady() call (when flow-control unblocks us).
*
* <p>To reduce the cost of locking, BinderStream endeavors to make only a single call to this class
* for single-message calls (the most common).
*
* <p><b>IMPORTANT:</b> To avoid potential deadlocks, this class may only call unsynchronized
* methods of the BinderTransport class.
*/
abstract class Outbound {
private final BinderTransport transport;
private final int callId;
private final StatsTraceContext statsTraceContext;
enum State {
INITIAL,
PREFIX_SENT,
ALL_MESSAGES_SENT,
SUFFIX_SENT,
CLOSED,
}
/*
* Represents the state of data we've sent in binder transactions.
*/
@GuardedBy("this")
private State outboundState = State.INITIAL; // Represents what we've delivered.
// ----------------------------------
// For reporting to StatsTraceContext.
/** Indicates we're ready to send the prefix. */
private boolean prefixReady;
@Nullable private InputStream firstMessage;
@Nullable private Queue<InputStream> messageQueue;
/**
* Indicates we have everything ready to send the suffix. This implies we have all outgoing
* messages, and any additional data which needs to be send after the last message. (e.g.
* trailers).
*/
private boolean suffixReady;
/**
* The index of the next transaction we'll send, allowing the receiver to re-assemble out-of-order
* messages.
*/
@GuardedBy("this")
private int transactionIndex;
// ----------------------------------
// For reporting to StatsTraceContext.
private int numDeliveredMessages;
private int messageSize;
private Outbound(BinderTransport transport, int callId, StatsTraceContext statsTraceContext) {
this.transport = transport;
this.callId = callId;
this.statsTraceContext = statsTraceContext;
}
final StatsTraceContext getStatsTraceContext() {
return statsTraceContext;
}
/** Call to add a message to be delivered. */
@GuardedBy("this")
final void addMessage(InputStream message) throws StatusException {
onPrefixReady(); // This is implied.
if (messageQueue != null) {
messageQueue.add(message);
} else if (firstMessage == null) {
firstMessage = message;
} else {
messageQueue = new ConcurrentLinkedQueue<>();
messageQueue.add(message);
}
}
@GuardedBy("this")
protected final void onPrefixReady() {
this.prefixReady = true;
}
@GuardedBy("this")
protected final void onSuffixReady() {
this.suffixReady = true;
}
// =====================
// Updates to delivery.
@GuardedBy("this")
private void onOutboundState(State outboundState) {
checkTransition(this.outboundState, outboundState);
this.outboundState = outboundState;
}
// ===================
// Internals.
@GuardedBy("this")
protected final boolean messageAvailable() {
if (messageQueue != null) {
return !messageQueue.isEmpty();
} else if (firstMessage != null) {
return numDeliveredMessages == 0;
} else {
return false;
}
}
@Nullable
@GuardedBy("this")
private final InputStream peekNextMessage() {
if (numDeliveredMessages == 0) {
return firstMessage;
} else if (messageQueue != null) {
return messageQueue.peek();
}
return null;
}
@GuardedBy("this")
private final boolean canSend() {
switch (outboundState) {
case INITIAL:
if (!prefixReady) {
return false;
}
break;
case PREFIX_SENT:
// We can only send something if we have messages or the suffix.
// Note that if we have the suffix but no messages in this state, it means we've been closed
// early.
if (!messageAvailable() && !suffixReady) {
return false;
}
break;
case ALL_MESSAGES_SENT:
if (!suffixReady) {
return false;
}
break;
default:
return false;
}
return isReady();
}
final boolean isReady() {
return transport.isReady();
}
@GuardedBy("this")
final void onTransportReady() throws StatusException {
// The transport has become ready, attempt sending.
send();
}
@GuardedBy("this")
final void send() throws StatusException {
while (canSend()) {
try {
sendInternal();
} catch (StatusException se) {
// Ensure we don't send anything else and rethrow.
onOutboundState(State.CLOSED);
throw se;
}
}
}
@GuardedBy("this")
@SuppressWarnings("fallthrough")
protected final void sendInternal() throws StatusException {
Parcel parcel = Parcel.obtain();
int flags = 0;
parcel.writeInt(0); // Placeholder for flags. Will be filled in below.
parcel.writeInt(transactionIndex++);
try {
switch (outboundState) {
case INITIAL:
flags |= TransactionUtils.FLAG_PREFIX;
flags |= writePrefix(parcel);
onOutboundState(State.PREFIX_SENT);
if (!messageAvailable() && !suffixReady) {
break;
}
// Fall-through.
case PREFIX_SENT:
InputStream messageStream = peekNextMessage();
if (messageStream != null) {
flags |= TransactionUtils.FLAG_MESSAGE_DATA;
flags |= writeMessageData(parcel, messageStream);
} else {
checkState(suffixReady);
}
if (suffixReady && !messageAvailable()) {
onOutboundState(State.ALL_MESSAGES_SENT);
} else {
// There's still more message data to deliver, break out.
break;
}
// Fall-through.
case ALL_MESSAGES_SENT:
flags |= TransactionUtils.FLAG_SUFFIX;
flags |= writeSuffix(parcel);
onOutboundState(State.SUFFIX_SENT);
break;
default:
throw new AssertionError();
}
TransactionUtils.fillInFlags(parcel, flags);
transport.sendTransaction(callId, parcel);
statsTraceContext.outboundWireSize(parcel.dataSize());
statsTraceContext.outboundUncompressedSize(parcel.dataSize());
} catch (IOException e) {
throw Status.INTERNAL.withCause(e).asException();
} finally {
parcel.recycle();
}
}
protected final void unregister() {
transport.unregisterCall(callId);
}
@Override
public synchronized String toString() {
return getClass().getSimpleName()
+ "[S="
+ outboundState
+ "/NDM="
+ numDeliveredMessages
+ "]";
}
/**
* Write prefix data to the given {@link Parcel}.
*
* @param parcel the transaction parcel to write to.
* @return any additional flags to be set on the transaction.
*/
@GuardedBy("this")
protected abstract int writePrefix(Parcel parcel) throws IOException, StatusException;
/**
* Write suffix data to the given {@link Parcel}.
*
* @param parcel the transaction parcel to write to.
* @return any additional flags to be set on the transaction.
*/
@GuardedBy("this")
protected abstract int writeSuffix(Parcel parcel) throws IOException, StatusException;
@GuardedBy("this")
private final int writeMessageData(Parcel parcel, InputStream stream) throws IOException {
int flags = 0;
boolean dataRemaining = false;
if (stream instanceof ParcelableInputStream) {
flags |= TransactionUtils.FLAG_MESSAGE_DATA_IS_PARCELABLE;
messageSize = ((ParcelableInputStream) stream).writeToParcel(parcel);
} else {
byte[] block = BlockPool.acquireBlock();
try {
int size = stream.read(block);
if (size <= 0) {
parcel.writeInt(0);
} else {
parcel.writeInt(size);
parcel.writeByteArray(block, 0, size);
messageSize += size;
if (size == block.length) {
flags |= TransactionUtils.FLAG_MESSAGE_DATA_IS_PARTIAL;
dataRemaining = true;
}
}
} finally {
BlockPool.releaseBlock(block);
}
}
if (!dataRemaining) {
stream.close();
int index = numDeliveredMessages++;
if (index > 0) {
checkNotNull(messageQueue).poll();
}
statsTraceContext.outboundMessage(index);
statsTraceContext.outboundMessageSent(index, messageSize, messageSize);
messageSize = 0;
}
return flags;
}
// ======================================
// Client-side outbound transactions.
static final class ClientOutbound extends Outbound {
private final MethodDescriptor<?, ?> method;
private final Metadata headers;
private final StatsTraceContext statsTraceContext;
ClientOutbound(
BinderTransport transport,
int callId,
MethodDescriptor<?, ?> method,
Metadata headers,
StatsTraceContext statsTraceContext) {
super(transport, callId, statsTraceContext);
this.method = method;
this.headers = headers;
this.statsTraceContext = statsTraceContext;
onPrefixReady(); // Client prefix is available immediately.
}
@Override
@GuardedBy("this")
protected int writePrefix(Parcel parcel) throws IOException, StatusException {
parcel.writeString(method.getFullMethodName());
MetadataHelper.writeMetadata(parcel, headers);
statsTraceContext.clientOutboundHeaders();
if (method.getType().serverSendsOneMessage()) {
return TransactionUtils.FLAG_EXPECT_SINGLE_MESSAGE;
}
return 0;
}
@GuardedBy("this")
void sendSingleMessageAndHalfClose(@Nullable InputStream singleMessage) throws StatusException {
if (singleMessage != null) {
addMessage(singleMessage);
}
onSuffixReady();
send();
}
@GuardedBy("this")
void sendHalfClose() throws StatusException {
onSuffixReady();
send();
}
@Override
@GuardedBy("this")
protected int writeSuffix(Parcel parcel) throws IOException {
// Client doesn't include anything in the suffix.
return 0;
}
}
// ======================================
// Server-side outbound transactions.
static final class ServerOutbound extends Outbound {
@GuardedBy("this")
@Nullable
private Metadata headers;
@GuardedBy("this")
@Nullable
private Status closeStatus;
@GuardedBy("this")
@Nullable
private Metadata trailers;
ServerOutbound(BinderTransport transport, int callId, StatsTraceContext statsTraceContext) {
super(transport, callId, statsTraceContext);
}
@GuardedBy("this")
void sendHeaders(Metadata headers) throws StatusException {
this.headers = headers;
onPrefixReady();
send();
}
@Override
@GuardedBy("this")
protected int writePrefix(Parcel parcel) throws IOException, StatusException {
MetadataHelper.writeMetadata(parcel, headers);
return 0;
}
@GuardedBy("this")
void sendSingleMessageAndClose(
@Nullable Metadata pendingHeaders,
@Nullable InputStream pendingSingleMessage,
Status closeStatus,
Metadata trailers)
throws StatusException {
if (this.closeStatus != null) {
return;
}
if (pendingHeaders != null) {
this.headers = pendingHeaders;
}
onPrefixReady();
if (pendingSingleMessage != null) {
addMessage(pendingSingleMessage);
}
checkState(this.trailers == null);
this.closeStatus = closeStatus;
this.trailers = trailers;
onSuffixReady();
send();
}
@GuardedBy("this")
void sendClose(Status closeStatus, Metadata trailers) throws StatusException {
if (this.closeStatus != null) {
return;
}
checkState(this.trailers == null);
this.closeStatus = closeStatus;
this.trailers = trailers;
onPrefixReady();
onSuffixReady();
send();
}
@Override
@GuardedBy("this")
protected int writeSuffix(Parcel parcel) throws IOException, StatusException {
int flags = TransactionUtils.writeStatus(parcel, closeStatus);
MetadataHelper.writeMetadata(parcel, trailers);
// TODO: This is an ugly place for this side-effect.
unregister();
return flags;
}
}
// ======================================
// Helper methods.
private static void checkTransition(State current, State next) {
switch (next) {
case PREFIX_SENT:
checkState(current == State.INITIAL);
break;
case ALL_MESSAGES_SENT:
checkState(current == State.PREFIX_SENT);
break;
case SUFFIX_SENT:
checkState(current == State.ALL_MESSAGES_SENT);
break;
case CLOSED: // hah.
break;
default:
throw new AssertionError();
}
}
}

View File

@ -0,0 +1,210 @@
/*
* 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import android.os.Parcel;
import android.os.Parcelable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
/**
* An inputstream to serialize a single Android Parcelable object for gRPC calls, with support for
* serializing to a native Android Parcel, and for when a Parcelable is sent in-process.
*
* <p><b>Important:</b> It's not actually possible to marshall a parcelable to raw bytes without
* losing data, since a parcelable may contain file descriptors. While this class <i>does</i>
* support marshalling into bytes, this is only supported for the purposes of debugging/logging, and
* we intentionally don't support unmarshalling back to a parcelable.
*
* <p>This class really just wraps a Parcelable instance and masquerardes as an inputstream. See
* {@code ProtoLiteUtils} for a similar example of this pattern.
*
* <p>An instance of this class maybe be created from two sources.
*
* <ul>
* <li>To wrap a Parcelable instance we plan to send.
* <li>To wrap a Parcelable instance we've just received (and read from a Parcel).
* </ul>
*
* <p>In the first case, we expect to serialize to a {@link Parcel}, with a call to {@link
* #writeToParcel}.
*
* <p>In the second case, we only expect the Parcelable to be fetched (and not re-serialized).
*
* <p>For in-process gRPC calls, the same InputStream used to send the Parcelable (the first case),
* will also be used to parse the parcelable from the stream, in which case we shortcut serializing
* internally (possibly skipping it entirely if the instance is immutable).
*/
final class ParcelableInputStream<P extends Parcelable> extends InputStream {
@Nullable private final Parcelable.Creator<P> creator;
private final boolean safeToReturnValue;
private final P value;
@Nullable InputStream delegateStream;
@Nullable P sharableValue;
ParcelableInputStream(
@Nullable Parcelable.Creator<P> creator, P value, boolean safeToReturnValue) {
this.creator = creator;
this.value = value;
this.safeToReturnValue = safeToReturnValue;
// If we're not given a creator, the value must be safe to return unchanged.
checkArgument(creator != null || safeToReturnValue);
}
/**
* Create a stream from a {@link Parcel} object. Note that this immediately reads the Parcelable
* object, allowing the Parcel to be recycled after calling this method.
*/
@SuppressWarnings("unchecked")
static <P extends Parcelable> ParcelableInputStream<P> readFromParcel(
Parcel parcel, ClassLoader classLoader) {
P value = (P) parcel.readParcelable(classLoader);
return new ParcelableInputStream<>(null, value, true);
}
/** Create a stream for a Parcelable object. */
static <P extends Parcelable> ParcelableInputStream<P> forInstance(
P value, Parcelable.Creator<P> creator) {
return new ParcelableInputStream<>(creator, value, false);
}
/** Create a stream for a Parcelable object, treating the object as immutable. */
static <P extends Parcelable> ParcelableInputStream<P> forImmutableInstance(
P value, Parcelable.Creator<P> creator) {
return new ParcelableInputStream<>(creator, value, true);
}
private InputStream getDelegateStream() {
if (delegateStream == null) {
Parcel parcel = Parcel.obtain();
parcel.writeParcelable(value, 0);
byte[] res = parcel.marshall();
parcel.recycle();
delegateStream = new ByteArrayInputStream(res);
}
return delegateStream;
}
@Override
public int read() throws IOException {
return getDelegateStream().read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return getDelegateStream().read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
if (n <= 0) {
return 0;
}
return getDelegateStream().skip(n);
}
@Override
public int available() throws IOException {
return getDelegateStream().available();
}
@Override
public void close() throws IOException {
if (delegateStream != null) {
delegateStream.close();
}
}
@Override
public void mark(int readLimit) {
// If there's no delegate stream yet, the current position is 0. That's the same
// as the default mark position, so there's nothing to do.
if (delegateStream != null) {
delegateStream.mark(readLimit);
}
}
@Override
public void reset() throws IOException {
if (delegateStream != null) {
delegateStream.reset();
}
}
@Override
public boolean markSupported() {
// We know our delegate (ByteArrayInputStream) supports mark/reset.
return true;
}
/**
* Write the {@link Parcelable} this stream wraps to the given {@link Parcel}.
*
* <p>This will retain any android-specific data (e.g. file descriptors) which can't simply be
* serialized to bytes.
*
* @return The number of bytes written to the parcel.
*/
int writeToParcel(Parcel parcel) {
int startPos = parcel.dataPosition();
parcel.writeParcelable(value, value.describeContents());
return parcel.dataPosition() - startPos;
}
/**
* Get the parcelable as if it had been serialized/de-serialized.
*
* <p>If the parcelable is immutable, or it was already de-serialized from a Parcel (I.e. this
* instance was created with #readFromParcel), the value will be returned directly.
*/
P getParcelable() {
if (safeToReturnValue) {
// We can just return the value directly.
return value;
} else {
// We need to serialize/de-serialize to a parcel internally.
if (sharableValue == null) {
sharableValue = marshallUnmarshall(value, checkNotNull(creator));
}
return sharableValue;
}
}
private static <P extends Parcelable> P marshallUnmarshall(
P value, Parcelable.Creator<P> creator) {
// Serialize/de-serialize the object directly instead of using Parcel.writeParcelable,
// since there's no need to write out the class name.
Parcel parcel = Parcel.obtain();
value.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
P result = creator.createFromParcel(parcel);
parcel.recycle();
return result;
}
@Override
public String toString() {
return "ParcelableInputStream[V: " + value + "]";
}
}

View File

@ -0,0 +1,114 @@
/*
* 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 io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ClientTransport.PingCallback;
import io.grpc.internal.TimeProvider;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Tracks an ongoing ping request for a client-side binder transport. We only handle a single active
* ping at a time, since that's all gRPC appears to need.
*/
final class PingTracker {
interface PingSender {
/**
* Send a ping to the remote endpoint. We expect a subsequent call to {@link #onPingResponse}
* with the same ID (assuming the ping succeeds).
*/
void sendPing(int id) throws StatusException;
}
private final TimeProvider timeProvider;
private final PingSender pingSender;
@GuardedBy("this")
@Nullable
private Ping pendingPing;
@GuardedBy("this")
private int nextPingId;
PingTracker(TimeProvider timeProvider, PingSender pingSender) {
this.timeProvider = timeProvider;
this.pingSender = pingSender;
}
/**
* Start a ping.
*
* <p>See also {@link ClientTransport#ping}.
*
* @param callback The callback to report the ping result on.
* @param executor An executor to call callbacks on.
* <p>Note that only one ping callback will be active at a time.
*/
synchronized void startPing(PingCallback callback, Executor executor) {
pendingPing = new Ping(callback, executor, nextPingId++);
try {
pingSender.sendPing(pendingPing.id);
} catch (StatusException se) {
pendingPing.fail(se.getStatus());
pendingPing = null;
}
}
/** Callback when a ping response with the given ID is received. */
synchronized void onPingResponse(int id) {
if (pendingPing != null && pendingPing.id == id) {
pendingPing.success();
pendingPing = null;
}
}
private final class Ping {
private final PingCallback callback;
private final Executor executor;
private final int id;
private final long startTimeNanos;
@GuardedBy("this")
private boolean done;
Ping(PingCallback callback, Executor executor, int id) {
this.callback = callback;
this.executor = executor;
this.id = id;
this.startTimeNanos = timeProvider.currentTimeNanos();
}
private synchronized void fail(Status status) {
if (!done) {
done = true;
executor.execute(() -> callback.onFailure(status.asException()));
}
}
private synchronized void success() {
if (!done) {
done = true;
executor.execute(
() -> callback.onSuccess(timeProvider.currentTimeNanos() - startTimeNanos));
}
}
}
}

View File

@ -0,0 +1,183 @@
/*
* 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 io.grpc.Attributes;
import io.grpc.Compressor;
import io.grpc.Deadline;
import io.grpc.DecompressorRegistry;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientStreamListener;
import io.grpc.internal.InsightBuilder;
import java.io.InputStream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* The client side of a single RPC, which sends a single request message.
*
* <p>An instance of this class is effectively a go-between, receiving messages from the gRPC
* ClientCall instance (via calls on the ClientStream interface we implement), and sending them out
* on the transport, as well as receiving messages from the transport, and passing the resultant
* data back to the gRPC ClientCall instance (via calls on the ClientStreamListener instance we're
* given).
*
* <p>These two communication directions are largely independent of each other, with the {@link
* Outbound} handling the gRPC to transport direction, and the {@link Inbound} class handling
* transport to gRPC direction.
*
* <p>Since the Inbound and Outbound halves are largely independent, their state is also
* synchronized independently.
*/
final class SingleMessageClientStream implements ClientStream {
private final Inbound.ClientInbound inbound;
private final Outbound.ClientOutbound outbound;
private final Attributes attributes;
@Nullable private InputStream pendingSingleMessage;
SingleMessageClientStream(
Inbound.ClientInbound inbound, Outbound.ClientOutbound outbound, Attributes attributes) {
this.inbound = inbound;
this.outbound = outbound;
this.attributes = attributes;
}
@Override
public void start(ClientStreamListener listener) {
synchronized (inbound) {
inbound.init(outbound, listener);
}
if (outbound.isReady()) {
listener.onReady();
}
}
@Override
public boolean isReady() {
return outbound.isReady();
}
@Override
public void request(int numMessages) {
synchronized (inbound) {
inbound.requestMessages(numMessages);
}
}
@Override
public void writeMessage(InputStream message) {
if (pendingSingleMessage != null) {
synchronized (inbound) {
inbound.closeAbnormal(Status.INTERNAL.withDescription("too many messages"));
}
} else {
pendingSingleMessage = message;
}
}
@Override
public void halfClose() {
try {
synchronized (outbound) {
outbound.sendSingleMessageAndHalfClose(pendingSingleMessage);
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
@Override
public void cancel(Status status) {
synchronized (inbound) {
inbound.closeOnCancel(status);
}
}
@Override
public Attributes getAttributes() {
return attributes;
}
@Override
public final String toString() {
return "SingleMessageClientStream[" + inbound + "/" + outbound + "]";
}
// =====================
// Misc stubbed & unsupported methods.
@Override
public final void flush() {
// Ignore.
}
@Override
public final void setCompressor(Compressor compressor) {
// Ignore.
}
@Override
public final void setMessageCompression(boolean enable) {
// Ignore.
}
@Override
public void setDeadline(@Nonnull Deadline deadline) {
// Ignore. (Deadlines should still work at a higher level).
}
@Override
public void setAuthority(String authority) {
// Ignore.
}
@Override
public void setMaxInboundMessageSize(int maxSize) {
// Ignore.
}
@Override
public void setMaxOutboundMessageSize(int maxSize) {
// Ignore.
}
@Override
public void appendTimeoutInsight(InsightBuilder insight) {
// Ignore
}
@Override
public void setFullStreamDecompression(boolean fullStreamDecompression) {
// Ignore.
}
@Override
public void setDecompressorRegistry(DecompressorRegistry decompressorRegistry) {
// Ignore.
}
@Override
public void optimizeForDirectExecutor() {
// Ignore.
}
}

View File

@ -0,0 +1,174 @@
/*
* 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 io.grpc.Attributes;
import io.grpc.Compressor;
import io.grpc.Decompressor;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerStreamListener;
import io.grpc.internal.StatsTraceContext;
import java.io.InputStream;
import javax.annotation.Nullable;
/**
* The server side of a single RPC, which sends a single response message.
*
* <p>An instance of this class is effectively a go-between, receiving messages from the gRPC
* ServerCall instance (via calls on the ServerStream interface we implement), and sending them out
* on the transport, as well as receiving messages from the transport, and passing the resultant
* data back to the gRPC ServerCall instance (via calls on the ServerStreamListener instance we're
* given).
*
* <p>These two communication directions are largely independent of each other, with the {@link
* Outbound} handling the gRPC to transport direction, and the {@link Inbound} class handling
* transport to gRPC direction.
*
* <p>Since the Inbound and Outbound halves are largely independent, their state is also
* synchronized independently.
*/
final class SingleMessageServerStream implements ServerStream {
private final Inbound.ServerInbound inbound;
private final Outbound.ServerOutbound outbound;
private final Attributes attributes;
@Nullable private Metadata pendingHeaders;
@Nullable private InputStream pendingSingleMessage;
SingleMessageServerStream(
Inbound.ServerInbound inbound, Outbound.ServerOutbound outbound, Attributes attributes) {
this.inbound = inbound;
this.outbound = outbound;
this.attributes = attributes;
}
@Override
public void setListener(ServerStreamListener listener) {
synchronized (inbound) {
inbound.init(outbound, listener);
}
}
@Override
public boolean isReady() {
return outbound.isReady();
}
@Override
public void request(int numMessages) {
synchronized (inbound) {
inbound.requestMessages(numMessages);
}
}
@Override
public void writeHeaders(Metadata headers) {
pendingHeaders = headers;
}
@Override
public void writeMessage(InputStream message) {
if (pendingSingleMessage != null) {
synchronized (inbound) {
inbound.closeAbnormal(Status.INTERNAL.withDescription("too many messages"));
}
} else {
pendingSingleMessage = message;
}
}
@Override
public void close(Status status, Metadata trailers) {
try {
synchronized (outbound) {
outbound.sendSingleMessageAndClose(pendingHeaders, pendingSingleMessage, status, trailers);
}
synchronized (inbound) {
inbound.onCloseSent(status);
}
} catch (StatusException se) {
synchronized (inbound) {
inbound.closeAbnormal(se.getStatus());
}
}
}
@Override
public void cancel(Status status) {
synchronized (inbound) {
inbound.closeOnCancel(status);
}
}
@Override
public StatsTraceContext statsTraceContext() {
return outbound.getStatsTraceContext();
}
@Override
public Attributes getAttributes() {
return attributes;
}
@Nullable
@Override
public String getAuthority() {
return attributes.get(BinderTransport.SERVER_AUTHORITY);
}
@Override
public String toString() {
return "SingleMessageServerStream[" + inbound + "/" + outbound + "]";
}
// =====================
// Misc stubbed & unsupported methods.
@Override
public final void flush() {
// Ignore.
}
@Override
public final void setCompressor(Compressor compressor) {
// Ignore.
}
@Override
public final void setMessageCompression(boolean enable) {
// Ignore.
}
@Override
public void setDecompressor(Decompressor decompressor) {
// Ignore.
}
@Override
public void optimizeForDirectExecutor() {
// Ignore.
}
@Override
public int streamId() {
return -1;
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.Parcel;
import io.grpc.MethodDescriptor.MethodType;
import io.grpc.Status;
import javax.annotation.Nullable;
/** Constants and helpers for managing inbound / outbound transactions. */
final class TransactionUtils {
/** Set when the transaction contains rpc prefix data. */
static final int FLAG_PREFIX = 0x1;
/** Set when the transaction contains some message data. */
static final int FLAG_MESSAGE_DATA = 0x2;
/** Set when the transaction contains rpc suffix data. */
static final int FLAG_SUFFIX = 0x4;
/** Set when the transaction is an out-of-band close event. */
static final int FLAG_OUT_OF_BAND_CLOSE = 0x8;
/**
* When a transaction contains client prefix data, this will be set if the rpc being made is
* expected to return a single message. (I.e the method type is either {@link MethodType#UNARY},
* or {@link MethodType#CLIENT_STREAMING}).
*/
static final int FLAG_EXPECT_SINGLE_MESSAGE = 0x10;
/** Set when the included status data includes a description string. */
static final int FLAG_STATUS_DESCRIPTION = 0x20;
/** When a transaction contains message data, this will be set if the message is a parcelable. */
static final int FLAG_MESSAGE_DATA_IS_PARCELABLE = 0x40;
/**
* When a transaction contains message data, this will be set if the message is only partial, and
* further transactions are required.
*/
static final int FLAG_MESSAGE_DATA_IS_PARTIAL = 0x80;
static final int STATUS_CODE_SHIFT = 16;
static final int STATUS_CODE_MASK = 0xff0000;
/** The maximum string length for a status description. */
private static final int MAX_STATUS_DESCRIPTION_LENGTH = 1000;
private TransactionUtils() {}
static boolean hasFlag(int flags, int flag) {
return (flags & flag) != 0;
}
@Nullable
private static String getTruncatedDescription(Status status) {
String desc = status.getDescription();
if (desc != null && desc.length() > MAX_STATUS_DESCRIPTION_LENGTH) {
desc = desc.substring(0, MAX_STATUS_DESCRIPTION_LENGTH);
}
return desc;
}
static Status readStatus(int flags, Parcel parcel) {
Status status = Status.fromCodeValue((flags & STATUS_CODE_MASK) >> STATUS_CODE_SHIFT);
if ((flags & FLAG_STATUS_DESCRIPTION) != 0) {
status = status.withDescription(parcel.readString());
}
return status;
}
static int writeStatus(Parcel parcel, Status status) {
int flags = status.getCode().value() << STATUS_CODE_SHIFT;
String desc = getTruncatedDescription(status);
if (desc != null) {
flags |= FLAG_STATUS_DESCRIPTION;
parcel.writeString(desc);
}
return flags;
}
static void fillInFlags(Parcel parcel, int flags) {
int pos = parcel.dataPosition();
parcel.setDataPosition(0);
parcel.writeInt(flags);
parcel.setDataPosition(pos);
}
}

View File

@ -0,0 +1,61 @@
/*
* 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;
import static com.google.common.truth.Truth.assertThat;
import android.content.ComponentName;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public final class AndroidComponentAddressTest {
private final Context appContext = ApplicationProvider.getApplicationContext();
private final ComponentName hostComponent = new ComponentName(appContext, appContext.getClass());
@Test
public void testAuthority() {
AndroidComponentAddress addr = AndroidComponentAddress.forContext(appContext);
assertThat(addr.getAuthority()).isEqualTo(appContext.getPackageName());
}
@Test
public void testComponent() {
AndroidComponentAddress addr = AndroidComponentAddress.forComponent(hostComponent);
assertThat(addr.getComponent()).isSameInstanceAs(hostComponent);
}
@Test
public void testEquality() {
new EqualsTester()
.addEqualityGroup(
AndroidComponentAddress.forComponent(hostComponent),
AndroidComponentAddress.forContext(appContext),
AndroidComponentAddress.forLocalComponent(appContext, appContext.getClass()),
AndroidComponentAddress.forRemoteComponent(
appContext.getPackageName(), appContext.getClass().getName()))
.addEqualityGroup(
AndroidComponentAddress.forRemoteComponent("appy.mcappface", ".McActivity"))
.addEqualityGroup(AndroidComponentAddress.forLocalComponent(appContext, getClass()))
.testEquals();
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.grpc.binder.internal;
package io.grpc.binder;
import static com.google.common.truth.Truth.assertThat;

View File

@ -0,0 +1,62 @@
/*
* 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;
import static com.google.common.truth.Truth.assertThat;
import io.grpc.Status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowProcess;
@RunWith(RobolectricTestRunner.class)
public final class SecurityPoliciesTest {
private static final int MY_UID = 1234;
private static final int OTHER_UID = MY_UID + 1;
private static final String PERMISSION_DENIED_REASONS = "some reasons";
private SecurityPolicy policy;
@Before
public void setUp() {
ShadowProcess.setUid(MY_UID);
}
@Test
public void testInternalOnly() throws Exception {
policy = SecurityPolicies.internalOnly();
assertThat(policy.checkAuthorization(MY_UID).getCode()).isEqualTo(Status.OK.getCode());
assertThat(policy.checkAuthorization(OTHER_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}
@Test
public void testPermissionDenied() throws Exception {
policy = SecurityPolicies.permissionDenied(PERMISSION_DENIED_REASONS);
assertThat(policy.checkAuthorization(MY_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorization(MY_UID).getDescription())
.isEqualTo(PERMISSION_DENIED_REASONS);
assertThat(policy.checkAuthorization(OTHER_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorization(OTHER_UID).getDescription())
.isEqualTo(PERMISSION_DENIED_REASONS);
}
}

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;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.base.Function;
import io.grpc.Status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowProcess;
@RunWith(RobolectricTestRunner.class)
public final class ServerSecurityPolicyTest {
private static final String SERVICE1 = "service_one";
private static final String SERVICE2 = "service_two";
private static final String SERVICE3 = "service_three";
private static final int MY_UID = 1234;
private static final int OTHER_UID = MY_UID + 1;
ServerSecurityPolicy policy;
@Before
public void setUp() {
ShadowProcess.setUid(MY_UID);
}
@Test
public void testDefaultInternalOnly() {
policy = new ServerSecurityPolicy();
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE1).getCode())
.isEqualTo(Status.OK.getCode());
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE2).getCode())
.isEqualTo(Status.OK.getCode());
}
@Test
public void testInternalOnly_AnotherUid() {
policy = new ServerSecurityPolicy();
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE1).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE2).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}
@Test
public void testBuilderDefault() {
policy = ServerSecurityPolicy.newBuilder().build();
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE1).getCode())
.isEqualTo(Status.OK.getCode());
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE1).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}
@Test
public void testPerService() {
policy =
ServerSecurityPolicy.newBuilder()
.servicePolicy(SERVICE2, policy((uid) -> Status.OK))
.build();
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE1).getCode())
.isEqualTo(Status.OK.getCode());
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE1).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE2).getCode())
.isEqualTo(Status.OK.getCode());
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE2).getCode())
.isEqualTo(Status.OK.getCode());
}
@Test
public void testPerServiceNoDefault() {
policy =
ServerSecurityPolicy.newBuilder()
.servicePolicy(SERVICE1, policy((uid) -> Status.INTERNAL))
.servicePolicy(
SERVICE2, policy((uid) -> uid == OTHER_UID ? Status.OK : Status.PERMISSION_DENIED))
.build();
// Uses the specified policy for service1.
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE1).getCode())
.isEqualTo(Status.INTERNAL.getCode());
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE1).getCode())
.isEqualTo(Status.INTERNAL.getCode());
// Uses the specified policy for service2.
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE2).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE2).getCode())
.isEqualTo(Status.OK.getCode());
// Falls back to the default.
assertThat(policy.checkAuthorizationForService(MY_UID, SERVICE3).getCode())
.isEqualTo(Status.OK.getCode());
assertThat(policy.checkAuthorizationForService(OTHER_UID, SERVICE3).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}
private static SecurityPolicy policy(Function<Integer, Status> func) {
return new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return func.apply(uid);
}
};
}
}

View File

@ -0,0 +1,112 @@
/*
* 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.checkState;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.when;
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import android.os.IBinder;
import android.os.Parcel;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.testing.TestingExecutors;
import io.grpc.Attributes;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.internal.ServerStream;
import io.grpc.internal.ServerTransportListener;
import java.util.concurrent.ScheduledExecutorService;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.LooperMode;
/**
* Low-level server-side transport tests for binder channel. Like BinderChannelSmokeTest, this
* convers edge cases not exercised by AbstractTransportTest, but it deals with the
* binderTransport.BinderServerTransport directly.
*/
@LooperMode(PAUSED)
@RunWith(RobolectricTestRunner.class)
public final class BinderServerTransportTest {
@Rule public MockitoRule mocks = MockitoJUnit.rule();
private final ScheduledExecutorService executorService =
TestingExecutors.sameThreadScheduledExecutor();
private final TestTransportListener transportListener = new TestTransportListener();
@Mock IBinder mockBinder;
BinderTransport.BinderServerTransport transport;
@Before
public void setUp() throws Exception {
transport =
new BinderTransport.BinderServerTransport(
executorService, Attributes.EMPTY, ImmutableList.of(), mockBinder);
}
@Test
public void testSetupTransactionFailureCausesMultipleShutdowns_b153460678() throws Exception {
// Make the binder fail the setup transaction.
when(mockBinder.transact(anyInt(), any(Parcel.class), isNull(), anyInt())).thenReturn(false);
transport.setServerTransportListener(transportListener);
// Now shut it down.
transport.shutdownNow(Status.UNKNOWN.withDescription("reasons"));
assertThat(transportListener.terminated).isTrue();
}
private static final class TestTransportListener implements ServerTransportListener {
public boolean ready;
public boolean terminated;
/**
* Called when a new stream was created by the remote client.
*
* @param stream the newly created stream.
* @param method the fully qualified method name being called on the server.
* @param headers containing metadata for the call.
*/
@Override
public void streamCreated(ServerStream stream, String method, Metadata headers) {}
@Override
public Attributes transportReady(Attributes attributes) {
ready = true;
return attributes;
}
@Override
public void transportTerminated() {
checkState(!terminated, "Terminated twice");
terminated = true;
}
}
}

View File

@ -0,0 +1,129 @@
/*
* 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.truth.Truth.assertThat;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class BlockInputStreamTest {
private final byte[] buff = new byte[1024];
@Test
public void testNoBytes() throws Exception {
try (BlockInputStream bis = new BlockInputStream(new byte[0])) {
assertThat(bis.read()).isEqualTo(-1);
}
}
@Test
public void testNoBlocks() throws Exception {
try (BlockInputStream bis = new BlockInputStream(new byte[0][], 0)) {
assertThat(bis.read()).isEqualTo(-1);
}
}
@Test
public void testSingleBlock() throws Exception {
BlockInputStream bis =
new BlockInputStream(new byte[][] {getBytes(10, 1)}, 10);
assertThat(bis.read(buff, 0, 20)).isEqualTo(10);
assertBytes(buff, 0, 10, 1);
}
@Test
public void testMultipleBlocks() throws Exception {
BlockInputStream bis =
new BlockInputStream(new byte[][] {getBytes(10, 1), getBytes(10, 2)}, 20);
assertThat(bis.read(buff, 0, 20)).isEqualTo(20);
assertBytes(buff, 0, 10, 1);
assertBytes(buff, 10, 10, 2);
}
@Test
public void testMultipleBlocks_drain() throws Exception {
BlockInputStream bis =
new BlockInputStream(new byte[][] {getBytes(10, 1), getBytes(10, 2)}, 20);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bis.drainTo(baos);
byte[] data = baos.toByteArray();
assertThat(data).hasLength(20);
assertBytes(data, 0, 10, 1);
assertBytes(data, 10, 10, 2);
}
@Test
public void testMultipleBlocksLessData() throws Exception {
BlockInputStream bis =
new BlockInputStream(new byte[][] {getBytes(10, 1), getBytes(10, 2)}, 15);
assertThat(bis.read(buff, 0, 20)).isEqualTo(15);
assertBytes(buff, 0, 10, 1);
assertBytes(buff, 10, 5, 2);
}
@Test
public void testMultipleBlocksLessData_drain() throws Exception {
BlockInputStream bis =
new BlockInputStream(new byte[][] {getBytes(10, 1), getBytes(10, 2)}, 15);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bis.drainTo(baos);
byte[] data = baos.toByteArray();
assertThat(data).hasLength(15);
assertBytes(data, 0, 10, 1);
assertBytes(data, 10, 5, 2);
}
@Test
public void testMultipleBlocksEmptyFinalBlock() throws Exception {
BlockInputStream bis =
new BlockInputStream(new byte[][] {getBytes(10, 1), getBytes(0, 0)}, 10);
assertThat(bis.read(buff, 0, 20)).isEqualTo(10);
assertBytes(buff, 0, 10, 1);
assertThat(bis.read(buff, 0, 20)).isEqualTo(-1);
assertThat(bis.read()).isEqualTo(-1);
}
@Test
public void testMultipleBlocksEmptyFinalBlock_drain() throws Exception {
BlockInputStream bis =
new BlockInputStream(new byte[][] {getBytes(10, 1), getBytes(0, 0)}, 10);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bis.drainTo(baos);
byte[] data = baos.toByteArray();
assertThat(data).hasLength(10);
assertBytes(data, 0, 10, 1);
}
private static byte[] getBytes(int size, int val) {
byte[] res = new byte[size];
Arrays.fill(res, 0, size, (byte) val);
return res;
}
private static void assertBytes(byte[] data, int off, int len, int val) {
for (int i = off; i < off + len; i++) {
assertThat(data[i]).isEqualTo((byte) val);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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 com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public final class BoundClientAddressTest {
private static final int MY_UID = 1234;
private static final int OTHER_UID = 1235;
private static final int OTHER_UID_2 = 1236;
@Test
public void testEquality() {
new EqualsTester()
.addEqualityGroup(new BoundClientAddress(MY_UID), new BoundClientAddress(MY_UID))
.addEqualityGroup(new BoundClientAddress(OTHER_UID))
.addEqualityGroup(new BoundClientAddress(OTHER_UID_2))
.testEquals();
}
}

View File

@ -0,0 +1,127 @@
/*
* 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.truth.Truth.assertThat;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.common.io.ByteStreams;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public final class ParcelableInputStreamTest {
private final TestParcelable testParcelable = new TestParcelable("testing");
private final TestParcelable testParcelableWithFds =
new TestParcelable("testing_with_fds", Parcelable.CONTENTS_FILE_DESCRIPTOR);
@Test
public void testGetParcelable() throws Exception {
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.forInstance(testParcelable, TestParcelable.CREATOR);
// We should serialize/deserialize the parcelable.
TestParcelable parceable = stream.getParcelable();
assertThat(parceable).isEqualTo(testParcelable);
assertThat(parceable).isNotSameInstanceAs(testParcelable);
// But just once.
assertThat(stream.getParcelable()).isSameInstanceAs(parceable);
}
@Test
public void testGetParcelableWithFds() throws Exception {
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.forInstance(testParcelableWithFds, TestParcelable.CREATOR);
// We should serialize/deserialize the parcelable.
TestParcelable parceable = stream.getParcelable();
assertThat(parceable).isEqualTo(testParcelableWithFds);
assertThat(parceable).isNotSameInstanceAs(testParcelableWithFds);
// But just once.
assertThat(stream.getParcelable()).isSameInstanceAs(parceable);
}
@Test
public void testGetParcelableImmutable() throws Exception {
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.forImmutableInstance(testParcelable, TestParcelable.CREATOR);
// We should return the parcelable directly.
TestParcelable parceable = stream.getParcelable();
assertThat(parceable).isSameInstanceAs(testParcelable);
}
@Test
public void testGetParcelableImmutableWithFds() throws Exception {
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.forImmutableInstance(testParcelableWithFds, TestParcelable.CREATOR);
// We should return the parcelable directly.
TestParcelable parceable = stream.getParcelable();
assertThat(parceable).isSameInstanceAs(testParcelableWithFds);
}
@Test
public void testWriteToParcel() throws Exception {
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.forImmutableInstance(testParcelable, TestParcelable.CREATOR);
Parcel parcel = Parcel.obtain();
stream.writeToParcel(parcel);
parcel.setDataPosition(0);
assertThat((TestParcelable) parcel.readParcelable(getClass().getClassLoader()))
.isEqualTo(testParcelable);
}
@Test
public void testCreateFromParcel() throws Exception {
Parcel parcel = Parcel.obtain();
parcel.writeParcelable(testParcelable, 0);
parcel.setDataPosition(0);
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.readFromParcel(parcel, getClass().getClassLoader());
assertThat(stream.getParcelable()).isEqualTo(testParcelable);
}
@Test
public void testAsRegularInputStream() throws Exception {
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.forInstance(testParcelable, TestParcelable.CREATOR);
byte[] data = ByteStreams.toByteArray(stream);
Parcel parcel = Parcel.obtain();
parcel.unmarshall(data, 0, data.length);
parcel.setDataPosition(0);
assertThat((TestParcelable) parcel.readParcelable(getClass().getClassLoader()))
.isEqualTo(testParcelable);
}
@Test
public void testAsRegularInputStreamFds() throws Exception {
ParcelableInputStream<TestParcelable> stream =
ParcelableInputStream.forInstance(testParcelableWithFds, TestParcelable.CREATOR);
byte[] data = ByteStreams.toByteArray(stream);
assertThat(data.length).isNotEqualTo(0);
}
}

View File

@ -0,0 +1,142 @@
/*
* 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.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.util.concurrent.TimeUnit.SECONDS;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.internal.ClientTransport;
import io.grpc.internal.FakeClock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class PingTrackerTest {
private final FakeClock clock = new FakeClock();
@Nullable private Status pingFailureStatus;
private List<Integer> sentPings;
private TestCallback callback;
private PingTracker pingTracker;
@Before
public void setUp() {
sentPings = new ArrayList<>();
callback = new TestCallback();
pingTracker =
new PingTracker(
clock.getTimeProvider(),
(id) -> {
sentPings.add(id);
if (pingFailureStatus != null) {
throw pingFailureStatus.asException();
}
});
}
@Test
public void successfulPing() throws Exception {
pingTracker.startPing(callback, directExecutor());
assertThat(sentPings).hasSize(1);
callback.assertNotCalled();
clock.forwardTime(3, SECONDS);
pingTracker.onPingResponse(sentPings.get(0));
callback.assertSuccess(Duration.ofSeconds(3).toNanos());
}
@Test
public void failedPing() throws Exception {
pingFailureStatus = Status.INTERNAL.withDescription("Hello");
pingTracker.startPing(callback, directExecutor());
callback.assertFailure(pingFailureStatus);
}
@Test
public void noSuccessAfterFailure() throws Exception {
pingFailureStatus = Status.INTERNAL.withDescription("Hello");
pingTracker.startPing(callback, directExecutor());
pingTracker.onPingResponse(sentPings.get(0));
callback.assertFailure(pingFailureStatus);
}
@Test
public void noMultiSuccess() throws Exception {
pingTracker.startPing(callback, directExecutor());
pingTracker.onPingResponse(sentPings.get(0));
pingTracker.onPingResponse(sentPings.get(0));
callback.assertSuccess(); // Checks we were only called once.
}
private static final class TestCallback implements ClientTransport.PingCallback {
private int numCallbacks;
private boolean success;
private boolean failure;
private Throwable failureException;
private long roundtripTimeNanos;
@Override
public synchronized void onSuccess(long roundtripTimeNanos) {
numCallbacks += 1;
success = true;
this.roundtripTimeNanos = roundtripTimeNanos;
}
@Override
public synchronized void onFailure(Throwable failureException) {
numCallbacks += 1;
failure = true;
this.failureException = failureException;
}
public void assertNotCalled() {
assertThat(numCallbacks).isEqualTo(0);
}
public void assertSuccess() {
assertThat(numCallbacks).isEqualTo(1);
assertThat(success).isTrue();
}
public void assertSuccess(long expectRoundTripTimeNanos) {
assertSuccess();
assertThat(roundtripTimeNanos).isEqualTo(expectRoundTripTimeNanos);
}
public void assertFailure(Status status) {
assertThat(numCallbacks).isEqualTo(1);
assertThat(failure).isTrue();
assertThat(((StatusException) failureException).getStatus()).isSameInstanceAs(status);
}
public void assertFailure(Status.Code statusCode) {
assertThat(numCallbacks).isEqualTo(1);
assertThat(failure).isTrue();
assertThat(((StatusException) failureException).getStatus().getCode()).isEqualTo(statusCode);
}
}
}

View File

@ -0,0 +1,71 @@
/*
* 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.Parcel;
import android.os.Parcelable;
/** A parcelable for testing. */
public class TestParcelable implements Parcelable {
private final String msg;
private final int contents;
public TestParcelable(String msg) {
this(msg, 0);
}
public TestParcelable(String msg, int contents) {
this.msg = msg;
this.contents = contents;
}
@Override
public int describeContents() {
return contents;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(msg);
}
@Override
public int hashCode() {
return msg.hashCode();
}
@Override
public boolean equals(Object other) {
if (other instanceof TestParcelable) {
return msg.equals(((TestParcelable) other).msg);
}
return false;
}
public static final Parcelable.Creator<TestParcelable> CREATOR =
new Parcelable.Creator<TestParcelable>() {
@Override
public TestParcelable createFromParcel(Parcel parcel) {
return new TestParcelable(parcel.readString(), 0);
}
@Override
public TestParcelable[] newArray(int size) {
return new TestParcelable[size];
}
};
}

View File

@ -186,10 +186,12 @@ subprojects {
// Test dependencies.
junit: 'junit:junit:4.12',
mockito: 'org.mockito:mockito-core:3.3.3',
mockito_android: 'org.mockito:mockito-android:3.8.0',
truth: 'com.google.truth:truth:1.0.1',
guava_testlib: "com.google.guava:guava-testlib:${guavaVersion}",
androidx_annotation: "androidx.annotation:annotation:1.1.0",
androidx_core: "androidx.core:core:1.3.0",
androidx_lifecycle_service: "androidx.lifecycle:lifecycle-service:2.3.0",
androidx_test: "androidx.test:core:1.3.0",
androidx_test_rules: "androidx.test:rules:1.3.0",
androidx_test_ext_junit: "androidx.test.ext:junit:1.1.2",