1 /* 2 * Copyright (C) 2015 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 // TODO (b/35202196): move this class out of the root of the package. 18 package com.android.settings.password; 19 20 import static android.app.Activity.RESULT_FIRST_USER; 21 import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_LOCK_ATTEMPTS_FAILED; 22 23 import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; 24 25 import android.app.Dialog; 26 import android.app.KeyguardManager; 27 import android.app.RemoteLockscreenValidationSession; 28 import android.app.admin.DevicePolicyManager; 29 import android.app.admin.ManagedSubscriptionsPolicy; 30 import android.app.admin.flags.Flags; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.DialogInterface; 34 import android.content.Intent; 35 import android.content.pm.UserInfo; 36 import android.hardware.biometrics.BiometricManager; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.UserHandle; 40 import android.os.UserManager; 41 import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient; 42 import android.telecom.TelecomManager; 43 import android.text.TextUtils; 44 import android.util.FeatureFlagUtils; 45 import android.util.Log; 46 import android.view.View; 47 import android.widget.Button; 48 import android.widget.CheckBox; 49 import android.widget.TextView; 50 51 import androidx.annotation.Nullable; 52 import androidx.appcompat.app.AlertDialog; 53 import androidx.fragment.app.DialogFragment; 54 import androidx.fragment.app.FragmentManager; 55 56 import com.android.internal.widget.LockPatternUtils; 57 import com.android.internal.widget.LockscreenCredential; 58 import com.android.settings.R; 59 import com.android.settings.Utils; 60 import com.android.settings.core.InstrumentedFragment; 61 62 import com.google.android.setupdesign.GlifLayout; 63 64 /** 65 * Base fragment to be shared for PIN/Pattern/Password confirmation fragments. 66 */ 67 public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFragment { 68 public static final String TAG = ConfirmDeviceCredentialBaseFragment.class.getSimpleName(); 69 public static final String TITLE_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.title"; 70 public static final String HEADER_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.header"; 71 public static final String DETAILS_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.details"; 72 public static final String DARK_THEME = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.darkTheme"; 73 public static final String SHOW_CANCEL_BUTTON = 74 SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showCancelButton"; 75 public static final String SHOW_WHEN_LOCKED = 76 SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showWhenLocked"; 77 public static final String USE_FADE_ANIMATION = 78 SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.useFadeAnimation"; 79 public static final String IS_REMOTE_LOCKSCREEN_VALIDATION = 80 SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.isRemoteLockscreenValidation"; 81 82 protected static final int USER_TYPE_PRIMARY = 1; 83 protected static final int USER_TYPE_MANAGED_PROFILE = 2; 84 protected static final int USER_TYPE_SECONDARY = 3; 85 86 /** Time we wait before clearing a wrong input attempt (e.g. pattern) and the error message. */ 87 protected static final long CLEAR_WRONG_ATTEMPT_TIMEOUT_MS = 3000; 88 89 protected static final String FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION = 90 "remote_lockscreen_validation"; 91 92 protected boolean mReturnCredentials = false; 93 protected boolean mReturnGatekeeperPassword = false; 94 protected boolean mForceVerifyPath = false; 95 protected GlifLayout mGlifLayout; 96 protected CheckBox mCheckBox; 97 protected Button mCancelButton; 98 /** Button allowing managed profile password reset, null when is not shown. */ 99 @Nullable protected Button mForgotButton; 100 protected int mEffectiveUserId; 101 protected int mUserId; 102 protected UserManager mUserManager; 103 protected LockPatternUtils mLockPatternUtils; 104 protected DevicePolicyManager mDevicePolicyManager; 105 protected TextView mErrorTextView; 106 protected final Handler mHandler = new Handler(); 107 protected boolean mFrp; 108 protected boolean mRemoteValidation; 109 protected boolean mRequestWriteRepairModePassword; 110 protected boolean mRepairMode; 111 protected CharSequence mAlternateButtonText; 112 protected BiometricManager mBiometricManager; 113 @Nullable protected RemoteLockscreenValidationSession mRemoteLockscreenValidationSession; 114 /** Credential saved so the credential can be set for device if remote validation passes */ 115 @Nullable protected RemoteLockscreenValidationClient mRemoteLockscreenValidationClient; 116 protected RemoteLockscreenValidationFragment mRemoteLockscreenValidationFragment; 117 isInternalActivity()118 private boolean isInternalActivity() { 119 return (getActivity() instanceof ConfirmLockPassword.InternalActivity) 120 || (getActivity() instanceof ConfirmLockPattern.InternalActivity); 121 } 122 123 @Override onCreate(@ullable Bundle savedInstanceState)124 public void onCreate(@Nullable Bundle savedInstanceState) { 125 super.onCreate(savedInstanceState); 126 final Intent intent = getActivity().getIntent(); 127 mAlternateButtonText = intent.getCharSequenceExtra( 128 KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); 129 mReturnCredentials = intent.getBooleanExtra( 130 ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, false); 131 132 mReturnGatekeeperPassword = intent.getBooleanExtra( 133 ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false); 134 mForceVerifyPath = intent.getBooleanExtra( 135 ChooseLockSettingsHelper.EXTRA_KEY_FORCE_VERIFY, false); 136 mRequestWriteRepairModePassword = intent.getBooleanExtra( 137 ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW, false); 138 139 if (intent.getBooleanExtra(IS_REMOTE_LOCKSCREEN_VALIDATION, false)) { 140 if (FeatureFlagUtils.isEnabled(getContext(), 141 FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION)) { 142 mRemoteValidation = true; 143 } else { 144 onRemoteLockscreenValidationFailure( 145 "Remote lockscreen validation not enabled."); 146 } 147 } 148 if (mRemoteValidation) { 149 mRemoteLockscreenValidationSession = intent.getParcelableExtra( 150 KeyguardManager.EXTRA_REMOTE_LOCKSCREEN_VALIDATION_SESSION, 151 RemoteLockscreenValidationSession.class); 152 if (mRemoteLockscreenValidationSession == null 153 || mRemoteLockscreenValidationSession.getRemainingAttempts() == 0) { 154 onRemoteLockscreenValidationFailure("RemoteLockscreenValidationSession is null or " 155 + "no more attempts for remote lockscreen validation."); 156 } 157 158 ComponentName remoteLockscreenValidationServiceComponent = 159 intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName.class); 160 if (remoteLockscreenValidationServiceComponent == null) { 161 onRemoteLockscreenValidationFailure( 162 "RemoteLockscreenValidationService ComponentName is null"); 163 } 164 mRemoteLockscreenValidationClient = RemoteLockscreenValidationClient 165 .create(getContext(), remoteLockscreenValidationServiceComponent); 166 if (!mRemoteLockscreenValidationClient.isServiceAvailable()) { 167 onRemoteLockscreenValidationFailure(String.format( 168 "RemoteLockscreenValidationService at %s is not available", 169 remoteLockscreenValidationServiceComponent.getClassName())); 170 } 171 172 mRemoteLockscreenValidationFragment = 173 (RemoteLockscreenValidationFragment) getFragmentManager() 174 .findFragmentByTag(FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION); 175 if (mRemoteLockscreenValidationFragment == null) { 176 mRemoteLockscreenValidationFragment = new RemoteLockscreenValidationFragment(); 177 getFragmentManager().beginTransaction().add(mRemoteLockscreenValidationFragment, 178 FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION).commit(); 179 } 180 } 181 182 // Only take this argument into account if it belongs to the current profile. 183 mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(), 184 isInternalActivity()); 185 mFrp = (mUserId == LockPatternUtils.USER_FRP); 186 mRepairMode = (mUserId == LockPatternUtils.USER_REPAIR_MODE); 187 mUserManager = UserManager.get(getActivity()); 188 mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId); 189 mLockPatternUtils = new LockPatternUtils(getActivity()); 190 mDevicePolicyManager = (DevicePolicyManager) getActivity().getSystemService( 191 Context.DEVICE_POLICY_SERVICE); 192 mBiometricManager = getActivity().getSystemService(BiometricManager.class); 193 } 194 195 @Override onViewCreated(View view, @Nullable Bundle savedInstanceState)196 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 197 super.onViewCreated(view, savedInstanceState); 198 mCancelButton = view.findViewById(R.id.cancelButton); 199 boolean showCancelButton = mRemoteValidation || getActivity().getIntent().getBooleanExtra( 200 SHOW_CANCEL_BUTTON, false); 201 boolean hasAlternateButton = (mFrp || mRemoteValidation) && !TextUtils.isEmpty( 202 mAlternateButtonText); 203 mCancelButton.setVisibility(showCancelButton || hasAlternateButton 204 ? View.VISIBLE : View.GONE); 205 if (hasAlternateButton) { 206 mCancelButton.setText(mAlternateButtonText); 207 } 208 mCancelButton.setOnClickListener(v -> { 209 if (hasAlternateButton) { 210 getActivity().setResult(KeyguardManager.RESULT_ALTERNATE); 211 getActivity().finish(); 212 } else if (mRemoteValidation) { 213 onRemoteLockscreenValidationFailure("Forgot lockscreen credential button pressed."); 214 } 215 }); 216 setupForgotButtonIfManagedProfile(view); 217 218 mCheckBox = view.findViewById(R.id.checkbox); 219 if (mCheckBox != null && mRemoteValidation) { 220 mCheckBox.setVisibility(View.VISIBLE); 221 } 222 setupEmergencyCallButtonIfManagedSubscription(view); 223 } 224 setupEmergencyCallButtonIfManagedSubscription(View view)225 private void setupEmergencyCallButtonIfManagedSubscription(View view) { 226 int policyType = getContext().getSystemService( 227 DevicePolicyManager.class).getManagedSubscriptionsPolicy().getPolicyType(); 228 229 if (policyType == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) { 230 Button emergencyCallButton = view.findViewById(R.id.emergencyCallButton); 231 if (emergencyCallButton == null) { 232 Log.wtf(TAG, 233 "Emergency call button not found in managed profile credential dialog"); 234 return; 235 } 236 emergencyCallButton.setVisibility(View.VISIBLE); 237 emergencyCallButton.setOnClickListener(v -> { 238 final Intent intent = getActivity() 239 .getSystemService(TelecomManager.class) 240 .createLaunchEmergencyDialerIntent(null) 241 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 242 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 243 getActivity().startActivity(intent); 244 getActivity().finish(); 245 }); 246 } 247 } 248 setupForgotButtonIfManagedProfile(View view)249 private void setupForgotButtonIfManagedProfile(View view) { 250 if (mUserManager.isManagedProfile(mUserId) 251 && mUserManager.isQuietModeEnabled(UserHandle.of(mUserId)) 252 && mDevicePolicyManager.canProfileOwnerResetPasswordWhenLocked(mUserId)) { 253 mForgotButton = view.findViewById(R.id.forgotButton); 254 if (mForgotButton == null) { 255 Log.wtf(TAG, "Forgot button not found in managed profile credential dialog"); 256 return; 257 } 258 mForgotButton.setVisibility(View.VISIBLE); 259 mForgotButton.setOnClickListener(v -> { 260 final Intent intent = new Intent(); 261 intent.setClassName(SETTINGS_PACKAGE_NAME, ForgotPasswordActivity.class.getName()); 262 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 263 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 264 getActivity().startActivity(intent); 265 getActivity().finish(); 266 }); 267 } 268 } 269 270 // User could be locked while Effective user is unlocked even though the effective owns the 271 // credential. Otherwise, fingerprint can't unlock fbe/keystore through 272 // verifyTiedProfileChallenge. In such case, we also wanna show the user message that 273 // fingerprint is disabled due to device restart. isStrongAuthRequired()274 protected boolean isStrongAuthRequired() { 275 return mFrp || mRepairMode 276 || !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId) 277 || !mUserManager.isUserUnlocked(mUserId); 278 } 279 280 @Override onResume()281 public void onResume() { 282 super.onResume(); 283 refreshLockScreen(); 284 } 285 refreshLockScreen()286 protected void refreshLockScreen() { 287 updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId)); 288 } 289 setAccessibilityTitle(CharSequence supplementalText)290 protected void setAccessibilityTitle(CharSequence supplementalText) { 291 Intent intent = getActivity().getIntent(); 292 if (intent != null) { 293 CharSequence titleText = intent.getCharSequenceExtra( 294 ConfirmDeviceCredentialBaseFragment.TITLE_TEXT); 295 if (supplementalText == null) { 296 return; 297 } 298 if (titleText == null) { 299 getActivity().setTitle(supplementalText); 300 } else { 301 String accessibilityTitle = 302 new StringBuilder(titleText).append(",").append(supplementalText).toString(); 303 getActivity().setTitle(Utils.createAccessibleSequence(titleText, accessibilityTitle)); 304 } 305 } 306 } 307 308 @Override onPause()309 public void onPause() { 310 super.onPause(); 311 } 312 313 @Override onDestroy()314 public void onDestroy() { 315 if (mRemoteLockscreenValidationClient != null) { 316 mRemoteLockscreenValidationClient.disconnect(); 317 } 318 super.onDestroy(); 319 } 320 authenticationSucceeded()321 protected abstract void authenticationSucceeded(); 322 prepareEnterAnimation()323 public void prepareEnterAnimation() { 324 } 325 startEnterAnimation()326 public void startEnterAnimation() { 327 } 328 reportFailedAttempt()329 protected void reportFailedAttempt() { 330 updateErrorMessage( 331 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); 332 mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId); 333 } 334 updateErrorMessage(int numAttempts)335 protected void updateErrorMessage(int numAttempts) { 336 final int maxAttempts = 337 mLockPatternUtils.getMaximumFailedPasswordsForWipe(mEffectiveUserId); 338 if (maxAttempts <= 0 || numAttempts <= 0) { 339 return; 340 } 341 342 // Update the on-screen error string 343 if (mErrorTextView != null) { 344 final String message = getActivity().getString( 345 R.string.lock_failed_attempts_before_wipe, numAttempts, maxAttempts); 346 showError(message, 0); 347 } 348 349 // Only show popup dialog before the last attempt and before wipe 350 final int remainingAttempts = maxAttempts - numAttempts; 351 if (remainingAttempts > 1) { 352 return; 353 } 354 final FragmentManager fragmentManager = getChildFragmentManager(); 355 final int userType = getUserTypeForWipe(); 356 if (remainingAttempts == 1) { 357 // Last try 358 final String title = getActivity().getString( 359 R.string.lock_last_attempt_before_wipe_warning_title); 360 final String overrideMessageId = getLastTryOverrideErrorMessageId(userType); 361 final int defaultMessageId = getLastTryDefaultErrorMessage(userType); 362 final String message = mDevicePolicyManager.getResources().getString( 363 overrideMessageId, () -> getString(defaultMessageId)); 364 LastTryDialog.show(fragmentManager, title, message, 365 android.R.string.ok, false /* dismiss */); 366 } else { 367 // Device, profile, or secondary user is wiped 368 final String message = getWipeMessage(userType); 369 LastTryDialog.show(fragmentManager, null /* title */, message, 370 com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, 371 true /* dismiss */); 372 } 373 } 374 getUserTypeForWipe()375 private int getUserTypeForWipe() { 376 final UserInfo userToBeWiped = mUserManager.getUserInfo( 377 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId)); 378 UserHandle primaryUser = UserHandle.SYSTEM; 379 if (Flags.headlessSingleUserFixes()) { 380 UserHandle mainUser = mUserManager.getMainUser(); 381 if (mainUser != null ) { 382 primaryUser = mainUser; 383 } 384 } 385 if (userToBeWiped == null || userToBeWiped.getUserHandle().equals(primaryUser)) { 386 return USER_TYPE_PRIMARY; 387 } else if (userToBeWiped.isManagedProfile()) { 388 return USER_TYPE_MANAGED_PROFILE; 389 } else { 390 return USER_TYPE_SECONDARY; 391 } 392 } 393 getLastTryOverrideErrorMessageId(int userType)394 protected abstract String getLastTryOverrideErrorMessageId(int userType); getLastTryDefaultErrorMessage(int userType)395 protected abstract int getLastTryDefaultErrorMessage(int userType); 396 getWipeMessage(int userType)397 private String getWipeMessage(int userType) { 398 switch (userType) { 399 case USER_TYPE_PRIMARY: 400 return getString(com.android.settingslib 401 .R.string.failed_attempts_now_wiping_device); 402 case USER_TYPE_MANAGED_PROFILE: 403 return mDevicePolicyManager.getResources().getString( 404 WORK_PROFILE_LOCK_ATTEMPTS_FAILED, 405 () -> getString( 406 com.android.settingslib.R.string.failed_attempts_now_wiping_profile)); 407 case USER_TYPE_SECONDARY: 408 return getString(com.android.settingslib.R.string.failed_attempts_now_wiping_user); 409 default: 410 throw new IllegalArgumentException("Unrecognized user type:" + userType); 411 } 412 } 413 414 private final Runnable mResetErrorRunnable = new Runnable() { 415 @Override 416 public void run() { 417 mErrorTextView.setText(""); 418 } 419 }; 420 showError(CharSequence msg, long timeout)421 protected void showError(CharSequence msg, long timeout) { 422 mErrorTextView.setText(msg); 423 onShowError(); 424 mHandler.removeCallbacks(mResetErrorRunnable); 425 if (timeout != 0) { 426 mHandler.postDelayed(mResetErrorRunnable, timeout); 427 } 428 } 429 clearResetErrorRunnable()430 protected void clearResetErrorRunnable() { 431 mHandler.removeCallbacks(mResetErrorRunnable); 432 } 433 validateGuess(LockscreenCredential credentialGuess)434 protected void validateGuess(LockscreenCredential credentialGuess) { 435 mRemoteLockscreenValidationFragment.validateLockscreenGuess( 436 mRemoteLockscreenValidationClient, credentialGuess, 437 mRemoteLockscreenValidationSession.getSourcePublicKey(), mCheckBox.isChecked()); 438 } 439 updateRemoteLockscreenValidationViews()440 protected void updateRemoteLockscreenValidationViews() { 441 if (!mRemoteValidation || mRemoteLockscreenValidationFragment == null) { 442 return; 443 } 444 445 boolean enable = mRemoteLockscreenValidationFragment.isRemoteValidationInProgress(); 446 mGlifLayout.setProgressBarShown(enable); 447 mCheckBox.setEnabled(!enable); 448 mCancelButton.setEnabled(!enable); 449 } 450 451 /** 452 * Finishes the activity with result code {@link android.app.Activity#RESULT_FIRST_USER} 453 * after logging the error message. 454 * @param message Optional message to log. 455 */ onRemoteLockscreenValidationFailure(String message)456 public void onRemoteLockscreenValidationFailure(String message) { 457 if (!TextUtils.isEmpty(message)) { 458 Log.w(TAG, message); 459 } 460 getActivity().setResult(RESULT_FIRST_USER); 461 getActivity().finish(); 462 } 463 onShowError()464 protected abstract void onShowError(); 465 showError(int msg, long timeout)466 protected void showError(int msg, long timeout) { 467 showError(getText(msg), timeout); 468 } 469 470 public static class LastTryDialog extends DialogFragment { 471 private static final String TAG = LastTryDialog.class.getSimpleName(); 472 473 private static final String ARG_TITLE = "title"; 474 private static final String ARG_MESSAGE = "message"; 475 private static final String ARG_BUTTON = "button"; 476 private static final String ARG_DISMISS = "dismiss"; 477 show(FragmentManager from, String title, String message, int button, boolean dismiss)478 static boolean show(FragmentManager from, String title, String message, int button, 479 boolean dismiss) { 480 LastTryDialog existent = (LastTryDialog) from.findFragmentByTag(TAG); 481 if (existent != null && !existent.isRemoving()) { 482 return false; 483 } 484 Bundle args = new Bundle(); 485 args.putString(ARG_TITLE, title); 486 args.putString(ARG_MESSAGE, message); 487 args.putInt(ARG_BUTTON, button); 488 args.putBoolean(ARG_DISMISS, dismiss); 489 490 DialogFragment dialog = new LastTryDialog(); 491 dialog.setArguments(args); 492 dialog.show(from, TAG); 493 from.executePendingTransactions(); 494 return true; 495 } 496 hide(FragmentManager from)497 static void hide(FragmentManager from) { 498 LastTryDialog dialog = (LastTryDialog) from.findFragmentByTag(TAG); 499 if (dialog != null) { 500 dialog.dismissAllowingStateLoss(); 501 from.executePendingTransactions(); 502 } 503 } 504 505 /** 506 * Dialog setup. 507 * <p> 508 * To make it less likely that the dialog is dismissed accidentally, for example if the 509 * device is malfunctioning or if the device is in a pocket, we set 510 * {@code setCanceledOnTouchOutside(false)}. 511 */ 512 @Override onCreateDialog(Bundle savedInstanceState)513 public Dialog onCreateDialog(Bundle savedInstanceState) { 514 Dialog dialog = new AlertDialog.Builder(getActivity()) 515 .setTitle(getArguments().getString(ARG_TITLE)) 516 .setMessage(getArguments().getString(ARG_MESSAGE)) 517 .setPositiveButton(getArguments().getInt(ARG_BUTTON), null) 518 .create(); 519 dialog.setCanceledOnTouchOutside(false); 520 return dialog; 521 } 522 523 @Override onDismiss(final DialogInterface dialog)524 public void onDismiss(final DialogInterface dialog) { 525 super.onDismiss(dialog); 526 if (getActivity() != null && getArguments().getBoolean(ARG_DISMISS)) { 527 getActivity().finish(); 528 } 529 } 530 } 531 } 532