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;
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.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
23 import static android.companion.CompanionDeviceManager.REASON_INTERNAL_ERROR;
24 import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR;
25 import static android.content.ComponentName.createRelative;
26 
27 import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
28 import static com.android.server.companion.MetricUtils.logCreateAssociation;
29 import static com.android.server.companion.PackageUtils.enforceUsesCompanionDeviceFeature;
30 import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
31 import static com.android.server.companion.RolesUtils.addRoleHolderForAssociation;
32 import static com.android.server.companion.RolesUtils.isRoleHolder;
33 import static com.android.server.companion.Utils.prepareForIpc;
34 
35 import static java.util.Objects.requireNonNull;
36 
37 import android.annotation.NonNull;
38 import android.annotation.Nullable;
39 import android.annotation.SuppressLint;
40 import android.annotation.UserIdInt;
41 import android.app.PendingIntent;
42 import android.companion.AssociatedDevice;
43 import android.companion.AssociationInfo;
44 import android.companion.AssociationRequest;
45 import android.companion.IAssociationRequestCallback;
46 import android.content.ComponentName;
47 import android.content.Context;
48 import android.content.Intent;
49 import android.content.IntentSender;
50 import android.content.pm.PackageManagerInternal;
51 import android.net.MacAddress;
52 import android.os.Binder;
53 import android.os.Bundle;
54 import android.os.Handler;
55 import android.os.RemoteException;
56 import android.os.ResultReceiver;
57 import android.os.UserHandle;
58 import android.util.Slog;
59 
60 import java.util.List;
61 
62 /**
63  * Class responsible for handling incoming {@link AssociationRequest}s.
64  * The main responsibilities of an {@link AssociationRequestsProcessor} are:
65  * <ul>
66  * <li> Requests validation and checking if the package that would own the association holds all
67  * necessary permissions.
68  * <li> Communication with the requester via a provided
69  * {@link android.companion.CompanionDeviceManager.Callback}.
70  * <li> Constructing an {@link Intent} for collecting user's approval (if needed), and handling the
71  * approval.
72  * <li> Calling to {@link CompanionDeviceManagerService} to create an association when/if the
73  * request was found valid and was approved by user.
74  * </ul>
75  *
76  * The class supports two variants of the "Association Flow": the full variant, and the shortened
77  * (a.k.a. No-UI) variant.
78  * Both flows start similarly: in
79  * {@link #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)}
80  * invoked from
81  * {@link CompanionDeviceManagerService.CompanionDeviceManagerImpl#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
82  * method call.
83  * Then an {@link AssociationRequestsProcessor} makes a decision whether user's confirmation is
84  * required.
85  *
86  * If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes
87  * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback, ResultReceiver)}
88  * which after calling to  {@link CompanionDeviceManagerService} to create an association, notifies
89  * the requester via
90  * {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}.
91  *
92  * If the user's approval is required: an {@link AssociationRequestsProcessor} constructs a
93  * {@link PendingIntent} for the approval UI and sends it back to the requester via
94  * {@link android.companion.CompanionDeviceManager.Callback#onAssociationPending(IntentSender)}.
95  * When/if user approves the request,  {@link AssociationRequestsProcessor} receives a "callback"
96  * from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes
97  * {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)}
98  * which one more time checks that the packages holds all necessary permissions before proceeding to
99  * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback, ResultReceiver)}.
100  *
101  * @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)
102  * @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback,
103  * ResultReceiver, MacAddress)
104  */
105 @SuppressLint("LongLogTag")
106 class AssociationRequestsProcessor {
107     private static final String TAG = "CDM_AssociationRequestsProcessor";
108 
109     private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY =
110             createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity");
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 CompanionDeviceManagerService mService;
131     private final @NonNull PackageManagerInternal mPackageManager;
132     private final @NonNull AssociationStoreImpl mAssociationStore;
133 
AssociationRequestsProcessor(@onNull CompanionDeviceManagerService service, @NonNull AssociationStoreImpl associationStore)134     AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
135             @NonNull AssociationStoreImpl associationStore) {
136         mContext = service.getContext();
137         mService = service;
138         mPackageManager = service.mPackageManagerInternal;
139         mAssociationStore = associationStore;
140     }
141 
142     /**
143      * Handle incoming {@link AssociationRequest}s, sent via
144      * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
145      */
processNewAssociationRequest(@onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @NonNull IAssociationRequestCallback callback)146     void processNewAssociationRequest(@NonNull AssociationRequest request,
147             @NonNull String packageName, @UserIdInt int userId,
148             @NonNull IAssociationRequestCallback callback) {
149         requireNonNull(request, "Request MUST NOT be null");
150         if (request.isSelfManaged()) {
151             requireNonNull(request.getDisplayName(), "AssociationRequest.displayName "
152                     + "MUST NOT be null.");
153         }
154         requireNonNull(packageName, "Package name MUST NOT be null");
155         requireNonNull(callback, "Callback MUST NOT be null");
156 
157         final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
158         if (DEBUG) {
159             Slog.d(TAG, "processNewAssociationRequest() "
160                     + "request=" + request + ", "
161                     + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")");
162         }
163 
164         // 1. Enforce permissions and other requirements.
165         enforcePermissionsForAssociation(mContext, request, packageUid);
166         enforceUsesCompanionDeviceFeature(mContext, userId, packageName);
167 
168         // 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER
169         // to perform discovery NOR to collect user consent).
170         if (request.isSelfManaged() && !request.isForceConfirmation()
171                 && !willAddRoleHolder(request, packageName, userId)) {
172             // 2a. Create association right away.
173             createAssociationAndNotifyApplication(request, packageName, userId,
174                     /* macAddress */ null, callback, /* resultReceiver */ null);
175             return;
176         }
177 
178         // 2b. Build a PendingIntent for launching the confirmation UI, and send it back to the app:
179 
180         // 2b.1. Populate the request with required info.
181         request.setPackageName(packageName);
182         request.setUserId(userId);
183         request.setSkipPrompt(mayAssociateWithoutPrompt(packageName, userId));
184 
185         // 2b.2. Prepare extras and create an Intent.
186         final Bundle extras = new Bundle();
187         extras.putParcelable(EXTRA_ASSOCIATION_REQUEST, request);
188         extras.putBinder(EXTRA_APPLICATION_CALLBACK, callback.asBinder());
189         extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
190 
191         final Intent intent = new Intent();
192         intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
193         intent.putExtras(extras);
194 
195         // 2b.3. Create a PendingIntent.
196         final PendingIntent pendingIntent = createPendingIntent(packageUid, intent);
197 
198         // 2b.4. Send the PendingIntent back to the app.
199         try {
200             callback.onAssociationPending(pendingIntent);
201         } catch (RemoteException ignore) { }
202     }
203 
204     /**
205      * Process another AssociationRequest in CompanionDeviceActivity to cancel current dialog.
206      */
buildAssociationCancellationIntent(@onNull String packageName, @UserIdInt int userId)207     PendingIntent buildAssociationCancellationIntent(@NonNull String packageName,
208             @UserIdInt int userId) {
209         requireNonNull(packageName, "Package name MUST NOT be null");
210 
211         enforceUsesCompanionDeviceFeature(mContext, userId, packageName);
212 
213         final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
214 
215         final Bundle extras = new Bundle();
216         extras.putBoolean(EXTRA_FORCE_CANCEL_CONFIRMATION, true);
217 
218         final Intent intent = new Intent();
219         intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
220         intent.putExtras(extras);
221 
222         return createPendingIntent(packageUid, intent);
223     }
224 
processAssociationRequestApproval(@onNull AssociationRequest request, @NonNull IAssociationRequestCallback callback, @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress)225     private void processAssociationRequestApproval(@NonNull AssociationRequest request,
226             @NonNull IAssociationRequestCallback callback,
227             @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) {
228         final String packageName = request.getPackageName();
229         final int userId = request.getUserId();
230         final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
231 
232         if (DEBUG) {
233             Slog.d(TAG, "processAssociationRequestApproval()\n"
234                     + "   package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n"
235                     + "   request=" + request + "\n"
236                     + "   macAddress=" + macAddress + "\n");
237         }
238 
239         // 1. Need to check permissions again in case something changed, since we first received
240         // this request.
241         try {
242             enforcePermissionsForAssociation(mContext, request, packageUid);
243         } catch (SecurityException e) {
244             // Since, at this point the caller is our own UI, we need to catch the exception on
245             // forward it back to the application via the callback.
246             try {
247                 callback.onFailure(e.getMessage());
248             } catch (RemoteException ignore) { }
249             return;
250         }
251 
252         // 2. Create association and notify the application.
253         createAssociationAndNotifyApplication(request, packageName, userId, macAddress, callback,
254                 resultReceiver);
255     }
256 
createAssociationAndNotifyApplication( @onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback, @NonNull ResultReceiver resultReceiver)257     private void createAssociationAndNotifyApplication(
258             @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId,
259             @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback,
260             @NonNull ResultReceiver resultReceiver) {
261         final long callingIdentity = Binder.clearCallingIdentity();
262         try {
263             createAssociation(userId, packageName, macAddress, request.getDisplayName(),
264                     request.getDeviceProfile(), request.getAssociatedDevice(),
265                     request.isSelfManaged(),
266                     callback, resultReceiver);
267         } finally {
268             Binder.restoreCallingIdentity(callingIdentity);
269         }
270     }
271 
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)272     public void createAssociation(@UserIdInt int userId, @NonNull String packageName,
273             @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
274             @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice,
275             boolean selfManaged, @Nullable IAssociationRequestCallback callback,
276             @Nullable ResultReceiver resultReceiver) {
277         final int id = mService.getNewAssociationIdForPackage(userId, packageName);
278         final long timestamp = System.currentTimeMillis();
279 
280         final AssociationInfo association = new AssociationInfo(id, userId, packageName,
281                 macAddress, displayName, deviceProfile, associatedDevice, selfManaged,
282                 /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE,
283                 /* systemDataSyncFlags */ 0);
284 
285         if (deviceProfile != null) {
286             // If the "Device Profile" is specified, make the companion application a holder of the
287             // corresponding role.
288             addRoleHolderForAssociation(mService.getContext(), association, success -> {
289                 if (success) {
290                     addAssociationToStore(association, deviceProfile);
291 
292                     sendCallbackAndFinish(association, callback, resultReceiver);
293                 } else {
294                     Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName
295                             + " to the list of " + deviceProfile + " holders.");
296 
297                     sendCallbackAndFinish(null, callback, resultReceiver);
298                 }
299             });
300         } else {
301             addAssociationToStore(association, null);
302 
303             sendCallbackAndFinish(association, callback, resultReceiver);
304         }
305 
306         // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
307         // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
308         // that there are other devices with the same profile, so the role holder won't be removed.
309     }
310 
enableSystemDataSync(int associationId, int flags)311     public void enableSystemDataSync(int associationId, int flags) {
312         AssociationInfo association = mAssociationStore.getAssociationById(associationId);
313         AssociationInfo updated = AssociationInfo.builder(association)
314                 .setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build();
315         mAssociationStore.updateAssociation(updated);
316     }
317 
disableSystemDataSync(int associationId, int flags)318     public void disableSystemDataSync(int associationId, int flags) {
319         AssociationInfo association = mAssociationStore.getAssociationById(associationId);
320         AssociationInfo updated = AssociationInfo.builder(association)
321                 .setSystemDataSyncFlags(association.getSystemDataSyncFlags() & (~flags)).build();
322         mAssociationStore.updateAssociation(updated);
323     }
324 
addAssociationToStore(@onNull AssociationInfo association, @Nullable String deviceProfile)325     private void addAssociationToStore(@NonNull AssociationInfo association,
326             @Nullable String deviceProfile) {
327         Slog.i(TAG, "New CDM association created=" + association);
328 
329         mAssociationStore.addAssociation(association);
330 
331         mService.updateSpecialAccessPermissionForAssociatedPackage(association);
332 
333         logCreateAssociation(deviceProfile);
334     }
335 
sendCallbackAndFinish(@ullable AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver)336     private void sendCallbackAndFinish(@Nullable AssociationInfo association,
337             @Nullable IAssociationRequestCallback callback,
338             @Nullable ResultReceiver resultReceiver) {
339         if (association != null) {
340             // Send the association back via the app's callback
341             if (callback != null) {
342                 try {
343                     callback.onAssociationCreated(association);
344                 } catch (RemoteException ignore) {
345                 }
346             }
347 
348             // Send the association back to CompanionDeviceActivity, so that it can report
349             // back to the app via Activity.setResult().
350             if (resultReceiver != null) {
351                 final Bundle data = new Bundle();
352                 data.putParcelable(EXTRA_ASSOCIATION, association);
353                 resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data);
354             }
355         } else {
356             // Send the association back via the app's callback
357             if (callback != null) {
358                 try {
359                     callback.onFailure(REASON_INTERNAL_ERROR);
360                 } catch (RemoteException ignore) {
361                 }
362             }
363 
364             // Send the association back to CompanionDeviceActivity, so that it can report
365             // back to the app via Activity.setResult().
366             if (resultReceiver != null) {
367                 final Bundle data = new Bundle();
368                 resultReceiver.send(RESULT_INTERNAL_ERROR, data);
369             }
370         }
371     }
372 
willAddRoleHolder(@onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId)373     private boolean willAddRoleHolder(@NonNull AssociationRequest request,
374             @NonNull String packageName, @UserIdInt int userId) {
375         final String deviceProfile = request.getDeviceProfile();
376         if (deviceProfile == null) return false;
377 
378         final boolean isRoleHolder = Binder.withCleanCallingIdentity(
379                 () -> isRoleHolder(mContext, userId, packageName, deviceProfile));
380 
381         // Don't need to "grant" the role, if the package already holds the role.
382         return !isRoleHolder;
383     }
384 
createPendingIntent(int packageUid, Intent intent)385     private PendingIntent createPendingIntent(int packageUid, Intent intent) {
386         final PendingIntent pendingIntent;
387         final long token = Binder.clearCallingIdentity();
388 
389         // Using uid of the application that will own the association (usually the same
390         // application that sent the request) allows us to have multiple "pending" association
391         // requests at the same time.
392         // If the application already has a pending association request, that PendingIntent
393         // will be cancelled except application wants to cancel the request by the system.
394         try {
395             pendingIntent = PendingIntent.getActivityAsUser(
396                     mContext, /*requestCode */ packageUid, intent,
397                     FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
398                     /* options= */ null, UserHandle.CURRENT);
399         } finally {
400             Binder.restoreCallingIdentity(token);
401         }
402 
403         return pendingIntent;
404     }
405 
406     private final ResultReceiver mOnRequestConfirmationReceiver =
407             new ResultReceiver(Handler.getMain()) {
408         @Override
409         protected void onReceiveResult(int resultCode, Bundle data) {
410             if (DEBUG) {
411                 Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() "
412                         + "code=" + resultCode + ", " + "data=" + data);
413             }
414 
415             if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
416                 Slog.w(TAG, "Unknown result code:" + resultCode);
417                 return;
418             }
419 
420             final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST, android.companion.AssociationRequest.class);
421             final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub
422                     .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK));
423             final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER, android.os.ResultReceiver.class);
424 
425             requireNonNull(request);
426             requireNonNull(callback);
427             requireNonNull(resultReceiver);
428 
429             final MacAddress macAddress;
430             if (request.isSelfManaged()) {
431                 macAddress = null;
432             } else {
433                 macAddress = data.getParcelable(EXTRA_MAC_ADDRESS, android.net.MacAddress.class);
434                 requireNonNull(macAddress);
435             }
436 
437             processAssociationRequestApproval(request, callback, resultReceiver, macAddress);
438         }
439     };
440 
mayAssociateWithoutPrompt(@onNull String packageName, @UserIdInt int userId)441     private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
442         // Throttle frequent associations
443         final long now = System.currentTimeMillis();
444         final List<AssociationInfo> associationForPackage =
445                 mAssociationStore.getAssociationsForPackage(userId, packageName);
446         // Number of "recent" associations.
447         int recent = 0;
448         for (AssociationInfo association : associationForPackage) {
449             final boolean isRecent =
450                     now - association.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS;
451             if (isRecent) {
452                 if (++recent >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
453                     Slog.w(TAG, "Too many associations: " + packageName + " already "
454                             + "associated " + recent + " devices within the last "
455                             + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS + "ms");
456                     return false;
457                 }
458             }
459         }
460 
461         return PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName);
462     }
463 }
464