Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

android: add AndroidChannelBuilder #4172

Merged
merged 8 commits into from
Apr 23, 2018

Conversation

ericgribkoff
Copy link
Contributor

@ericgribkoff ericgribkoff commented Mar 5, 2018

Introduces a wrapper around an OkHttp channel that accepts an Android Contextand registers as a network listener to automatically invoke the necessary methods on ManagedChannel (enterIdle and resetConnectBackoff) when the device connection state changes.

This will have to be a new artifact with a direct Android dependency: interacting with the Android network monitoring APIs in the existing OkHttp package via reflection could work, but we need to provide the connectivity manager with implementations of Android callback interfaces, which would require using the java.lang.reflect.Proxy API to construct these reflectively. It seems much cleaner to just have (yet one more) new artifact for Android-specific functionality.

Manually tested with a local app. Robolectric tests to follow. This reduces the burden on users to take advantage of the gRPC channel API methods: this can all be accomplished with gRPC's public API, but doing so requires users to (a) keep track of every channel reference and (b) understand the Android network monitoring APIs, which have changed over time, and how to plumb these changes to the respective gRPC channel methods.

cc @srini100

@ericgribkoff ericgribkoff added the kokoro:force-run Add this label to a PR to tell Kokoro to re-run all tests. Not generally necessary label Mar 9, 2018
@kokoro-team kokoro-team removed the kokoro:force-run Add this label to a PR to tell Kokoro to re-run all tests. Not generally necessary label Mar 9, 2018
@ericgribkoff ericgribkoff added the kokoro:force-run Add this label to a PR to tell Kokoro to re-run all tests. Not generally necessary label Mar 9, 2018
@kokoro-team kokoro-team removed the kokoro:force-run Add this label to a PR to tell Kokoro to re-run all tests. Not generally necessary label Mar 9, 2018
@ericgribkoff
Copy link
Contributor Author

I revised the AndroidChannel implementation somewhat and added Robolectric tests.

The tests cover both API levels before and after Android 24, which added the ConnectivityManager#registerDefaultNetworkCallback API. The registerDefaultNetworkCallback API is used to determine both when to invoke enterIdle for graceful network transitions and resetConnectBackoff when the device re-establishes a connection.

Devices with a pre-24 OS fallback to the older BroadcastReceiver API, which supports invoking resetConnectBackoff when the device comes back online but doesn't give the necessary signal to know when to invoke enterIdle. If it proves to be needed, we may be able to extract enough information out of pre-24 APIs to reliably determine when to invoke enterIdle, but the implementation of this would get messy compared to using the nicely suited new default network APIs. And, for now, resetConnectBackoff alone is enough for pre-24 devices to capture the primary benefit of having a connection-aware channel, avoiding downtime after the network comes back.

@ericgribkoff ericgribkoff requested review from zpencer and removed request for zpencer March 16, 2018 07:52
@ericgribkoff
Copy link
Contributor Author

@ejona86 Friendly ping (branch cut for v1.12 is in five days, looks like this is definitely at risk for making the cut now?)

mavenCentral()
mavenLocal()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to pull in the snapshots repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer to leave off the grpc-okhttp dependency for now, as it would be hard to remove to support mixing with cronet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/**
* 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to specify implementation details like okhttp and "backed" and some delegate managedchannel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


/** Always fails. Call {@link #forAddress(String, int, Context)} instead. */
public static AndroidChannelBuilder forTarget(String target) {
throw new UnsupportedOperationException("call forTarget(String, Context) instead");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in the API review meeting, these could be supported and we'd remove the Context from the forTarget call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Context is now optional and set via the context() builder method.


// 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work for libraries? I thought many of these fields only work for applications, as when the library is compiled the final value isn't known and nothing rebuilds the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These work for libraries - they are statically available constants about the device, not compile-time build constants. Javadoc here, and these are used in Cronet and inAndroidPlatform in okhttp3.


private void unregisterNetworkListener() {
if (needToUnregisterListener) {
synchronized (lock) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to check needToUnregisterListener again within the lock?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good catch - fixed by switch to guardedby

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest making this just @GuardedBy("lock"). It's only going to be rarely used, because shutdown isn't called frequently. Little benefit in optimizing the avoidance of the lock.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

private void unregisterNetworkListener() {
if (needToUnregisterListener) {
synchronized (lock) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does repeating the Build check here help dead code elimination? If not, is it necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use runnable instead of this conditional

private final Context context;
private final ConnectivityManager connectivityManager;

private DefaultNetworkCallback defaultNetworkCallback;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are only used for shut down, you could consider just storing a Runnable for clean-up. It's fine as-is though.

(I'm mainly considering it because of the Build.VERSION.SDK_INT >= Build.VERSION_CODES.N check during shut down)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor Author

@ericgribkoff ericgribkoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Comments addressed.

mavenCentral()
mavenLocal()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


/**
* 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/** Always fails. Call {@link #forAddress(String, int, Context)} instead. */
public static AndroidChannelBuilder forTarget(String target) {
throw new UnsupportedOperationException("call forTarget(String, Context) instead");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Context is now optional and set via the context() builder method.


// 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These work for libraries - they are statically available constants about the device, not compile-time build constants. Javadoc here, and these are used in Cronet and inAndroidPlatform in okhttp3.

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

private final Context context;
private final ConnectivityManager connectivityManager;

private DefaultNetworkCallback defaultNetworkCallback;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


private void unregisterNetworkListener() {
if (needToUnregisterListener) {
synchronized (lock) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good catch - fixed by switch to guardedby

private void unregisterNetworkListener() {
if (needToUnregisterListener) {
synchronized (lock) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use runnable instead of this conditional


private static final Class<?> findClass(String className) {
try {
return Class.forName(className);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need the literal Class.forName("io.grpc.okhttp.OkHttpChannelBuilder"), otherwise ProGuard won't detect the reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
unregisterRunnable =
new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to understand these annotations at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Analogous to SuppressWarnings. Without it there are linter errors about unregisterNetworkCallback requiring Android L in the runnable.

@Nullable private final Context context;
@Nullable private final ConnectivityManager connectivityManager;

@Nullable private DefaultNetworkCallback defaultNetworkCallback;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had sort of imagined these two fields would just be local variables, since the Runnables could just get a closure of them. This is fine too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@ericgribkoff
Copy link
Contributor Author

@ejona86 Made a few small changes to address comments and catch the security exception thrown for apps that don't have the ACCESS_NETWORK_STATE permission. This should be ready to merge now, PTAL

@ericgribkoff ericgribkoff merged commit ba86a86 into grpc:master Apr 23, 2018
@ericgribkoff ericgribkoff deleted the android_channel branch April 23, 2018 18:49
@lock lock bot locked as resolved and limited conversation to collaborators Jan 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants