Skip to content

Commit e58c998

Browse files
authoredNov 19, 2024··
AndroidComponentAddress includes a target UserHandle (#11670)
The target UserHandle is best modeled as part of the SocketAddress not the Channel since it's part of the server's location. This change allows a NameResolver to select different target users over time within a single Channel.
1 parent 6a92a2a commit e58c998

File tree

5 files changed

+181
-20
lines changed

5 files changed

+181
-20
lines changed
 

‎binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java

+84-6
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818

1919
import static android.content.Intent.URI_ANDROID_APP_SCHEME;
2020
import static com.google.common.base.Preconditions.checkArgument;
21+
import static com.google.common.base.Preconditions.checkState;
2122

2223
import android.content.ComponentName;
2324
import android.content.Context;
2425
import android.content.Intent;
26+
import android.os.UserHandle;
27+
import com.google.common.base.Objects;
28+
import io.grpc.ExperimentalApi;
2529
import java.net.SocketAddress;
2630
import javax.annotation.Nullable;
2731

@@ -41,18 +45,25 @@
4145
* fields, namely, an action of {@link ApiConstants#ACTION_BIND}, an empty category set and null
4246
* type and data URI.
4347
*
44-
* <p>The semantics of {@link #equals(Object)} are the same as {@link Intent#filterEquals(Intent)}.
48+
* <p>Optionally contains a {@link UserHandle} that must be considered wherever the {@link Intent}
49+
* is evaluated.
50+
*
51+
* <p>{@link #equals(Object)} uses {@link Intent#filterEquals(Intent)} semantics to compare Intents.
4552
*/
4653
public final class AndroidComponentAddress extends SocketAddress {
4754
private static final long serialVersionUID = 0L;
4855

4956
private final Intent bindIntent; // "Explicit", having either a component or package restriction.
5057

51-
protected AndroidComponentAddress(Intent bindIntent) {
58+
@Nullable
59+
private final UserHandle targetUser; // null means the same user that hosts this process.
60+
61+
protected AndroidComponentAddress(Intent bindIntent, @Nullable UserHandle targetUser) {
5262
checkArgument(
5363
bindIntent.getComponent() != null || bindIntent.getPackage() != null,
5464
"'bindIntent' must be explicit. Specify either a package or ComponentName.");
5565
this.bindIntent = bindIntent;
66+
this.targetUser = targetUser;
5667
}
5768

5869
/**
@@ -99,7 +110,7 @@ public static AndroidComponentAddress forRemoteComponent(
99110
* @throws IllegalArgumentException if 'intent' isn't "explicit"
100111
*/
101112
public static AndroidComponentAddress forBindIntent(Intent intent) {
102-
return new AndroidComponentAddress(intent.cloneFilter());
113+
return new AndroidComponentAddress(intent.cloneFilter(), null);
103114
}
104115

105116
/**
@@ -108,7 +119,7 @@ public static AndroidComponentAddress forBindIntent(Intent intent) {
108119
*/
109120
public static AndroidComponentAddress forComponent(ComponentName component) {
110121
return new AndroidComponentAddress(
111-
new Intent(ApiConstants.ACTION_BIND).setComponent(component));
122+
new Intent(ApiConstants.ACTION_BIND).setComponent(component), null);
112123
}
113124

114125
/**
@@ -141,6 +152,9 @@ public ComponentName getComponent() {
141152
/**
142153
* Returns this address as an explicit {@link Intent} suitable for passing to {@link
143154
* Context#bindService}.
155+
*
156+
* <p>NB: The returned Intent does not specify a target Android user. If {@link #getTargetUser()}
157+
* is non-null, {@link Context#bindServiceAsUser} should be called instead.
144158
*/
145159
public Intent asBindIntent() {
146160
return bindIntent.cloneFilter(); // Intent is mutable so return a copy.
@@ -177,13 +191,77 @@ public int hashCode() {
177191
public boolean equals(Object obj) {
178192
if (obj instanceof AndroidComponentAddress) {
179193
AndroidComponentAddress that = (AndroidComponentAddress) obj;
180-
return bindIntent.filterEquals(that.bindIntent);
194+
return bindIntent.filterEquals(that.bindIntent)
195+
&& Objects.equal(this.targetUser, that.targetUser);
181196
}
182197
return false;
183198
}
184199

185200
@Override
186201
public String toString() {
187-
return "AndroidComponentAddress[" + bindIntent + "]";
202+
StringBuilder builder = new StringBuilder("AndroidComponentAddress[");
203+
if (targetUser != null) {
204+
builder.append(targetUser);
205+
builder.append("@");
206+
}
207+
builder.append(bindIntent);
208+
builder.append("]");
209+
return builder.toString();
210+
}
211+
212+
/**
213+
* Identifies the Android user in which the bind Intent will be evaluated.
214+
*
215+
* <p>Returns the {@link UserHandle}, or null which means that the Android user hosting the
216+
* current process will be used.
217+
*/
218+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
219+
@Nullable
220+
public UserHandle getTargetUser() {
221+
return targetUser;
222+
}
223+
224+
public static Builder newBuilder() {
225+
return new Builder();
226+
}
227+
228+
/** Fluently builds instances of {@link AndroidComponentAddress}. */
229+
public static class Builder {
230+
Intent bindIntent;
231+
UserHandle targetUser;
232+
233+
/**
234+
* Sets the binding {@link Intent} to one having the "filter matching" fields of 'intent'.
235+
*
236+
* <p>'intent' must be "explicit", i.e. having either a target component ({@link
237+
* Intent#getComponent()}) or package restriction ({@link Intent#getPackage()}).
238+
*/
239+
public Builder setBindIntent(Intent intent) {
240+
this.bindIntent = intent.cloneFilter();
241+
return this;
242+
}
243+
244+
/**
245+
* Sets the binding {@link Intent} to one with the specified 'component' and default values for
246+
* all other fields, for convenience.
247+
*/
248+
public Builder setBindIntentFromComponent(ComponentName component) {
249+
this.bindIntent = new Intent(ApiConstants.ACTION_BIND).setComponent(component);
250+
return this;
251+
}
252+
253+
/** See {@link AndroidComponentAddress#getTargetUser()}. */
254+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
255+
public Builder setTargetUser(@Nullable UserHandle targetUser) {
256+
this.targetUser = targetUser;
257+
return this;
258+
}
259+
260+
public AndroidComponentAddress build() {
261+
// We clone any incoming mutable intent in the setter, not here. AndroidComponentAddress
262+
// itself is immutable so multiple instances built from here can safely share 'bindIntent'.
263+
checkState(bindIntent != null, "Required property 'bindIntent' unset");
264+
return new AndroidComponentAddress(bindIntent, targetUser);
265+
}
188266
}
189267
}

‎binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java

+14-6
Original file line numberDiff line numberDiff line change
@@ -236,20 +236,28 @@ public BinderChannelBuilder securityPolicy(SecurityPolicy securityPolicy) {
236236
}
237237

238238
/**
239-
* Provides the target {@UserHandle} of the remote Android service.
239+
* Specifies the {@link UserHandle} to be searched for the remote Android Service by default.
240240
*
241-
* <p>When targetUserHandle is set, Context.bindServiceAsUser will used and additional Android
242-
* permissions will be required. If your usage does not require cross-user communications, please
243-
* do not set this field. It is the caller's responsibility to make sure that it holds the
244-
* corresponding permissions.
241+
* <p>Used only as a fallback if the direct or resolved {@link AndroidComponentAddress} doesn't
242+
* specify a {@link UserHandle}. If neither the Channel nor the {@link AndroidComponentAddress}
243+
* specifies a target user, the {@link UserHandle} of the current process will be used.
245244
*
245+
* <p>Targeting a Service in a different Android user is uncommon and requires special permissions
246+
* normally reserved for system apps. See {@link android.content.Context#bindServiceAsUser} for
247+
* details.
248+
*
249+
* @deprecated This method's name is misleading because it implies an impersonated client identity
250+
* when it's actually specifying part of the server's location. It's also no longer necessary
251+
* since the target user is part of {@link AndroidComponentAddress}. Prefer to specify target
252+
* user in the address instead, either directly or via a {@link io.grpc.NameResolverProvider}.
246253
* @param targetUserHandle the target user to bind into.
247254
* @return this
248255
*/
249256
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
250257
@RequiresApi(30)
258+
@Deprecated
251259
public BinderChannelBuilder bindAsUser(UserHandle targetUserHandle) {
252-
transportFactoryBuilder.setTargetUserHandle(targetUserHandle);
260+
transportFactoryBuilder.setDefaultTargetUserHandle(targetUserHandle);
253261
return this;
254262
}
255263

‎binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
5050
final ObjectPool<ScheduledExecutorService> scheduledExecutorPool;
5151
final ObjectPool<? extends Executor> offloadExecutorPool;
5252
final SecurityPolicy securityPolicy;
53-
@Nullable final UserHandle targetUserHandle;
53+
@Nullable final UserHandle defaultTargetUserHandle;
5454
final BindServiceFlags bindServiceFlags;
5555
final InboundParcelablePolicy inboundParcelablePolicy;
5656
final OneWayBinderProxy.Decorator binderDecorator;
@@ -70,7 +70,7 @@ private BinderClientTransportFactory(Builder builder) {
7070
scheduledExecutorPool = checkNotNull(builder.scheduledExecutorPool);
7171
offloadExecutorPool = checkNotNull(builder.offloadExecutorPool);
7272
securityPolicy = checkNotNull(builder.securityPolicy);
73-
targetUserHandle = builder.targetUserHandle;
73+
defaultTargetUserHandle = builder.defaultTargetUserHandle;
7474
bindServiceFlags = checkNotNull(builder.bindServiceFlags);
7575
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
7676
binderDecorator = checkNotNull(builder.binderDecorator);
@@ -123,7 +123,7 @@ public static final class Builder implements ClientTransportFactoryBuilder {
123123
ObjectPool<ScheduledExecutorService> scheduledExecutorPool =
124124
SharedResourcePool.forResource(GrpcUtil.TIMER_SERVICE);
125125
SecurityPolicy securityPolicy = SecurityPolicies.internalOnly();
126-
@Nullable UserHandle targetUserHandle;
126+
@Nullable UserHandle defaultTargetUserHandle;
127127
BindServiceFlags bindServiceFlags = BindServiceFlags.DEFAULTS;
128128
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
129129
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
@@ -165,8 +165,8 @@ public Builder setSecurityPolicy(SecurityPolicy securityPolicy) {
165165
return this;
166166
}
167167

168-
public Builder setTargetUserHandle(@Nullable UserHandle targetUserHandle) {
169-
this.targetUserHandle = targetUserHandle;
168+
public Builder setDefaultTargetUserHandle(@Nullable UserHandle defaultTargetUserHandle) {
169+
this.defaultTargetUserHandle = defaultTargetUserHandle;
170170
return this;
171171
}
172172

‎binder/src/main/java/io/grpc/binder/internal/BinderTransport.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,9 @@ public BinderClientTransport(
611611
factory.sourceContext,
612612
factory.channelCredentials,
613613
targetAddress.asBindIntent(),
614-
factory.targetUserHandle,
614+
targetAddress.getTargetUser() != null
615+
? targetAddress.getTargetUser()
616+
: factory.defaultTargetUserHandle,
615617
factory.bindServiceFlags.toInteger(),
616618
this);
617619
}

‎binder/src/test/java/io/grpc/binder/AndroidComponentAddressTest.java

+75-2
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818

1919
import static android.content.Intent.URI_ANDROID_APP_SCHEME;
2020
import static com.google.common.truth.Truth.assertThat;
21+
import static org.junit.Assert.assertThrows;
2122

2223
import android.content.ComponentName;
2324
import android.content.Context;
2425
import android.content.Intent;
2526
import android.net.Uri;
27+
import android.os.Parcel;
28+
import android.os.UserHandle;
2629
import androidx.test.core.app.ApplicationProvider;
2730
import com.google.common.testing.EqualsTester;
2831
import java.net.URISyntaxException;
@@ -83,6 +86,32 @@ public void testAsBindIntent() {
8386
assertThat(addr.asBindIntent().filterEquals(bindIntent)).isTrue();
8487
}
8588

89+
@Test
90+
public void testPostCreateIntentMutation() {
91+
Intent bindIntent = new Intent().setAction("foo-action").setComponent(hostComponent);
92+
AndroidComponentAddress addr = AndroidComponentAddress.forBindIntent(bindIntent);
93+
bindIntent.setAction("bar-action");
94+
assertThat(addr.asBindIntent().getAction()).isEqualTo("foo-action");
95+
}
96+
97+
@Test
98+
public void testPostBuildIntentMutation() {
99+
Intent bindIntent = new Intent().setAction("foo-action").setComponent(hostComponent);
100+
AndroidComponentAddress addr =
101+
AndroidComponentAddress.newBuilder().setBindIntent(bindIntent).build();
102+
bindIntent.setAction("bar-action");
103+
assertThat(addr.asBindIntent().getAction()).isEqualTo("foo-action");
104+
}
105+
106+
@Test
107+
public void testBuilderMissingRequired() {
108+
IllegalStateException ise =
109+
assertThrows(
110+
IllegalStateException.class,
111+
() -> AndroidComponentAddress.newBuilder().setTargetUser(newUserHandle(123)).build());
112+
assertThat(ise.getMessage()).contains("bindIntent");
113+
}
114+
86115
@Test
87116
@Config(sdk = 30)
88117
public void testAsAndroidAppUriSdk30() throws URISyntaxException {
@@ -117,13 +146,21 @@ public void testEquality() {
117146
AndroidComponentAddress.forContext(appContext),
118147
AndroidComponentAddress.forLocalComponent(appContext, appContext.getClass()),
119148
AndroidComponentAddress.forRemoteComponent(
120-
appContext.getPackageName(), appContext.getClass().getName()))
149+
appContext.getPackageName(), appContext.getClass().getName()),
150+
AndroidComponentAddress.newBuilder()
151+
.setBindIntentFromComponent(hostComponent)
152+
.setTargetUser(null)
153+
.build())
121154
.addEqualityGroup(
122155
AndroidComponentAddress.forRemoteComponent("appy.mcappface", ".McActivity"))
123156
.addEqualityGroup(AndroidComponentAddress.forLocalComponent(appContext, getClass()))
124157
.addEqualityGroup(
125158
AndroidComponentAddress.forBindIntent(
126-
new Intent().setAction("custom-action").setComponent(hostComponent)))
159+
new Intent().setAction("custom-action").setComponent(hostComponent)),
160+
AndroidComponentAddress.newBuilder()
161+
.setBindIntent(new Intent().setAction("custom-action").setComponent(hostComponent))
162+
.setTargetUser(null)
163+
.build())
127164
.addEqualityGroup(
128165
AndroidComponentAddress.forBindIntent(
129166
new Intent()
@@ -133,6 +170,31 @@ public void testEquality() {
133170
.testEquals();
134171
}
135172

173+
@Test
174+
public void testUnequalTargetUsers() {
175+
new EqualsTester()
176+
.addEqualityGroup(
177+
AndroidComponentAddress.newBuilder()
178+
.setBindIntentFromComponent(hostComponent)
179+
.setTargetUser(newUserHandle(10))
180+
.build(),
181+
AndroidComponentAddress.newBuilder()
182+
.setBindIntentFromComponent(hostComponent)
183+
.setTargetUser(newUserHandle(10))
184+
.build())
185+
.addEqualityGroup(
186+
AndroidComponentAddress.newBuilder()
187+
.setBindIntentFromComponent(hostComponent)
188+
.setTargetUser(newUserHandle(11))
189+
.build())
190+
.addEqualityGroup(
191+
AndroidComponentAddress.newBuilder()
192+
.setBindIntentFromComponent(hostComponent)
193+
.setTargetUser(null)
194+
.build())
195+
.testEquals();
196+
}
197+
136198
@Test
137199
@Config(sdk = 30)
138200
public void testPackageFilterEquality30AndUp() {
@@ -163,4 +225,15 @@ public void testPackageFilterEqualityPre30() {
163225
.setComponent(new ComponentName("pkg", "cls"))))
164226
.testEquals();
165227
}
228+
229+
private static UserHandle newUserHandle(int userId) {
230+
Parcel parcel = Parcel.obtain();
231+
try {
232+
parcel.writeInt(userId);
233+
parcel.setDataPosition(0);
234+
return new UserHandle(parcel);
235+
} finally {
236+
parcel.recycle();
237+
}
238+
}
166239
}

0 commit comments

Comments
 (0)
Please sign in to comment.