| /* |
| * 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(); |
| } |
| } |
| } |
| } |
| } |