blob: 7d2fdea409dd37e4bb6b3e6a498b0a4f0e31cd98 [file] [log] [blame]
/*
* Copyright 2018 The gRPC Authors
*
* 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 com.google.common.base.Preconditions;
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.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.net.ssl.SSLSocketFactory;
/**
* 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.
*
* <p>Currently only compatible with gRPC's OkHttp transport, which must be available at runtime.
*
* <p>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<AndroidChannelBuilder> {
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));
}
public static AndroidChannelBuilder fromBuilder(ManagedChannelBuilder<?> builder) {
return new AndroidChannelBuilder(builder);
}
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);
}
}
private AndroidChannelBuilder(ManagedChannelBuilder<?> delegateBuilder) {
this.delegateBuilder = Preconditions.checkNotNull(delegateBuilder, "delegateBuilder");
}
/** Enables automatic monitoring of the device's network state. */
public AndroidChannelBuilder context(Context context) {
this.context = context;
return this;
}
/**
* Set the delegate channel builder's transportExecutor.
*
* @deprecated Use {@link #fromBuilder(ManagedChannelBuilder)} with a pre-configured
* ManagedChannelBuilder instead.
*/
@Deprecated
public AndroidChannelBuilder transportExecutor(@Nullable Executor transportExecutor) {
try {
OKHTTP_CHANNEL_BUILDER_CLASS
.getMethod("transportExecutor", Executor.class)
.invoke(delegateBuilder, transportExecutor);
return this;
} catch (Exception e) {
throw new RuntimeException("Failed to invoke transportExecutor on delegate builder", e);
}
}
/**
* Set the delegate channel builder's sslSocketFactory.
*
* @deprecated Use {@link #fromBuilder(ManagedChannelBuilder)} with a pre-configured
* ManagedChannelBuilder instead.
*/
@Deprecated
public AndroidChannelBuilder sslSocketFactory(SSLSocketFactory factory) {
try {
OKHTTP_CHANNEL_BUILDER_CLASS
.getMethod("sslSocketFactory", SSLSocketFactory.class)
.invoke(delegateBuilder, factory);
return this;
} catch (Exception e) {
throw new RuntimeException("Failed to invoke sslSocketFactory on delegate builder", e);
}
}
/**
* Set the delegate channel builder's scheduledExecutorService.
*
* @deprecated Use {@link #fromBuilder(ManagedChannelBuilder)} with a pre-configured
* ManagedChannelBuilder instead.
*/
@Deprecated
public AndroidChannelBuilder scheduledExecutorService(
ScheduledExecutorService scheduledExecutorService) {
try {
OKHTTP_CHANNEL_BUILDER_CLASS
.getMethod("scheduledExecutorService", ScheduledExecutorService.class)
.invoke(delegateBuilder, scheduledExecutorService);
return this;
} catch (Exception e) {
throw new RuntimeException(
"Failed to invoke scheduledExecutorService on delegate builder", e);
}
}
@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);
try {
configureNetworkMonitoring();
} catch (SecurityException e) {
Log.w(
LOG_TAG,
"Failed to configure network monitoring. Does app have ACCESS_NETWORK_STATE"
+ " permission?",
e);
}
} 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.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
final DefaultNetworkCallback defaultNetworkCallback = new DefaultNetworkCallback();
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 <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
MethodDescriptor<RequestT, ResponseT> 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 {
// Registering a listener may immediate invoke onAvailable/onLost: the API docs do not specify
// if the methods are always invoked once, then again on any change, or only on change. When
// onAvailable() is invoked immediately without an actual network change, it's preferable to
// (spuriously) resetConnectBackoff() rather than enterIdle(), as the former is a no-op if the
// channel has already moved to CONNECTING.
private boolean isConnected = false;
@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();
}
}
}
}
}