| /* |
| * Copyright (C) 2020 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.google.android.companiondevicesupport.trust; |
| |
| import static com.android.car.ui.core.CarUi.requireToolbar; |
| import static com.android.car.ui.toolbar.Toolbar.State.SUBPAGE; |
| import static com.google.android.connecteddevice.api.RemoteFeature.ACTION_ASSOCIATION_SETTING; |
| import static com.google.android.connecteddevice.api.RemoteFeature.ASSOCIATED_DEVICE_DATA_NAME_EXTRA; |
| import static com.google.android.connecteddevice.trust.TrustedDeviceConstants.TRUSTED_DEVICE_ERROR_DISCONNECTED_DURING_ENROLLMENT; |
| import static com.google.android.connecteddevice.trust.TrustedDeviceConstants.TRUSTED_DEVICE_ERROR_NO_CONNECTION; |
| import static com.google.android.connecteddevice.util.SafeLog.logd; |
| import static com.google.android.connecteddevice.util.SafeLog.loge; |
| import static com.google.android.connecteddevice.util.SafeLog.logw; |
| |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.app.KeyguardManager; |
| import android.content.DialogInterface; |
| import android.content.DialogInterface.OnClickListener; |
| import android.content.Intent; |
| import android.os.Bundle; |
| import androidx.fragment.app.DialogFragment; |
| import androidx.fragment.app.FragmentActivity; |
| import android.text.Html; |
| import android.text.Spanned; |
| import android.widget.Toast; |
| import androidx.activity.result.ActivityResultLauncher; |
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; |
| import androidx.annotation.Nullable; |
| import androidx.lifecycle.ViewModelProvider; |
| import com.android.car.ui.toolbar.ToolbarController; |
| import com.google.android.companiondevicesupport.R; |
| import com.google.android.connecteddevice.model.AssociatedDevice; |
| import com.google.android.connecteddevice.trust.TrustedDeviceConstants; |
| import com.google.android.connecteddevice.trust.TrustedDeviceViewModel; |
| import com.google.android.connecteddevice.trust.TrustedDeviceViewModel.EnrollmentState; |
| import com.google.android.connecteddevice.trust.api.TrustedDevice; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| /** Activity for enrolling and viewing trusted devices. */ |
| public class TrustedDeviceActivity extends FragmentActivity { |
| |
| private static final String TAG = "TrustedDeviceActivity"; |
| |
| private static final String ACTION_LOCK_SETTINGS = "android.car.settings.SCREEN_LOCK_ACTIVITY"; |
| |
| private static final String DEVICE_DETAIL_FRAGMENT_TAG = "TrustedDeviceDetailFragmentTag"; |
| |
| private static final String DEVICE_NOT_CONNECTED_DIALOG_TAG = |
| "DeviceNotConnectedDialogFragmentTag"; |
| |
| private static final String CREATE_PROFILE_LOCK_DIALOG_TAG = "CreateProfileLockDialogFragmentTag"; |
| |
| private static final String UNLOCK_PROFILE_TO_FINISH_DIALOG_TAG = |
| "UnlockProfileToFinishDialogFragmentTag"; |
| |
| private static final String CREATE_PHONE_LOCK_DIALOG_TAG = "CreatePhoneLockDialogFragmentTag"; |
| |
| private static final String ENROLLMENT_ERROR_DIALOG_TAG = "EnrollmentErrorDialogFragmentTag"; |
| |
| /** {@code true} if a PIN/Pattern/Password has just been set as a screen lock. */ |
| private final AtomicBoolean isScreenLockNewlyCreated = new AtomicBoolean(false); |
| |
| /** |
| * {@code true} if this activity is relaunched for enrollment and the activity needs to be |
| * finished after enrollment has completed. |
| */ |
| private final AtomicBoolean wasRelaunched = new AtomicBoolean(false); |
| |
| private KeyguardManager keyguardManager; |
| |
| private ToolbarController toolbar; |
| |
| private TrustedDeviceViewModel model; |
| |
| private ActivityResultLauncher<Intent> createCredentialLauncher; |
| private ActivityResultLauncher<Intent> verifyCredentialLauncher; |
| private ActivityResultLauncher<Intent> retrieveDeviceLauncher; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| setTheme(R.style.Theme_CompanionDevice_Car_CarUi_WithToolbar); |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.settings_base_activity); |
| observeViewModel(); |
| |
| // ActivityResultLauncher has to be registered before the activity state reaches STARTED. |
| retrieveDeviceLauncher = |
| registerForActivityResult( |
| new StartActivityForResult(), result -> onAssociatedDeviceRetrieved(result.getData())); |
| createCredentialLauncher = |
| registerForActivityResult(new StartActivityForResult(), result -> onLockScreenCreated()); |
| verifyCredentialLauncher = |
| registerForActivityResult( |
| new StartActivityForResult(), result -> onCredentialVerified(result.getResultCode())); |
| |
| resumePreviousState(savedInstanceState); |
| |
| toolbar = requireToolbar(this); |
| toolbar.setState(SUBPAGE); |
| toolbar.setTitle(R.string.trusted_device_feature_title); |
| toolbar.getProgressBar().setVisible(true); |
| toolbar.registerBackListener(this::onBackListener); |
| |
| extractAssociatedDevice(); |
| |
| isScreenLockNewlyCreated.set(false); |
| wasRelaunched.set(false); |
| } |
| |
| @Override |
| protected void onNewIntent(Intent intent) { |
| super.onNewIntent(intent); |
| wasRelaunched.set(true); |
| if (isStartedForEnrollment(intent)) { |
| model.processEnrollment(); |
| } |
| } |
| |
| private boolean onBackListener() { |
| if (model.getEnrollmentState().getValue() != EnrollmentState.NONE) { |
| model.abortEnrollment(); |
| } |
| return false; |
| } |
| |
| private void resumePreviousState(Bundle saveInstanceState) { |
| if (saveInstanceState == null) { |
| return; |
| } |
| CreateProfileLockDialogFragment createProfileLockDialogFragment = |
| (CreateProfileLockDialogFragment) |
| getSupportFragmentManager().findFragmentByTag(CREATE_PROFILE_LOCK_DIALOG_TAG); |
| if (createProfileLockDialogFragment != null) { |
| createProfileLockDialogFragment.setOnConfirmListener((d, w) -> createScreenLock()); |
| } |
| |
| UnlockProfileDialogFragment unlockProfileDialogFragment = |
| (UnlockProfileDialogFragment) |
| getSupportFragmentManager().findFragmentByTag(UNLOCK_PROFILE_TO_FINISH_DIALOG_TAG); |
| if (unlockProfileDialogFragment != null) { |
| unlockProfileDialogFragment.setOnConfirmListener((d, w) -> validateCredential()); |
| } |
| } |
| |
| private void observeViewModel() { |
| model = new ViewModelProvider(this).get(TrustedDeviceViewModel.class); |
| model.getAssociatedDevice().observe(this, this::onAssociatedDeviceEnrolled); |
| model.getEnrollmentError().observe(this, this::onEnrollmentError); |
| |
| model.getEnrollmentState().observe(this, this::onEnrollmentStateChanged); |
| |
| model |
| .getEnabledDevice() |
| .observe(this, device -> runOnUiThread(() -> showEnrollmentSuccessToast(device))); |
| model |
| .isCurrentAssociatedDeviceRemoved() |
| .observe( |
| this, |
| isFinished -> { |
| if (isFinished) { |
| finish(); |
| } |
| }); |
| } |
| |
| private void onAssociatedDeviceEnrolled(AssociatedDevice device) { |
| if (device == null) { |
| return; |
| } |
| showTrustedDeviceDetailFragment(device); |
| Intent incomingIntent = getIntent(); |
| if (isStartedForEnrollment(incomingIntent)) { |
| model.processEnrollment(); |
| } |
| } |
| |
| private void onEnrollmentError(Integer error) { |
| if (error == null) { |
| return; |
| } |
| logd(TAG, "Got enrollment error: " + error); |
| switch (error) { |
| case TRUSTED_DEVICE_ERROR_DISCONNECTED_DURING_ENROLLMENT: |
| case TRUSTED_DEVICE_ERROR_NO_CONNECTION: |
| handleNoConnectionError(); |
| break; |
| default: |
| runOnUiThread(() -> showEnrollmentErrorDialogFragment(error)); |
| } |
| } |
| |
| private void onEnrollmentStateChanged(EnrollmentState state) { |
| logd(TAG, "new enrollment state: " + state.name()); |
| switch (state) { |
| case WAITING_FOR_PASSWORD_SETUP: |
| runOnUiThread(this::promptToCreatePassword); |
| break; |
| case CREDENTIAL_PENDING: |
| validateCredential(); |
| break; |
| case FINISHED: |
| model.resetEnrollmentState(); |
| finishEnrollment(); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| private void extractAssociatedDevice() { |
| Intent intent = getIntent(); |
| String action = intent.getAction(); |
| if (!TrustedDeviceConstants.INTENT_ACTION_TRUSTED_DEVICE_SETTING.equals(action)) { |
| retrieveAssociatedDevice(); |
| return; |
| } |
| AssociatedDevice device = intent.getParcelableExtra(ASSOCIATED_DEVICE_DATA_NAME_EXTRA); |
| if (device == null) { |
| logd(TAG, "Failed to extract associated device intent, start retrieving associated device."); |
| retrieveAssociatedDevice(); |
| return; |
| } |
| model.setAssociatedDevice(device); |
| } |
| |
| private void handleNoConnectionError() { |
| logd(TAG, "Phone disconnected"); |
| CreateProfileLockDialogFragment fragment = |
| (CreateProfileLockDialogFragment) |
| getSupportFragmentManager().findFragmentByTag(CREATE_PROFILE_LOCK_DIALOG_TAG); |
| if (fragment != null) { |
| logd(TAG, "Dismiss create lock screen dialogue"); |
| fragment.dismiss(); |
| } |
| runOnUiThread(this::showDeviceNotConnectedDialog); |
| } |
| |
| private void validateCredential() { |
| KeyguardManager keyguardManager = getKeyguardManager(); |
| if (keyguardManager == null) { |
| logd(TAG, "KeyguardManager was null. Aborting."); |
| return; |
| } |
| if (isScreenLockNewlyCreated.get()) { |
| showUnlockProfileDialogFragment(); |
| return; |
| } |
| promptToVerifyCredential(); |
| } |
| |
| private void promptToVerifyCredential() { |
| @SuppressWarnings("deprecation") // Car does not support Biometric lock as of now. |
| Intent confirmIntent = |
| keyguardManager.createConfirmDeviceCredentialIntent( |
| "PLACEHOLDER PROMPT TITLE", "PLACEHOLDER PROMPT MESSAGE"); |
| if (confirmIntent == null) { |
| loge(TAG, "User either has no lock screen, or a token is already registered."); |
| return; |
| } |
| if (verifyCredentialLauncher == null) { |
| loge(TAG, "No ActivityResultLauncher registered for verifying credential. "); |
| return; |
| } |
| logd(TAG, "Prompting user to validate credentials."); |
| verifyCredentialLauncher.launch(confirmIntent); |
| } |
| |
| private void onCredentialVerified(int resultCode) { |
| EnrollmentState state = model.getEnrollmentState().getValue(); |
| if (state != EnrollmentState.CREDENTIAL_PENDING) { |
| logw(TAG, "Credential verified but enrollment in incorrect state: " + state + ", ignore."); |
| return; |
| } |
| if (resultCode == RESULT_OK) { |
| logd(TAG, "Credentials accepted."); |
| model.onCredentialVerified(); |
| return; |
| } |
| loge(TAG, "Lock screen was unsuccessful. Returned result code: " + resultCode + "."); |
| model.abortEnrollment(); |
| } |
| |
| private static boolean isStartedForEnrollment(Intent intent) { |
| return intent != null |
| && intent.getBooleanExtra(TrustedDeviceConstants.INTENT_EXTRA_ENROLL_NEW_TOKEN, false); |
| } |
| |
| private void finishEnrollment() { |
| if (!wasRelaunched.get()) { |
| // If the activity is not relaunched for enrollment, it needs to be finished to make the |
| // foreground return to the previous screen. |
| finish(); |
| } |
| } |
| |
| private void promptToCreatePassword() { |
| if (isDeviceSecure()) { |
| return; |
| } |
| CreateProfileLockDialogFragment fragment = |
| CreateProfileLockDialogFragment.newInstance( |
| /* onConfirmListener= */ (d, w) -> createScreenLock(), |
| /* onCancelListener= */ (d, w) -> model.abortEnrollment()); |
| fragment.show(getSupportFragmentManager(), CREATE_PROFILE_LOCK_DIALOG_TAG); |
| } |
| |
| private void createScreenLock() { |
| if (isDeviceSecure()) { |
| return; |
| } |
| logd(TAG, "User has not set a lock screen. Redirecting to set up."); |
| if (createCredentialLauncher == null) { |
| loge(TAG, "No ActivityResultLauncher registered for creating lock screen. "); |
| return; |
| } |
| Intent intent = new Intent(ACTION_LOCK_SETTINGS); |
| createCredentialLauncher.launch(intent); |
| } |
| |
| private void onLockScreenCreated() { |
| EnrollmentState state = model.getEnrollmentState().getValue(); |
| if (state != EnrollmentState.WAITING_FOR_PASSWORD_SETUP) { |
| logw(TAG, "Lockscreen created but enrollment in incorrect state: " + state + ", ignore."); |
| return; |
| } |
| if (!isDeviceSecure()) { |
| loge(TAG, "Failed to create lock screen."); |
| isScreenLockNewlyCreated.set(false); |
| model.abortEnrollment(); |
| return; |
| } |
| isScreenLockNewlyCreated.set(true); |
| model.processEnrollment(); |
| } |
| |
| private boolean isDeviceSecure() { |
| KeyguardManager keyguardManager = getKeyguardManager(); |
| if (keyguardManager == null) { |
| return false; |
| } |
| return keyguardManager.isDeviceSecure(); |
| } |
| |
| private void retrieveAssociatedDevice() { |
| if (retrieveDeviceLauncher == null) { |
| loge(TAG, "No ActivityResultLauncher registered for retrieving associated device. "); |
| return; |
| } |
| Intent intent = new Intent(ACTION_ASSOCIATION_SETTING); |
| retrieveDeviceLauncher.launch(intent); |
| } |
| |
| private void showTrustedDeviceDetailFragment(AssociatedDevice device) { |
| toolbar.getProgressBar().setVisible(false); |
| TrustedDeviceDetailFragment fragment = TrustedDeviceDetailFragment.newInstance(device); |
| getSupportFragmentManager() |
| .beginTransaction() |
| .replace(R.id.fragment_container, fragment, DEVICE_DETAIL_FRAGMENT_TAG) |
| .commit(); |
| } |
| |
| private void showUnlockProfileDialogFragment() { |
| isScreenLockNewlyCreated.set(false); |
| UnlockProfileDialogFragment fragment = |
| UnlockProfileDialogFragment.newInstance( |
| /* onConfirmListener= */ (d, w) -> validateCredential(), |
| /* onCancelListener= */ (d, w) -> model.abortEnrollment()); |
| fragment.show(getSupportFragmentManager(), UNLOCK_PROFILE_TO_FINISH_DIALOG_TAG); |
| } |
| |
| private void showDeviceNotConnectedDialog() { |
| DeviceNotConnectedDialogFragment fragment = new DeviceNotConnectedDialogFragment(); |
| fragment.show(getSupportFragmentManager(), DEVICE_NOT_CONNECTED_DIALOG_TAG); |
| } |
| |
| private void showEnrollmentSuccessToast(TrustedDevice device) { |
| if (device == null) { |
| return; |
| } |
| AssociatedDevice addedDevice = model.getAssociatedDevice().getValue(); |
| if (addedDevice == null) { |
| loge(TAG, "No associated device retrieved when a trusted device has been added."); |
| return; |
| } |
| if (!addedDevice.getDeviceId().equals(device.getDeviceId())) { |
| loge(TAG, "Id of the enrolled trusted device doesn't match id of the current device"); |
| return; |
| } |
| String message = |
| getString(R.string.trusted_device_enrollment_success_message, addedDevice.getDeviceName()); |
| Spanned styledMessage = Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY); |
| runOnUiThread(() -> Toast.makeText(this, styledMessage, Toast.LENGTH_SHORT).show()); |
| } |
| |
| private void showEnrollmentErrorDialogFragment(int error) { |
| switch (error) { |
| case TrustedDeviceConstants.TRUSTED_DEVICE_ERROR_DEVICE_NOT_SECURED: |
| CreatePhoneLockDialogFragment createPhoneLockDialogFragment = |
| new CreatePhoneLockDialogFragment(); |
| createPhoneLockDialogFragment.show( |
| getSupportFragmentManager(), CREATE_PHONE_LOCK_DIALOG_TAG); |
| break; |
| case TrustedDeviceConstants.TRUSTED_DEVICE_ERROR_MESSAGE_TYPE_UNKNOWN: |
| case TrustedDeviceConstants.TRUSTED_DEVICE_ERROR_UNKNOWN: |
| EnrollmentErrorDialogFragment enrollmentErrorDialogFragment = |
| new EnrollmentErrorDialogFragment(); |
| enrollmentErrorDialogFragment.show( |
| getSupportFragmentManager(), ENROLLMENT_ERROR_DIALOG_TAG); |
| break; |
| default: |
| loge(TAG, "Encountered unexpected error: " + error + "."); |
| } |
| } |
| |
| @Nullable |
| private KeyguardManager getKeyguardManager() { |
| if (keyguardManager == null) { |
| keyguardManager = getSystemService(KeyguardManager.class); |
| } |
| if (keyguardManager == null) { |
| loge(TAG, "Unable to get KeyguardManager."); |
| } |
| return keyguardManager; |
| } |
| |
| private void onAssociatedDeviceRetrieved(Intent data) { |
| if (data == null) { |
| loge(TAG, "Intent is null. Failed to extract associated device from intent."); |
| finish(); |
| return; |
| } |
| AssociatedDevice device = data.getParcelableExtra(ASSOCIATED_DEVICE_DATA_NAME_EXTRA); |
| if (device == null) { |
| loge(TAG, "Associated device extracted from intent is null."); |
| finish(); |
| return; |
| } |
| model.setAssociatedDevice(device); |
| logd(TAG, "on associated device retrieved from association activity: " + device); |
| } |
| |
| /** Dialog Fragment to notify that the device is not actively connected. */ |
| public static class DeviceNotConnectedDialogFragment extends DialogFragment { |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| return new AlertDialog.Builder(getActivity()) |
| .setTitle(getString(R.string.device_not_connected_dialog_title)) |
| .setMessage(getString(R.string.device_not_connected_dialog_message)) |
| .setNegativeButton(getString(R.string.ok), null) |
| .setCancelable(true) |
| .create(); |
| } |
| } |
| |
| /** Dialog Fragment to notify that a profile lock is needed to continue enrollment. */ |
| public static class CreateProfileLockDialogFragment extends DialogFragment { |
| private OnClickListener onConfirmListener; |
| private OnClickListener onCancelListener; |
| |
| static CreateProfileLockDialogFragment newInstance( |
| OnClickListener onConfirmListener, OnClickListener onCancelListener) { |
| CreateProfileLockDialogFragment fragment = new CreateProfileLockDialogFragment(); |
| fragment.setOnConfirmListener(onConfirmListener); |
| fragment.setOnCancelListener(onCancelListener); |
| return fragment; |
| } |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| Dialog dialog = |
| new AlertDialog.Builder(getActivity()) |
| .setTitle(getString(R.string.create_profile_lock_dialog_title)) |
| .setMessage(getString(R.string.create_profile_lock_dialog_message)) |
| .setNegativeButton(getString(R.string.cancel), onCancelListener) |
| .setPositiveButton(getString(R.string.continue_button), onConfirmListener) |
| .create(); |
| dialog.setCanceledOnTouchOutside(false); |
| return dialog; |
| } |
| |
| void setOnConfirmListener(DialogInterface.OnClickListener onConfirmListener) { |
| this.onConfirmListener = onConfirmListener; |
| } |
| |
| void setOnCancelListener(OnClickListener onCancelListener) { |
| this.onCancelListener = onCancelListener; |
| } |
| } |
| |
| /** Dialog Fragment to notify that the user needs to unlock again to finish enrollment. */ |
| public static class UnlockProfileDialogFragment extends DialogFragment { |
| private OnClickListener onConfirmListener; |
| private OnClickListener onCancelListener; |
| |
| static UnlockProfileDialogFragment newInstance( |
| OnClickListener onConfirmListener, OnClickListener onCancelListener) { |
| UnlockProfileDialogFragment fragment = new UnlockProfileDialogFragment(); |
| fragment.setOnConfirmListener(onConfirmListener); |
| fragment.setOnCancelListener(onCancelListener); |
| return fragment; |
| } |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| Dialog dialog = |
| new AlertDialog.Builder(getActivity()) |
| .setTitle(getString(R.string.unlock_profile_to_finish_title)) |
| .setMessage(getString(R.string.unlock_profile_to_finish_message)) |
| .setNegativeButton(getString(R.string.cancel), onCancelListener) |
| .setPositiveButton(getString(R.string.continue_button), onConfirmListener) |
| .create(); |
| dialog.setCanceledOnTouchOutside(false); |
| return dialog; |
| } |
| |
| void setOnConfirmListener(OnClickListener onConfirmListener) { |
| this.onConfirmListener = onConfirmListener; |
| } |
| |
| void setOnCancelListener(OnClickListener onCancelListener) { |
| this.onCancelListener = onCancelListener; |
| } |
| } |
| |
| /** Dialog Fragment to notify that the user needs to set up phone unlock before enrollment. */ |
| public static class CreatePhoneLockDialogFragment extends DialogFragment { |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| return new AlertDialog.Builder(getActivity()) |
| .setTitle(getString(R.string.create_phone_lock_dialog_title)) |
| .setMessage(getString(R.string.create_phone_lock_dialog_message)) |
| .setPositiveButton(getString(R.string.ok), null) |
| .setCancelable(true) |
| .create(); |
| } |
| } |
| |
| /** Dialog Fragment to notify error during enrollment. */ |
| public static class EnrollmentErrorDialogFragment extends DialogFragment { |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| return new AlertDialog.Builder(getActivity()) |
| .setTitle(getString(R.string.trusted_device_enrollment_error_dialog_title)) |
| .setMessage(getString(R.string.trusted_device_enrollment_error_dialog_message)) |
| .setPositiveButton(getString(R.string.ok), null) |
| .setCancelable(true) |
| .create(); |
| } |
| } |
| } |