• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.companion.association;
18 
19 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
20 import static android.app.PendingIntent.FLAG_IMMUTABLE;
21 import static android.app.PendingIntent.FLAG_ONE_SHOT;
22 import static android.companion.CompanionDeviceManager.REASON_INTERNAL_ERROR;
23 import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR;
24 import static android.content.ComponentName.createRelative;
25 import static android.content.pm.PackageManager.FEATURE_WATCH;
26 
27 import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
28 import static com.android.server.companion.utils.PermissionsUtils.enforcePermissionForCreatingAssociation;
29 import static com.android.server.companion.utils.RolesUtils.addRoleHolderForAssociation;
30 import static com.android.server.companion.utils.RolesUtils.isRoleHolder;
31 import static com.android.server.companion.utils.Utils.prepareForIpc;
32 
33 import static java.util.Objects.requireNonNull;
34 
35 import android.annotation.NonNull;
36 import android.annotation.Nullable;
37 import android.annotation.SuppressLint;
38 import android.annotation.UserIdInt;
39 import android.app.ActivityOptions;
40 import android.app.PendingIntent;
41 import android.companion.AssociatedDevice;
42 import android.companion.AssociationInfo;
43 import android.companion.AssociationRequest;
44 import android.companion.IAssociationRequestCallback;
45 import android.content.ComponentName;
46 import android.content.Context;
47 import android.content.Intent;
48 import android.content.IntentSender;
49 import android.content.pm.PackageManagerInternal;
50 import android.net.MacAddress;
51 import android.os.Binder;
52 import android.os.Bundle;
53 import android.os.Handler;
54 import android.os.RemoteException;
55 import android.os.ResultReceiver;
56 import android.os.UserHandle;
57 import android.util.Slog;
58 
59 import com.android.internal.R;
60 import com.android.server.companion.CompanionDeviceManagerService;
61 import com.android.server.companion.utils.PackageUtils;
62 
63 import java.util.List;
64 
65 /**
66  * Class responsible for handling incoming {@link AssociationRequest}s.
67  * The main responsibilities of an {@link AssociationRequestsProcessor} are:
68  * <ul>
69  * <li> Requests validation and checking if the package that would own the association holds all
70  * necessary permissions.
71  * <li> Communication with the requester via a provided
72  * {@link android.companion.CompanionDeviceManager.Callback}.
73  * <li> Constructing an {@link Intent} for collecting user's approval (if needed), and handling the
74  * approval.
75  * <li> Calling to {@link CompanionDeviceManagerService} to create an association when/if the
76  * request was found valid and was approved by user.
77  * </ul>
78  *
79  * The class supports two variants of the "Association Flow": the full variant, and the shortened
80  * (a.k.a. No-UI) variant.
81  * Both flows start similarly: in
82  * {@link #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)}
83  * invoked from
84  * {@link CompanionDeviceManagerService.CompanionDeviceManagerImpl#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
85  * method call.
86  * Then an {@link AssociationRequestsProcessor} makes a decision whether user's confirmation is
87  * required.
88  *
89  * If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes
90  * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback, ResultReceiver)}
91  * which after calling to  {@link CompanionDeviceManagerService} to create an association, notifies
92  * the requester via
93  * {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}.
94  *
95  * If the user's approval is required: an {@link AssociationRequestsProcessor} constructs a
96  * {@link PendingIntent} for the approval UI and sends it back to the requester via
97  * {@link android.companion.CompanionDeviceManager.Callback#onAssociationPending(IntentSender)}.
98  * When/if user approves the request,  {@link AssociationRequestsProcessor} receives a "callback"
99  * from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes
100  * {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)}
101  * which one more time checks that the packages holds all necessary permissions before proceeding to
102  * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback, ResultReceiver)}.
103  *
104  * @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)
105  * @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback,
106  * ResultReceiver, MacAddress)
107  */
108 @SuppressLint("LongLogTag")
109 public class AssociationRequestsProcessor {
110     private static final String TAG = "CDM_AssociationRequestsProcessor";
111 
112     // AssociationRequestsProcessor <-> UI
113     private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
114     private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
115     private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
116     private static final String EXTRA_FORCE_CANCEL_CONFIRMATION = "cancel_confirmation";
117 
118     // AssociationRequestsProcessor -> UI
119     private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
120     private static final String EXTRA_ASSOCIATION = "association";
121 
122     // UI -> AssociationRequestsProcessor
123     private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0;
124     private static final String EXTRA_MAC_ADDRESS = "mac_address";
125 
126     private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
127     private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
128 
129     private final @NonNull Context mContext;
130     private final @NonNull PackageManagerInternal mPackageManagerInternal;
131     private final @NonNull AssociationStore mAssociationStore;
132     @NonNull
133     private final ComponentName mCompanionAssociationActivity;
134 
AssociationRequestsProcessor(@onNull Context context, @NonNull PackageManagerInternal packageManagerInternal, @NonNull AssociationStore associationStore)135     public AssociationRequestsProcessor(@NonNull Context context,
136             @NonNull PackageManagerInternal packageManagerInternal,
137             @NonNull AssociationStore associationStore) {
138         mContext = context;
139         mPackageManagerInternal = packageManagerInternal;
140         mAssociationStore = associationStore;
141         mCompanionAssociationActivity = createRelative(
142                 mContext.getString(R.string.config_companionDeviceManagerPackage),
143                 ".CompanionAssociationActivity");
144     }
145 
146     /**
147      * Handle incoming {@link AssociationRequest}s, sent via
148      * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest,
149      * IAssociationRequestCallback, String, int)}
150      */
processNewAssociationRequest(@onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @NonNull IAssociationRequestCallback callback)151     public void processNewAssociationRequest(@NonNull AssociationRequest request,
152             @NonNull String packageName, @UserIdInt int userId,
153             @NonNull IAssociationRequestCallback callback) {
154         requireNonNull(request, "Request MUST NOT be null");
155         if (request.isSelfManaged()) {
156             requireNonNull(request.getDisplayName(), "AssociationRequest.displayName "
157                     + "MUST NOT be null.");
158         }
159         requireNonNull(packageName, "Package name MUST NOT be null");
160         requireNonNull(callback, "Callback MUST NOT be null");
161 
162         final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId);
163         Slog.d(TAG, "processNewAssociationRequest() " + "request=" + request + ", " + "package=u"
164                 + userId + "/" + packageName + " (uid=" + packageUid + ")");
165 
166         // 1. Enforce permissions and other requirements.
167         enforcePermissionForCreatingAssociation(mContext, request, packageUid);
168         enforceUsesCompanionDeviceFeature(mContext, userId, packageName);
169 
170         // 2a. Check if association can be created without launching UI (i.e. CDM needs NEITHER
171         // to perform discovery NOR to collect user consent).
172         if (request.isSelfManaged() && !request.isForceConfirmation()
173                 && !willAddRoleHolder(request, packageName, userId)) {
174             // 2a.1. Create association right away.
175             createAssociationAndNotifyApplication(request, packageName, userId,
176                     /* macAddress */ null, callback, /* resultReceiver */ null);
177             return;
178         }
179 
180         // 2a.2. Report an error if a 3p app tries to create a non-self-managed association and
181         //       launch UI on watch.
182         if (mContext.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
183             String errorMessage = "3p apps are not allowed to create associations on watch.";
184             Slog.e(TAG, errorMessage);
185             try {
186                 callback.onFailure(errorMessage);
187             } catch (RemoteException e) {
188                 // ignored
189             }
190             return;
191         }
192 
193         // 2b. Build a PendingIntent for launching the confirmation UI, and send it back to the app:
194 
195         // 2b.1. Populate the request with required info.
196         request.setPackageName(packageName);
197         request.setUserId(userId);
198         request.setSkipPrompt(mayAssociateWithoutPrompt(packageName, userId));
199 
200         // 2b.2. Prepare extras and create an Intent.
201         final Bundle extras = new Bundle();
202         extras.putParcelable(EXTRA_ASSOCIATION_REQUEST, request);
203         extras.putBinder(EXTRA_APPLICATION_CALLBACK, callback.asBinder());
204         extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
205 
206         final Intent intent = new Intent();
207         intent.setComponent(mCompanionAssociationActivity);
208         intent.putExtras(extras);
209 
210         // 2b.3. Create a PendingIntent.
211         final PendingIntent pendingIntent = createPendingIntent(packageUid, intent);
212 
213         // 2b.4. Send the PendingIntent back to the app.
214         try {
215             callback.onAssociationPending(pendingIntent);
216         } catch (RemoteException ignore) {
217         }
218     }
219 
220     /**
221      * Process another AssociationRequest in CompanionDeviceActivity to cancel current dialog.
222      */
buildAssociationCancellationIntent(@onNull String packageName, @UserIdInt int userId)223     public PendingIntent buildAssociationCancellationIntent(@NonNull String packageName,
224             @UserIdInt int userId) {
225         requireNonNull(packageName, "Package name MUST NOT be null");
226 
227         enforceUsesCompanionDeviceFeature(mContext, userId, packageName);
228 
229         final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId);
230 
231         final Bundle extras = new Bundle();
232         extras.putBoolean(EXTRA_FORCE_CANCEL_CONFIRMATION, true);
233 
234         final Intent intent = new Intent();
235         intent.setComponent(mCompanionAssociationActivity);
236         intent.putExtras(extras);
237 
238         return createPendingIntent(packageUid, intent);
239     }
240 
processAssociationRequestApproval(@onNull AssociationRequest request, @NonNull IAssociationRequestCallback callback, @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress)241     private void processAssociationRequestApproval(@NonNull AssociationRequest request,
242             @NonNull IAssociationRequestCallback callback,
243             @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) {
244         final String packageName = request.getPackageName();
245         final int userId = request.getUserId();
246         final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId);
247 
248         // 1. Need to check permissions again in case something changed, since we first received
249         // this request.
250         try {
251             enforcePermissionForCreatingAssociation(mContext, request, packageUid);
252         } catch (SecurityException e) {
253             // Since, at this point the caller is our own UI, we need to catch the exception on
254             // forward it back to the application via the callback.
255             try {
256                 callback.onFailure(e.getMessage());
257             } catch (RemoteException ignore) {
258             }
259             return;
260         }
261 
262         // 2. Create association and notify the application.
263         createAssociationAndNotifyApplication(request, packageName, userId, macAddress, callback,
264                 resultReceiver);
265     }
266 
createAssociationAndNotifyApplication( @onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback, @NonNull ResultReceiver resultReceiver)267     private void createAssociationAndNotifyApplication(
268             @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId,
269             @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback,
270             @NonNull ResultReceiver resultReceiver) {
271         Binder.withCleanCallingIdentity(() -> {
272             createAssociation(userId, packageName, macAddress, request.getDisplayName(),
273                     request.getDeviceProfile(), request.getAssociatedDevice(),
274                     request.isSelfManaged(),
275                     callback, resultReceiver);
276         });
277     }
278 
279     /**
280      * Create an association.
281      */
createAssociation(@serIdInt int userId, @NonNull String packageName, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver)282     public void createAssociation(@UserIdInt int userId, @NonNull String packageName,
283             @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
284             @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice,
285             boolean selfManaged, @Nullable IAssociationRequestCallback callback,
286             @Nullable ResultReceiver resultReceiver) {
287         final int id = mAssociationStore.getNextId();
288         final long timestamp = System.currentTimeMillis();
289 
290         final AssociationInfo association = new AssociationInfo(id, userId, packageName,
291                 /* tag */ null, macAddress, displayName, deviceProfile, associatedDevice,
292                 selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false,
293                 /* pending */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0);
294 
295         // Add role holder for association (if specified) and add new association to store.
296         maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver);
297     }
298 
299     /**
300      * Grant a role if specified and add an association to store.
301      */
maybeGrantRoleAndStoreAssociation(@onNull AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver)302     public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association,
303             @Nullable IAssociationRequestCallback callback,
304             @Nullable ResultReceiver resultReceiver) {
305         // If the "Device Profile" is specified, make the companion application a holder of the
306         // corresponding role.
307         // If it is null, then the operation will succeed without granting any role.
308         addRoleHolderForAssociation(mContext, association, success -> {
309             if (success) {
310                 Slog.i(TAG, "Added " + association.getDeviceProfile() + " role to userId="
311                         + association.getUserId() + ", packageName="
312                         + association.getPackageName());
313                 mAssociationStore.addAssociation(association);
314                 sendCallbackAndFinish(association, callback, resultReceiver);
315             } else {
316                 Slog.e(TAG, "Failed to add u" + association.getUserId()
317                         + "\\" + association.getPackageName()
318                         + " to the list of " + association.getDeviceProfile() + " holders.");
319                 sendCallbackAndFinish(null, callback, resultReceiver);
320             }
321         });
322     }
323 
324     /**
325      * Enable system data sync.
326      */
enableSystemDataSync(int associationId, int flags)327     public void enableSystemDataSync(int associationId, int flags) {
328         AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
329                 associationId);
330         AssociationInfo updated = (new AssociationInfo.Builder(association))
331                 .setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build();
332         mAssociationStore.updateAssociation(updated);
333     }
334 
335     /**
336      * Disable system data sync.
337      */
disableSystemDataSync(int associationId, int flags)338     public void disableSystemDataSync(int associationId, int flags) {
339         AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
340                 associationId);
341         AssociationInfo updated = (new AssociationInfo.Builder(association))
342                 .setSystemDataSyncFlags(association.getSystemDataSyncFlags() & (~flags)).build();
343         mAssociationStore.updateAssociation(updated);
344     }
345 
346     /**
347      * Set association tag.
348      */
setAssociationTag(int associationId, String tag)349     public void setAssociationTag(int associationId, String tag) {
350         Slog.i(TAG, "Setting association tag=[" + tag + "] to id=[" + associationId + "]...");
351 
352         AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
353                 associationId);
354         association = (new AssociationInfo.Builder(association)).setTag(tag).build();
355         mAssociationStore.updateAssociation(association);
356     }
357 
sendCallbackAndFinish(@ullable AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver)358     private void sendCallbackAndFinish(@Nullable AssociationInfo association,
359             @Nullable IAssociationRequestCallback callback,
360             @Nullable ResultReceiver resultReceiver) {
361         if (association != null) {
362             // Send the association back via the app's callback
363             if (callback != null) {
364                 try {
365                     callback.onAssociationCreated(association);
366                 } catch (RemoteException ignore) {
367                 }
368             }
369 
370             // Send the association back to CompanionDeviceActivity, so that it can report
371             // back to the app via Activity.setResult().
372             if (resultReceiver != null) {
373                 final Bundle data = new Bundle();
374                 data.putParcelable(EXTRA_ASSOCIATION, association);
375                 resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data);
376             }
377         } else {
378             // Send the association back via the app's callback
379             if (callback != null) {
380                 try {
381                     callback.onFailure(REASON_INTERNAL_ERROR);
382                 } catch (RemoteException ignore) {
383                 }
384             }
385 
386             // Send the association back to CompanionDeviceActivity, so that it can report
387             // back to the app via Activity.setResult().
388             if (resultReceiver != null) {
389                 final Bundle data = new Bundle();
390                 resultReceiver.send(RESULT_INTERNAL_ERROR, data);
391             }
392         }
393     }
394 
willAddRoleHolder(@onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId)395     private boolean willAddRoleHolder(@NonNull AssociationRequest request,
396             @NonNull String packageName, @UserIdInt int userId) {
397         final String deviceProfile = request.getDeviceProfile();
398         if (deviceProfile == null) return false;
399 
400         final boolean isRoleHolder = Binder.withCleanCallingIdentity(
401                 () -> isRoleHolder(mContext, userId, packageName, deviceProfile));
402 
403         // Don't need to "grant" the role, if the package already holds the role.
404         return !isRoleHolder;
405     }
406 
createPendingIntent(int packageUid, Intent intent)407     private PendingIntent createPendingIntent(int packageUid, Intent intent) {
408         final PendingIntent pendingIntent;
409 
410         // Using uid of the application that will own the association (usually the same
411         // application that sent the request) allows us to have multiple "pending" association
412         // requests at the same time.
413         // If the application already has a pending association request, that PendingIntent
414         // will be cancelled except application wants to cancel the request by the system.
415         return Binder.withCleanCallingIdentity(() ->
416                 PendingIntent.getActivityAsUser(
417                         mContext, /*requestCode */ packageUid, intent,
418                         FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
419                         ActivityOptions.makeBasic()
420                                 .setPendingIntentCreatorBackgroundActivityStartMode(
421                                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
422                                 .toBundle(),
423                         UserHandle.CURRENT)
424         );
425     }
426 
427     private final ResultReceiver mOnRequestConfirmationReceiver =
428             new ResultReceiver(Handler.getMain()) {
429                 @Override
430                 protected void onReceiveResult(int resultCode, Bundle data) {
431                     if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
432                         Slog.w(TAG, "Unknown result code:" + resultCode);
433                         return;
434                     }
435 
436                     final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST,
437                             android.companion.AssociationRequest.class);
438                     final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub
439                             .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK));
440                     final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER,
441                             android.os.ResultReceiver.class);
442 
443                     requireNonNull(request);
444                     requireNonNull(callback);
445                     requireNonNull(resultReceiver);
446 
447                     final MacAddress macAddress;
448                     if (request.isSelfManaged()) {
449                         macAddress = null;
450                     } else {
451                         macAddress = data.getParcelable(EXTRA_MAC_ADDRESS,
452                                 android.net.MacAddress.class);
453                         requireNonNull(macAddress);
454                     }
455 
456                     processAssociationRequestApproval(request, callback, resultReceiver,
457                             macAddress);
458                 }
459             };
460 
mayAssociateWithoutPrompt(@onNull String packageName, @UserIdInt int userId)461     private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
462         // Throttle frequent associations
463         final long now = System.currentTimeMillis();
464         final List<AssociationInfo> associationForPackage =
465                 mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
466         // Number of "recent" associations.
467         int recent = 0;
468         for (AssociationInfo association : associationForPackage) {
469             final boolean isRecent =
470                     now - association.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS;
471             if (isRecent) {
472                 if (++recent >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
473                     Slog.w(TAG, "Too many associations: " + packageName + " already "
474                             + "associated " + recent + " devices within the last "
475                             + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS + "ms");
476                     return false;
477                 }
478             }
479         }
480 
481         return PackageUtils.isPackageAllowlisted(mContext, mPackageManagerInternal, packageName);
482     }
483 }
484