diff --git a/RELEASING.md b/RELEASING.md
index cbc26905c2..40c495d28d 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -63,6 +63,7 @@ them before continuing, and set them again when resuming.
$ MAJOR=1 MINOR=7 PATCH=0 # Set appropriately for new release
$ VERSION_FILES=(
build.gradle
+ android/build.gradle
android-interop-testing/app/build.gradle
core/src/main/java/io/grpc/internal/GrpcUtil.java
cronet/build.gradle
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000000..86309689dd
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,53 @@
+apply plugin: 'com.android.library'
+
+description = 'gRPC: Android'
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ mavenCentral()
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+ classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+ }
+}
+
+apply plugin: "net.ltgt.errorprone"
+
+android {
+ compileSdkVersion 27
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 27
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+ lintOptions {
+ abortOnError false
+ }
+}
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+}
+
+dependencies {
+ implementation 'io.grpc:grpc-core:1.12.0-SNAPSHOT' // CURRENT_GRPC_VERSION
+ testImplementation 'io.grpc:grpc-okhttp:1.12.0-SNAPSHOT' // CURRENT_GRPC_VERSION
+
+ testImplementation 'junit:junit:4.12'
+ testImplementation 'org.robolectric:robolectric:3.7.1'
+ testImplementation 'com.google.truth:truth:0.39'
+}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..5383673959
--- /dev/null
+++ b/android/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java
new file mode 100644
index 0000000000..413ee59636
--- /dev/null
+++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2018, gRPC Authors All rights reserved.
+ *
+ * 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.android;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.util.Log;
+import com.google.common.annotations.VisibleForTesting;
+import io.grpc.CallOptions;
+import io.grpc.ClientCall;
+import io.grpc.ConnectivityState;
+import io.grpc.ExperimentalApi;
+import io.grpc.ForwardingChannelBuilder;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.MethodDescriptor;
+import io.grpc.internal.GrpcUtil;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Builds a {@link ManagedChannel} that, when provided with a {@link Context}, will automatically
+ * monitor the Android device's network state to smoothly handle intermittent network failures.
+ *
+ *
Requires the Android ACCESS_NETWORK_STATE permission.
+ *
+ * @since 1.12.0
+ */
+@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4056")
+public final class AndroidChannelBuilder extends ForwardingChannelBuilder {
+
+ private static final String LOG_TAG = "AndroidChannelBuilder";
+
+ @Nullable private static final Class> OKHTTP_CHANNEL_BUILDER_CLASS = findOkHttp();
+
+ private static final Class> findOkHttp() {
+ try {
+ return Class.forName("io.grpc.okhttp.OkHttpChannelBuilder");
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ private final ManagedChannelBuilder delegateBuilder;
+
+ @Nullable private Context context;
+
+ public static final AndroidChannelBuilder forTarget(String target) {
+ return new AndroidChannelBuilder(target);
+ }
+
+ public static AndroidChannelBuilder forAddress(String name, int port) {
+ return forTarget(GrpcUtil.authorityFromHostAndPort(name, port));
+ }
+
+ private AndroidChannelBuilder(String target) {
+ if (OKHTTP_CHANNEL_BUILDER_CLASS == null) {
+ throw new UnsupportedOperationException("No ManagedChannelBuilder found on the classpath");
+ }
+ try {
+ delegateBuilder =
+ (ManagedChannelBuilder)
+ OKHTTP_CHANNEL_BUILDER_CLASS
+ .getMethod("forTarget", String.class)
+ .invoke(null, target);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create ManagedChannelBuilder", e);
+ }
+ }
+
+ /** Enables automatic monitoring of the device's network state. */
+ public AndroidChannelBuilder context(Context context) {
+ this.context = context;
+ return this;
+ }
+
+ @Override
+ protected ManagedChannelBuilder> delegate() {
+ return delegateBuilder;
+ }
+
+ @Override
+ public ManagedChannel build() {
+ return new AndroidChannel(delegateBuilder.build(), context);
+ }
+
+ /**
+ * Wraps an OkHttp channel and handles invoking the appropriate methods (e.g., {@link
+ * ManagedChannel#resetConnectBackoff}) when the device network state changes.
+ */
+ @VisibleForTesting
+ static final class AndroidChannel extends ManagedChannel {
+
+ private final ManagedChannel delegate;
+
+ @Nullable private final Context context;
+ @Nullable private final ConnectivityManager connectivityManager;
+
+ private final Object lock = new Object();
+
+ @GuardedBy("lock")
+ private Runnable unregisterRunnable;
+
+ @VisibleForTesting
+ AndroidChannel(final ManagedChannel delegate, @Nullable Context context) {
+ this.delegate = delegate;
+ this.context = context;
+
+ if (context != null) {
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ configureNetworkMonitoring();
+ } else {
+ connectivityManager = null;
+ }
+ }
+
+ @GuardedBy("lock")
+ private void configureNetworkMonitoring() {
+ // Eagerly check current network state to verify app has required permissions
+ NetworkInfo currentNetwork;
+ try {
+ currentNetwork = connectivityManager.getActiveNetworkInfo();
+ } catch (SecurityException e) {
+ Log.w(
+ LOG_TAG,
+ "Failed to configure network monitoring. Does app have ACCESS_NETWORK_STATE"
+ + " permission?",
+ e);
+ return;
+ }
+
+ // Android N added the registerDefaultNetworkCallback API to listen to changes in the device's
+ // default network. For earlier Android API levels, use the BroadcastReceiver API.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
+ // The connection status may change before registration of the listener is complete, but
+ // this will at worst result in invoking resetConnectBackoff() instead of enterIdle() (or
+ // vice versa) on the first network change.
+ boolean isConnected = currentNetwork != null && currentNetwork.isConnected();
+
+ final DefaultNetworkCallback defaultNetworkCallback =
+ new DefaultNetworkCallback(isConnected);
+ connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
+ unregisterRunnable =
+ new Runnable() {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void run() {
+ connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
+ }
+ };
+ } else {
+ final NetworkReceiver networkReceiver = new NetworkReceiver();
+ IntentFilter networkIntentFilter =
+ new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(networkReceiver, networkIntentFilter);
+ unregisterRunnable =
+ new Runnable() {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void run() {
+ context.unregisterReceiver(networkReceiver);
+ }
+ };
+ }
+ }
+
+ private void unregisterNetworkListener() {
+ synchronized (lock) {
+ if (unregisterRunnable != null) {
+ unregisterRunnable.run();
+ unregisterRunnable = null;
+ }
+ }
+ }
+
+ @Override
+ public ManagedChannel shutdown() {
+ unregisterNetworkListener();
+ return delegate.shutdown();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return delegate.isShutdown();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return delegate.isTerminated();
+ }
+
+ @Override
+ public ManagedChannel shutdownNow() {
+ unregisterNetworkListener();
+ return delegate.shutdownNow();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+ return delegate.awaitTermination(timeout, unit);
+ }
+
+ @Override
+ public ClientCall newCall(
+ MethodDescriptor methodDescriptor, CallOptions callOptions) {
+ return delegate.newCall(methodDescriptor, callOptions);
+ }
+
+ @Override
+ public String authority() {
+ return delegate.authority();
+ }
+
+ @Override
+ public ConnectivityState getState(boolean requestConnection) {
+ return delegate.getState(requestConnection);
+ }
+
+ @Override
+ public void notifyWhenStateChanged(ConnectivityState source, Runnable callback) {
+ delegate.notifyWhenStateChanged(source, callback);
+ }
+
+ @Override
+ public void resetConnectBackoff() {
+ delegate.resetConnectBackoff();
+ }
+
+ @Override
+ public void enterIdle() {
+ delegate.enterIdle();
+ }
+
+ /** Respond to changes in the default network. Only used on API levels 24+. */
+ @TargetApi(Build.VERSION_CODES.N)
+ private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback {
+ private boolean isConnected = false;
+
+ private DefaultNetworkCallback(boolean isConnected) {
+ this.isConnected = isConnected;
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ if (isConnected) {
+ delegate.enterIdle();
+ } else {
+ delegate.resetConnectBackoff();
+ }
+ isConnected = true;
+ }
+
+ @Override
+ public void onLost(Network network) {
+ isConnected = false;
+ }
+ }
+
+ /** Respond to network changes. Only used on API levels < 24. */
+ private class NetworkReceiver extends BroadcastReceiver {
+ private boolean isConnected = false;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ConnectivityManager conn =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = conn.getActiveNetworkInfo();
+ boolean wasConnected = isConnected;
+ isConnected = networkInfo != null && networkInfo.isConnected();
+ if (isConnected && !wasConnected) {
+ delegate.resetConnectBackoff();
+ }
+ }
+ }
+ }
+}
diff --git a/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java
new file mode 100644
index 0000000000..828390d846
--- /dev/null
+++ b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2018, gRPC Authors All rights reserved.
+ *
+ * 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.android;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.N;
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.RuntimeEnvironment.getApiLevel;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import io.grpc.CallOptions;
+import io.grpc.ClientCall;
+import io.grpc.ManagedChannel;
+import io.grpc.MethodDescriptor;
+import java.util.HashSet;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowConnectivityManager;
+import org.robolectric.shadows.ShadowNetwork;
+import org.robolectric.shadows.ShadowNetworkInfo;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {AndroidChannelBuilderTest.ShadowDefaultNetworkListenerConnectivityManager.class})
+public final class AndroidChannelBuilderTest {
+ private static final NetworkInfo WIFI_CONNECTED =
+ ShadowNetworkInfo.newInstance(
+ NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true);
+ private static final NetworkInfo WIFI_DISCONNECTED =
+ ShadowNetworkInfo.newInstance(
+ NetworkInfo.DetailedState.DISCONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, false);
+ private final NetworkInfo MOBILE_CONNECTED =
+ ShadowNetworkInfo.newInstance(
+ NetworkInfo.DetailedState.CONNECTED,
+ ConnectivityManager.TYPE_MOBILE,
+ ConnectivityManager.TYPE_MOBILE_MMS,
+ true,
+ true);
+ private final NetworkInfo MOBILE_DISCONNECTED =
+ ShadowNetworkInfo.newInstance(
+ NetworkInfo.DetailedState.DISCONNECTED,
+ ConnectivityManager.TYPE_MOBILE,
+ ConnectivityManager.TYPE_MOBILE_MMS,
+ true,
+ false);
+
+ private ConnectivityManager connectivityManager;
+ private ShadowConnectivityManager shadowConnectivityManager;
+
+ @Before
+ public void setUp() {
+ connectivityManager =
+ (ConnectivityManager)
+ RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE);
+ shadowConnectivityManager = shadowOf(connectivityManager);
+ }
+
+ @Test
+ public void channelBuilderClassFoundReflectively() {
+ // This should not throw with OkHttpChannelBuilder on the classpath
+ AndroidChannelBuilder.forTarget("target");
+ }
+
+ @Test
+ @Config(sdk = 23)
+ public void nullContextDoesNotThrow_api23() {
+ TestChannel delegateChannel = new TestChannel();
+ ManagedChannel androidChannel = new AndroidChannelBuilder.AndroidChannel(delegateChannel, null);
+
+ // Network change and shutdown should be no-op for the channel without an Android Context
+ shadowConnectivityManager.setActiveNetworkInfo(WIFI_CONNECTED);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ androidChannel.shutdown();
+
+ assertThat(delegateChannel.resetCount).isEqualTo(0);
+ }
+
+ @Test
+ @Config(sdk = 24)
+ public void nullContextDoesNotThrow_api24() {
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_DISCONNECTED);
+ TestChannel delegateChannel = new TestChannel();
+ ManagedChannel androidChannel = new AndroidChannelBuilder.AndroidChannel(delegateChannel, null);
+
+ // Network change and shutdown should be no-op for the channel without an Android Context
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_CONNECTED);
+ androidChannel.shutdown();
+
+ assertThat(delegateChannel.resetCount).isEqualTo(0);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(0);
+ }
+
+ @Test
+ @Config(sdk = 23)
+ public void resetConnectBackoff_api23() {
+ TestChannel delegateChannel = new TestChannel();
+ ManagedChannel androidChannel =
+ new AndroidChannelBuilder.AndroidChannel(
+ delegateChannel, RuntimeEnvironment.application.getApplicationContext());
+ assertThat(delegateChannel.resetCount).isEqualTo(0);
+
+ // On API levels < 24, the broadcast receiver will invoke resetConnectBackoff() on the first
+ // connectivity action broadcast regardless of previous connection status
+ shadowConnectivityManager.setActiveNetworkInfo(WIFI_CONNECTED);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ assertThat(delegateChannel.resetCount).isEqualTo(1);
+
+ // The broadcast receiver may fire when the active network status has not actually changed
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ assertThat(delegateChannel.resetCount).isEqualTo(1);
+
+ // Drop the connection
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ assertThat(delegateChannel.resetCount).isEqualTo(1);
+
+ // Notify that a new but not connected network is available
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_DISCONNECTED);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ assertThat(delegateChannel.resetCount).isEqualTo(1);
+
+ // Establish a connection
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_CONNECTED);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ assertThat(delegateChannel.resetCount).isEqualTo(2);
+
+ // Disconnect, then shutdown the channel and verify that the broadcast receiver has been
+ // unregistered
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ androidChannel.shutdown();
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_CONNECTED);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+
+ assertThat(delegateChannel.resetCount).isEqualTo(2);
+ // enterIdle is not called on API levels < 24
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(0);
+ }
+
+ @Test
+ @Config(sdk = 24)
+ public void resetConnectBackoffAndEnterIdle_api24() {
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_DISCONNECTED);
+ TestChannel delegateChannel = new TestChannel();
+ ManagedChannel androidChannel =
+ new AndroidChannelBuilder.AndroidChannel(
+ delegateChannel, RuntimeEnvironment.application.getApplicationContext());
+ assertThat(delegateChannel.resetCount).isEqualTo(0);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(0);
+
+ // Establish an initial network connection
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_CONNECTED);
+ assertThat(delegateChannel.resetCount).isEqualTo(1);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(0);
+
+ // Switch to another network to trigger enterIdle()
+ shadowConnectivityManager.setActiveNetworkInfo(WIFI_CONNECTED);
+ assertThat(delegateChannel.resetCount).isEqualTo(1);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(1);
+
+ // Switch to an offline network and then to null
+ shadowConnectivityManager.setActiveNetworkInfo(WIFI_DISCONNECTED);
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ assertThat(delegateChannel.resetCount).isEqualTo(1);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(1);
+
+ // Establish a connection
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_CONNECTED);
+ assertThat(delegateChannel.resetCount).isEqualTo(2);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(1);
+
+ // Disconnect, then shutdown the channel and verify that the callback has been unregistered
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ androidChannel.shutdown();
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_CONNECTED);
+
+ assertThat(delegateChannel.resetCount).isEqualTo(2);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(1);
+ }
+
+ @Test
+ @Config(sdk = 24)
+ public void newChannelWithConnection_entersIdleOnConnectionChange_api24() {
+ shadowConnectivityManager.setActiveNetworkInfo(MOBILE_CONNECTED);
+ TestChannel delegateChannel = new TestChannel();
+ ManagedChannel androidChannel =
+ new AndroidChannelBuilder.AndroidChannel(
+ delegateChannel, RuntimeEnvironment.application.getApplicationContext());
+
+ shadowConnectivityManager.setActiveNetworkInfo(WIFI_CONNECTED);
+ assertThat(delegateChannel.resetCount).isEqualTo(0);
+ assertThat(delegateChannel.enterIdleCount).isEqualTo(1);
+
+ androidChannel.shutdown();
+ }
+
+ @Test
+ @Config(sdk = 23)
+ public void shutdownNowUnregistersBroadcastReceiver_api23() {
+ TestChannel delegateChannel = new TestChannel();
+ ManagedChannel androidChannel =
+ new AndroidChannelBuilder.AndroidChannel(
+ delegateChannel, RuntimeEnvironment.application.getApplicationContext());
+
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+ androidChannel.shutdownNow();
+ shadowConnectivityManager.setActiveNetworkInfo(WIFI_CONNECTED);
+ RuntimeEnvironment.application.sendBroadcast(
+ new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
+
+ assertThat(delegateChannel.resetCount).isEqualTo(0);
+ }
+
+ @Test
+ @Config(sdk = 24)
+ public void shutdownNowUnregistersNetworkCallback_api24() {
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ TestChannel delegateChannel = new TestChannel();
+ ManagedChannel androidChannel =
+ new AndroidChannelBuilder.AndroidChannel(
+ delegateChannel, RuntimeEnvironment.application.getApplicationContext());
+
+ androidChannel.shutdownNow();
+ shadowConnectivityManager.setActiveNetworkInfo(WIFI_CONNECTED);
+
+ assertThat(delegateChannel.resetCount).isEqualTo(0);
+ }
+
+ /**
+ * Extends Robolectric ShadowConnectivityManager to handle Android N's
+ * registerDefaultNetworkCallback API.
+ */
+ @Implements(value = ConnectivityManager.class)
+ public static class ShadowDefaultNetworkListenerConnectivityManager
+ extends ShadowConnectivityManager {
+ private HashSet defaultNetworkCallbacks = new HashSet<>();
+
+ public ShadowDefaultNetworkListenerConnectivityManager() {
+ super();
+ }
+
+ @Override
+ public void setActiveNetworkInfo(NetworkInfo activeNetworkInfo) {
+ if (getApiLevel() >= N) {
+ NetworkInfo previousNetworkInfo = getActiveNetworkInfo();
+ if (activeNetworkInfo != null && activeNetworkInfo.isConnected()) {
+ notifyDefaultNetworkCallbacksOnAvailable(
+ ShadowNetwork.newInstance(activeNetworkInfo.getType() /* use type as network ID */));
+ } else if (previousNetworkInfo != null) {
+ notifyDefaultNetworkCallbacksOnLost(
+ ShadowNetwork.newInstance(
+ previousNetworkInfo.getType() /* use type as network ID */));
+ }
+ }
+ super.setActiveNetworkInfo(activeNetworkInfo);
+ }
+
+ private void notifyDefaultNetworkCallbacksOnAvailable(Network network) {
+ for (ConnectivityManager.NetworkCallback networkCallback : defaultNetworkCallbacks) {
+ networkCallback.onAvailable(network);
+ }
+ }
+
+ private void notifyDefaultNetworkCallbacksOnLost(Network network) {
+ for (ConnectivityManager.NetworkCallback networkCallback : defaultNetworkCallbacks) {
+ networkCallback.onLost(network);
+ }
+ }
+
+ @Implementation(minSdk = N)
+ protected void registerDefaultNetworkCallback(
+ ConnectivityManager.NetworkCallback networkCallback) {
+ defaultNetworkCallbacks.add(networkCallback);
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ @Override
+ public void unregisterNetworkCallback(ConnectivityManager.NetworkCallback networkCallback) {
+ if (getApiLevel() >= N) {
+ if (networkCallback != null || defaultNetworkCallbacks.contains(networkCallback)) {
+ defaultNetworkCallbacks.remove(networkCallback);
+ }
+ }
+ super.unregisterNetworkCallback(networkCallback);
+ }
+ }
+
+ private static class TestChannel extends ManagedChannel {
+ int resetCount;
+ int enterIdleCount;
+
+ @Override
+ public ManagedChannel shutdown() {
+ return null;
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return false;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return false;
+ }
+
+ @Override
+ public ManagedChannel shutdownNow() {
+ return null;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+ return false;
+ }
+
+ @Override
+ public ClientCall newCall(
+ MethodDescriptor methodDescriptor, CallOptions callOptions) {
+ return null;
+ }
+
+ @Override
+ public String authority() {
+ return null;
+ }
+
+ @Override
+ public void resetConnectBackoff() {
+ resetCount++;
+ }
+
+ @Override
+ public void enterIdle() {
+ enterIdleCount++;
+ }
+ }
+}
diff --git a/buildscripts/kokoro/android.sh b/buildscripts/kokoro/android.sh
index 598647cda4..0d4ce60d03 100755
--- a/buildscripts/kokoro/android.sh
+++ b/buildscripts/kokoro/android.sh
@@ -23,13 +23,19 @@ ln -s "/tmp/protobuf-${PROTOBUF_VERSION}/$(uname -s)-$(uname -p)" /tmp/protobuf
./gradlew install
-# Build Cronet
+# Build grpc-cronet
pushd cronet
./cronet_deps.sh
../gradlew build
popd
+# Build grpc-android
+
+pushd android
+../gradlew build
+popd
+
# Build examples
cd ./examples/android/clientcache