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