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