Skip to content

Commit ba86a86

Browse files
authored
android: add AndroidChannelBuilder (#4172)
1 parent a47266e commit ba86a86

File tree

6 files changed

+739
-1
lines changed

6 files changed

+739
-1
lines changed

RELEASING.md

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ them before continuing, and set them again when resuming.
6363
$ MAJOR=1 MINOR=7 PATCH=0 # Set appropriately for new release
6464
$ VERSION_FILES=(
6565
build.gradle
66+
android/build.gradle
6667
android-interop-testing/app/build.gradle
6768
core/src/main/java/io/grpc/internal/GrpcUtil.java
6869
cronet/build.gradle

android/build.gradle

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
apply plugin: 'com.android.library'
2+
3+
description = 'gRPC: Android'
4+
5+
buildscript {
6+
repositories {
7+
google()
8+
jcenter()
9+
mavenCentral()
10+
maven {
11+
url "https://plugins.gradle.org/m2/"
12+
}
13+
}
14+
dependencies {
15+
classpath 'com.android.tools.build:gradle:3.0.1'
16+
classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
17+
}
18+
}
19+
20+
apply plugin: "net.ltgt.errorprone"
21+
22+
android {
23+
compileSdkVersion 27
24+
defaultConfig {
25+
minSdkVersion 14
26+
targetSdkVersion 27
27+
versionCode 1
28+
versionName "1.0"
29+
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
30+
}
31+
testOptions {
32+
unitTests {
33+
includeAndroidResources = true
34+
}
35+
}
36+
lintOptions {
37+
abortOnError false
38+
}
39+
}
40+
41+
repositories {
42+
mavenCentral()
43+
mavenLocal()
44+
}
45+
46+
dependencies {
47+
implementation 'io.grpc:grpc-core:1.12.0-SNAPSHOT' // CURRENT_GRPC_VERSION
48+
testImplementation 'io.grpc:grpc-okhttp:1.12.0-SNAPSHOT' // CURRENT_GRPC_VERSION
49+
50+
testImplementation 'junit:junit:4.12'
51+
testImplementation 'org.robolectric:robolectric:3.7.1'
52+
testImplementation 'com.google.truth:truth:0.39'
53+
}

android/src/main/AndroidManifest.xml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="io.grpc.android">
3+
4+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
5+
6+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/*
2+
* Copyright 2018, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.android;
18+
19+
import android.annotation.TargetApi;
20+
import android.content.BroadcastReceiver;
21+
import android.content.Context;
22+
import android.content.Intent;
23+
import android.content.IntentFilter;
24+
import android.net.ConnectivityManager;
25+
import android.net.Network;
26+
import android.net.NetworkInfo;
27+
import android.os.Build;
28+
import android.util.Log;
29+
import com.google.common.annotations.VisibleForTesting;
30+
import io.grpc.CallOptions;
31+
import io.grpc.ClientCall;
32+
import io.grpc.ConnectivityState;
33+
import io.grpc.ExperimentalApi;
34+
import io.grpc.ForwardingChannelBuilder;
35+
import io.grpc.ManagedChannel;
36+
import io.grpc.ManagedChannelBuilder;
37+
import io.grpc.MethodDescriptor;
38+
import io.grpc.internal.GrpcUtil;
39+
import java.util.concurrent.TimeUnit;
40+
import javax.annotation.Nullable;
41+
import javax.annotation.concurrent.GuardedBy;
42+
43+
/**
44+
* Builds a {@link ManagedChannel} that, when provided with a {@link Context}, will automatically
45+
* monitor the Android device's network state to smoothly handle intermittent network failures.
46+
*
47+
* <p>Requires the Android ACCESS_NETWORK_STATE permission.
48+
*
49+
* @since 1.12.0
50+
*/
51+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4056")
52+
public final class AndroidChannelBuilder extends ForwardingChannelBuilder<AndroidChannelBuilder> {
53+
54+
private static final String LOG_TAG = "AndroidChannelBuilder";
55+
56+
@Nullable private static final Class<?> OKHTTP_CHANNEL_BUILDER_CLASS = findOkHttp();
57+
58+
private static final Class<?> findOkHttp() {
59+
try {
60+
return Class.forName("io.grpc.okhttp.OkHttpChannelBuilder");
61+
} catch (ClassNotFoundException e) {
62+
return null;
63+
}
64+
}
65+
66+
private final ManagedChannelBuilder delegateBuilder;
67+
68+
@Nullable private Context context;
69+
70+
public static final AndroidChannelBuilder forTarget(String target) {
71+
return new AndroidChannelBuilder(target);
72+
}
73+
74+
public static AndroidChannelBuilder forAddress(String name, int port) {
75+
return forTarget(GrpcUtil.authorityFromHostAndPort(name, port));
76+
}
77+
78+
private AndroidChannelBuilder(String target) {
79+
if (OKHTTP_CHANNEL_BUILDER_CLASS == null) {
80+
throw new UnsupportedOperationException("No ManagedChannelBuilder found on the classpath");
81+
}
82+
try {
83+
delegateBuilder =
84+
(ManagedChannelBuilder)
85+
OKHTTP_CHANNEL_BUILDER_CLASS
86+
.getMethod("forTarget", String.class)
87+
.invoke(null, target);
88+
} catch (Exception e) {
89+
throw new RuntimeException("Failed to create ManagedChannelBuilder", e);
90+
}
91+
}
92+
93+
/** Enables automatic monitoring of the device's network state. */
94+
public AndroidChannelBuilder context(Context context) {
95+
this.context = context;
96+
return this;
97+
}
98+
99+
@Override
100+
protected ManagedChannelBuilder<?> delegate() {
101+
return delegateBuilder;
102+
}
103+
104+
@Override
105+
public ManagedChannel build() {
106+
return new AndroidChannel(delegateBuilder.build(), context);
107+
}
108+
109+
/**
110+
* Wraps an OkHttp channel and handles invoking the appropriate methods (e.g., {@link
111+
* ManagedChannel#resetConnectBackoff}) when the device network state changes.
112+
*/
113+
@VisibleForTesting
114+
static final class AndroidChannel extends ManagedChannel {
115+
116+
private final ManagedChannel delegate;
117+
118+
@Nullable private final Context context;
119+
@Nullable private final ConnectivityManager connectivityManager;
120+
121+
private final Object lock = new Object();
122+
123+
@GuardedBy("lock")
124+
private Runnable unregisterRunnable;
125+
126+
@VisibleForTesting
127+
AndroidChannel(final ManagedChannel delegate, @Nullable Context context) {
128+
this.delegate = delegate;
129+
this.context = context;
130+
131+
if (context != null) {
132+
connectivityManager =
133+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
134+
configureNetworkMonitoring();
135+
} else {
136+
connectivityManager = null;
137+
}
138+
}
139+
140+
@GuardedBy("lock")
141+
private void configureNetworkMonitoring() {
142+
// Eagerly check current network state to verify app has required permissions
143+
NetworkInfo currentNetwork;
144+
try {
145+
currentNetwork = connectivityManager.getActiveNetworkInfo();
146+
} catch (SecurityException e) {
147+
Log.w(
148+
LOG_TAG,
149+
"Failed to configure network monitoring. Does app have ACCESS_NETWORK_STATE"
150+
+ " permission?",
151+
e);
152+
return;
153+
}
154+
155+
// Android N added the registerDefaultNetworkCallback API to listen to changes in the device's
156+
// default network. For earlier Android API levels, use the BroadcastReceiver API.
157+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
158+
// The connection status may change before registration of the listener is complete, but
159+
// this will at worst result in invoking resetConnectBackoff() instead of enterIdle() (or
160+
// vice versa) on the first network change.
161+
boolean isConnected = currentNetwork != null && currentNetwork.isConnected();
162+
163+
final DefaultNetworkCallback defaultNetworkCallback =
164+
new DefaultNetworkCallback(isConnected);
165+
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
166+
unregisterRunnable =
167+
new Runnable() {
168+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
169+
@Override
170+
public void run() {
171+
connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
172+
}
173+
};
174+
} else {
175+
final NetworkReceiver networkReceiver = new NetworkReceiver();
176+
IntentFilter networkIntentFilter =
177+
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
178+
context.registerReceiver(networkReceiver, networkIntentFilter);
179+
unregisterRunnable =
180+
new Runnable() {
181+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
182+
@Override
183+
public void run() {
184+
context.unregisterReceiver(networkReceiver);
185+
}
186+
};
187+
}
188+
}
189+
190+
private void unregisterNetworkListener() {
191+
synchronized (lock) {
192+
if (unregisterRunnable != null) {
193+
unregisterRunnable.run();
194+
unregisterRunnable = null;
195+
}
196+
}
197+
}
198+
199+
@Override
200+
public ManagedChannel shutdown() {
201+
unregisterNetworkListener();
202+
return delegate.shutdown();
203+
}
204+
205+
@Override
206+
public boolean isShutdown() {
207+
return delegate.isShutdown();
208+
}
209+
210+
@Override
211+
public boolean isTerminated() {
212+
return delegate.isTerminated();
213+
}
214+
215+
@Override
216+
public ManagedChannel shutdownNow() {
217+
unregisterNetworkListener();
218+
return delegate.shutdownNow();
219+
}
220+
221+
@Override
222+
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
223+
return delegate.awaitTermination(timeout, unit);
224+
}
225+
226+
@Override
227+
public <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
228+
MethodDescriptor<RequestT, ResponseT> methodDescriptor, CallOptions callOptions) {
229+
return delegate.newCall(methodDescriptor, callOptions);
230+
}
231+
232+
@Override
233+
public String authority() {
234+
return delegate.authority();
235+
}
236+
237+
@Override
238+
public ConnectivityState getState(boolean requestConnection) {
239+
return delegate.getState(requestConnection);
240+
}
241+
242+
@Override
243+
public void notifyWhenStateChanged(ConnectivityState source, Runnable callback) {
244+
delegate.notifyWhenStateChanged(source, callback);
245+
}
246+
247+
@Override
248+
public void resetConnectBackoff() {
249+
delegate.resetConnectBackoff();
250+
}
251+
252+
@Override
253+
public void enterIdle() {
254+
delegate.enterIdle();
255+
}
256+
257+
/** Respond to changes in the default network. Only used on API levels 24+. */
258+
@TargetApi(Build.VERSION_CODES.N)
259+
private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback {
260+
private boolean isConnected = false;
261+
262+
private DefaultNetworkCallback(boolean isConnected) {
263+
this.isConnected = isConnected;
264+
}
265+
266+
@Override
267+
public void onAvailable(Network network) {
268+
if (isConnected) {
269+
delegate.enterIdle();
270+
} else {
271+
delegate.resetConnectBackoff();
272+
}
273+
isConnected = true;
274+
}
275+
276+
@Override
277+
public void onLost(Network network) {
278+
isConnected = false;
279+
}
280+
}
281+
282+
/** Respond to network changes. Only used on API levels < 24. */
283+
private class NetworkReceiver extends BroadcastReceiver {
284+
private boolean isConnected = false;
285+
286+
@Override
287+
public void onReceive(Context context, Intent intent) {
288+
ConnectivityManager conn =
289+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
290+
NetworkInfo networkInfo = conn.getActiveNetworkInfo();
291+
boolean wasConnected = isConnected;
292+
isConnected = networkInfo != null && networkInfo.isConnected();
293+
if (isConnected && !wasConnected) {
294+
delegate.resetConnectBackoff();
295+
}
296+
}
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)