| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.companiondevicemanager; |
| |
| import static android.companion.CompanionDeviceManager.REASON_CANCELED; |
| import static android.companion.CompanionDeviceManager.REASON_DISCOVERY_TIMEOUT; |
| import static android.companion.CompanionDeviceManager.REASON_INTERNAL_ERROR; |
| import static android.companion.CompanionDeviceManager.REASON_USER_REJECTED; |
| import static android.companion.CompanionDeviceManager.RESULT_DISCOVERY_TIMEOUT; |
| import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR; |
| import static android.companion.CompanionDeviceManager.RESULT_USER_REJECTED; |
| import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; |
| |
| import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState; |
| import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.MULTI_DEVICES_SUMMARIES; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.PERMISSION_TYPES; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME_MULTI; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICON; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.SUMMARIES; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_SELF_MANAGED_PROFILES; |
| import static com.android.companiondevicemanager.CompanionDeviceResources.TITLES; |
| import static com.android.companiondevicemanager.Utils.getApplicationLabel; |
| import static com.android.companiondevicemanager.Utils.getHtmlFromResources; |
| import static com.android.companiondevicemanager.Utils.getIcon; |
| import static com.android.companiondevicemanager.Utils.getImageColor; |
| import static com.android.companiondevicemanager.Utils.getVendorHeaderIcon; |
| import static com.android.companiondevicemanager.Utils.getVendorHeaderName; |
| import static com.android.companiondevicemanager.Utils.hasVendorIcon; |
| import static com.android.companiondevicemanager.Utils.prepareResultReceiverForIpc; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SuppressLint; |
| import android.companion.AssociatedDevice; |
| import android.companion.AssociationInfo; |
| import android.companion.AssociationRequest; |
| import android.companion.CompanionDeviceManager; |
| import android.companion.IAssociationRequestCallback; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.graphics.BlendMode; |
| import android.graphics.BlendModeColorFilter; |
| import android.graphics.Color; |
| import android.graphics.drawable.Drawable; |
| import android.net.MacAddress; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.text.Spanned; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewTreeObserver; |
| import android.widget.Button; |
| import android.widget.ImageButton; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.ProgressBar; |
| import android.widget.RelativeLayout; |
| import android.widget.TextView; |
| |
| import androidx.constraintlayout.widget.ConstraintLayout; |
| import androidx.fragment.app.FragmentActivity; |
| import androidx.fragment.app.FragmentManager; |
| import androidx.recyclerview.widget.LinearLayoutManager; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * A CompanionDevice activity response for showing the available |
| * nearby devices to be associated with. |
| */ |
| @SuppressLint("LongLogTag") |
| public class CompanionDeviceActivity extends FragmentActivity implements |
| CompanionVendorHelperDialogFragment.CompanionVendorHelperDialogListener { |
| private static final boolean DEBUG = false; |
| private static final String TAG = "CDM_CompanionDeviceActivity"; |
| |
| // Keep the following constants in sync with |
| // frameworks/base/services/companion/java/ |
| // com/android/server/companion/AssociationRequestsProcessor.java |
| |
| // AssociationRequestsProcessor <-> UI |
| private static final String EXTRA_APPLICATION_CALLBACK = "application_callback"; |
| private static final String EXTRA_ASSOCIATION_REQUEST = "association_request"; |
| private static final String EXTRA_RESULT_RECEIVER = "result_receiver"; |
| private static final String EXTRA_FORCE_CANCEL_CONFIRMATION = "cancel_confirmation"; |
| |
| private static final String FRAGMENT_DIALOG_TAG = "fragment_dialog"; |
| |
| // AssociationRequestsProcessor -> UI |
| private static final int RESULT_CODE_ASSOCIATION_CREATED = 0; |
| private static final String EXTRA_ASSOCIATION = "association"; |
| |
| // UI -> AssociationRequestsProcessor |
| private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0; |
| private static final String EXTRA_MAC_ADDRESS = "mac_address"; |
| |
| private AssociationRequest mRequest; |
| private IAssociationRequestCallback mAppCallback; |
| private ResultReceiver mCdmServiceReceiver; |
| |
| // Always present widgets. |
| private TextView mTitle; |
| private TextView mSummary; |
| |
| // Present for single device and multiple device only. |
| private ImageView mProfileIcon; |
| |
| // Only present for selfManaged devices. |
| private ImageView mVendorHeaderImage; |
| private TextView mVendorHeaderName; |
| private ImageButton mVendorHeaderButton; |
| |
| // Progress indicator is only shown while we are looking for the first suitable device for a |
| // multiple device association. |
| private ProgressBar mMultipleDeviceSpinner; |
| // Progress indicator is only shown while we are looking for the first suitable device for a |
| // single device association. |
| private ProgressBar mSingleDeviceSpinner; |
| |
| // Present for self-managed association requests and "single-device" regular association |
| // regular. |
| private Button mButtonAllow; |
| private Button mButtonNotAllow; |
| // Present for multiple devices' association requests only. |
| private Button mButtonNotAllowMultipleDevices; |
| |
| // Present for top and bottom borders for permissions list and device list. |
| private View mBorderTop; |
| private View mBorderBottom; |
| |
| private LinearLayout mAssociationConfirmationDialog; |
| // Contains device list, permission list and top/bottom borders. |
| private ConstraintLayout mConstraintList; |
| // Only present for self-managed association requests. |
| private RelativeLayout mVendorHeader; |
| // A linearLayout for mButtonNotAllowMultipleDevices, user will press this layout instead |
| // of the button for accessibility. |
| private LinearLayout mNotAllowMultipleDevicesLayout; |
| |
| // The recycler view is only shown for multiple-device regular association request, after |
| // at least one matching device is found. |
| private @Nullable RecyclerView mDeviceListRecyclerView; |
| private @Nullable DeviceListAdapter mDeviceAdapter; |
| |
| |
| // The recycler view is only shown for selfManaged and singleDevice association request. |
| private @Nullable RecyclerView mPermissionListRecyclerView; |
| private @Nullable PermissionListAdapter mPermissionListAdapter; |
| |
| // The flag used to prevent double taps, that may lead to sending several requests for creating |
| // an association to CDM. |
| private boolean mApproved; |
| private boolean mCancelled; |
| // A reference to the device selected by the user, to be sent back to the application via |
| // onActivityResult() after the association is created. |
| private @Nullable DeviceFilterPair<?> mSelectedDevice; |
| |
| private @Nullable List<Integer> mPermissionTypes; |
| |
| private LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this); |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| if (DEBUG) Log.d(TAG, "onCreate()"); |
| boolean forceCancelDialog = getIntent().getBooleanExtra("cancel_confirmation", false); |
| // Must handle the force cancel request in onNewIntent. |
| if (forceCancelDialog) { |
| Log.i(TAG, "The confirmation does not exist, skipping the cancel request"); |
| finish(); |
| } |
| |
| super.onCreate(savedInstanceState); |
| getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); |
| } |
| |
| @Override |
| protected void onStart() { |
| super.onStart(); |
| if (DEBUG) Log.d(TAG, "onStart()"); |
| |
| final Intent intent = getIntent(); |
| mRequest = intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST); |
| mAppCallback = IAssociationRequestCallback.Stub.asInterface( |
| intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); |
| mCdmServiceReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); |
| |
| requireNonNull(mRequest); |
| requireNonNull(mAppCallback); |
| requireNonNull(mCdmServiceReceiver); |
| |
| // Start discovery services if needed. |
| if (!mRequest.isSelfManaged()) { |
| CompanionDeviceDiscoveryService.startForRequest(this, mRequest); |
| // TODO(b/217749191): Create the ViewModel for the LiveData |
| CompanionDeviceDiscoveryService.getDiscoveryState().observe( |
| /* LifeCycleOwner */ this, this::onDiscoveryStateChanged); |
| } |
| // Init UI. |
| initUI(); |
| } |
| |
| @SuppressWarnings("MissingSuperCall") // TODO: Fix me |
| @Override |
| protected void onNewIntent(Intent intent) { |
| // Force cancels the CDM dialog if this activity receives another intent with |
| // EXTRA_FORCE_CANCEL_CONFIRMATION. |
| boolean forCancelDialog = intent.getBooleanExtra(EXTRA_FORCE_CANCEL_CONFIRMATION, false); |
| |
| if (forCancelDialog) { |
| Log.i(TAG, "Cancelling the user confirmation"); |
| |
| cancel(/* discoveryTimeOut */ false, |
| /* userRejected */ false, /* internalError */ false); |
| return; |
| } |
| |
| // Handle another incoming request (while we are not done with the original - mRequest - |
| // yet). |
| final AssociationRequest request = requireNonNull( |
| intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST)); |
| |
| if (DEBUG) Log.d(TAG, "onNewIntent(), request=" + request); |
| |
| // We can only "process" one request at a time. |
| final IAssociationRequestCallback appCallback = IAssociationRequestCallback.Stub |
| .asInterface(intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK)); |
| try { |
| requireNonNull(appCallback).onFailure("Busy."); |
| } catch (RemoteException ignore) { |
| } |
| } |
| |
| @Override |
| protected void onStop() { |
| super.onStop(); |
| if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing()); |
| |
| // TODO: handle config changes without cancelling. |
| if (!isDone()) { |
| cancel(/* discoveryTimeOut */ false, |
| /* userRejected */ false, /* internalError */ false); // will finish() |
| } |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| if (DEBUG) Log.d(TAG, "onDestroy()"); |
| } |
| |
| @Override |
| public void onBackPressed() { |
| if (DEBUG) Log.d(TAG, "onBackPressed()"); |
| super.onBackPressed(); |
| } |
| |
| @Override |
| public void finish() { |
| if (DEBUG) Log.d(TAG, "finish()", new Exception("Stack Trace Dump")); |
| super.finish(); |
| } |
| |
| private void initUI() { |
| if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest); |
| |
| final String packageName = mRequest.getPackageName(); |
| final int userId = mRequest.getUserId(); |
| final CharSequence appLabel; |
| |
| try { |
| appLabel = getApplicationLabel(this, packageName, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.w(TAG, "Package u" + userId + "/" + packageName + " not found."); |
| |
| CompanionDeviceDiscoveryService.stop(this); |
| setResultAndFinish(null, RESULT_INTERNAL_ERROR); |
| return; |
| } |
| |
| setContentView(R.layout.activity_confirmation); |
| |
| mConstraintList = findViewById(R.id.constraint_list); |
| mAssociationConfirmationDialog = findViewById(R.id.association_confirmation); |
| mVendorHeader = findViewById(R.id.vendor_header); |
| |
| mBorderTop = findViewById(R.id.border_top); |
| mBorderBottom = findViewById(R.id.border_bottom); |
| |
| mTitle = findViewById(R.id.title); |
| mSummary = findViewById(R.id.summary); |
| |
| mProfileIcon = findViewById(R.id.profile_icon); |
| |
| mVendorHeaderImage = findViewById(R.id.vendor_header_image); |
| mVendorHeaderName = findViewById(R.id.vendor_header_name); |
| mVendorHeaderButton = findViewById(R.id.vendor_header_button); |
| |
| mDeviceListRecyclerView = findViewById(R.id.device_list); |
| |
| mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device); |
| mSingleDeviceSpinner = findViewById(R.id.spinner_single_device); |
| mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick); |
| |
| mPermissionListRecyclerView = findViewById(R.id.permission_list); |
| mPermissionListAdapter = new PermissionListAdapter(this); |
| |
| mButtonAllow = findViewById(R.id.btn_positive); |
| mButtonNotAllow = findViewById(R.id.btn_negative); |
| mButtonNotAllowMultipleDevices = findViewById(R.id.btn_negative_multiple_devices); |
| mNotAllowMultipleDevicesLayout = findViewById(R.id.negative_multiple_devices_layout); |
| |
| mButtonAllow.setOnClickListener(this::onPositiveButtonClick); |
| mButtonNotAllow.setOnClickListener(this::onNegativeButtonClick); |
| mNotAllowMultipleDevicesLayout.setOnClickListener(this::onNegativeButtonClick); |
| |
| mVendorHeaderButton.setOnClickListener(this::onShowHelperDialog); |
| |
| if (mRequest.isSelfManaged()) { |
| initUiForSelfManagedAssociation(); |
| } else if (mRequest.isSingleDevice()) { |
| initUiForSingleDevice(appLabel); |
| } else { |
| initUiForMultipleDevices(appLabel); |
| } |
| } |
| |
| private void onDiscoveryStateChanged(DiscoveryState newState) { |
| if (newState == FINISHED_TIMEOUT |
| && CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) { |
| cancel(/* discoveryTimeOut */ true, |
| /* userRejected */ false, /* internalError */ false); |
| } |
| } |
| |
| private void onUserSelectedDevice(@NonNull DeviceFilterPair<?> selectedDevice) { |
| final MacAddress macAddress = selectedDevice.getMacAddress(); |
| mRequest.setDisplayName(selectedDevice.getDisplayName()); |
| mRequest.setAssociatedDevice(new AssociatedDevice(selectedDevice.getDevice())); |
| onAssociationApproved(macAddress); |
| } |
| |
| private void onAssociationApproved(@Nullable MacAddress macAddress) { |
| if (isDone()) { |
| if (DEBUG) Log.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled")); |
| return; |
| } |
| mApproved = true; |
| |
| if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress); |
| |
| if (!mRequest.isSelfManaged()) { |
| requireNonNull(macAddress); |
| CompanionDeviceDiscoveryService.stop(this); |
| } |
| |
| final Bundle data = new Bundle(); |
| data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest); |
| data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder()); |
| if (macAddress != null) { |
| data.putParcelable(EXTRA_MAC_ADDRESS, macAddress); |
| } |
| |
| data.putParcelable(EXTRA_RESULT_RECEIVER, |
| prepareResultReceiverForIpc(mOnAssociationCreatedReceiver)); |
| |
| mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data); |
| } |
| |
| private void cancel(boolean discoveryTimeout, boolean userRejected, boolean internalError) { |
| if (DEBUG) { |
| Log.i(TAG, "cancel(), discoveryTimeout=" |
| + discoveryTimeout |
| + ", userRejected=" |
| + userRejected |
| + ", internalError=" |
| + internalError, new Exception("Stack Trace Dump")); |
| } |
| |
| if (isDone()) { |
| if (DEBUG) Log.w(TAG, "Already done: " + (mApproved ? "Approved" : "Cancelled")); |
| return; |
| } |
| mCancelled = true; |
| |
| // Stop discovery service if it was used. |
| if (!mRequest.isSelfManaged() || discoveryTimeout) { |
| CompanionDeviceDiscoveryService.stop(this); |
| } |
| |
| final String cancelReason; |
| final int resultCode; |
| if (userRejected) { |
| cancelReason = REASON_USER_REJECTED; |
| resultCode = RESULT_USER_REJECTED; |
| } else if (discoveryTimeout) { |
| cancelReason = REASON_DISCOVERY_TIMEOUT; |
| resultCode = RESULT_DISCOVERY_TIMEOUT; |
| } else if (internalError) { |
| cancelReason = REASON_INTERNAL_ERROR; |
| resultCode = RESULT_INTERNAL_ERROR; |
| } else { |
| cancelReason = REASON_CANCELED; |
| resultCode = CompanionDeviceManager.RESULT_CANCELED; |
| } |
| |
| // First send callback to the app directly... |
| try { |
| mAppCallback.onFailure(cancelReason); |
| } catch (RemoteException ignore) { |
| } |
| |
| // ... then set result and finish ("sending" onActivityResult()). |
| setResultAndFinish(null, resultCode); |
| } |
| |
| private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) { |
| Log.i(TAG, "setResultAndFinish(), association=" |
| + (association == null ? "null" : association) |
| + "resultCode=" + resultCode); |
| |
| final Intent data = new Intent(); |
| if (association != null) { |
| data.putExtra(CompanionDeviceManager.EXTRA_ASSOCIATION, association); |
| if (!association.isSelfManaged()) { |
| data.putExtra(CompanionDeviceManager.EXTRA_DEVICE, mSelectedDevice.getDevice()); |
| } |
| } |
| setResult(resultCode, data); |
| |
| finish(); |
| } |
| |
| private void initUiForSelfManagedAssociation() { |
| if (DEBUG) Log.i(TAG, "initUiFor_SelfManaged_Association()"); |
| |
| final CharSequence deviceName = mRequest.getDisplayName(); |
| final String deviceProfile = mRequest.getDeviceProfile(); |
| final String packageName = mRequest.getPackageName(); |
| final int userId = mRequest.getUserId(); |
| final Drawable vendorIcon; |
| final CharSequence vendorName; |
| final Spanned title; |
| |
| if (!SUPPORTED_SELF_MANAGED_PROFILES.contains(deviceProfile)) { |
| throw new RuntimeException("Unsupported profile " + deviceProfile); |
| } |
| |
| mPermissionTypes = new ArrayList<>(); |
| |
| try { |
| vendorIcon = getVendorHeaderIcon(this, packageName, userId); |
| vendorName = getVendorHeaderName(this, packageName, userId); |
| mVendorHeaderImage.setImageDrawable(vendorIcon); |
| if (hasVendorIcon(this, packageName, userId)) { |
| int color = getImageColor(this); |
| mVendorHeaderImage.setColorFilter(getResources().getColor(color, /* Theme= */null)); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.e(TAG, "Package u" + userId + "/" + packageName + " not found."); |
| cancel(/* discoveryTimeout */ false, |
| /* userRejected */ false, /* internalError */ true); |
| return; |
| } |
| |
| title = getHtmlFromResources(this, TITLES.get(deviceProfile), deviceName); |
| mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile)); |
| |
| // Summary is not needed for selfManaged dialog. |
| mSummary.setVisibility(View.GONE); |
| |
| setupPermissionList(); |
| |
| mTitle.setText(title); |
| mVendorHeaderName.setText(vendorName); |
| mVendorHeader.setVisibility(View.VISIBLE); |
| mVendorHeader.setVisibility(View.VISIBLE); |
| mProfileIcon.setVisibility(View.GONE); |
| mDeviceListRecyclerView.setVisibility(View.GONE); |
| // Top and bottom borders should be gone for selfManaged dialog. |
| mBorderTop.setVisibility(View.GONE); |
| mBorderBottom.setVisibility(View.GONE); |
| } |
| |
| private void initUiForSingleDevice(CharSequence appLabel) { |
| if (DEBUG) Log.i(TAG, "initUiFor_SingleDevice()"); |
| |
| final String deviceProfile = mRequest.getDeviceProfile(); |
| |
| mPermissionTypes = new ArrayList<>(); |
| |
| CompanionDeviceDiscoveryService.getScanResult().observe(this, |
| deviceFilterPairs -> updateSingleDeviceUi( |
| deviceFilterPairs, deviceProfile, appLabel)); |
| |
| mSingleDeviceSpinner.setVisibility(View.VISIBLE); |
| // Hide permission list and confirmation dialog first before the |
| // first matched device is found. |
| mPermissionListRecyclerView.setVisibility(View.GONE); |
| mDeviceListRecyclerView.setVisibility(View.GONE); |
| mAssociationConfirmationDialog.setVisibility(View.GONE); |
| } |
| |
| private void updateSingleDeviceUi(List<DeviceFilterPair<?>> deviceFilterPairs, |
| String deviceProfile, CharSequence appLabel) { |
| // Ignore "empty" scan reports. |
| if (deviceFilterPairs.isEmpty()) return; |
| |
| mSelectedDevice = requireNonNull(deviceFilterPairs.get(0)); |
| // No need to show user consent dialog if it is a singleDevice |
| // and isSkipPrompt(true) AssociationRequest. |
| // See AssociationRequestsProcessor#mayAssociateWithoutPrompt. |
| if (mRequest.isSkipPrompt()) { |
| mSingleDeviceSpinner.setVisibility(View.GONE); |
| onUserSelectedDevice(mSelectedDevice); |
| return; |
| } |
| |
| final String deviceName = mSelectedDevice.getDisplayName(); |
| final Spanned title; |
| final Spanned summary; |
| final Drawable profileIcon; |
| |
| if (!SUPPORTED_PROFILES.contains(deviceProfile)) { |
| throw new RuntimeException("Unsupported profile " + deviceProfile); |
| } |
| |
| if (deviceProfile == null) { |
| summary = getHtmlFromResources(this, SUMMARIES.get(null), deviceName); |
| mConstraintList.setVisibility(View.GONE); |
| } else { |
| summary = getHtmlFromResources( |
| this, SUMMARIES.get(deviceProfile), getString(R.string.device_type)); |
| mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile)); |
| setupPermissionList(); |
| } |
| |
| title = getHtmlFromResources(this, TITLES.get(deviceProfile), appLabel, deviceName); |
| profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); |
| |
| mTitle.setText(title); |
| mSummary.setText(summary); |
| mProfileIcon.setImageDrawable(profileIcon); |
| mSingleDeviceSpinner.setVisibility(View.GONE); |
| mAssociationConfirmationDialog.setVisibility(View.VISIBLE); |
| } |
| |
| private void initUiForMultipleDevices(CharSequence appLabel) { |
| if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()"); |
| |
| final String deviceProfile = mRequest.getDeviceProfile(); |
| |
| final String profileName; |
| final String profileNameMulti; |
| final Spanned summary; |
| final Drawable profileIcon; |
| final int summaryResourceId; |
| |
| if (!SUPPORTED_PROFILES.contains(deviceProfile)) { |
| throw new RuntimeException("Unsupported profile " + deviceProfile); |
| } |
| |
| profileName = getString(PROFILES_NAME.get(deviceProfile)); |
| profileNameMulti = getString(PROFILES_NAME_MULTI.get(deviceProfile)); |
| profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile)); |
| summaryResourceId = MULTI_DEVICES_SUMMARIES.get(deviceProfile); |
| |
| if (deviceProfile == null) { |
| summary = getHtmlFromResources(this, summaryResourceId); |
| } else { |
| summary = getHtmlFromResources(this, summaryResourceId, profileName, appLabel); |
| } |
| |
| final Spanned title = getHtmlFromResources( |
| this, R.string.chooser_title, profileNameMulti, appLabel); |
| |
| mTitle.setText(title); |
| mSummary.setText(summary); |
| mProfileIcon.setImageDrawable(profileIcon); |
| |
| mDeviceListRecyclerView.setAdapter(mDeviceAdapter); |
| mDeviceListRecyclerView.setLayoutManager(new LinearLayoutManager(this)); |
| |
| CompanionDeviceDiscoveryService.getScanResult().observe(this, |
| deviceFilterPairs -> { |
| // Dismiss the progress bar once there's one device found for multiple devices. |
| if (deviceFilterPairs.size() >= 1) { |
| mMultipleDeviceSpinner.setVisibility(View.GONE); |
| } |
| |
| mDeviceAdapter.setDevices(deviceFilterPairs); |
| }); |
| |
| // "Remove" consent button: users would need to click on the list item. |
| mButtonAllow.setVisibility(View.GONE); |
| mButtonNotAllow.setVisibility(View.GONE); |
| mDeviceListRecyclerView.setVisibility(View.VISIBLE); |
| mButtonNotAllowMultipleDevices.setVisibility(View.VISIBLE); |
| mNotAllowMultipleDevicesLayout.setVisibility(View.VISIBLE); |
| mConstraintList.setVisibility(View.VISIBLE); |
| mMultipleDeviceSpinner.setVisibility(View.VISIBLE); |
| } |
| |
| private void onListItemClick(int position) { |
| if (DEBUG) Log.d(TAG, "onListItemClick() " + position); |
| |
| final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position); |
| |
| if (mSelectedDevice != null) { |
| if (DEBUG) Log.w(TAG, "Already selected."); |
| return; |
| } |
| // Notify the adapter to highlight the selected item. |
| mDeviceAdapter.setSelectedPosition(position); |
| |
| mSelectedDevice = requireNonNull(selectedDevice); |
| |
| onUserSelectedDevice(selectedDevice); |
| } |
| |
| private void onPositiveButtonClick(View v) { |
| if (DEBUG) Log.d(TAG, "on_Positive_ButtonClick()"); |
| |
| // Disable the button, to prevent more clicks. |
| v.setEnabled(false); |
| |
| if (mRequest.isSelfManaged()) { |
| onAssociationApproved(null); |
| } else { |
| onUserSelectedDevice(mSelectedDevice); |
| } |
| } |
| |
| private void onNegativeButtonClick(View v) { |
| if (DEBUG) Log.d(TAG, "on_Negative_ButtonClick()"); |
| |
| // Disable the button, to prevent more clicks. |
| v.setEnabled(false); |
| |
| cancel(/* discoveryTimeout */ false, /* userRejected */ true, /* internalError */ false); |
| } |
| |
| private void onShowHelperDialog(View view) { |
| FragmentManager fragmentManager = getSupportFragmentManager(); |
| CompanionVendorHelperDialogFragment fragmentDialog = |
| CompanionVendorHelperDialogFragment.newInstance(mRequest); |
| |
| mAssociationConfirmationDialog.setVisibility(View.INVISIBLE); |
| |
| fragmentDialog.show(fragmentManager, /* Tag */ FRAGMENT_DIALOG_TAG); |
| } |
| |
| private boolean isDone() { |
| return mApproved || mCancelled; |
| } |
| |
| // Set up the mPermissionListRecyclerView, including set up the adapter, |
| // initiate the layoutManager for the recyclerview, add listeners for monitoring the scrolling |
| // and when mPermissionListRecyclerView is fully populated. |
| // Lastly, disable the Allow and Don't allow buttons. |
| private void setupPermissionList() { |
| mPermissionListAdapter.setPermissionType(mPermissionTypes); |
| mPermissionListRecyclerView.setAdapter(mPermissionListAdapter); |
| mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager); |
| |
| disableButtons(); |
| |
| // Enable buttons once users scroll down to the bottom of the permission list. |
| mPermissionListRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrollStateChanged(RecyclerView recyclerView, int newState) { |
| super.onScrollStateChanged(recyclerView, newState); |
| if (!recyclerView.canScrollVertically(1)) { |
| enableButtons(); |
| } |
| } |
| }); |
| // Enable buttons if last item in the permission list is visible to the users when |
| // mPermissionListRecyclerView is fully populated. |
| mPermissionListRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener( |
| new ViewTreeObserver.OnGlobalLayoutListener() { |
| @Override |
| public void onGlobalLayout() { |
| LinearLayoutManager layoutManager = |
| (LinearLayoutManager) mPermissionListRecyclerView |
| .getLayoutManager(); |
| int lastVisibleItemPosition = |
| layoutManager.findLastCompletelyVisibleItemPosition(); |
| int numItems = mPermissionListRecyclerView.getAdapter().getItemCount(); |
| |
| if (lastVisibleItemPosition >= numItems - 1) { |
| enableButtons(); |
| } |
| |
| mPermissionListRecyclerView.getViewTreeObserver() |
| .removeOnGlobalLayoutListener(this); |
| } |
| }); |
| |
| mConstraintList.setVisibility(View.VISIBLE); |
| mPermissionListRecyclerView.setVisibility(View.VISIBLE); |
| } |
| |
| // Disable and grey out the Allow and Don't allow buttons if the last permission in the |
| // permission list is not visible to the users. |
| private void disableButtons() { |
| mButtonAllow.setEnabled(false); |
| mButtonNotAllow.setEnabled(false); |
| mButtonAllow.setTextColor( |
| getResources().getColor(android.R.color.system_neutral1_400, null)); |
| mButtonNotAllow.setTextColor( |
| getResources().getColor(android.R.color.system_neutral1_400, null)); |
| mButtonAllow.getBackground().setColorFilter( |
| (new BlendModeColorFilter(Color.LTGRAY, BlendMode.DARKEN))); |
| mButtonNotAllow.getBackground().setColorFilter( |
| (new BlendModeColorFilter(Color.LTGRAY, BlendMode.DARKEN))); |
| } |
| // Enable and restore the color for the Allow and Don't allow buttons if the last permission in |
| // the permission list is visible to the users. |
| private void enableButtons() { |
| mButtonAllow.setEnabled(true); |
| mButtonNotAllow.setEnabled(true); |
| mButtonAllow.getBackground().setColorFilter(null); |
| mButtonNotAllow.getBackground().setColorFilter(null); |
| mButtonAllow.setTextColor( |
| getResources().getColor(android.R.color.system_neutral1_900, null)); |
| mButtonNotAllow.setTextColor( |
| getResources().getColor(android.R.color.system_neutral1_900, null)); |
| } |
| |
| private final ResultReceiver mOnAssociationCreatedReceiver = |
| new ResultReceiver(Handler.getMain()) { |
| @Override |
| protected void onReceiveResult(int resultCode, Bundle data) { |
| if (resultCode == RESULT_CODE_ASSOCIATION_CREATED) { |
| final AssociationInfo association = data.getParcelable( |
| EXTRA_ASSOCIATION, AssociationInfo.class); |
| requireNonNull(association); |
| setResultAndFinish(association, CompanionDeviceManager.RESULT_OK); |
| } else { |
| setResultAndFinish(null, resultCode); |
| } |
| } |
| }; |
| |
| @Override |
| public void onShowHelperDialogFailed() { |
| cancel(/* discoveryTimeout */ false, /* userRejected */ false, /* internalError */ true); |
| } |
| |
| @Override |
| public void onHelperDialogDismissed() { |
| mAssociationConfirmationDialog.setVisibility(View.VISIBLE); |
| } |
| } |