1 /* 2 * Copyright (C) 2017 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.companiondevicemanager; 18 19 import static android.companion.CompanionDeviceManager.REASON_CANCELED; 20 import static android.companion.CompanionDeviceManager.REASON_DISCOVERY_TIMEOUT; 21 import static android.companion.CompanionDeviceManager.REASON_INTERNAL_ERROR; 22 import static android.companion.CompanionDeviceManager.REASON_USER_REJECTED; 23 import static android.companion.CompanionDeviceManager.RESULT_DISCOVERY_TIMEOUT; 24 import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR; 25 import static android.companion.CompanionDeviceManager.RESULT_USER_REJECTED; 26 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 27 28 import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState; 29 import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT; 30 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICONS; 31 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_NAMES; 32 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_PERMISSIONS; 33 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_SUMMARIES; 34 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_TITLES; 35 import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES; 36 import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_SELF_MANAGED_PROFILES; 37 import static com.android.companiondevicemanager.Utils.getApplicationLabel; 38 import static com.android.companiondevicemanager.Utils.getHtmlFromResources; 39 import static com.android.companiondevicemanager.Utils.getIcon; 40 import static com.android.companiondevicemanager.Utils.getImageColor; 41 import static com.android.companiondevicemanager.Utils.getVendorHeaderIcon; 42 import static com.android.companiondevicemanager.Utils.getVendorHeaderName; 43 import static com.android.companiondevicemanager.Utils.hasVendorIcon; 44 import static com.android.companiondevicemanager.Utils.prepareResultReceiverForIpc; 45 46 import static java.util.Objects.requireNonNull; 47 48 import android.annotation.NonNull; 49 import android.annotation.Nullable; 50 import android.annotation.SuppressLint; 51 import android.companion.AssociatedDevice; 52 import android.companion.AssociationInfo; 53 import android.companion.AssociationRequest; 54 import android.companion.CompanionDeviceManager; 55 import android.companion.IAssociationRequestCallback; 56 import android.content.Intent; 57 import android.content.pm.PackageManager; 58 import android.graphics.BlendMode; 59 import android.graphics.BlendModeColorFilter; 60 import android.graphics.Color; 61 import android.graphics.drawable.Drawable; 62 import android.net.MacAddress; 63 import android.os.Bundle; 64 import android.os.Handler; 65 import android.os.RemoteException; 66 import android.os.ResultReceiver; 67 import android.text.Spanned; 68 import android.util.Slog; 69 import android.view.View; 70 import android.view.ViewTreeObserver; 71 import android.view.accessibility.AccessibilityNodeInfo; 72 import android.widget.Button; 73 import android.widget.ImageButton; 74 import android.widget.ImageView; 75 import android.widget.LinearLayout; 76 import android.widget.ProgressBar; 77 import android.widget.RelativeLayout; 78 import android.widget.TextView; 79 80 import androidx.constraintlayout.widget.ConstraintLayout; 81 import androidx.fragment.app.FragmentActivity; 82 import androidx.fragment.app.FragmentManager; 83 import androidx.recyclerview.widget.LinearLayoutManager; 84 import androidx.recyclerview.widget.RecyclerView; 85 86 import java.util.ArrayList; 87 import java.util.List; 88 89 /** 90 * A CompanionDevice activity response for showing the available 91 * nearby devices to be associated with. 92 */ 93 @SuppressLint("LongLogTag") 94 public class CompanionAssociationActivity extends FragmentActivity implements 95 CompanionVendorHelperDialogFragment.CompanionVendorHelperDialogListener { 96 private static final String TAG = "CDM_CompanionDeviceActivity"; 97 98 // Keep the following constants in sync with 99 // frameworks/base/services/companion/java/ 100 // com/android/server/companion/AssociationRequestsProcessor.java 101 102 // AssociationRequestsProcessor <-> UI 103 private static final String EXTRA_APPLICATION_CALLBACK = "application_callback"; 104 private static final String EXTRA_ASSOCIATION_REQUEST = "association_request"; 105 private static final String EXTRA_RESULT_RECEIVER = "result_receiver"; 106 private static final String EXTRA_FORCE_CANCEL_CONFIRMATION = "cancel_confirmation"; 107 108 private static final String FRAGMENT_DIALOG_TAG = "fragment_dialog"; 109 110 // AssociationRequestsProcessor -> UI 111 private static final int RESULT_CODE_ASSOCIATION_CREATED = 0; 112 private static final String EXTRA_ASSOCIATION = "association"; 113 114 // UI -> AssociationRequestsProcessor 115 private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0; 116 private static final String EXTRA_MAC_ADDRESS = "mac_address"; 117 118 private AssociationRequest mRequest; 119 private IAssociationRequestCallback mAppCallback; 120 private ResultReceiver mCdmServiceReceiver; 121 122 // Present for application's name. 123 private CharSequence mAppLabel; 124 125 // Always present widgets. 126 private TextView mTitle; 127 private TextView mSummary; 128 129 // Present for single device and multiple device only. 130 private ImageView mProfileIcon; 131 132 // Only present for selfManaged devices. 133 private ImageView mVendorHeaderImage; 134 private TextView mVendorHeaderName; 135 private ImageButton mVendorHeaderButton; 136 137 // Progress indicator is only shown while we are looking for the first suitable device for a 138 // multiple device association. 139 private ProgressBar mMultipleDeviceSpinner; 140 // Progress indicator is only shown while we are looking for the first suitable device for a 141 // single device association. 142 private ProgressBar mSingleDeviceSpinner; 143 144 // Present for self-managed association requests and "single-device" regular association 145 // regular. 146 private Button mButtonAllow; 147 private Button mButtonNotAllow; 148 // Present for multiple devices' association requests only. 149 private Button mButtonNotAllowMultipleDevices; 150 151 // Present for top and bottom borders for permissions list and device list. 152 private View mBorderTop; 153 private View mBorderBottom; 154 155 private LinearLayout mAssociationConfirmationDialog; 156 // Contains device list, permission list and top/bottom borders. 157 private ConstraintLayout mConstraintList; 158 // Only present for self-managed association requests. 159 private RelativeLayout mVendorHeader; 160 // A linearLayout for mButtonNotAllowMultipleDevices, user will press this layout instead 161 // of the button for accessibility. 162 private LinearLayout mNotAllowMultipleDevicesLayout; 163 164 // The recycler view is only shown for multiple-device regular association request, after 165 // at least one matching device is found. 166 private @Nullable RecyclerView mDeviceListRecyclerView; 167 private @Nullable DeviceListAdapter mDeviceAdapter; 168 169 // The recycler view is shown for non-null profile association request. 170 private @Nullable RecyclerView mPermissionListRecyclerView; 171 private @Nullable PermissionListAdapter mPermissionListAdapter; 172 173 // The flag used to prevent double taps, that may lead to sending several requests for creating 174 // an association to CDM. 175 private boolean mApproved; 176 private boolean mCancelled; 177 // A reference to the device selected by the user, to be sent back to the application via 178 // onActivityResult() after the association is created. 179 private @Nullable DeviceFilterPair<?> mSelectedDevice; 180 181 private final LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this); 182 183 @Override onCreate(Bundle savedInstanceState)184 public void onCreate(Bundle savedInstanceState) { 185 boolean forceCancelDialog = getIntent().getBooleanExtra(EXTRA_FORCE_CANCEL_CONFIRMATION, 186 false); 187 // Must handle the force cancel request in onNewIntent. 188 if (forceCancelDialog) { 189 Slog.i(TAG, "The confirmation does not exist, skipping the cancel request"); 190 finish(); 191 } 192 193 super.onCreate(savedInstanceState); 194 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 195 } 196 197 @Override onStart()198 protected void onStart() { 199 super.onStart(); 200 201 final Intent intent = getIntent(); 202 mRequest = intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST, AssociationRequest.class); 203 mAppCallback = IAssociationRequestCallback.Stub.asInterface( 204 intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); 205 mCdmServiceReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER, 206 ResultReceiver.class); 207 208 requireNonNull(mRequest); 209 requireNonNull(mAppCallback); 210 requireNonNull(mCdmServiceReceiver); 211 212 // Start discovery services if needed. 213 if (!mRequest.isSelfManaged()) { 214 boolean started = CompanionDeviceDiscoveryService.startForRequest(this, mRequest); 215 if (!started) { 216 return; 217 } 218 // TODO(b/217749191): Create the ViewModel for the LiveData 219 CompanionDeviceDiscoveryService.getDiscoveryState().observe( 220 /* LifeCycleOwner */ this, this::onDiscoveryStateChanged); 221 } 222 // Init UI. 223 initUI(); 224 } 225 226 @Override onNewIntent(@onNull Intent intent)227 protected void onNewIntent(@NonNull Intent intent) { 228 super.onNewIntent(intent); 229 230 // Force cancels the CDM dialog if this activity receives another intent with 231 // EXTRA_FORCE_CANCEL_CONFIRMATION. 232 boolean forCancelDialog = intent.getBooleanExtra(EXTRA_FORCE_CANCEL_CONFIRMATION, false); 233 if (forCancelDialog) { 234 Slog.i(TAG, "Cancelling the user confirmation"); 235 cancel(/* discoveryTimeOut */ false, /* userRejected */ false, 236 /* internalError */ false); 237 return; 238 } 239 240 // Handle another incoming request (while we are not done with the original - mRequest - 241 // yet). We can only "process" one request at a time. 242 final IAssociationRequestCallback appCallback = IAssociationRequestCallback.Stub 243 .asInterface(intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); 244 try { 245 requireNonNull(appCallback).onFailure("Busy."); 246 } catch (RemoteException ignore) { 247 } 248 } 249 250 @Override onStop()251 protected void onStop() { 252 super.onStop(); 253 254 // TODO: handle config changes without cancelling. 255 if (!isDone()) { 256 cancel(/* discoveryTimeOut */ false, 257 /* userRejected */ false, /* internalError */ false); // will finish() 258 } 259 } 260 initUI()261 private void initUI() { 262 Slog.d(TAG, "initUI(), request=" + mRequest); 263 264 final String packageName = mRequest.getPackageName(); 265 final int userId = mRequest.getUserId(); 266 final CharSequence appLabel; 267 268 try { 269 appLabel = getApplicationLabel(this, packageName, userId); 270 } catch (PackageManager.NameNotFoundException e) { 271 Slog.w(TAG, "Package u" + userId + "/" + packageName + " not found."); 272 273 CompanionDeviceDiscoveryService.stop(this); 274 setResultAndFinish(null, RESULT_INTERNAL_ERROR); 275 return; 276 } 277 278 setContentView(R.layout.activity_confirmation); 279 280 mAppLabel = appLabel; 281 282 mConstraintList = findViewById(R.id.constraint_list); 283 mAssociationConfirmationDialog = findViewById(R.id.association_confirmation); 284 mVendorHeader = findViewById(R.id.vendor_header); 285 286 mBorderTop = findViewById(R.id.border_top); 287 mBorderBottom = findViewById(R.id.border_bottom); 288 289 mTitle = findViewById(R.id.title); 290 mSummary = findViewById(R.id.summary); 291 292 mProfileIcon = findViewById(R.id.profile_icon); 293 294 mVendorHeaderImage = findViewById(R.id.vendor_header_image); 295 mVendorHeaderName = findViewById(R.id.vendor_header_name); 296 mVendorHeaderButton = findViewById(R.id.vendor_header_button); 297 298 mDeviceListRecyclerView = findViewById(R.id.device_list); 299 300 mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device); 301 mSingleDeviceSpinner = findViewById(R.id.spinner_single_device); 302 303 mPermissionListRecyclerView = findViewById(R.id.permission_list); 304 mPermissionListAdapter = new PermissionListAdapter(this); 305 306 mButtonAllow = findViewById(R.id.btn_positive); 307 mButtonNotAllow = findViewById(R.id.btn_negative); 308 mButtonNotAllowMultipleDevices = findViewById(R.id.btn_negative_multiple_devices); 309 mNotAllowMultipleDevicesLayout = findViewById(R.id.negative_multiple_devices_layout); 310 311 mButtonAllow.setOnClickListener(this::onPositiveButtonClick); 312 mButtonNotAllow.setOnClickListener(this::onNegativeButtonClick); 313 mNotAllowMultipleDevicesLayout.setOnClickListener(this::onNegativeButtonClick); 314 315 mVendorHeaderButton.setOnClickListener(this::onShowHelperDialog); 316 317 if (mRequest.isSelfManaged()) { 318 initUiForSelfManagedAssociation(); 319 } else if (mRequest.isSingleDevice()) { 320 initUiForSingleDevice(); 321 } else { 322 initUiForMultipleDevices(); 323 } 324 } 325 onDiscoveryStateChanged(DiscoveryState newState)326 private void onDiscoveryStateChanged(DiscoveryState newState) { 327 if (newState == FINISHED_TIMEOUT 328 && CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) { 329 cancel(/* discoveryTimeOut */ true, 330 /* userRejected */ false, /* internalError */ false); 331 } 332 } 333 onUserSelectedDevice(@onNull DeviceFilterPair<?> selectedDevice)334 private void onUserSelectedDevice(@NonNull DeviceFilterPair<?> selectedDevice) { 335 final MacAddress macAddress = selectedDevice.getMacAddress(); 336 mRequest.setDisplayName(selectedDevice.getDisplayName()); 337 mRequest.setAssociatedDevice(new AssociatedDevice(selectedDevice.getDevice())); 338 onAssociationApproved(macAddress); 339 } 340 onAssociationApproved(@ullable MacAddress macAddress)341 private void onAssociationApproved(@Nullable MacAddress macAddress) { 342 if (isDone()) { 343 Slog.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled")); 344 return; 345 } 346 mApproved = true; 347 348 Slog.i(TAG, "onAssociationApproved() macAddress=" + macAddress); 349 350 if (!mRequest.isSelfManaged()) { 351 requireNonNull(macAddress); 352 CompanionDeviceDiscoveryService.stop(this); 353 } 354 355 final Bundle data = new Bundle(); 356 data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest); 357 data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder()); 358 if (macAddress != null) { 359 data.putParcelable(EXTRA_MAC_ADDRESS, macAddress); 360 } 361 362 data.putParcelable(EXTRA_RESULT_RECEIVER, 363 prepareResultReceiverForIpc(mOnAssociationCreatedReceiver)); 364 365 mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data); 366 } 367 cancel(boolean discoveryTimeout, boolean userRejected, boolean internalError)368 private void cancel(boolean discoveryTimeout, boolean userRejected, boolean internalError) { 369 if (isDone()) { 370 Slog.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled")); 371 return; 372 } 373 mCancelled = true; 374 375 // Stop discovery service if it was used. 376 if (!mRequest.isSelfManaged() || discoveryTimeout) { 377 CompanionDeviceDiscoveryService.stop(this); 378 } 379 380 final String cancelReason; 381 final int resultCode; 382 if (userRejected) { 383 cancelReason = REASON_USER_REJECTED; 384 resultCode = RESULT_USER_REJECTED; 385 } else if (discoveryTimeout) { 386 cancelReason = REASON_DISCOVERY_TIMEOUT; 387 resultCode = RESULT_DISCOVERY_TIMEOUT; 388 } else if (internalError) { 389 cancelReason = REASON_INTERNAL_ERROR; 390 resultCode = RESULT_INTERNAL_ERROR; 391 } else { 392 cancelReason = REASON_CANCELED; 393 resultCode = CompanionDeviceManager.RESULT_CANCELED; 394 } 395 396 // First send callback to the app directly... 397 try { 398 Slog.i(TAG, "Sending onFailure to app due to reason=" + cancelReason); 399 mAppCallback.onFailure(cancelReason); 400 } catch (RemoteException ignore) { 401 } 402 403 // ... then set result and finish ("sending" onActivityResult()). 404 setResultAndFinish(null, resultCode); 405 } 406 setResultAndFinish(@ullable AssociationInfo association, int resultCode)407 private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) { 408 Slog.i(TAG, "setResultAndFinish(), association=" 409 + (association == null ? "null" : association) 410 + "resultCode=" + resultCode); 411 412 final Intent data = new Intent(); 413 if (association != null) { 414 data.putExtra(CompanionDeviceManager.EXTRA_ASSOCIATION, association); 415 if (!association.isSelfManaged()) { 416 data.putExtra(CompanionDeviceManager.EXTRA_DEVICE, mSelectedDevice.getDevice()); 417 } 418 } 419 setResult(resultCode, data); 420 421 finish(); 422 } 423 initUiForSelfManagedAssociation()424 private void initUiForSelfManagedAssociation() { 425 Slog.d(TAG, "initUiForSelfManagedAssociation()"); 426 427 final CharSequence deviceName = mRequest.getDisplayName(); 428 final String deviceProfile = mRequest.getDeviceProfile(); 429 final String packageName = mRequest.getPackageName(); 430 final int userId = mRequest.getUserId(); 431 final Drawable vendorIcon; 432 final CharSequence vendorName; 433 final Spanned title; 434 435 if (!SUPPORTED_SELF_MANAGED_PROFILES.contains(deviceProfile)) { 436 throw new RuntimeException("Unsupported profile " + deviceProfile); 437 } 438 439 try { 440 vendorIcon = getVendorHeaderIcon(this, packageName, userId); 441 vendorName = getVendorHeaderName(this, packageName, userId); 442 mVendorHeaderImage.setImageDrawable(vendorIcon); 443 if (hasVendorIcon(this, packageName, userId)) { 444 int color = getImageColor(this); 445 mVendorHeaderImage.setColorFilter(getResources().getColor(color, /* Theme= */null)); 446 } 447 } catch (PackageManager.NameNotFoundException e) { 448 Slog.e(TAG, "Package u" + userId + "/" + packageName + " not found."); 449 cancel(/* discoveryTimeout */ false, 450 /* userRejected */ false, /* internalError */ true); 451 return; 452 } 453 454 title = getHtmlFromResources(this, PROFILE_TITLES.get(deviceProfile), deviceName); 455 456 if (PROFILE_SUMMARIES.containsKey(deviceProfile)) { 457 final int summaryResourceId = PROFILE_SUMMARIES.get(deviceProfile); 458 final Spanned summary = getHtmlFromResources(this, summaryResourceId, 459 deviceName); 460 mSummary.setText(summary); 461 } else { 462 mSummary.setVisibility(View.GONE); 463 } 464 465 setupPermissionList(deviceProfile); 466 467 mTitle.setText(title); 468 mVendorHeaderName.setText(vendorName); 469 mVendorHeader.setVisibility(View.VISIBLE); 470 mProfileIcon.setVisibility(View.GONE); 471 mDeviceListRecyclerView.setVisibility(View.GONE); 472 // Top and bottom borders should be gone for selfManaged dialog. 473 mBorderTop.setVisibility(View.GONE); 474 mBorderBottom.setVisibility(View.GONE); 475 } 476 initUiForSingleDevice()477 private void initUiForSingleDevice() { 478 Slog.d(TAG, "initUiForSingleDevice()"); 479 480 final String deviceProfile = mRequest.getDeviceProfile(); 481 482 if (!SUPPORTED_PROFILES.contains(deviceProfile)) { 483 throw new RuntimeException("Unsupported profile " + deviceProfile); 484 } 485 486 final Drawable profileIcon = getIcon(this, PROFILE_ICONS.get(deviceProfile)); 487 mProfileIcon.setImageDrawable(profileIcon); 488 489 CompanionDeviceDiscoveryService.getScanResult().observe(this, deviceFilterPairs -> { 490 if (deviceFilterPairs.isEmpty()) { 491 return; 492 } 493 mSelectedDevice = requireNonNull(deviceFilterPairs.get(0)); 494 updateSingleDeviceUi(); 495 }); 496 497 mSingleDeviceSpinner.setVisibility(View.VISIBLE); 498 // Hide permission list and confirmation dialog first before the 499 // first matched device is found. 500 mPermissionListRecyclerView.setVisibility(View.GONE); 501 mDeviceListRecyclerView.setVisibility(View.GONE); 502 mAssociationConfirmationDialog.setVisibility(View.GONE); 503 } 504 initUiForMultipleDevices()505 private void initUiForMultipleDevices() { 506 Slog.d(TAG, "initUiForMultipleDevices()"); 507 508 final Drawable profileIcon; 509 final Spanned title; 510 final String deviceProfile = mRequest.getDeviceProfile(); 511 512 if (!SUPPORTED_PROFILES.contains(deviceProfile)) { 513 throw new RuntimeException("Unsupported profile " + deviceProfile); 514 } 515 516 profileIcon = getIcon(this, PROFILE_ICONS.get(deviceProfile)); 517 518 if (deviceProfile == null) { 519 title = getHtmlFromResources(this, R.string.chooser_title_non_profile, mAppLabel); 520 mButtonNotAllowMultipleDevices.setText(R.string.consent_no); 521 } else { 522 title = getHtmlFromResources(this, 523 R.string.chooser_title, getString(PROFILE_NAMES.get(deviceProfile))); 524 } 525 526 mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked); 527 528 mTitle.setText(title); 529 mProfileIcon.setImageDrawable(profileIcon); 530 531 mDeviceListRecyclerView.setAdapter(mDeviceAdapter); 532 mDeviceListRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 533 534 CompanionDeviceDiscoveryService.getScanResult().observe(this, 535 deviceFilterPairs -> { 536 // Dismiss the progress bar once there's one device found for multiple devices. 537 if (deviceFilterPairs.size() >= 1) { 538 mMultipleDeviceSpinner.setVisibility(View.GONE); 539 } 540 541 mDeviceAdapter.setDevices(deviceFilterPairs); 542 }); 543 544 mSummary.setVisibility(View.GONE); 545 // "Remove" consent button: users would need to click on the list item. 546 mButtonAllow.setVisibility(View.GONE); 547 mButtonNotAllow.setVisibility(View.GONE); 548 mDeviceListRecyclerView.setVisibility(View.VISIBLE); 549 mButtonNotAllowMultipleDevices.setVisibility(View.VISIBLE); 550 mNotAllowMultipleDevicesLayout.setVisibility(View.VISIBLE); 551 mConstraintList.setVisibility(View.VISIBLE); 552 mMultipleDeviceSpinner.setVisibility(View.VISIBLE); 553 } 554 onDeviceClicked(int position)555 private void onDeviceClicked(int position) { 556 final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position); 557 // To prevent double tap on the selected device. 558 if (mSelectedDevice != null) { 559 Slog.w(TAG, "Already selected."); 560 return; 561 } 562 // Notify the adapter to highlight the selected item. 563 mDeviceAdapter.setSelectedPosition(position); 564 565 mSelectedDevice = requireNonNull(selectedDevice); 566 567 Slog.d(TAG, "onDeviceClicked(): " + mSelectedDevice.toShortString()); 568 569 updateSingleDeviceUi(); 570 571 mSummary.setVisibility(View.VISIBLE); 572 mButtonAllow.setVisibility(View.VISIBLE); 573 mButtonNotAllow.setVisibility(View.VISIBLE); 574 mDeviceListRecyclerView.setVisibility(View.GONE); 575 mNotAllowMultipleDevicesLayout.setVisibility(View.GONE); 576 } 577 updateSingleDeviceUi()578 private void updateSingleDeviceUi() { 579 // No need to show permission consent dialog if it is a isSkipPrompt(true) 580 // AssociationRequest. See AssociationRequestsProcessor#mayAssociateWithoutPrompt. 581 if (mRequest.isSkipPrompt()) { 582 Slog.d(TAG, "Skipping the permission consent dialog."); 583 onUserSelectedDevice(mSelectedDevice); 584 return; 585 } 586 587 mSingleDeviceSpinner.setVisibility(View.GONE); 588 mAssociationConfirmationDialog.setVisibility(View.VISIBLE); 589 590 final String deviceProfile = mRequest.getDeviceProfile(); 591 final int summaryResourceId = PROFILE_SUMMARIES.get(deviceProfile); 592 final String remoteDeviceName = mSelectedDevice.getDisplayName(); 593 final Spanned title = getHtmlFromResources( 594 this, PROFILE_TITLES.get(deviceProfile), mAppLabel, remoteDeviceName); 595 final Spanned summary; 596 597 if (deviceProfile == null && mRequest.isSingleDevice()) { 598 summary = getHtmlFromResources(this, summaryResourceId, remoteDeviceName); 599 mConstraintList.setVisibility(View.GONE); 600 } else if (deviceProfile == null) { 601 onUserSelectedDevice(mSelectedDevice); 602 return; 603 } else { 604 summary = getHtmlFromResources( 605 this, summaryResourceId, getString(R.string.device_type)); 606 setupPermissionList(deviceProfile); 607 } 608 609 mTitle.setText(title); 610 mSummary.setText(summary); 611 } 612 onPositiveButtonClick(View v)613 private void onPositiveButtonClick(View v) { 614 Slog.d(TAG, "onPositiveButtonClick()"); 615 616 // Disable the button, to prevent more clicks. 617 v.setEnabled(false); 618 619 if (mRequest.isSelfManaged()) { 620 onAssociationApproved(null); 621 } else { 622 onUserSelectedDevice(mSelectedDevice); 623 } 624 } 625 onNegativeButtonClick(View v)626 private void onNegativeButtonClick(View v) { 627 Slog.d(TAG, "onNegativeButtonClick()"); 628 629 // Disable the button, to prevent more clicks. 630 v.setEnabled(false); 631 632 cancel(/* discoveryTimeout */ false, /* userRejected */ true, /* internalError */ false); 633 } 634 onShowHelperDialog(View view)635 private void onShowHelperDialog(View view) { 636 FragmentManager fragmentManager = getSupportFragmentManager(); 637 CompanionVendorHelperDialogFragment fragmentDialog = 638 CompanionVendorHelperDialogFragment.newInstance(mRequest); 639 640 mAssociationConfirmationDialog.setVisibility(View.INVISIBLE); 641 642 fragmentDialog.show(fragmentManager, /* Tag */ FRAGMENT_DIALOG_TAG); 643 } 644 isDone()645 private boolean isDone() { 646 return mApproved || mCancelled; 647 } 648 649 // Set up the mPermissionListRecyclerView, including set up the adapter, 650 // initiate the layoutManager for the recyclerview, add listeners for monitoring the scrolling 651 // and when mPermissionListRecyclerView is fully populated. 652 // Lastly, disable the Allow and Don't allow buttons. setupPermissionList(String deviceProfile)653 private void setupPermissionList(String deviceProfile) { 654 final List<Integer> permissionTypes = new ArrayList<>( 655 PROFILE_PERMISSIONS.get(deviceProfile)); 656 if (permissionTypes.isEmpty()) { 657 // Nothing to do if there are no permission types. 658 return; 659 } 660 661 mPermissionListAdapter.setPermissionType(permissionTypes); 662 mPermissionListRecyclerView.setAdapter(mPermissionListAdapter); 663 mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager); 664 665 disableButtons(); 666 667 LinearLayoutManager permissionListLayoutManager = 668 (LinearLayoutManager) mPermissionListRecyclerView 669 .getLayoutManager(); 670 671 // Enable buttons once users scroll down to the bottom of the permission list. 672 mPermissionListRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 673 @Override 674 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 675 enableAllowButtonIfNeeded(permissionListLayoutManager); 676 } 677 }); 678 // Enable buttons if last item in the permission list is visible to the users when 679 // mPermissionListRecyclerView is fully populated. 680 mPermissionListRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener( 681 new ViewTreeObserver.OnGlobalLayoutListener() { 682 @Override 683 public void onGlobalLayout() { 684 enableAllowButtonIfNeeded(permissionListLayoutManager); 685 mPermissionListRecyclerView.getViewTreeObserver() 686 .removeOnGlobalLayoutListener(this); 687 } 688 }); 689 690 // Set accessibility for the recyclerView that to be able scroll up/down for voice access. 691 mPermissionListRecyclerView.setAccessibilityDelegate(new View.AccessibilityDelegate() { 692 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 693 super.onInitializeAccessibilityNodeInfo(host, info); 694 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN); 695 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP); 696 } 697 }); 698 699 mConstraintList.setVisibility(View.VISIBLE); 700 mPermissionListRecyclerView.setVisibility(View.VISIBLE); 701 } 702 703 // Enable the Allow button if the last element in the PermissionListRecyclerView is reached. enableAllowButtonIfNeeded(LinearLayoutManager layoutManager)704 private void enableAllowButtonIfNeeded(LinearLayoutManager layoutManager) { 705 int lastVisibleItemPosition = 706 layoutManager.findLastCompletelyVisibleItemPosition(); 707 int numItems = mPermissionListRecyclerView.getAdapter().getItemCount(); 708 709 if (lastVisibleItemPosition >= numItems - 1) { 710 enableButtons(); 711 } 712 } 713 714 // Disable and grey out the Allow and Don't allow buttons if the last permission in the 715 // permission list is not visible to the users. disableButtons()716 private void disableButtons() { 717 mButtonAllow.setEnabled(false); 718 mButtonNotAllow.setEnabled(false); 719 mButtonAllow.setTextColor( 720 getResources().getColor(android.R.color.system_neutral1_400, null)); 721 mButtonNotAllow.setTextColor( 722 getResources().getColor(android.R.color.system_neutral1_400, null)); 723 mButtonAllow.getBackground().setColorFilter( 724 (new BlendModeColorFilter(Color.LTGRAY, BlendMode.DARKEN))); 725 mButtonNotAllow.getBackground().setColorFilter( 726 (new BlendModeColorFilter(Color.LTGRAY, BlendMode.DARKEN))); 727 } 728 // Enable and restore the color for the Allow and Don't allow buttons if the last permission in 729 // the permission list is visible to the users. enableButtons()730 private void enableButtons() { 731 mButtonAllow.setEnabled(true); 732 mButtonNotAllow.setEnabled(true); 733 mButtonAllow.getBackground().setColorFilter(null); 734 mButtonNotAllow.getBackground().setColorFilter(null); 735 mButtonAllow.setTextColor( 736 getResources().getColor(android.R.color.system_neutral1_900, null)); 737 mButtonNotAllow.setTextColor( 738 getResources().getColor(android.R.color.system_neutral1_900, null)); 739 } 740 741 private final ResultReceiver mOnAssociationCreatedReceiver = 742 new ResultReceiver(Handler.getMain()) { 743 @Override 744 protected void onReceiveResult(int resultCode, Bundle data) { 745 if (resultCode == RESULT_CODE_ASSOCIATION_CREATED) { 746 final AssociationInfo association = data.getParcelable( 747 EXTRA_ASSOCIATION, AssociationInfo.class); 748 requireNonNull(association); 749 setResultAndFinish(association, CompanionDeviceManager.RESULT_OK); 750 } else { 751 setResultAndFinish(null, resultCode); 752 } 753 } 754 }; 755 756 @Override onShowHelperDialogFailed()757 public void onShowHelperDialogFailed() { 758 cancel(/* discoveryTimeout */ false, /* userRejected */ false, /* internalError */ true); 759 } 760 761 @Override onHelperDialogDismissed()762 public void onHelperDialogDismissed() { 763 mAssociationConfirmationDialog.setVisibility(View.VISIBLE); 764 } 765 } 766