Skip to content

Commit 64a8fce

Browse files
committed
android: add AndroidChannelBuilder
1 parent 5e8b8c2 commit 64a8fce

File tree

3 files changed

+311
-0
lines changed

3 files changed

+311
-0
lines changed

android/build.gradle

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

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,255 @@
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 com.google.common.annotations.VisibleForTesting;
29+
import io.grpc.CallOptions;
30+
import io.grpc.ClientCall;
31+
import io.grpc.ConnectivityState;
32+
import io.grpc.ExperimentalApi;
33+
import io.grpc.ForwardingChannelBuilder;
34+
import io.grpc.ManagedChannel;
35+
import io.grpc.ManagedChannelBuilder;
36+
import io.grpc.MethodDescriptor;
37+
import io.grpc.internal.GrpcUtil;
38+
import io.grpc.okhttp.OkHttpChannelBuilder;
39+
import java.util.concurrent.TimeUnit;
40+
41+
/**
42+
* Builds a {@link ManagedChannel} that automatically monitors the Android device's network state.
43+
* Network changes are propagated to the underlying OkHttp-backed {@ManagedChannel} to smoothly
44+
* handle intermittent network failures.
45+
*
46+
* <p>gRPC Cronet users should use {@code CronetChannelBuilder} directly, as Cronet itself monitors
47+
* the device network state.
48+
*
49+
* <p>Requires the Android ACCESS_NETWORK_STATE permission.
50+
*
51+
* @since 1.12.0
52+
*/
53+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4056")
54+
public final class AndroidChannelBuilder extends ForwardingChannelBuilder<AndroidChannelBuilder> {
55+
56+
private final ManagedChannelBuilder delegateBuilder;
57+
private final Context context;
58+
59+
/** Always fails. Call {@link #forAddress(String, int, Context)} instead. */
60+
public static AndroidChannelBuilder forTarget(String target) {
61+
throw new UnsupportedOperationException("call forTarget(String, Context) instead");
62+
}
63+
64+
/** Always fails. Call {@link #forAddress(String, int, Context)} instead. */
65+
public static AndroidChannelBuilder forAddress(String name, int port) {
66+
throw new UnsupportedOperationException("call forAddress(String, int, Context) instead");
67+
}
68+
69+
/** Creates a new builder for the given target and Android context. */
70+
public static final AndroidChannelBuilder forTarget(String target, Context context) {
71+
return new AndroidChannelBuilder(target, context);
72+
}
73+
74+
/** Creates a new builder for the given host, port, and Android context. */
75+
public static AndroidChannelBuilder forAddress(String name, int port, Context context) {
76+
return forTarget(GrpcUtil.authorityFromHostAndPort(name, port), context);
77+
}
78+
79+
private AndroidChannelBuilder(String target, Context context) {
80+
delegateBuilder = OkHttpChannelBuilder.forTarget(target);
81+
this.context = context;
82+
}
83+
84+
@Override
85+
protected ManagedChannelBuilder<?> delegate() {
86+
return delegateBuilder;
87+
}
88+
89+
@Override
90+
public ManagedChannel build() {
91+
return new AndroidChannel(delegateBuilder.build(), context);
92+
}
93+
94+
/**
95+
* Wraps an OkHttp channel and handles invoking the appropriate methods (e.g., {@link
96+
* ManagedChannel#resetConnectBackoff}) when the device network state changes.
97+
*/
98+
@VisibleForTesting
99+
static final class AndroidChannel extends ManagedChannel {
100+
101+
private final ManagedChannel delegate;
102+
private final Context context;
103+
private final ConnectivityManager connectivityManager;
104+
105+
private DefaultNetworkCallback defaultNetworkCallback;
106+
private NetworkReceiver networkReceiver;
107+
108+
private final Object lock = new Object();
109+
110+
// May only go from true to false, and lock must be held when assigning this
111+
private volatile boolean needToUnregisterListener = true;
112+
113+
@VisibleForTesting
114+
AndroidChannel(final ManagedChannel delegate, Context context) {
115+
this.delegate = delegate;
116+
this.context = context;
117+
connectivityManager =
118+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
119+
120+
// Android N added the registerDefaultNetworkCallback API to listen to changes in the device's
121+
// default network. For earlier Android API levels, use the BroadcastReceiver API.
122+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
123+
NetworkInfo currentNetwork = connectivityManager.getActiveNetworkInfo();
124+
125+
// The connection status may change before registration of the listener is complete, but
126+
// this will at worst result in invoking resetConnectBackoff() instead of enterIdle() (or
127+
// vice versa) on the first network change.
128+
boolean isConnected = currentNetwork != null && currentNetwork.isConnected();
129+
130+
defaultNetworkCallback = new DefaultNetworkCallback(isConnected);
131+
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
132+
} else {
133+
networkReceiver = new NetworkReceiver();
134+
IntentFilter networkIntentFilter =
135+
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
136+
context.registerReceiver(networkReceiver, networkIntentFilter);
137+
}
138+
}
139+
140+
private void unregisterNetworkListener() {
141+
if (needToUnregisterListener) {
142+
synchronized (lock) {
143+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
144+
connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
145+
defaultNetworkCallback = null;
146+
} else {
147+
context.unregisterReceiver(networkReceiver);
148+
networkReceiver = null;
149+
}
150+
needToUnregisterListener = false;
151+
}
152+
}
153+
}
154+
155+
@Override
156+
public ManagedChannel shutdown() {
157+
unregisterNetworkListener();
158+
return delegate.shutdown();
159+
}
160+
161+
@Override
162+
public boolean isShutdown() {
163+
return delegate.isShutdown();
164+
}
165+
166+
@Override
167+
public boolean isTerminated() {
168+
return delegate.isTerminated();
169+
}
170+
171+
@Override
172+
public ManagedChannel shutdownNow() {
173+
unregisterNetworkListener();
174+
return delegate.shutdownNow();
175+
}
176+
177+
@Override
178+
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
179+
return delegate.awaitTermination(timeout, unit);
180+
}
181+
182+
@Override
183+
public <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
184+
MethodDescriptor<RequestT, ResponseT> methodDescriptor, CallOptions callOptions) {
185+
return delegate.newCall(methodDescriptor, callOptions);
186+
}
187+
188+
@Override
189+
public String authority() {
190+
return delegate.authority();
191+
}
192+
193+
@Override
194+
public ConnectivityState getState(boolean requestConnection) {
195+
return delegate.getState(requestConnection);
196+
}
197+
198+
@Override
199+
public void notifyWhenStateChanged(ConnectivityState source, Runnable callback) {
200+
delegate.notifyWhenStateChanged(source, callback);
201+
}
202+
203+
@Override
204+
public void resetConnectBackoff() {
205+
delegate.resetConnectBackoff();
206+
}
207+
208+
@Override
209+
public void enterIdle() {
210+
delegate.enterIdle();
211+
}
212+
213+
/** Respond to changes in the default network. Only used on API levels 24+. */
214+
@TargetApi(Build.VERSION_CODES.N)
215+
private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback {
216+
private boolean isConnected = false;
217+
218+
private DefaultNetworkCallback(boolean isConnected) {
219+
this.isConnected = isConnected;
220+
}
221+
222+
@Override
223+
public void onAvailable(Network network) {
224+
if (isConnected) {
225+
delegate.enterIdle();
226+
} else {
227+
delegate.resetConnectBackoff();
228+
}
229+
isConnected = true;
230+
}
231+
232+
@Override
233+
public void onLost(Network network) {
234+
isConnected = false;
235+
}
236+
}
237+
238+
/** Respond to network changes. Only used on API levels < 24. */
239+
private class NetworkReceiver extends BroadcastReceiver {
240+
private boolean isConnected = false;
241+
242+
@Override
243+
public void onReceive(Context context, Intent intent) {
244+
ConnectivityManager conn =
245+
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
246+
NetworkInfo networkInfo = conn.getActiveNetworkInfo();
247+
boolean wasConnected = isConnected;
248+
isConnected = networkInfo != null && networkInfo.isConnected();
249+
if (isConnected && !wasConnected) {
250+
delegate.resetConnectBackoff();
251+
}
252+
}
253+
}
254+
}
255+
}

0 commit comments

Comments
 (0)