mirror of https://github.com/grpc/grpc-java.git
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:
parent
2239dd717c
commit
8e18c11bbd
|
@ -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 }
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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 + "]";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + "]";
|
||||
}
|
||||
}
|
|
@ -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)]" : "]");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + "]";
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue