mirror of https://github.com/grpc/grpc-java.git
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:
parent
3ccc6792d5
commit
c6d48f7cb1
|
@ -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 }
|
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.grpc.binder">
|
||||
</manifest>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue