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