Enable indirect addressing using <intent-filter>s. (#10550)

AndroidComponentAddress now accepts an Intent with merely a package
restriction, not a full ComponentName. This lets clients avoid hard
coding Service class names that they don't control.

Fixes #9062
This commit is contained in:
John Cormie 2023-09-12 17:27:35 -07:00 committed by GitHub
parent 5f480de2ee
commit 134b0490d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 20 deletions

View File

@ -8,7 +8,15 @@
<application android:debuggable="true"> <application android:debuggable="true">
<uses-library android:name="android.test.runner" /> <uses-library android:name="android.test.runner" />
<service android:name="io.grpc.binder.HostServices$HostService1" /> <service android:name="io.grpc.binder.HostServices$HostService1" android:exported="false">
<service android:name="io.grpc.binder.HostServices$HostService2" /> <intent-filter>
<action android:name="action1"/>
</intent-filter>
</service>
<service android:name="io.grpc.binder.HostServices$HostService2" android:exported="false">
<intent-filter>
<action android:name="action2"/>
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>

View File

@ -22,6 +22,7 @@ import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -32,7 +33,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import io.grpc.CallOptions; import io.grpc.CallOptions;
import io.grpc.Channel; import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptors; import io.grpc.ClientInterceptors;
import io.grpc.ConnectivityState; import io.grpc.ConnectivityState;
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall; import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
@ -234,6 +234,18 @@ public final class BinderChannelSmokeTest {
assertThat(doCall("Hello").get()).isEqualTo("Hello"); assertThat(doCall("Hello").get()).isEqualTo("Hello");
} }
@Test
public void testConnectViaIntentFilter() throws Exception {
// Compare with the <intent-filter> mapping in AndroidManifest.xml.
channel =
BinderChannelBuilder.forAddress(
AndroidComponentAddress.forBindIntent(
new Intent().setAction("action1").setPackage(appContext.getPackageName())),
appContext)
.build();
assertThat(doCall("Hello").get()).isEqualTo("Hello");
}
@Test @Test
public void testUncaughtServerException() throws Exception { public void testUncaughtServerException() throws Exception {
// Use a poison parcelable to cause an unexpected Exception in the server's onTransact(). // Use a poison parcelable to cause an unexpected Exception in the server's onTransact().

View File

@ -23,30 +23,35 @@ import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import java.net.SocketAddress; import java.net.SocketAddress;
import javax.annotation.Nullable;
/** /**
* The target of an Android {@link android.app.Service} binding. * The target of an Android {@link android.app.Service} binding.
* *
* <p>Consists of a {@link ComponentName} reference to the Service and the action, data URI, type, * <p>Consists of an explicit {@link Intent} that identifies an {@link android.os.IBinder} returned
* and category set for an {@link Intent} used to bind to it. All together, these fields identify * by some Service's {@link android.app.Service#onBind(Intent)} method. You can specify that Service
* the {@link android.os.IBinder} that would be returned by some implementation of {@link * by {@link ComponentName} or let Android resolve it using the Intent's other fields (package,
* android.app.Service#onBind(Intent)}. Indeed, the semantics of {@link #equals(Object)} match * action, data URI, type and category set). See <a
* Android's internal equivalence relation for caching the result of calling this method. See <a
* href="https://developer.android.com/guide/components/bound-services">Bound Services Overview</a> * href="https://developer.android.com/guide/components/bound-services">Bound Services Overview</a>
* for more. * and <a href="https://developer.android.com/guide/components/intents-filters">Intents and Intent
* Filters</a> for more.
* *
* <p>For convenience in the common case where a {@link android.app.Service} exposes just one {@link * <p>For convenience in the common case where a {@link android.app.Service} exposes just one {@link
* android.os.IBinder} IPC interface, we provide default values for the binding {@link Intent} * android.os.IBinder} IPC interface, we provide default values for the binding {@link Intent}
* fields, namely, an action of {@link ApiConstants#ACTION_BIND}, an empty category set and null * fields, namely, an action of {@link ApiConstants#ACTION_BIND}, an empty category set and null
* type and data URI. * type and data URI.
*
* <p>The semantics of {@link #equals(Object)} are the same as {@link Intent#filterEquals(Intent)}.
*/ */
public final class AndroidComponentAddress extends SocketAddress { public final class AndroidComponentAddress extends SocketAddress {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
private final Intent bindIntent; // An "explicit" Intent. In other words, getComponent() != null. private final Intent bindIntent; // "Explicit", having either a component or package restriction.
protected AndroidComponentAddress(Intent bindIntent) { protected AndroidComponentAddress(Intent bindIntent) {
checkArgument(bindIntent.getComponent() != null, "Missing required component"); checkArgument(
bindIntent.getComponent() != null || bindIntent.getPackage() != null,
"'bindIntent' must be explicit. Specify either a package or ComponentName.");
this.bindIntent = bindIntent; this.bindIntent = bindIntent;
} }
@ -79,14 +84,19 @@ public final class AndroidComponentAddress extends SocketAddress {
} }
/** /**
* Creates a new address that refers to <code>intent</code>'s component and that uses the "filter * Creates a new address that uses the "filter matching" fields of <code>intent</code> as the
* matching" fields of <code>intent</code> as the binding {@link Intent}. * binding {@link Intent}.
*
* <p><code>intent</code> must be "explicit", i.e. having either a target component ({@link
* Intent#getComponent()}) or package restriction ({@link Intent#getPackage()}). See <a
* href="https://developer.android.com/guide/components/intents-filters">Intents and Intent
* Filters</a> for more.
* *
* <p>A multi-tenant {@link android.app.Service} can call this from its {@link * <p>A multi-tenant {@link android.app.Service} can call this from its {@link
* android.app.Service#onBind(Intent)} method to locate an appropriate {@link io.grpc.Server} by * android.app.Service#onBind(Intent)} method to locate an appropriate {@link io.grpc.Server} by
* listening address. * listening address.
* *
* @throws IllegalArgumentException if intent's component is null * @throws IllegalArgumentException if 'intent' isn't "explicit"
*/ */
public static AndroidComponentAddress forBindIntent(Intent intent) { public static AndroidComponentAddress forBindIntent(Intent intent) {
return new AndroidComponentAddress(intent.cloneFilter()); return new AndroidComponentAddress(intent.cloneFilter());
@ -104,12 +114,26 @@ public final class AndroidComponentAddress extends SocketAddress {
/** /**
* Returns the Authority which is the package name of the target app. * Returns the Authority which is the package name of the target app.
* *
* <p>See {@link android.content.ComponentName}. * <p>See {@link android.content.ComponentName} and {@link Intent#getPackage()}.
*/ */
public String getAuthority() { public String getAuthority() {
return getComponent().getPackageName(); return getPackage();
} }
/**
* Returns the package target of the wrapped {@link Intent}, either from its package restriction
* or, if not present, its fully qualified {@link ComponentName}.
*/
public String getPackage() {
if (bindIntent.getPackage() != null) {
return bindIntent.getPackage();
} else {
return bindIntent.getComponent().getPackageName();
}
}
/** Returns the {@link ComponentName} of this binding {@link Intent}, or null if one isn't set. */
@Nullable
public ComponentName getComponent() { public ComponentName getComponent() {
return bindIntent.getComponent(); return bindIntent.getComponent();
} }
@ -131,7 +155,7 @@ public final class AndroidComponentAddress extends SocketAddress {
Intent intentForUri = bindIntent; Intent intentForUri = bindIntent;
if (intentForUri.getPackage() == null) { if (intentForUri.getPackage() == null) {
// URI_ANDROID_APP_SCHEME requires an "explicit package name" which isn't set by any of our // URI_ANDROID_APP_SCHEME requires an "explicit package name" which isn't set by any of our
// factory methods. Oddly, our explicit ComponentName is not enough. // factory methods. Oddly, a ComponentName is not enough.
intentForUri = intentForUri.cloneFilter().setPackage(getComponent().getPackageName()); intentForUri = intentForUri.cloneFilter().setPackage(getComponent().getPackageName());
} }
return intentForUri.toUri(URI_ANDROID_APP_SCHEME); return intentForUri.toUri(URI_ANDROID_APP_SCHEME);

View File

@ -784,9 +784,7 @@ public abstract class BinderTransport
Context sourceContext, AndroidComponentAddress targetAddress) { Context sourceContext, AndroidComponentAddress targetAddress) {
return InternalLogId.allocate( return InternalLogId.allocate(
BinderClientTransport.class, BinderClientTransport.class,
sourceContext.getClass().getSimpleName() sourceContext.getClass().getSimpleName() + "->" + targetAddress);
+ "->"
+ targetAddress.getComponent().toShortString());
} }
private static Attributes buildClientAttributes( private static Attributes buildClientAttributes(

View File

@ -49,6 +49,26 @@ public final class AndroidComponentAddressTest {
assertThat(addr.getComponent()).isSameInstanceAs(hostComponent); assertThat(addr.getComponent()).isSameInstanceAs(hostComponent);
} }
@Test
public void testTargetPackageNullComponentName() {
AndroidComponentAddress addr =
AndroidComponentAddress.forBindIntent(
new Intent().setPackage("com.foo").setAction(ApiConstants.ACTION_BIND));
assertThat(addr.getPackage()).isEqualTo("com.foo");
assertThat(addr.getComponent()).isNull();
}
@Test
public void testTargetPackageNonNullComponentName() {
AndroidComponentAddress addr =
AndroidComponentAddress.forBindIntent(
new Intent()
.setComponent(new ComponentName("com.foo", "com.foo.BarService"))
.setPackage("com.foo")
.setAction(ApiConstants.ACTION_BIND));
assertThat(addr.getPackage()).isEqualTo("com.foo");
}
@Test @Test
public void testAsBindIntent() { public void testAsBindIntent() {
Intent bindIntent = Intent bindIntent =