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">
<uses-library android:name="android.test.runner" />
<service android:name="io.grpc.binder.HostServices$HostService1" />
<service android:name="io.grpc.binder.HostServices$HostService2" />
<service android:name="io.grpc.binder.HostServices$HostService1" android:exported="false">
<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>
</manifest>

View File

@ -22,6 +22,7 @@ import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
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 io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptors;
import io.grpc.ConnectivityState;
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
@ -234,6 +234,18 @@ public final class BinderChannelSmokeTest {
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
public void testUncaughtServerException() throws Exception {
// 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.Intent;
import java.net.SocketAddress;
import javax.annotation.Nullable;
/**
* 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,
* and category set for an {@link Intent} used to bind to it. All together, these fields identify
* the {@link android.os.IBinder} that would be returned by some implementation of {@link
* android.app.Service#onBind(Intent)}. Indeed, the semantics of {@link #equals(Object)} match
* Android's internal equivalence relation for caching the result of calling this method. See <a
* <p>Consists of an explicit {@link Intent} that identifies an {@link android.os.IBinder} returned
* by some Service's {@link android.app.Service#onBind(Intent)} method. You can specify that Service
* by {@link ComponentName} or let Android resolve it using the Intent's other fields (package,
* action, data URI, type and category set). See <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
* 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
* 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 {
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) {
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;
}
@ -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
* matching" fields of <code>intent</code> as the binding {@link Intent}.
* Creates a new address that uses the "filter matching" fields of <code>intent</code> as the
* 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
* android.app.Service#onBind(Intent)} method to locate an appropriate {@link io.grpc.Server} by
* listening address.
*
* @throws IllegalArgumentException if intent's component is null
* @throws IllegalArgumentException if 'intent' isn't "explicit"
*/
public static AndroidComponentAddress forBindIntent(Intent intent) {
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.
*
* <p>See {@link android.content.ComponentName}.
* <p>See {@link android.content.ComponentName} and {@link Intent#getPackage()}.
*/
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() {
return bindIntent.getComponent();
}
@ -131,7 +155,7 @@ public final class AndroidComponentAddress extends SocketAddress {
Intent intentForUri = bindIntent;
if (intentForUri.getPackage() == null) {
// 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());
}
return intentForUri.toUri(URI_ANDROID_APP_SCHEME);

View File

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

View File

@ -49,6 +49,26 @@ public final class AndroidComponentAddressTest {
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
public void testAsBindIntent() {
Intent bindIntent =