binder: Some basic binderchannel util code (#7796)

This just adds the ServiceBinding class and
BindServiceFlags, internal utils.

Most binderchannel code relies heavily on Java8 features,
so I'm keeping that requirement, since grpc-java plans to
require Java8 eventually anyway.
This commit is contained in:
markb74 2021-03-24 23:40:11 +01:00 committed by GitHub
parent 3ccc6792d5
commit c6d48f7cb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 986 additions and 1 deletions

45
binder/build.gradle Normal file
View File

@ -0,0 +1,45 @@
plugins {
id "maven-publish"
id "com.android.library"
id "ru.vyarus.animalsniffer"
}
description = 'gRPC BinderChannel'
android {
compileSdkVersion 29
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions { abortOnError false }
}
repositories {
google()
jcenter()
}
dependencies {
api project(':grpc-core')
guavaDependency 'implementation'
implementation libraries.androidx_annotation
testImplementation libraries.androidx_core
testImplementation libraries.androidx_test
testImplementation libraries.junit
testImplementation libraries.mockito
testImplementation (libraries.robolectric) {
// Unreleased change: https://github.com/robolectric/robolectric/pull/5432
exclude group: 'com.google.auto.service', module: 'auto-service'
}
testImplementation libraries.truth
}
[publishMavenPublicationToMavenRepository]*.onlyIf { false }

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.grpc.binder">
</manifest>

View File

@ -0,0 +1,227 @@
/*
* Copyright 2021 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder.internal;
import static android.content.Context.BIND_ABOVE_CLIENT;
import static android.content.Context.BIND_ADJUST_WITH_ACTIVITY;
import static android.content.Context.BIND_ALLOW_OOM_MANAGEMENT;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_IMPORTANT;
import static android.content.Context.BIND_INCLUDE_CAPABILITIES;
import static android.content.Context.BIND_NOT_FOREGROUND;
import static android.content.Context.BIND_NOT_PERCEPTIBLE;
import static android.content.Context.BIND_WAIVE_PRIORITY;
import static java.lang.Integer.toHexString;
import androidx.annotation.RequiresApi;
/**
* An immutable set of flags affecting the behavior of {@link android.content.Context#bindService}.
*
* <p>Only flags suitable for use with gRPC/binderchannel are available to manipulate here. The
* javadoc for each setter discusses each supported flag's semantics at the gRPC layer.
*/
public final class BindServiceFlags {
/**
* A set of default flags suitable for most applications.
*
* <p>The {@link Builder#setAutoCreate} flag is guaranteed to be set.
*/
public static final BindServiceFlags DEFAULTS = newBuilder().setAutoCreate(true).build();
private final int flags;
private BindServiceFlags(int flags) {
this.flags = flags;
}
/**
* Returns the sum of all set flags as an int suitable for passing to ({@link
* android.content.Context#bindService}.
*/
public int toInteger() {
return flags;
}
/**
* Returns a new instance of {@link Builder} with *no* flags set.
*
* <p>Callers should start with {@code DEFAULTS.toBuilder()} instead.
*/
static Builder newBuilder() {
return new Builder(0);
}
/** Returns a new instance of {@link Builder} with the same set of flags as {@code this}. */
public Builder toBuilder() {
return new Builder(flags);
}
/** Builds an instance of {@link BindServiceFlags}. */
public static final class Builder {
private int flags;
private Builder(int flags) {
this.flags = flags;
}
/**
* Sets or clears the {@link android.content.Context#BIND_ABOVE_CLIENT} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
public Builder setAboveClient(boolean newValue) {
return setFlag(BIND_ABOVE_CLIENT, newValue);
}
/**
* Sets or clears the {@link android.content.Context#BIND_ADJUST_WITH_ACTIVITY} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
public Builder setAdjustWithActivity(boolean newValue) {
return setFlag(BIND_ADJUST_WITH_ACTIVITY, newValue);
}
/**
* Sets or clears the {@link android.content.Context#BIND_ALLOW_OOM_MANAGEMENT} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
public Builder setAllowOomManagement(boolean newValue) {
return setFlag(BIND_ALLOW_OOM_MANAGEMENT, newValue);
}
/**
* Controls whether sending a call over the associated {@link io.grpc.Channel} will cause the
* target {@link android.app.Service} to be created and whether in-flight calls will keep it in
* existence absent any other binding in the system.
*
* <p>If false, RPCs will not succeed until the remote Service comes into existence for some
* other reason (if ever). See also {@link io.grpc.CallOptions#withWaitForReady()}.
*
* <p>See {@link android.content.Context#BIND_AUTO_CREATE} for more.
*
* @return this, for fluent construction
*/
public Builder setAutoCreate(boolean newValue) {
return setFlag(BIND_AUTO_CREATE, newValue);
}
/**
* Sets or clears the {@link android.content.Context#BIND_IMPORTANT} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
public Builder setImportant(boolean newValue) {
return setFlag(BIND_IMPORTANT, newValue);
}
/**
* Sets or clears the {@link android.content.Context#BIND_INCLUDE_CAPABILITIES} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
@RequiresApi(api = 29)
public Builder setIncludeCapabilities(boolean newValue) {
return setFlag(BIND_INCLUDE_CAPABILITIES, newValue);
}
/**
* Sets or clears the {@link android.content.Context#BIND_NOT_FOREGROUND} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
public Builder setNotForeground(boolean newValue) {
return setFlag(BIND_NOT_FOREGROUND, newValue);
}
/**
* Sets or clears the {@link android.content.Context#BIND_NOT_PERCEPTIBLE} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
@RequiresApi(api = 29)
public Builder setNotPerceptible(boolean newValue) {
return setFlag(BIND_NOT_PERCEPTIBLE, newValue);
}
/**
* Sets or clears the {@link android.content.Context#BIND_WAIVE_PRIORITY} flag.
*
* <p>This flag has no additional meaning at the gRPC layer. See the Android docs for more.
*
* @return this, for fluent construction
*/
public Builder setWaivePriority(boolean newValue) {
return setFlag(BIND_WAIVE_PRIORITY, newValue);
}
/**
* Returns a new instance of {@link BindServiceFlags} that reflects the state of this builder.
*/
public BindServiceFlags build() {
return new BindServiceFlags(flags);
}
private Builder setFlag(int flag, boolean newValue) {
if (newValue) {
flags |= flag;
} else {
flags &= ~flag;
}
return this;
}
}
@Override
public String toString() {
return "BindServiceFlags{" + toHexString(flags) + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BindServiceFlags that = (BindServiceFlags) o;
return flags == that.flags;
}
@Override
public int hashCode() {
return flags;
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2020 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder.internal;
import android.os.IBinder;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import io.grpc.Status;
/** An interface for managing a {@code Binder} connection. */
interface Bindable {
/**
* Callbacks from this class.
*
* <p>Methods will be called at most once, and always on the application's main thread.
*/
interface Observer {
/** We're now bound to the service. Only called once, and only if the binding succeeded. */
@MainThread
void onBound(IBinder binder);
/**
* We've disconnected from (or failed to bind to) the service. This will only be called once,
* after which no other calls will be made (but see note on threading above).
*
* @param reason why the connection failed or couldn't be established in the first place
*/
@MainThread
void onUnbound(Status reason);
}
/**
* Attempt to bind with the remote service.
*
* <p>Calling this multiple times or after {@link #unbind()} has no effect.
*/
@AnyThread
void bind();
/**
* Unbind from the remote service if connected.
*
* <p>Observers will be notified with reason code {@code CANCELLED}.
*
* <p>Subsequent calls to {@link #bind()} (or this method) will have no effect.
*/
@AnyThread
void unbind();
}

View File

@ -0,0 +1,215 @@
/*
* 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.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.Status;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
* Manages an Android binding that's restricted to at most one connection to the remote Service.
*
* <p>A note on synchronization & locking in this class. Clients of this class are likely to manage
* their own internal state via synchronization. In order to avoid deadlocks, we must not hold any
* locks while calling observer callbacks.
*
* <p>For this reason, while internal consistency is handled with synchronization (the state field),
* consistency on our observer callbacks is ensured by doing everything on the application's main
* thread.
*/
@ThreadSafe
final class ServiceBinding implements Bindable, ServiceConnection {
private static final Logger logger = Logger.getLogger(ServiceBinding.class.getName());
// States can only ever transition in one direction.
private enum State {
NOT_BINDING,
BINDING,
BOUND,
UNBOUND,
}
private final ComponentName targetComponent;
private final String bindAction;
private final int bindFlags;
private final Observer observer;
private final Executor mainThreadExecutor;
@GuardedBy("this")
private State state;
// The following fields are intentionally not guarded, since (aside from the constructor),
// they're only modified in the main thread. The constructor contains a synchronized block
// to ensure there's a write barrier when these fields are first written.
@Nullable private Context sourceContext; // Only null in the unbound state.
private State reportedState; // Only used on the main thread.
@AnyThread
ServiceBinding(
Executor mainThreadExecutor,
Context sourceContext,
ComponentName targetComponent,
String bindAction,
int bindFlags,
Observer observer) {
// We need to synchronize here ensure other threads see all
// non-final fields initialized after the constructor.
synchronized (this) {
this.targetComponent = targetComponent;
this.bindAction = bindAction;
this.bindFlags = bindFlags;
this.observer = observer;
this.sourceContext = sourceContext;
this.mainThreadExecutor = mainThreadExecutor;
state = State.NOT_BINDING;
reportedState = State.NOT_BINDING;
}
}
@MainThread
private void notifyBound(IBinder binder) {
if (reportedState == State.NOT_BINDING) {
reportedState = State.BOUND;
logger.log(Level.FINEST, "notify bound - notifying");
observer.onBound(binder);
}
}
@MainThread
private void notifyUnbound(Status reason) {
logger.log(Level.FINEST, "notify unbound ", reason);
clearReferences();
if (reportedState != State.UNBOUND) {
reportedState = State.UNBOUND;
logger.log(Level.FINEST, "notify unbound - notifying");
observer.onUnbound(reason);
}
}
@AnyThread
@Override
public synchronized void bind() {
if (state == State.NOT_BINDING) {
state = State.BINDING;
Intent bindIntent = new Intent(bindAction);
bindIntent.setComponent(targetComponent);
Status bindResult = bindInternal(sourceContext, bindIntent, this, bindFlags);
if (!bindResult.isOk()) {
state = State.UNBOUND;
mainThreadExecutor.execute(() -> notifyUnbound(bindResult));
}
}
}
private static Status bindInternal(
Context context, Intent bindIntent, ServiceConnection conn, int flags) {
try {
if (!context.bindService(bindIntent, conn, flags)) {
return Status.UNIMPLEMENTED.withDescription(
"bindService(" + bindIntent + ") returned false");
}
return Status.OK;
} catch (SecurityException e) {
return Status.PERMISSION_DENIED.withCause(e).withDescription(
"SecurityException from bindService");
} catch (RuntimeException e) {
return Status.INTERNAL.withCause(e).withDescription(
"RuntimeException from bindService");
}
}
@Override
@AnyThread
public void unbind() {
unbindInternal(Status.CANCELLED);
}
@AnyThread
void unbindInternal(Status reason) {
Context unbindFrom = null;
synchronized (this) {
if (state == State.BINDING || state == State.BOUND) {
unbindFrom = sourceContext;
}
state = State.UNBOUND;
}
mainThreadExecutor.execute(() -> notifyUnbound(reason));
if (unbindFrom != null) {
unbindFrom.unbindService(this);
}
}
@MainThread
private void clearReferences() {
sourceContext = null;
}
@Override
@MainThread
public void onServiceConnected(ComponentName className, IBinder binder) {
boolean bound = false;
synchronized (this) {
if (state == State.BINDING) {
state = State.BOUND;
bound = true;
}
}
if (bound) {
// We call notify directly because we know we're on the main thread already.
// (every millisecond counts in this path).
notifyBound(binder);
}
}
@Override
@MainThread
public void onServiceDisconnected(ComponentName name) {
unbindInternal(Status.UNAVAILABLE.withDescription("onServiceDisconnected: " + name));
}
@Override
@MainThread
public void onNullBinding(ComponentName name) {
unbindInternal(Status.UNIMPLEMENTED.withDescription("onNullBinding: " + name));
}
@Override
@MainThread
public void onBindingDied(ComponentName name) {
unbindInternal(Status.UNAVAILABLE.withDescription("onBindingDied: " + name));
}
@VisibleForTesting
synchronized boolean isSourceContextCleared() {
return sourceContext == null;
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2021 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder.internal;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class BindServiceFlagsTest {
@Test
public void shouldAutoCreateByDefault() {
assertThat(BindServiceFlags.DEFAULTS.toInteger() & Context.BIND_AUTO_CREATE).isNotEqualTo(0);
}
@Test
public void shouldCheckForInequality() {
assertThat(BindServiceFlags.newBuilder().setAutoCreate(true).build())
.isNotEqualTo(BindServiceFlags.newBuilder().build());
}
@Test
public void shouldCheckForEquality() {
assertThat(BindServiceFlags.DEFAULTS).isEqualTo(BindServiceFlags.DEFAULTS.toBuilder().build());
}
@Test
public void shouldReflectResetFlags() {
assertThat(
BindServiceFlags.newBuilder()
.setAutoCreate(true)
.setAutoCreate(false)
.setAutoCreate(true)
.build()
.toInteger())
.isEqualTo(Context.BIND_AUTO_CREATE);
}
@Test
public void shouldReflectReclearedFlags() {
assertThat(
BindServiceFlags.newBuilder()
.setAutoCreate(false)
.setAutoCreate(true)
.setAutoCreate(false)
.build()
.toInteger())
.isEqualTo(0);
}
@Test
public void shouldReflectSetFlags() {
assertThat(
BindServiceFlags.newBuilder()
.setAutoCreate(true)
.setAdjustWithActivity(true)
.setAboveClient(true)
.setAllowOomManagement(true)
.setImportant(true)
.setIncludeCapabilities(true)
.setNotForeground(true)
.setNotPerceptible(true)
.setWaivePriority(true)
.build()
.toInteger())
.isEqualTo(
Context.BIND_AUTO_CREATE
| Context.BIND_ADJUST_WITH_ACTIVITY
| Context.BIND_ABOVE_CLIENT
| Context.BIND_ALLOW_OOM_MANAGEMENT
| Context.BIND_IMPORTANT
| Context.BIND_INCLUDE_CAPABILITIES
| Context.BIND_NOT_FOREGROUND
| Context.BIND_NOT_PERCEPTIBLE
| Context.BIND_WAIVE_PRIORITY);
}
}

View File

@ -0,0 +1,331 @@
/*
* 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 android.content.Context.BIND_AUTO_CREATE;
import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.binder.util.Bindable.Observer;
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.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowApplication;
@LooperMode(PAUSED)
@RunWith(RobolectricTestRunner.class)
public final class ServiceBindingTest {
@Rule public MockitoRule mocks = MockitoJUnit.rule();
@Mock IBinder mockBinder;
private Application appContext;
private ComponentName serviceComponent;
private ShadowApplication shadowApplication;
private TestObserver observer;
private ServiceBinding binding;
@Before
public void setUp() {
appContext = ApplicationProvider.getApplicationContext();
serviceComponent = new ComponentName("DUMMY", "SERVICE");
observer = new TestObserver();
shadowApplication = shadowOf(appContext);
shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder);
binding = newBuilder().build();
shadowOf(getMainLooper()).idle();
}
private ServiceBindingBuilder newBuilder() {
return new ServiceBindingBuilder()
.setSourceContext(appContext)
.setTargetComponent(serviceComponent)
.setFlags(BIND_AUTO_CREATE)
.setObserver(observer);
}
@Test
public void testInitialState() throws Exception {
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
assertThat(observer.gotBoundEvent).isFalse();
assertThat(observer.gotUnboundEvent).isFalse();
assertThat(binding.isSourceContextCleared()).isFalse();
}
@Test
public void testBind() throws Exception {
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty();
assertThat(observer.gotBoundEvent).isTrue();
assertThat(observer.binder).isSameInstanceAs(mockBinder);
assertThat(observer.gotUnboundEvent).isFalse();
assertThat(binding.isSourceContextCleared()).isFalse();
}
@Test
public void testBindingIntent() throws Exception {
shadowApplication.setComponentNameAndServiceForBindService(null, null);
shadowApplication.setComponentNameAndServiceForBindServiceForIntent(
new Intent("foo").setComponent(serviceComponent), serviceComponent, mockBinder);
binding = newBuilder().setBindingAction("foo").build();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty();
}
@Test
public void testUnbind() throws Exception {
binding.unbind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
assertThat(observer.gotBoundEvent).isFalse();
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.CANCELLED);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testBindUnbind() throws Exception {
binding.bind();
shadowOf(getMainLooper()).idle();
binding.unbind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
assertThat(observer.gotBoundEvent).isTrue();
assertThat(observer.binder).isSameInstanceAs(mockBinder);
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.CANCELLED);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testBindUnbindQuickly() throws Exception {
binding.bind();
binding.unbind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
// Because unbinding happened so quickly, we won't have gotten the bind event.
assertThat(observer.gotBoundEvent).isFalse();
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.CANCELLED);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testUnbindBind() throws Exception {
binding.unbind();
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(shadowApplication.getBoundServiceConnections()).isEmpty();
assertThat(observer.gotBoundEvent).isFalse();
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.CANCELLED);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testBindFailure() throws Exception {
shadowApplication.declareComponentUnbindable(serviceComponent);
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(observer.gotBoundEvent).isFalse();
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.UNIMPLEMENTED);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testBindSecurityException() throws Exception {
SecurityException securityException = new SecurityException();
shadowApplication.setThrowInBindService(securityException);
binding.bind();
shadowOf(getMainLooper()).idle();
assertThat(observer.gotBoundEvent).isFalse();
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(observer.unboundReason.getCause()).isEqualTo(securityException);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testBindDisconnect() throws Exception {
binding.bind();
shadowOf(getMainLooper()).idle();
shadowApplication.getBoundServiceConnections().get(0).onServiceDisconnected(serviceComponent);
shadowOf(getMainLooper()).idle();
assertThat(observer.gotBoundEvent).isTrue();
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testBindDisconnectQuickly() throws Exception {
binding.bind();
shadowApplication.getBoundServiceConnections().get(0).onServiceDisconnected(serviceComponent);
shadowOf(getMainLooper()).idle();
assertThat(observer.gotBoundEvent).isFalse(); // We won't have had time to get the binder.
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
@Config(sdk = {28}) // For onNullBinding.
public void testBindReturnsNull() throws Exception {
binding.bind();
shadowOf(getMainLooper()).idle();
shadowApplication.getBoundServiceConnections().get(0).onNullBinding(serviceComponent);
shadowOf(getMainLooper()).idle();
assertThat(observer.gotBoundEvent).isTrue();
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.UNIMPLEMENTED);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
@Config(sdk = {28}) // For onNullBinding.
public void testBindReturnsNullQuickly() throws Exception {
binding.bind();
shadowApplication.getBoundServiceConnections().get(0).onNullBinding(serviceComponent);
shadowOf(getMainLooper()).idle();
assertThat(observer.gotBoundEvent).isFalse(); // We won't have had a chance to get the binder.
assertThat(observer.gotUnboundEvent).isTrue();
assertThat(observer.unboundReason.getCode()).isEqualTo(Code.UNIMPLEMENTED);
assertThat(binding.isSourceContextCleared()).isTrue();
}
@Test
public void testCallsAfterUnbindDontCrash() throws Exception {
binding.unbind();
shadowOf(getMainLooper()).idle();
assertThat(binding.isSourceContextCleared()).isTrue();
// The internal context is cleared. Try using the object to make sure it doesn't NPE.
binding.bind();
binding.unbind();
shadowOf(getMainLooper()).idle();
}
private void assertNoLockHeld() {
try {
binding.wait(1);
fail("Lock held on binding");
} catch (IllegalMonitorStateException ime) {
// Expected.
} catch (InterruptedException inte) {
throw new AssertionError("Interrupted exception when we shouldn't have been able to wait.", inte);
}
}
private class TestObserver implements Bindable.Observer {
public boolean gotBoundEvent;
public IBinder binder;
public boolean gotUnboundEvent;
public Status unboundReason;
@Override
public void onBound(IBinder binder) {
assertThat(gotBoundEvent).isFalse();
assertNoLockHeld();
gotBoundEvent = true;
this.binder = binder;
}
@Override
public void onUnbound(Status reason) {
assertThat(gotUnboundEvent).isFalse();
assertNoLockHeld();
gotUnboundEvent = true;
unboundReason = reason;
}
}
private static class ServiceBindingBuilder {
private Context sourceContext;
private Observer observer;
private ComponentName targetComponent;
private String bindAction;
private int bindServiceFlags;
public ServiceBindingBuilder setSourceContext(Context sourceContext) {
this.sourceContext = sourceContext;
return this;
}
public ServiceBindingBuilder setBindingAction(String bindAction) {
this.bindAction = bindAction;
return this;
}
public ServiceBindingBuilder setFlags(int bindServiceFlags) {
this.bindServiceFlags = bindServiceFlags;
return this;
}
public ServiceBindingBuilder setTargetComponent(ComponentName targetComponent) {
this.targetComponent = targetComponent;
return this;
}
public ServiceBindingBuilder setObserver(Observer observer) {
this.observer = observer;
return this;
}
public ServiceBinding build() {
return new ServiceBinding(
ContextCompat.getMainExecutor(sourceContext),
sourceContext,
targetComponent,
bindAction,
bindServiceFlags,
observer);
}
}
}

View File

@ -188,8 +188,10 @@ subprojects {
mockito: 'org.mockito:mockito-core:3.3.3',
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_test: "androidx.test:core:1.3.0",
robolectric: "org.robolectric:robolectric:4.3.1",
robolectric: "org.robolectric:robolectric:4.4",
// Benchmark dependencies
hdrhistogram: 'org.hdrhistogram:HdrHistogram:2.1.12',

View File

@ -91,4 +91,6 @@ if (settings.hasProperty('skipAndroid') && skipAndroid.toBoolean()) {
project(':grpc-android').projectDir = "$rootDir/android" as File
include ":grpc-android-interop-testing"
project(':grpc-android-interop-testing').projectDir = "$rootDir/android-interop-testing" as File
include ":grpc-binder"
project(':grpc-binder').projectDir = "$rootDir/binder" as File
}