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