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