diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 21b3fa4a5b3..f5374159df6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2785,6 +2785,7 @@ + - + + - - - \ No newline at end of file + android:pathData="M18.92,4.4L12.56,2.1C12.38,2.03 12.19,2 12,2C11.81,2 11.62,2.03 11.44,2.1L5.08,4.4C4.43,4.63 4,5.25 4,5.94V10.32C4.02,11.07 4.07,11.79 4.17,12.54C4.64,15.72 6.44,19.33 11.37,21.85C11.57,21.95 11.78,22 12,22C12.22,22 12.43,21.95 12.63,21.85C13.08,21.62 13.5,21.37 13.9,21.12C14.04,21.05 14.18,20.96 14.32,20.86C17.98,18.43 19.41,15.32 19.82,12.54C19.92,11.8 19.98,11.07 19.99,10.32V5.94C19.99,5.25 19.56,4.64 18.91,4.4H18.92ZM12.25,19.78C12.1,19.87 11.9,19.87 11.74,19.78C8.5,17.97 6.62,15.43 6.15,12.27C6.06,11.59 6.01,10.94 6,10.32V6.55C6,6.34 6.13,6.15 6.33,6.08L8.26,5.38C8.11,5.89 8.03,6.44 8.03,7.04C8.04,8.91 9.03,10.68 10.7,11.8C11.15,12.08 12.39,12.89 12.78,13.19C13.27,13.57 13.95,14.21 14.26,14.74C15.29,16.52 14.26,18.46 13.1,19.27C12.83,19.45 12.55,19.62 12.25,19.79V19.78ZM17.85,12.24C17.66,13.49 17.26,14.63 16.65,15.68C16.57,15.04 16.37,14.38 15.99,13.74C15.42,12.75 14.33,11.86 14.01,11.61C13.47,11.19 11.94,10.22 11.79,10.12C10.69,9.39 10.04,8.23 10.03,6.99C10.03,5.01 11.25,4.34 11.98,4.12C12.08,4.09 12.19,4.1 12.29,4.13L17.68,6.08C17.88,6.15 18.01,6.34 18.01,6.55V10.28C18,10.94 17.95,11.59 17.86,12.24H17.85Z" + android:fillColor="#000000"/> + + diff --git a/res/values/strings.xml b/res/values/strings.xml index f47620c1466..dea4320fc22 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4946,6 +4946,8 @@ adjust color turn screen dark, turn screen light + + color contrast @@ -12021,6 +12023,15 @@ Cancel + + Contrast + + Standard + + Medium + + High + "This app can only be opened in 1 window" diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index 3b514b77f9e..9e1dbad8ba3 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -515,6 +515,12 @@ android:title="@string/transparent_navigation_bar" android:summary="@string/transparent_navigation_bar_summary" /> + + { if (hasAlternateButton) { getActivity().setResult(KeyguardManager.RESULT_ALTERNATE); + getActivity().finish(); + } else if (mRemoteValidation) { + onRemoteLockscreenValidationFailure("Forgot lockscreen credential button pressed."); } - getActivity().finish(); }); setupForgotButtonIfManagedProfile(view); @@ -299,17 +309,11 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr if (mRemoteLockscreenValidationClient != null) { mRemoteLockscreenValidationClient.disconnect(); } - if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } super.onDestroy(); } protected abstract void authenticationSucceeded(); - protected abstract void onRemoteDeviceCredentialValidationResult( - RemoteLockscreenValidationResult result); - public void prepareEnterAnimation() { } @@ -411,43 +415,33 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr } protected void validateGuess(LockscreenCredential credentialGuess) { - if (mCheckBox.isChecked()) { - // Keep credential in memory since user wants to set guess as screen lock. - mDeviceCredentialGuess = credentialGuess; - } else if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } - - mRemoteLockscreenValidationClient.validateLockscreenGuess( - encryptDeviceCredentialGuess(credentialGuess.getCredential()), - new IRemoteLockscreenValidationCallback.Stub() { - @Override - public void onSuccess(RemoteLockscreenValidationResult result) { - mHandler.post(()->onRemoteDeviceCredentialValidationResult(result)); - } - - @Override - public void onFailure(String message) { - Log.e(TAG, "A failure occurred while trying " - + "to validate lockscreen guess: " + message); - mHandler.post(()->getActivity().finish()); - } - }); + mRemoteLockscreenValidationFragment.validateLockscreenGuess( + mRemoteLockscreenValidationClient, credentialGuess, + mRemoteLockscreenValidationSession.getSourcePublicKey(), mCheckBox.isChecked()); } - private byte[] encryptDeviceCredentialGuess(byte[] guess) { - try { - byte[] encodedPublicKey = mRemoteLockscreenValidationSession.getSourcePublicKey(); - PublicKey publicKey = SecureBox.decodePublicKey(encodedPublicKey); - return SecureBox.encrypt( - publicKey, - /* sharedSecret= */ null, - LockPatternUtils.ENCRYPTED_REMOTE_CREDENTIALS_HEADER, - guess); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - Log.w(TAG, "Error encrypting device credential guess. Returning empty byte[].", e); - return new byte[0]; + protected void updateRemoteLockscreenValidationViews() { + if (!mRemoteValidation || mRemoteLockscreenValidationFragment == null) { + return; } + + boolean enable = mRemoteLockscreenValidationFragment.isRemoteValidationInProgress(); + mGlifLayout.setProgressBarShown(enable); + mCheckBox.setEnabled(!enable); + mCancelButton.setEnabled(!enable); + } + + /** + * Finishes the activity with result code {@link android.app.Activity#RESULT_FIRST_USER} + * after logging the error message. + * @param message Optional message to log. + */ + public void onRemoteLockscreenValidationFailure(String message) { + if (!TextUtils.isEmpty(message)) { + Log.w(TAG, message); + } + getActivity().setResult(RESULT_FIRST_USER); + getActivity().finish(); } protected abstract void onShowError(); diff --git a/src/com/android/settings/password/ConfirmLockPassword.java b/src/com/android/settings/password/ConfirmLockPassword.java index 81bd8c231a6..03b89f25051 100644 --- a/src/com/android/settings/password/ConfirmLockPassword.java +++ b/src/com/android/settings/password/ConfirmLockPassword.java @@ -71,8 +71,6 @@ import com.android.settings.R; import com.android.settingslib.animation.AppearAnimationUtils; import com.android.settingslib.animation.DisappearAnimationUtils; -import com.google.android.setupdesign.GlifLayout; - import java.util.ArrayList; public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { @@ -127,7 +125,8 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { public static class ConfirmLockPasswordFragment extends ConfirmDeviceCredentialBaseFragment implements OnClickListener, OnEditorActionListener, - CredentialCheckResultTracker.Listener, SaveChosenLockWorkerBase.Listener { + CredentialCheckResultTracker.Listener, SaveChosenLockWorkerBase.Listener, + RemoteLockscreenValidationFragment.Listener { private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result"; private ImeAwareEditText mPasswordEntry; private TextViewInputDisabler mPasswordEntryInputDisabler; @@ -140,7 +139,6 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { private AppearAnimationUtils mAppearAnimationUtils; private DisappearAnimationUtils mDisappearAnimationUtils; private boolean mIsManagedProfile; - private GlifLayout mGlifLayout; private CharSequence mCheckBoxLabel; // required constructor for fragments @@ -255,6 +253,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { ? R.string.lockpassword_forgot_password : R.string.lockpassword_forgot_pin); } + updateRemoteLockscreenValidationViews(); } if (mForgotButton != null) { @@ -405,6 +404,9 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mCountdownTimer = null; } mCredentialCheckResultTracker.setListener(null); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(null, /* handler= */ null); + } } @Override @@ -426,6 +428,9 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId)); } mCredentialCheckResultTracker.setListener(this); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(this, mHandler); + } } @Override @@ -436,13 +441,17 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { private void updatePasswordEntry() { final boolean isLockedOut = mLockPatternUtils.getLockoutAttemptDeadline(mEffectiveUserId) != 0; - mPasswordEntry.setEnabled(!isLockedOut); - mPasswordEntryInputDisabler.setInputEnabled(!isLockedOut); - if (isLockedOut) { - mImm.hideSoftInputFromWindow(mPasswordEntry.getWindowToken(), 0 /*flags*/); - } else { + final boolean isRemoteLockscreenValidationInProgress = + mRemoteLockscreenValidationFragment != null + && mRemoteLockscreenValidationFragment.isRemoteValidationInProgress(); + boolean shouldEnableInput = !isLockedOut && !isRemoteLockscreenValidationInProgress; + mPasswordEntry.setEnabled(shouldEnableInput); + mPasswordEntryInputDisabler.setInputEnabled(shouldEnableInput); + if (shouldEnableInput) { mPasswordEntry.scheduleShowSoftInput(); mPasswordEntry.requestFocus(); + } else { + mImm.hideSoftInputFromWindow(mPasswordEntry.getWindowToken(), /* flags= */0); } } @@ -472,7 +481,8 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { if (mRemoteValidation) { validateGuess(credential); - mGlifLayout.setProgressBarShown(true); + updateRemoteLockscreenValidationViews(); + updatePasswordEntry(); return; } @@ -604,14 +614,15 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } @Override - protected void onRemoteDeviceCredentialValidationResult( + public void onRemoteLockscreenValidationResult( RemoteLockscreenValidationResult result) { switch (result.getResultCode()) { case RemoteLockscreenValidationResult.RESULT_GUESS_VALID: - if (mCheckBox.isChecked()) { + if (mCheckBox.isChecked() && mRemoteLockscreenValidationFragment + .getLockscreenCredential() != null) { + Log.i(TAG, "Setting device screen lock to the other device's screen lock."); ChooseLockPassword.SaveAndFinishWorker saveAndFinishWorker = new ChooseLockPassword.SaveAndFinishWorker(); - Log.i(TAG, "Setting device screen lock to the other device's screen lock."); getFragmentManager().beginTransaction().add(saveAndFinishWorker, null) .commit(); getFragmentManager().executePendingTransactions(); @@ -619,14 +630,14 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { saveAndFinishWorker.start( mLockPatternUtils, /* requestGatekeeperPassword= */ true, - mDeviceCredentialGuess, + mRemoteLockscreenValidationFragment.getLockscreenCredential(), /* currentCredential= */ null, mEffectiveUserId); - return; + } else { + mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), + /* timeoutMs= */ 0, mEffectiveUserId); } - mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), - /* timeoutMs= */ 0, mEffectiveUserId); - break; + return; case RemoteLockscreenValidationResult.RESULT_GUESS_INVALID: mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(), /* timeoutMs= */ 0, mEffectiveUserId); @@ -636,12 +647,15 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { (int) result.getTimeoutMillis(), mEffectiveUserId); break; case RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS: - getActivity().finish(); - break; case RemoteLockscreenValidationResult.RESULT_SESSION_EXPIRED: - getActivity().finish(); + onRemoteLockscreenValidationFailure(String.format( + "Cannot continue remote lockscreen validation. ResultCode=%d", + result.getResultCode())); + break; } - mGlifLayout.setProgressBarShown(false); + updateRemoteLockscreenValidationViews(); + updatePasswordEntry(); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); } @Override @@ -701,21 +715,18 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } /** - * Callback for when the device credential guess used for remote validation was set as the - * current device's device credential. + * Callback for when the current device's lockscreen was set to the guess used for + * remote lockscreen validation. */ @Override public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { - if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } + Log.i(TAG, "Device lockscreen has been set to remote device's lockscreen."); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); Intent result = new Intent(); if (mRemoteValidation && containsGatekeeperPasswordHandle(resultData)) { result.putExtra(EXTRA_KEY_GK_PW_HANDLE, getGatekeeperPasswordHandle(resultData)); } - - mGlifLayout.setProgressBarShown(false); mCredentialCheckResultTracker.setResult(/* matched= */ true, result, /* timeoutMs= */ 0, mEffectiveUserId); } diff --git a/src/com/android/settings/password/ConfirmLockPattern.java b/src/com/android/settings/password/ConfirmLockPattern.java index 7c217399cd5..7db25fd830f 100644 --- a/src/com/android/settings/password/ConfirmLockPattern.java +++ b/src/com/android/settings/password/ConfirmLockPattern.java @@ -59,8 +59,6 @@ import com.android.settingslib.animation.AppearAnimationCreator; import com.android.settingslib.animation.AppearAnimationUtils; import com.android.settingslib.animation.DisappearAnimationUtils; -import com.google.android.setupdesign.GlifLayout; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -97,7 +95,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment implements AppearAnimationCreator, CredentialCheckResultTracker.Listener, - SaveChosenLockWorkerBase.Listener { + SaveChosenLockWorkerBase.Listener, RemoteLockscreenValidationFragment.Listener { private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result"; @@ -107,7 +105,6 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { private boolean mDisappearing = false; private CountDownTimer mCountdownTimer; - private GlifLayout mGlifLayout; private View mSudContent; // caller-supplied text for various prompts @@ -239,6 +236,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { if (mCancelButton != null && TextUtils.isEmpty(mAlternateButtonText)) { mCancelButton.setText(R.string.lockpassword_forgot_pattern); } + updateRemoteLockscreenValidationViews(); } if (mForgotButton != null) { @@ -259,6 +257,9 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { mCountdownTimer.cancel(); } mCredentialCheckResultTracker.setListener(null); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(null, /* handler= */ null); + } } @Override @@ -281,6 +282,12 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { updateStage(Stage.NeedToUnlock); } mCredentialCheckResultTracker.setListener(this); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(this, mHandler); + if (mRemoteLockscreenValidationFragment.isRemoteValidationInProgress()) { + mLockPatternView.setEnabled(false); + } + } } @Override @@ -502,7 +509,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { if (mRemoteValidation) { validateGuess(credential); - mGlifLayout.setProgressBarShown(true); + updateRemoteLockscreenValidationViews(); return; } @@ -617,11 +624,12 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } @Override - protected void onRemoteDeviceCredentialValidationResult( + public void onRemoteLockscreenValidationResult( RemoteLockscreenValidationResult result) { switch (result.getResultCode()) { case RemoteLockscreenValidationResult.RESULT_GUESS_VALID: - if (mCheckBox.isChecked()) { + if (mCheckBox.isChecked() && mRemoteLockscreenValidationFragment + .getLockscreenCredential() != null) { Log.i(TAG, "Setting device screen lock to the other device's screen lock."); ChooseLockPattern.SaveAndFinishWorker saveAndFinishWorker = new ChooseLockPattern.SaveAndFinishWorker(); @@ -632,14 +640,14 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { saveAndFinishWorker.start( mLockPatternUtils, /* requestGatekeeperPassword= */ true, - mDeviceCredentialGuess, + mRemoteLockscreenValidationFragment.getLockscreenCredential(), /* currentCredential= */ null, mEffectiveUserId); - return; + } else { + mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), + /* timeoutMs= */ 0, mEffectiveUserId); } - mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), - /* timeoutMs= */ 0, mEffectiveUserId); - break; + return; case RemoteLockscreenValidationResult.RESULT_GUESS_INVALID: mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(), /* timeoutMs= */ 0, mEffectiveUserId); @@ -649,12 +657,14 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { (int) result.getTimeoutMillis(), mEffectiveUserId); break; case RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS: - getActivity().finish(); - break; case RemoteLockscreenValidationResult.RESULT_SESSION_EXPIRED: - getActivity().finish(); + onRemoteLockscreenValidationFailure(String.format( + "Cannot continue remote lockscreen validation. ResultCode=%d", + result.getResultCode())); + break; } - mGlifLayout.setProgressBarShown(false); + updateRemoteLockscreenValidationViews(); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); } @Override @@ -728,21 +738,18 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } /** - * Callback for when the device credential guess used for remote validation was set as the - * current device's device credential. + * Callback for when the current device's lockscreen to the guess used for + * remote lockscreen validation. */ @Override public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { - if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } + Log.i(TAG, "Device lockscreen has been set to remote device's lockscreen."); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); Intent result = new Intent(); if (mRemoteValidation && containsGatekeeperPasswordHandle(resultData)) { result.putExtra(EXTRA_KEY_GK_PW_HANDLE, getGatekeeperPasswordHandle(resultData)); } - - mGlifLayout.setProgressBarShown(false); mCredentialCheckResultTracker.setResult(/* matched= */ true, result, /* timeoutMs= */ 0, mEffectiveUserId); } diff --git a/src/com/android/settings/password/RemoteLockscreenValidationFragment.java b/src/com/android/settings/password/RemoteLockscreenValidationFragment.java new file mode 100644 index 00000000000..5819376c433 --- /dev/null +++ b/src/com/android/settings/password/RemoteLockscreenValidationFragment.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 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.settings.password; + +import android.app.RemoteLockscreenValidationResult; +import android.os.Bundle; +import android.os.Handler; +import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback; +import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient; +import android.util.Log; + +import androidx.fragment.app.Fragment; + +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockscreenCredential; +import com.android.security.SecureBox; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; + +/** + * A fragment used to hold state for remote lockscreen validation. + * If the original listener is ever re-created, the new listener must be set again using + * {@link #setListener} so that the validation result does not get handled by the old listener. + */ +public class RemoteLockscreenValidationFragment extends Fragment { + + private static final String TAG = RemoteLockscreenValidationFragment.class.getSimpleName(); + + private Listener mListener; + private Handler mHandler; + private boolean mIsInProgress; + private RemoteLockscreenValidationResult mResult; + private String mErrorMessage; + private LockscreenCredential mLockscreenCredential; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public void onDestroy() { + clearLockscreenCredential(); + if (mResult != null && mErrorMessage != null) { + Log.w(TAG, "Unprocessed remote lockscreen validation result"); + } + super.onDestroy(); + } + + /** + * @return {@code true} if remote lockscreen guess validation has started or + * the validation result has not yet been handled. + */ + public boolean isRemoteValidationInProgress() { + return mIsInProgress; + } + + /** + * Sets the listener and handler that will handle the result of remote lockscreen validation. + * Unprocessed results or failures will be handled after the listener is set. + */ + public void setListener(Listener listener, Handler handler) { + if (mListener == listener) { + return; + } + + mListener = listener; + mHandler = handler; + + if (mResult != null) { + handleResult(); + } else if (mErrorMessage != null) { + handleFailure(); + } + } + + /** + * @return {@link LockscreenCredential} if it was cached in {@link #validateLockscreenGuess}. + */ + public LockscreenCredential getLockscreenCredential() { + return mLockscreenCredential; + } + + /** + * Clears the {@link LockscreenCredential} if it was cached in {@link #validateLockscreenGuess}. + */ + public void clearLockscreenCredential() { + if (mLockscreenCredential != null) { + mLockscreenCredential.zeroize(); + mLockscreenCredential = null; + } + } + + /** + * Validates the lockscreen guess on the remote device. + * @param remoteLockscreenValidationClient the client that should be used to send the guess to + * for validation + * @param guess the {@link LockscreenCredential} guess that the user entered + * @param encryptionKey the key that should be used to encrypt the guess before validation + * @param shouldCacheGuess whether to cache to guess so it can be used to set the current + * device's lockscreen after validation succeeds. + */ + public void validateLockscreenGuess( + RemoteLockscreenValidationClient remoteLockscreenValidationClient, + LockscreenCredential guess, byte[] encryptionKey, boolean shouldCacheGuess) { + if (shouldCacheGuess) { + mLockscreenCredential = guess; + } + + remoteLockscreenValidationClient.validateLockscreenGuess( + encryptDeviceCredentialGuess(guess.getCredential(), encryptionKey), + new IRemoteLockscreenValidationCallback.Stub() { + @Override + public void onSuccess(RemoteLockscreenValidationResult result) { + mResult = result; + handleResult(); + } + + @Override + public void onFailure(String message) { + mErrorMessage = message; + handleFailure(); + } + }); + mIsInProgress = true; + } + + private byte[] encryptDeviceCredentialGuess(byte[] guess, byte[] encryptionKey) { + try { + PublicKey publicKey = SecureBox.decodePublicKey(encryptionKey); + return SecureBox.encrypt( + publicKey, + /* sharedSecret= */ null, + LockPatternUtils.ENCRYPTED_REMOTE_CREDENTIALS_HEADER, + guess); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + Log.w(TAG, "Error encrypting device credential guess. Returning empty byte[].", e); + return new byte[0]; + } + } + + private void handleResult() { + if (mHandler != null) { + mHandler.post(()-> { + if (mListener == null || mResult == null) { + return; + } + mIsInProgress = false; + mListener.onRemoteLockscreenValidationResult(mResult); + mResult = null; + }); + } + } + + private void handleFailure() { + if (mHandler != null) { + mHandler.post(()-> { + if (mListener == null || mErrorMessage == null) { + return; + } + mIsInProgress = false; + mListener.onRemoteLockscreenValidationFailure( + String.format("Remote lockscreen validation failed: %s", mErrorMessage)); + mErrorMessage = null; + }); + } + } + + interface Listener { + void onRemoteLockscreenValidationResult(RemoteLockscreenValidationResult result); + void onRemoteLockscreenValidationFailure(String message); + } +} diff --git a/src/com/android/settings/theme/ContrastPreferenceController.kt b/src/com/android/settings/theme/ContrastPreferenceController.kt new file mode 100644 index 00000000000..7f3844a9dd4 --- /dev/null +++ b/src/com/android/settings/theme/ContrastPreferenceController.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 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.settings.theme + +import android.app.UiModeManager +import android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_HIGH +import android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_MEDIUM +import android.app.UiModeManager.ContrastUtils.toContrastLevel +import android.content.Context +import android.content.Intent +import android.os.UserHandle +import android.text.TextUtils +import androidx.preference.Preference +import com.android.internal.annotations.VisibleForTesting +import com.android.settings.R +import com.android.settings.core.BasePreferenceController + +/** + * Controller that opens the contrast dialog and updates the text describing the current contrast + */ +class ContrastPreferenceController( + private val context: Context, + private val uiModeManager: UiModeManager) : BasePreferenceController(context, KEY) { + + companion object { + @VisibleForTesting + const val KEY = "contrast_preference" + } + + override fun getAvailabilityStatus(): Int { + return AVAILABLE + } + + override fun handlePreferenceTreeClick(preference: Preference): Boolean { + if (TextUtils.equals(preference.key, preferenceKey)) { + val intent = Intent(Intent.ACTION_SHOW_CONTRAST_DIALOG) + context.startActivityAsUser(intent, UserHandle(UserHandle.USER_CURRENT)) + return true + } + return false + } + + override fun getSummary(): CharSequence = getSummary(toContrastLevel(uiModeManager.contrast)) + + @VisibleForTesting + fun getSummary(contrast: Int): String { + return when (contrast) { + CONTRAST_LEVEL_HIGH -> context.getString(R.string.contrast_high) + CONTRAST_LEVEL_MEDIUM -> context.getString(R.string.contrast_medium) + else -> context.getString(R.string.contrast_standard) + } + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java index fc3e6c0953b..f4fa397e3d4 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java @@ -394,7 +394,7 @@ public class StylusDevicesControllerTest { assertThat(buttonsPref.isChecked()).isEqualTo(false); assertThat(Settings.Secure.getInt(mContext.getContentResolver(), - Secure.STYLUS_BUTTONS_ENABLED, -1)).isEqualTo(0); + Secure.STYLUS_BUTTONS_ENABLED, -1)).isEqualTo(1); } private void showScreen(StylusDevicesController controller) { diff --git a/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java b/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java index 77a013d5924..d26c33b6e68 100644 --- a/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java +++ b/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java @@ -197,7 +197,7 @@ public class ConfirmLockPasswordTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isTrue(); - assertThat(fragment.mDeviceCredentialGuess).isNotNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test @@ -223,7 +223,7 @@ public class ConfirmLockPasswordTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse(); - assertThat(fragment.mDeviceCredentialGuess).isNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test diff --git a/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java b/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java index 4374e388b79..2ed79885430 100644 --- a/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java +++ b/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java @@ -177,7 +177,7 @@ public class ConfirmLockPatternTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isTrue(); - assertThat(fragment.mDeviceCredentialGuess).isNotNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test @@ -203,7 +203,7 @@ public class ConfirmLockPatternTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse(); - assertThat(fragment.mDeviceCredentialGuess).isNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test diff --git a/tests/robotests/src/com/android/settings/theme/ContrastPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/theme/ContrastPreferenceControllerTest.java new file mode 100644 index 00000000000..dbd33728b85 --- /dev/null +++ b/tests/robotests/src/com/android/settings/theme/ContrastPreferenceControllerTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 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.settings.theme; + +import static android.app.UiModeManager.ContrastUtils; +import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_HIGH; +import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_MEDIUM; +import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_STANDARD; +import static android.provider.Settings.Secure.CONTRAST_LEVEL; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.UiModeManager; +import android.content.Context; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.util.stream.Stream; + +@RunWith(RobolectricTestRunner.class) +public class ContrastPreferenceControllerTest { + + @Rule + public MockitoRule mocks = MockitoJUnit.rule(); + + private ContrastPreferenceController mController; + + @Mock + private UiModeManager mMockUiModeManager; + private Context mContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = ApplicationProvider.getApplicationContext(); + mController = new ContrastPreferenceController(mContext, mMockUiModeManager); + } + + @Test + public void controllerIsAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void testHandlePreferenceTreeClick() { + Preference preference = new Preference(mContext); + preference.setKey(ContrastPreferenceController.KEY); + assertThat(mController.handlePreferenceTreeClick(preference)).isTrue(); + + Preference otherPreference = new Preference(mContext); + otherPreference.setKey("wrong key"); + assertThat(mController.handlePreferenceTreeClick(otherPreference)).isFalse(); + } + + @Test + public void controllerSummary() { + float initialContrast = mContext.getSystemService(UiModeManager.class).getContrast(); + try { + allContrastValues().forEach(contrastLevel -> { + float contrast = ContrastUtils.fromContrastLevel(contrastLevel); + clearInvocations(mMockUiModeManager); + when(mMockUiModeManager.getContrast()).thenReturn(contrast); + String summary = mController.getSummary().toString(); + verify(mMockUiModeManager).getContrast(); + assertThat(summary).isEqualTo(mController.getSummary(contrastLevel)); + }); + } finally { + putContrastInSettings(initialContrast); + } + } + + private static Stream allContrastValues() { + return Stream.of(CONTRAST_LEVEL_STANDARD, CONTRAST_LEVEL_MEDIUM, CONTRAST_LEVEL_HIGH); + } + + private void putContrastInSettings(float contrast) { + Settings.Secure.putFloat(mContext.getContentResolver(), CONTRAST_LEVEL, contrast); + } +}