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.PERMISSION_TYPES; 31 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME; 32 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICON; 33 import static com.android.companiondevicemanager.CompanionDeviceResources.SUMMARIES; 34 import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES; 35 import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_SELF_MANAGED_PROFILES; 36 import static com.android.companiondevicemanager.CompanionDeviceResources.TITLES; 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.Log; 69 import android.view.View; 70 import android.view.ViewTreeObserver; 71 import android.widget.Button; 72 import android.widget.ImageButton; 73 import android.widget.ImageView; 74 import android.widget.LinearLayout; 75 import android.widget.ProgressBar; 76 import android.widget.RelativeLayout; 77 import android.widget.TextView; 78 79 import androidx.constraintlayout.widget.ConstraintLayout; 80 import androidx.fragment.app.FragmentActivity; 81 import androidx.fragment.app.FragmentManager; 82 import androidx.recyclerview.widget.LinearLayoutManager; 83 import androidx.recyclerview.widget.RecyclerView; 84 85 import java.util.ArrayList; 86 import java.util.List; 87 88 /** 89 * A CompanionDevice activity response for showing the available 90 * nearby devices to be associated with. 91 */ 92 @SuppressLint("LongLogTag") 93 public class CompanionDeviceActivity extends FragmentActivity implements 94 CompanionVendorHelperDialogFragment.CompanionVendorHelperDialogListener { 95 private static final boolean DEBUG = false; 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 LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this); 182 183 @Override onCreate(Bundle savedInstanceState)184 public void onCreate(Bundle savedInstanceState) { 185 if (DEBUG) Log.d(TAG, "onCreate()"); 186 boolean forceCancelDialog = getIntent().getBooleanExtra("cancel_confirmation", false); 187 // Must handle the force cancel request in onNewIntent. 188 if (forceCancelDialog) { 189 Log.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 if (DEBUG) Log.d(TAG, "onStart()"); 201 202 final Intent intent = getIntent(); 203 mRequest = intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST); 204 mAppCallback = IAssociationRequestCallback.Stub.asInterface( 205 intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); 206 mCdmServiceReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); 207 208 requireNonNull(mRequest); 209 requireNonNull(mAppCallback); 210 requireNonNull(mCdmServiceReceiver); 211 212 // Start discovery services if needed. 213 if (!mRequest.isSelfManaged()) { 214 CompanionDeviceDiscoveryService.startForRequest(this, mRequest); 215 // TODO(b/217749191): Create the ViewModel for the LiveData 216 CompanionDeviceDiscoveryService.getDiscoveryState().observe( 217 /* LifeCycleOwner */ this, this::onDiscoveryStateChanged); 218 } 219 // Init UI. 220 initUI(); 221 } 222 223 @SuppressWarnings("MissingSuperCall") // TODO: Fix me 224 @Override onNewIntent(Intent intent)225 protected void onNewIntent(Intent intent) { 226 // Force cancels the CDM dialog if this activity receives another intent with 227 // EXTRA_FORCE_CANCEL_CONFIRMATION. 228 boolean forCancelDialog = intent.getBooleanExtra(EXTRA_FORCE_CANCEL_CONFIRMATION, false); 229 230 if (forCancelDialog) { 231 Log.i(TAG, "Cancelling the user confirmation"); 232 233 cancel(/* discoveryTimeOut */ false, 234 /* userRejected */ false, /* internalError */ false); 235 return; 236 } 237 238 // Handle another incoming request (while we are not done with the original - mRequest - 239 // yet). 240 final AssociationRequest request = requireNonNull( 241 intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST)); 242 243 if (DEBUG) Log.d(TAG, "onNewIntent(), request=" + request); 244 245 // We can only "process" one request at a time. 246 final IAssociationRequestCallback appCallback = IAssociationRequestCallback.Stub 247 .asInterface(intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); 248 try { 249 requireNonNull(appCallback).onFailure("Busy."); 250 } catch (RemoteException ignore) { 251 } 252 } 253 254 @Override onStop()255 protected void onStop() { 256 super.onStop(); 257 if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing()); 258 259 // TODO: handle config changes without cancelling. 260 if (!isDone()) { 261 cancel(/* discoveryTimeOut */ false, 262 /* userRejected */ false, /* internalError */ false); // will finish() 263 } 264 } 265 266 @Override onDestroy()267 protected void onDestroy() { 268 super.onDestroy(); 269 if (DEBUG) Log.d(TAG, "onDestroy()"); 270 } 271 272 @Override onBackPressed()273 public void onBackPressed() { 274 if (DEBUG) Log.d(TAG, "onBackPressed()"); 275 super.onBackPressed(); 276 } 277 278 @Override finish()279 public void finish() { 280 if (DEBUG) Log.d(TAG, "finish()", new Exception("Stack Trace Dump")); 281 super.finish(); 282 } 283 initUI()284 private void initUI() { 285 if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest); 286 287 final String packageName = mRequest.getPackageName(); 288 final int userId = mRequest.getUserId(); 289 final CharSequence appLabel; 290 291 try { 292 appLabel = getApplicationLabel(this, packageName, userId); 293 } catch (PackageManager.NameNotFoundException e) { 294 Log.w(TAG, "Package u" + userId + "/" + packageName + " not found."); 295 296 CompanionDeviceDiscoveryService.stop(this); 297 setResultAndFinish(null, RESULT_INTERNAL_ERROR); 298 return; 299 } 300 301 setContentView(R.layout.activity_confirmation); 302 303 mAppLabel = appLabel; 304 305 mConstraintList = findViewById(R.id.constraint_list); 306 mAssociationConfirmationDialog = findViewById(R.id.association_confirmation); 307 mVendorHeader = findViewById(R.id.vendor_header); 308 309 mBorderTop = findViewById(R.id.border_top); 310 mBorderBottom = findViewById(R.id.border_bottom); 311 312 mTitle = findViewById(R.id.title); 313 mSummary = findViewById(R.id.summary); 314 315 mProfileIcon = findViewById(R.id.profile_icon); 316 317 mVendorHeaderImage = findViewById(R.id.vendor_header_image); 318 mVendorHeaderName = findViewById(R.id.vendor_header_name); 319 mVendorHeaderButton = findViewById(R.id.vendor_header_button); 320 321 mDeviceListRecyclerView = findViewById(R.id.device_list); 322 323 mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device); 324 mSingleDeviceSpinner = findViewById(R.id.spinner_single_device); 325 326 mPermissionListRecyclerView = findViewById(R.id.permission_list); 327 mPermissionListAdapter = new PermissionListAdapter(this); 328 329 mButtonAllow = findViewById(R.id.btn_positive); 330 mButtonNotAllow = findViewById(R.id.btn_negative); 331 mButtonNotAllowMultipleDevices = findViewById(R.id.btn_negative_multiple_devices); 332 mNotAllowMultipleDevicesLayout = findViewById(R.id.negative_multiple_devices_layout); 333 334 mButtonAllow.setOnClickListener(this::onPositiveButtonClick); 335 mButtonNotAllow.setOnClickListener(this::onNegativeButtonClick); 336 mNotAllowMultipleDevicesLayout.setOnClickListener(this::onNegativeButtonClick); 337 338 mVendorHeaderButton.setOnClickListener(this::onShowHelperDialog); 339 340 if (mRequest.isSelfManaged()) { 341 initUiForSelfManagedAssociation(); 342 } else if (mRequest.isSingleDevice()) { 343 initUiForSingleDevice(appLabel); 344 } else { 345 initUiForMultipleDevices(appLabel); 346 } 347 } 348 onDiscoveryStateChanged(DiscoveryState newState)349 private void onDiscoveryStateChanged(DiscoveryState newState) { 350 if (newState == FINISHED_TIMEOUT 351 && CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) { 352 cancel(/* discoveryTimeOut */ true, 353 /* userRejected */ false, /* internalError */ false); 354 } 355 } 356 onUserSelectedDevice(@onNull DeviceFilterPair<?> selectedDevice)357 private void onUserSelectedDevice(@NonNull DeviceFilterPair<?> selectedDevice) { 358 final MacAddress macAddress = selectedDevice.getMacAddress(); 359 mRequest.setDisplayName(selectedDevice.getDisplayName()); 360 mRequest.setAssociatedDevice(new AssociatedDevice(selectedDevice.getDevice())); 361 onAssociationApproved(macAddress); 362 } 363 onAssociationApproved(@ullable MacAddress macAddress)364 private void onAssociationApproved(@Nullable MacAddress macAddress) { 365 if (isDone()) { 366 if (DEBUG) Log.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled")); 367 return; 368 } 369 mApproved = true; 370 371 if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress); 372 373 if (!mRequest.isSelfManaged()) { 374 requireNonNull(macAddress); 375 CompanionDeviceDiscoveryService.stop(this); 376 } 377 378 final Bundle data = new Bundle(); 379 data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest); 380 data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder()); 381 if (macAddress != null) { 382 data.putParcelable(EXTRA_MAC_ADDRESS, macAddress); 383 } 384 385 data.putParcelable(EXTRA_RESULT_RECEIVER, 386 prepareResultReceiverForIpc(mOnAssociationCreatedReceiver)); 387 388 mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data); 389 } 390 cancel(boolean discoveryTimeout, boolean userRejected, boolean internalError)391 private void cancel(boolean discoveryTimeout, boolean userRejected, boolean internalError) { 392 if (DEBUG) { 393 Log.i(TAG, "cancel(), discoveryTimeout=" 394 + discoveryTimeout 395 + ", userRejected=" 396 + userRejected 397 + ", internalError=" 398 + internalError, new Exception("Stack Trace Dump")); 399 } 400 401 if (isDone()) { 402 if (DEBUG) Log.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled")); 403 return; 404 } 405 mCancelled = true; 406 407 // Stop discovery service if it was used. 408 if (!mRequest.isSelfManaged() || discoveryTimeout) { 409 CompanionDeviceDiscoveryService.stop(this); 410 } 411 412 final String cancelReason; 413 final int resultCode; 414 if (userRejected) { 415 cancelReason = REASON_USER_REJECTED; 416 resultCode = RESULT_USER_REJECTED; 417 } else if (discoveryTimeout) { 418 cancelReason = REASON_DISCOVERY_TIMEOUT; 419 resultCode = RESULT_DISCOVERY_TIMEOUT; 420 } else if (internalError) { 421 cancelReason = REASON_INTERNAL_ERROR; 422 resultCode = RESULT_INTERNAL_ERROR; 423 } else { 424 cancelReason = REASON_CANCELED; 425 resultCode = CompanionDeviceManager.RESULT_CANCELED; 426 } 427 428 // First send callback to the app directly... 429 try { 430 mAppCallback.onFailure(cancelReason); 431 } catch (RemoteException ignore) { 432 } 433 434 // ... then set result and finish ("sending" onActivityResult()). 435 setResultAndFinish(null, resultCode); 436 } 437 setResultAndFinish(@ullable AssociationInfo association, int resultCode)438 private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) { 439 Log.i(TAG, "setResultAndFinish(), association=" 440 + (association == null ? "null" : association) 441 + "resultCode=" + resultCode); 442 443 final Intent data = new Intent(); 444 if (association != null) { 445 data.putExtra(CompanionDeviceManager.EXTRA_ASSOCIATION, association); 446 if (!association.isSelfManaged()) { 447 data.putExtra(CompanionDeviceManager.EXTRA_DEVICE, mSelectedDevice.getDevice()); 448 } 449 } 450 setResult(resultCode, data); 451 452 finish(); 453 } 454 initUiForSelfManagedAssociation()455 private void initUiForSelfManagedAssociation() { 456 if (DEBUG) Log.i(TAG, "initUiFor_SelfManaged_Association()"); 457 458 final CharSequence deviceName = mRequest.getDisplayName(); 459 final String deviceProfile = mRequest.getDeviceProfile(); 460 final String packageName = mRequest.getPackageName(); 461 final int userId = mRequest.getUserId(); 462 final Drawable vendorIcon; 463 final CharSequence vendorName; 464 final Spanned title; 465 466 if (!SUPPORTED_SELF_MANAGED_PROFILES.contains(deviceProfile)) { 467 throw new RuntimeException("Unsupported profile " + deviceProfile); 468 } 469 470 try { 471 vendorIcon = getVendorHeaderIcon(this, packageName, userId); 472 vendorName = getVendorHeaderName(this, packageName, userId); 473 mVendorHeaderImage.setImageDrawable(vendorIcon); 474 if (hasVendorIcon(this, packageName, userId)) { 475 int color = getImageColor(this); 476 mVendorHeaderImage.setColorFilter(getResources().getColor(color, /* Theme= */null)); 477 } 478 } catch (PackageManager.NameNotFoundException e) { 479 Log.e(TAG, "Package u" + userId + "/" + packageName + " not found."); 480 cancel(/* discoveryTimeout */ false, 481 /* userRejected */ false, /* internalError */ true); 482 return; 483 } 484 485 title = getHtmlFromResources(this, TITLES.get(deviceProfile), deviceName); 486 setupPermissionList(deviceProfile); 487 488 // Summary is not needed for selfManaged dialog. 489 mSummary.setVisibility(View.GONE); 490 mTitle.setText(title); 491 mVendorHeaderName.setText(vendorName); 492 mVendorHeader.setVisibility(View.VISIBLE); 493 mProfileIcon.setVisibility(View.GONE); 494 mDeviceListRecyclerView.setVisibility(View.GONE); 495 // Top and bottom borders should be gone for selfManaged dialog. 496 mBorderTop.setVisibility(View.GONE); 497 mBorderBottom.setVisibility(View.GONE); 498 } 499 initUiForSingleDevice(CharSequence appLabel)500 private void initUiForSingleDevice(CharSequence appLabel) { 501 if (DEBUG) Log.i(TAG, "initUiFor_SingleDevice()"); 502 503 final String deviceProfile = mRequest.getDeviceProfile(); 504 505 if (!SUPPORTED_PROFILES.contains(deviceProfile)) { 506 throw new RuntimeException("Unsupported profile " + deviceProfile); 507 } 508 509 CompanionDeviceDiscoveryService.getScanResult().observe(this, 510 deviceFilterPairs -> updateSingleDeviceUi( 511 deviceFilterPairs, deviceProfile, appLabel)); 512 513 mSingleDeviceSpinner.setVisibility(View.VISIBLE); 514 // Hide permission list and confirmation dialog first before the 515 // first matched device is found. 516 mPermissionListRecyclerView.setVisibility(View.GONE); 517 mDeviceListRecyclerView.setVisibility(View.GONE); 518 mAssociationConfirmationDialog.setVisibility(View.GONE); 519 } 520 updateSingleDeviceUi(List<DeviceFilterPair<?>> deviceFilterPairs, String deviceProfile, CharSequence appLabel)521 private void updateSingleDeviceUi(List<DeviceFilterPair<?>> deviceFilterPairs, 522 String deviceProfile, CharSequence appLabel) { 523 // Ignore "empty" scan reports. 524 if (deviceFilterPairs.isEmpty()) return; 525 526 mSelectedDevice = requireNonNull(deviceFilterPairs.get(0)); 527 528 final Drawable profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); 529 530 updatePermissionUi(); 531 532 mProfileIcon.setImageDrawable(profileIcon); 533 mAssociationConfirmationDialog.setVisibility(View.VISIBLE); 534 mSingleDeviceSpinner.setVisibility(View.GONE); 535 } 536 initUiForMultipleDevices(CharSequence appLabel)537 private void initUiForMultipleDevices(CharSequence appLabel) { 538 if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()"); 539 540 final Drawable profileIcon; 541 final Spanned title; 542 final String deviceProfile = mRequest.getDeviceProfile(); 543 544 if (!SUPPORTED_PROFILES.contains(deviceProfile)) { 545 throw new RuntimeException("Unsupported profile " + deviceProfile); 546 } 547 548 profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); 549 550 if (deviceProfile == null) { 551 title = getHtmlFromResources(this, R.string.chooser_title_non_profile, appLabel); 552 mButtonNotAllowMultipleDevices.setText(R.string.consent_no); 553 } else { 554 title = getHtmlFromResources(this, 555 R.string.chooser_title, getString(PROFILES_NAME.get(deviceProfile))); 556 } 557 558 mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked); 559 560 mTitle.setText(title); 561 mProfileIcon.setImageDrawable(profileIcon); 562 563 mDeviceListRecyclerView.setAdapter(mDeviceAdapter); 564 mDeviceListRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 565 566 CompanionDeviceDiscoveryService.getScanResult().observe(this, 567 deviceFilterPairs -> { 568 // Dismiss the progress bar once there's one device found for multiple devices. 569 if (deviceFilterPairs.size() >= 1) { 570 mMultipleDeviceSpinner.setVisibility(View.GONE); 571 } 572 573 mDeviceAdapter.setDevices(deviceFilterPairs); 574 }); 575 576 mSummary.setVisibility(View.GONE); 577 // "Remove" consent button: users would need to click on the list item. 578 mButtonAllow.setVisibility(View.GONE); 579 mButtonNotAllow.setVisibility(View.GONE); 580 mDeviceListRecyclerView.setVisibility(View.VISIBLE); 581 mButtonNotAllowMultipleDevices.setVisibility(View.VISIBLE); 582 mNotAllowMultipleDevicesLayout.setVisibility(View.VISIBLE); 583 mConstraintList.setVisibility(View.VISIBLE); 584 mMultipleDeviceSpinner.setVisibility(View.VISIBLE); 585 } 586 onDeviceClicked(int position)587 private void onDeviceClicked(int position) { 588 final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position); 589 // To prevent double tap on the selected device. 590 if (mSelectedDevice != null) { 591 if (DEBUG) Log.w(TAG, "Already selected."); 592 return; 593 } 594 // Notify the adapter to highlight the selected item. 595 mDeviceAdapter.setSelectedPosition(position); 596 597 mSelectedDevice = requireNonNull(selectedDevice); 598 599 Log.d(TAG, "onDeviceClicked(): " + mSelectedDevice.toShortString()); 600 601 updatePermissionUi(); 602 603 mSummary.setVisibility(View.VISIBLE); 604 mButtonAllow.setVisibility(View.VISIBLE); 605 mButtonNotAllow.setVisibility(View.VISIBLE); 606 mDeviceListRecyclerView.setVisibility(View.GONE); 607 mNotAllowMultipleDevicesLayout.setVisibility(View.GONE); 608 } 609 updatePermissionUi()610 private void updatePermissionUi() { 611 final String deviceProfile = mRequest.getDeviceProfile(); 612 final int summaryResourceId = SUMMARIES.get(deviceProfile); 613 final String remoteDeviceName = mSelectedDevice.getDisplayName(); 614 final Spanned title = getHtmlFromResources( 615 this, TITLES.get(deviceProfile), mAppLabel, remoteDeviceName); 616 final Spanned summary; 617 618 // No need to show permission consent dialog if it is a isSkipPrompt(true) 619 // AssociationRequest. See AssociationRequestsProcessor#mayAssociateWithoutPrompt. 620 if (mRequest.isSkipPrompt()) { 621 mSingleDeviceSpinner.setVisibility(View.GONE); 622 onUserSelectedDevice(mSelectedDevice); 623 return; 624 } 625 626 if (deviceProfile == null && mRequest.isSingleDevice()) { 627 summary = getHtmlFromResources(this, summaryResourceId, remoteDeviceName); 628 mConstraintList.setVisibility(View.GONE); 629 } else if (deviceProfile == null) { 630 onUserSelectedDevice(mSelectedDevice); 631 return; 632 } else { 633 summary = getHtmlFromResources( 634 this, summaryResourceId, getString(R.string.device_type)); 635 setupPermissionList(deviceProfile); 636 } 637 638 mTitle.setText(title); 639 mSummary.setText(summary); 640 } 641 onPositiveButtonClick(View v)642 private void onPositiveButtonClick(View v) { 643 if (DEBUG) Log.d(TAG, "on_Positive_ButtonClick()"); 644 645 // Disable the button, to prevent more clicks. 646 v.setEnabled(false); 647 648 if (mRequest.isSelfManaged()) { 649 onAssociationApproved(null); 650 } else { 651 onUserSelectedDevice(mSelectedDevice); 652 } 653 } 654 onNegativeButtonClick(View v)655 private void onNegativeButtonClick(View v) { 656 if (DEBUG) Log.d(TAG, "on_Negative_ButtonClick()"); 657 658 // Disable the button, to prevent more clicks. 659 v.setEnabled(false); 660 661 cancel(/* discoveryTimeout */ false, /* userRejected */ true, /* internalError */ false); 662 } 663 onShowHelperDialog(View view)664 private void onShowHelperDialog(View view) { 665 FragmentManager fragmentManager = getSupportFragmentManager(); 666 CompanionVendorHelperDialogFragment fragmentDialog = 667 CompanionVendorHelperDialogFragment.newInstance(mRequest); 668 669 mAssociationConfirmationDialog.setVisibility(View.INVISIBLE); 670 671 fragmentDialog.show(fragmentManager, /* Tag */ FRAGMENT_DIALOG_TAG); 672 } 673 isDone()674 private boolean isDone() { 675 return mApproved || mCancelled; 676 } 677 678 // Set up the mPermissionListRecyclerView, including set up the adapter, 679 // initiate the layoutManager for the recyclerview, add listeners for monitoring the scrolling 680 // and when mPermissionListRecyclerView is fully populated. 681 // Lastly, disable the Allow and Don't allow buttons. setupPermissionList(String deviceProfile)682 private void setupPermissionList(String deviceProfile) { 683 final List<Integer> permissionTypes = new ArrayList<>(PERMISSION_TYPES.get(deviceProfile)); 684 mPermissionListAdapter.setPermissionType(permissionTypes); 685 mPermissionListRecyclerView.setAdapter(mPermissionListAdapter); 686 mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager); 687 688 disableButtons(); 689 690 // Enable buttons once users scroll down to the bottom of the permission list. 691 mPermissionListRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 692 @Override 693 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 694 super.onScrollStateChanged(recyclerView, newState); 695 if (!recyclerView.canScrollVertically(1)) { 696 enableButtons(); 697 } 698 } 699 }); 700 // Enable buttons if last item in the permission list is visible to the users when 701 // mPermissionListRecyclerView is fully populated. 702 mPermissionListRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener( 703 new ViewTreeObserver.OnGlobalLayoutListener() { 704 @Override 705 public void onGlobalLayout() { 706 LinearLayoutManager layoutManager = 707 (LinearLayoutManager) mPermissionListRecyclerView 708 .getLayoutManager(); 709 int lastVisibleItemPosition = 710 layoutManager.findLastCompletelyVisibleItemPosition(); 711 int numItems = mPermissionListRecyclerView.getAdapter().getItemCount(); 712 713 if (lastVisibleItemPosition >= numItems - 1) { 714 enableButtons(); 715 } 716 717 mPermissionListRecyclerView.getViewTreeObserver() 718 .removeOnGlobalLayoutListener(this); 719 } 720 }); 721 722 mConstraintList.setVisibility(View.VISIBLE); 723 mPermissionListRecyclerView.setVisibility(View.VISIBLE); 724 } 725 726 // Disable and grey out the Allow and Don't allow buttons if the last permission in the 727 // permission list is not visible to the users. disableButtons()728 private void disableButtons() { 729 mButtonAllow.setEnabled(false); 730 mButtonNotAllow.setEnabled(false); 731 mButtonAllow.setTextColor( 732 getResources().getColor(android.R.color.system_neutral1_400, null)); 733 mButtonNotAllow.setTextColor( 734 getResources().getColor(android.R.color.system_neutral1_400, null)); 735 mButtonAllow.getBackground().setColorFilter( 736 (new BlendModeColorFilter(Color.LTGRAY, BlendMode.DARKEN))); 737 mButtonNotAllow.getBackground().setColorFilter( 738 (new BlendModeColorFilter(Color.LTGRAY, BlendMode.DARKEN))); 739 } 740 // Enable and restore the color for the Allow and Don't allow buttons if the last permission in 741 // the permission list is visible to the users. enableButtons()742 private void enableButtons() { 743 mButtonAllow.setEnabled(true); 744 mButtonNotAllow.setEnabled(true); 745 mButtonAllow.getBackground().setColorFilter(null); 746 mButtonNotAllow.getBackground().setColorFilter(null); 747 mButtonAllow.setTextColor( 748 getResources().getColor(android.R.color.system_neutral1_900, null)); 749 mButtonNotAllow.setTextColor( 750 getResources().getColor(android.R.color.system_neutral1_900, null)); 751 } 752 753 private final ResultReceiver mOnAssociationCreatedReceiver = 754 new ResultReceiver(Handler.getMain()) { 755 @Override 756 protected void onReceiveResult(int resultCode, Bundle data) { 757 if (resultCode == RESULT_CODE_ASSOCIATION_CREATED) { 758 final AssociationInfo association = data.getParcelable( 759 EXTRA_ASSOCIATION, AssociationInfo.class); 760 requireNonNull(association); 761 setResultAndFinish(association, CompanionDeviceManager.RESULT_OK); 762 } else { 763 setResultAndFinish(null, resultCode); 764 } 765 } 766 }; 767 768 @Override onShowHelperDialogFailed()769 public void onShowHelperDialogFailed() { 770 cancel(/* discoveryTimeout */ false, /* userRejected */ false, /* internalError */ true); 771 } 772 773 @Override onHelperDialogDismissed()774 public void onHelperDialogDismissed() { 775 mAssociationConfirmationDialog.setVisibility(View.VISIBLE); 776 } 777 } 778