From 64a8fce91c10fd037b3ccddcac3d95422ff1b80e Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Fri, 16 Mar 2018 00:21:19 -0700 Subject: [PATCH 1/7] android: add AndroidChannelBuilder --- android/build.gradle | 50 ++++ android/src/main/AndroidManifest.xml | 6 + .../grpc/android/AndroidChannelBuilder.java | 255 ++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 android/build.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/java/io/grpc/android/AndroidChannelBuilder.java diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000000..a2dadc2f1fe --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.library' + +description = 'gRPC: Android' + +buildscript { + repositories { + google() + jcenter() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.0.1' + } +} + +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() + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } +} + +dependencies { + implementation 'io.grpc:grpc-core:1.12.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation '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 00000000000..53836739596 --- /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 00000000000..3c46be79fc1 --- /dev/null +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -0,0 +1,255 @@ +/* + * 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 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 io.grpc.okhttp.OkHttpChannelBuilder; +import java.util.concurrent.TimeUnit; + +/** + * Builds a {@link ManagedChannel} that automatically monitors the Android device's network state. + * Network changes are propagated to the underlying OkHttp-backed {@ManagedChannel} to smoothly + * handle intermittent network failures. + * + *

gRPC Cronet users should use {@code CronetChannelBuilder} directly, as Cronet itself monitors + * the device network state. + * + *

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 final ManagedChannelBuilder delegateBuilder; + private final Context context; + + /** Always fails. Call {@link #forAddress(String, int, Context)} instead. */ + public static AndroidChannelBuilder forTarget(String target) { + throw new UnsupportedOperationException("call forTarget(String, Context) instead"); + } + + /** Always fails. Call {@link #forAddress(String, int, Context)} instead. */ + public static AndroidChannelBuilder forAddress(String name, int port) { + throw new UnsupportedOperationException("call forAddress(String, int, Context) instead"); + } + + /** Creates a new builder for the given target and Android context. */ + public static final AndroidChannelBuilder forTarget(String target, Context context) { + return new AndroidChannelBuilder(target, context); + } + + /** Creates a new builder for the given host, port, and Android context. */ + public static AndroidChannelBuilder forAddress(String name, int port, Context context) { + return forTarget(GrpcUtil.authorityFromHostAndPort(name, port), context); + } + + private AndroidChannelBuilder(String target, Context context) { + delegateBuilder = OkHttpChannelBuilder.forTarget(target); + this.context = context; + } + + @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; + private final Context context; + private final ConnectivityManager connectivityManager; + + private DefaultNetworkCallback defaultNetworkCallback; + private NetworkReceiver networkReceiver; + + private final Object lock = new Object(); + + // May only go from true to false, and lock must be held when assigning this + private volatile boolean needToUnregisterListener = true; + + @VisibleForTesting + AndroidChannel(final ManagedChannel delegate, Context context) { + this.delegate = delegate; + this.context = context; + connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + // 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) { + NetworkInfo currentNetwork = connectivityManager.getActiveNetworkInfo(); + + // 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(); + + defaultNetworkCallback = new DefaultNetworkCallback(isConnected); + connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); + } else { + networkReceiver = new NetworkReceiver(); + IntentFilter networkIntentFilter = + new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(networkReceiver, networkIntentFilter); + } + } + + private void unregisterNetworkListener() { + if (needToUnregisterListener) { + synchronized (lock) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) { + connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback = null; + } else { + context.unregisterReceiver(networkReceiver); + networkReceiver = null; + } + needToUnregisterListener = false; + } + } + } + + @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(); + } + } + } + } +} From e0a9dd1cb7799b460069f92667cfbc7e566fc1e2 Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Fri, 16 Mar 2018 00:29:16 -0700 Subject: [PATCH 2/7] tests --- RELEASING.md | 1 + .../android/AndroidChannelBuilderTest.java | 337 ++++++++++++++++++ buildscripts/kokoro/android.sh | 8 +- 3 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java diff --git a/RELEASING.md b/RELEASING.md index 451c67ab683..3b7a434d73e 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/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java new file mode 100644 index 00000000000..03c3f2c6348 --- /dev/null +++ b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java @@ -0,0 +1,337 @@ +/* + * 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 + @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 2a20eb74eb1..e92a9b39354 100755 --- a/buildscripts/kokoro/android.sh +++ b/buildscripts/kokoro/android.sh @@ -5,13 +5,19 @@ cat /VERSION BASE_DIR="$(pwd)" -# Build Cronet +# Build grpc-cronet cd "$BASE_DIR/github/grpc-java/cronet" ./cronet_deps.sh ../gradlew --include-build .. build +# Build grpc-android + +cd "$BASE_DIR/github/grpc-java/android" +../gradlew --include-build .. build + + # Install gRPC and codegen for the Android examples # (a composite gradle build can't find protoc-gen-grpc-java) From 7067773f4d03c0a9e5ebe26de151c06622f8a88a Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Fri, 20 Apr 2018 21:57:04 -0700 Subject: [PATCH 3/7] remove snapshot repo and direct okhttp dependency --- android/build.gradle | 5 +-- .../grpc/android/AndroidChannelBuilder.java | 38 ++++++++++++++----- .../android/AndroidChannelBuilderTest.java | 6 +++ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index a2dadc2f1fe..1182eb9f5fe 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -35,14 +35,11 @@ android { repositories { mavenCentral() mavenLocal() - maven { - url "https://oss.sonatype.org/content/repositories/snapshots" - } } dependencies { implementation 'io.grpc:grpc-core:1.12.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-okhttp: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' diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index 3c46be79fc1..327e8e384ac 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -35,16 +35,12 @@ import io.grpc.ManagedChannelBuilder; import io.grpc.MethodDescriptor; import io.grpc.internal.GrpcUtil; -import io.grpc.okhttp.OkHttpChannelBuilder; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; /** - * Builds a {@link ManagedChannel} that automatically monitors the Android device's network state. - * Network changes are propagated to the underlying OkHttp-backed {@ManagedChannel} to smoothly - * handle intermittent network failures. - * - *

gRPC Cronet users should use {@code CronetChannelBuilder} directly, as Cronet itself monitors - * the device network state. + * Builds a {@link ManagedChannel} that can automatically monitor the Android device's network state + * to smoothly handle intermittent network failures. * *

Requires the Android ACCESS_NETWORK_STATE permission. * @@ -53,8 +49,21 @@ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4056") public final class AndroidChannelBuilder extends ForwardingChannelBuilder { + @Nullable + private static final Class OKHTTP_CHANNEL_BUILDER_CLASS = + findClass("io.grpc.okhttp.OkHttpChannelBuilder"); + + private static final Class findClass(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + return null; + } + } + private final ManagedChannelBuilder delegateBuilder; - private final Context context; + + @Nullable private final Context context; /** Always fails. Call {@link #forAddress(String, int, Context)} instead. */ public static AndroidChannelBuilder forTarget(String target) { @@ -77,7 +86,18 @@ public static AndroidChannelBuilder forAddress(String name, int port, Context co } private AndroidChannelBuilder(String target, Context context) { - delegateBuilder = OkHttpChannelBuilder.forTarget(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); + } this.context = context; } diff --git a/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java index 03c3f2c6348..a1692cc5c83 100644 --- a/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java +++ b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java @@ -80,6 +80,12 @@ public void setUp() { 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 resetConnectBackoff_api23() { From 03daad54e0c399dc8f38c0fafbce359a41e80cdb Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Fri, 20 Apr 2018 22:27:39 -0700 Subject: [PATCH 4/7] make context optional via context() builder --- .../grpc/android/AndroidChannelBuilder.java | 57 ++++++++++--------- .../android/AndroidChannelBuilderTest.java | 30 ++++++++++ 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index 327e8e384ac..41754fb941e 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -39,8 +39,8 @@ import javax.annotation.Nullable; /** - * Builds a {@link ManagedChannel} that can automatically monitor the Android device's network state - * to smoothly handle intermittent network failures. + * 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. * @@ -63,29 +63,17 @@ private static final Class findClass(String className) { private final ManagedChannelBuilder delegateBuilder; - @Nullable private final Context context; + @Nullable private Context context; - /** Always fails. Call {@link #forAddress(String, int, Context)} instead. */ - public static AndroidChannelBuilder forTarget(String target) { - throw new UnsupportedOperationException("call forTarget(String, Context) instead"); + public static final AndroidChannelBuilder forTarget(String target) { + return new AndroidChannelBuilder(target); } - /** Always fails. Call {@link #forAddress(String, int, Context)} instead. */ public static AndroidChannelBuilder forAddress(String name, int port) { - throw new UnsupportedOperationException("call forAddress(String, int, Context) instead"); + return forTarget(GrpcUtil.authorityFromHostAndPort(name, port)); } - /** Creates a new builder for the given target and Android context. */ - public static final AndroidChannelBuilder forTarget(String target, Context context) { - return new AndroidChannelBuilder(target, context); - } - - /** Creates a new builder for the given host, port, and Android context. */ - public static AndroidChannelBuilder forAddress(String name, int port, Context context) { - return forTarget(GrpcUtil.authorityFromHostAndPort(name, port), context); - } - - private AndroidChannelBuilder(String target, Context context) { + private AndroidChannelBuilder(String target) { if (OKHTTP_CHANNEL_BUILDER_CLASS == null) { throw new UnsupportedOperationException("No ManagedChannelBuilder found on the classpath"); } @@ -98,7 +86,12 @@ private AndroidChannelBuilder(String target, Context context) { } 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 @@ -119,24 +112,34 @@ public ManagedChannel build() { static final class AndroidChannel extends ManagedChannel { private final ManagedChannel delegate; - private final Context context; - private final ConnectivityManager connectivityManager; - private DefaultNetworkCallback defaultNetworkCallback; - private NetworkReceiver networkReceiver; + @Nullable private final Context context; + @Nullable private final ConnectivityManager connectivityManager; + + @Nullable private DefaultNetworkCallback defaultNetworkCallback; + @Nullable private NetworkReceiver networkReceiver; private final Object lock = new Object(); // May only go from true to false, and lock must be held when assigning this - private volatile boolean needToUnregisterListener = true; + private volatile boolean needToUnregisterListener; @VisibleForTesting - AndroidChannel(final ManagedChannel delegate, Context context) { + AndroidChannel(final ManagedChannel delegate, @Nullable Context context) { this.delegate = delegate; this.context = context; - connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (context != null) { + connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + configureNetworkMonitoring(); + needToUnregisterListener = true; + } else { + connectivityManager = null; + } + } + + private void configureNetworkMonitoring() { // 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) { diff --git a/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java index a1692cc5c83..828390d8464 100644 --- a/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java +++ b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java @@ -86,6 +86,36 @@ public void channelBuilderClassFoundReflectively() { 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() { From 4638d7053c5820b05c437acf80cbc7ed3afae56a Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Fri, 20 Apr 2018 22:53:56 -0700 Subject: [PATCH 5/7] replace unregister boolean with Runnable, use @GuardedBy and errorprone plugin --- android/build.gradle | 6 +++ .../grpc/android/AndroidChannelBuilder.java | 39 ++++++++++++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 1182eb9f5fe..86309689dd2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,12 +7,18 @@ buildscript { 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 { diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index 41754fb941e..27b9938d259 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -37,6 +37,7 @@ 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 @@ -121,8 +122,8 @@ static final class AndroidChannel extends ManagedChannel { private final Object lock = new Object(); - // May only go from true to false, and lock must be held when assigning this - private volatile boolean needToUnregisterListener; + @GuardedBy("lock") + private Runnable unregisterRunnable; @VisibleForTesting AndroidChannel(final ManagedChannel delegate, @Nullable Context context) { @@ -133,12 +134,12 @@ static final class AndroidChannel extends ManagedChannel { connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); configureNetworkMonitoring(); - needToUnregisterListener = true; } else { connectivityManager = null; } } + @GuardedBy("lock") private void configureNetworkMonitoring() { // 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. @@ -152,25 +153,37 @@ private void configureNetworkMonitoring() { defaultNetworkCallback = new DefaultNetworkCallback(isConnected); connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); + unregisterRunnable = + new Runnable() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void run() { + connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback = null; + } + }; } else { 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); + networkReceiver = null; + } + }; } } private void unregisterNetworkListener() { - if (needToUnregisterListener) { - synchronized (lock) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) { - connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); - defaultNetworkCallback = null; - } else { - context.unregisterReceiver(networkReceiver); - networkReceiver = null; - } - needToUnregisterListener = false; + synchronized (lock) { + if (unregisterRunnable != null) { + unregisterRunnable.run(); + unregisterRunnable = null; } } } From 289c8c49c779c32abd4894ae43203748225eb476 Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Sun, 22 Apr 2018 13:16:28 -0700 Subject: [PATCH 6/7] proguard, remove local vars --- .../io/grpc/android/AndroidChannelBuilder.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index 27b9938d259..f63f3e7ac7c 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -50,13 +50,11 @@ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4056") public final class AndroidChannelBuilder extends ForwardingChannelBuilder { - @Nullable - private static final Class OKHTTP_CHANNEL_BUILDER_CLASS = - findClass("io.grpc.okhttp.OkHttpChannelBuilder"); + @Nullable private static final Class OKHTTP_CHANNEL_BUILDER_CLASS = findOkHttp(); - private static final Class findClass(String className) { + private static final Class findOkHttp() { try { - return Class.forName(className); + return Class.forName("io.grpc.okhttp.OkHttpChannelBuilder"); } catch (ClassNotFoundException e) { return null; } @@ -117,9 +115,6 @@ static final class AndroidChannel extends ManagedChannel { @Nullable private final Context context; @Nullable private final ConnectivityManager connectivityManager; - @Nullable private DefaultNetworkCallback defaultNetworkCallback; - @Nullable private NetworkReceiver networkReceiver; - private final Object lock = new Object(); @GuardedBy("lock") @@ -151,7 +146,8 @@ private void configureNetworkMonitoring() { // vice versa) on the first network change. boolean isConnected = currentNetwork != null && currentNetwork.isConnected(); - defaultNetworkCallback = new DefaultNetworkCallback(isConnected); + final DefaultNetworkCallback defaultNetworkCallback = + new DefaultNetworkCallback(isConnected); connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); unregisterRunnable = new Runnable() { @@ -159,11 +155,10 @@ private void configureNetworkMonitoring() { @Override public void run() { connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); - defaultNetworkCallback = null; } }; } else { - networkReceiver = new NetworkReceiver(); + final NetworkReceiver networkReceiver = new NetworkReceiver(); IntentFilter networkIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(networkReceiver, networkIntentFilter); @@ -173,7 +168,6 @@ public void run() { @Override public void run() { context.unregisterReceiver(networkReceiver); - networkReceiver = null; } }; } From 5ded1522b27f93c4f670fac9472603bfe4365763 Mon Sep 17 00:00:00 2001 From: Eric Gribkoff Date: Mon, 23 Apr 2018 09:07:50 -0700 Subject: [PATCH 7/7] Catch and log warning if ACCESS_NETWORK_STATE permission is not granted --- .../io/grpc/android/AndroidChannelBuilder.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index f63f3e7ac7c..413ee596364 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -25,6 +25,7 @@ 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; @@ -50,6 +51,8 @@ @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() { @@ -136,11 +139,22 @@ static final class AndroidChannel extends ManagedChannel { @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) { - NetworkInfo currentNetwork = connectivityManager.getActiveNetworkInfo(); - // 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.