• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.password;
18 
19 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
20 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
21 import static android.app.admin.DevicePolicyResources.Strings.Settings.PASSWORD_RECENTLY_USED;
22 import static android.app.admin.DevicePolicyResources.Strings.Settings.PIN_RECENTLY_USED;
23 import static android.app.admin.DevicePolicyResources.Strings.Settings.REENTER_WORK_PROFILE_PASSWORD_HEADER;
24 import static android.app.admin.DevicePolicyResources.Strings.Settings.REENTER_WORK_PROFILE_PIN_HEADER;
25 import static android.app.admin.DevicePolicyResources.Strings.Settings.SET_WORK_PROFILE_PASSWORD_HEADER;
26 import static android.app.admin.DevicePolicyResources.Strings.Settings.SET_WORK_PROFILE_PIN_HEADER;
27 import static android.app.admin.DevicePolicyResources.UNDEFINED;
28 
29 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
30 import static com.android.internal.widget.PasswordValidationError.CONTAINS_INVALID_CHARACTERS;
31 import static com.android.internal.widget.PasswordValidationError.CONTAINS_SEQUENCE;
32 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_DIGITS;
33 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LETTERS;
34 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LOWER_CASE;
35 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_DIGITS;
36 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_LETTER;
37 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_SYMBOLS;
38 import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_UPPER_CASE;
39 import static com.android.internal.widget.PasswordValidationError.RECENTLY_USED;
40 import static com.android.internal.widget.PasswordValidationError.TOO_LONG;
41 import static com.android.internal.widget.PasswordValidationError.TOO_SHORT;
42 import static com.android.internal.widget.PasswordValidationError.TOO_SHORT_WHEN_ALL_NUMERIC;
43 import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL;
44 import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_UNIFICATION_PROFILE_ID;
45 
46 import android.app.Activity;
47 import android.app.admin.DevicePolicyManager;
48 import android.app.admin.DevicePolicyManager.PasswordComplexity;
49 import android.app.admin.PasswordMetrics;
50 import android.app.settings.SettingsEnums;
51 import android.content.Context;
52 import android.content.Intent;
53 import android.graphics.Insets;
54 import android.graphics.Typeface;
55 import android.os.Bundle;
56 import android.os.Handler;
57 import android.os.Message;
58 import android.os.UserHandle;
59 import android.os.UserManager;
60 import android.text.Editable;
61 import android.text.InputType;
62 import android.text.Selection;
63 import android.text.Spannable;
64 import android.text.TextUtils;
65 import android.text.TextWatcher;
66 import android.util.Log;
67 import android.util.Pair;
68 import android.view.KeyEvent;
69 import android.view.LayoutInflater;
70 import android.view.View;
71 import android.view.ViewGroup;
72 import android.view.WindowManager;
73 import android.view.inputmethod.EditorInfo;
74 import android.widget.ImeAwareEditText;
75 import android.widget.TextView;
76 import android.widget.TextView.OnEditorActionListener;
77 
78 import androidx.annotation.StringRes;
79 import androidx.fragment.app.Fragment;
80 import androidx.recyclerview.widget.LinearLayoutManager;
81 import androidx.recyclerview.widget.RecyclerView;
82 
83 import com.android.internal.annotations.VisibleForTesting;
84 import com.android.internal.widget.LockPatternUtils;
85 import com.android.internal.widget.LockscreenCredential;
86 import com.android.internal.widget.PasswordValidationError;
87 import com.android.internal.widget.TextViewInputDisabler;
88 import com.android.internal.widget.VerifyCredentialResponse;
89 import com.android.settings.EncryptionInterstitial;
90 import com.android.settings.R;
91 import com.android.settings.SettingsActivity;
92 import com.android.settings.SetupWizardUtils;
93 import com.android.settings.Utils;
94 import com.android.settings.core.InstrumentedFragment;
95 import com.android.settings.notification.RedactionInterstitial;
96 import com.android.settingslib.utils.StringUtil;
97 
98 import com.google.android.setupcompat.template.FooterBarMixin;
99 import com.google.android.setupcompat.template.FooterButton;
100 import com.google.android.setupdesign.GlifLayout;
101 import com.google.android.setupdesign.util.ThemeHelper;
102 
103 import java.util.ArrayList;
104 import java.util.Collections;
105 import java.util.List;
106 
107 public class ChooseLockPassword extends SettingsActivity {
108     private static final String TAG = "ChooseLockPassword";
109 
110     static final String EXTRA_KEY_MIN_METRICS = "min_metrics";
111     static final String EXTRA_KEY_MIN_COMPLEXITY = "min_complexity";
112 
113     @Override
getIntent()114     public Intent getIntent() {
115         Intent modIntent = new Intent(super.getIntent());
116         modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName());
117         return modIntent;
118     }
119 
120     public static class IntentBuilder {
121 
122         private final Intent mIntent;
123 
IntentBuilder(Context context)124         public IntentBuilder(Context context) {
125             mIntent = new Intent(context, ChooseLockPassword.class);
126             mIntent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, false);
127             mIntent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, false);
128         }
129 
130         /**
131          * Sets the intended credential type i.e. whether it's numeric PIN or general password
132          * @param passwordType password type represented by one of the {@code PASSWORD_QUALITY_}
133          *   constants.
134          */
setPasswordType(int passwordType)135         public IntentBuilder setPasswordType(int passwordType) {
136             mIntent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, passwordType);
137             return this;
138         }
139 
setUserId(int userId)140         public IntentBuilder setUserId(int userId) {
141             mIntent.putExtra(Intent.EXTRA_USER_ID, userId);
142             return this;
143         }
144 
setRequestGatekeeperPasswordHandle( boolean requestGatekeeperPasswordHandle)145         public IntentBuilder setRequestGatekeeperPasswordHandle(
146                 boolean requestGatekeeperPasswordHandle) {
147             mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE,
148                     requestGatekeeperPasswordHandle);
149             return this;
150         }
151 
setPassword(LockscreenCredential password)152         public IntentBuilder setPassword(LockscreenCredential password) {
153             mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, password);
154             return this;
155         }
156 
setForFingerprint(boolean forFingerprint)157         public IntentBuilder setForFingerprint(boolean forFingerprint) {
158             mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, forFingerprint);
159             return this;
160         }
161 
setForFace(boolean forFace)162         public IntentBuilder setForFace(boolean forFace) {
163             mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, forFace);
164             return this;
165         }
166 
setForBiometrics(boolean forBiometrics)167         public IntentBuilder setForBiometrics(boolean forBiometrics) {
168             mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, forBiometrics);
169             return this;
170         }
171 
172         /** Sets the minimum password requirement in terms of complexity and metrics */
setPasswordRequirement(@asswordComplexity int level, PasswordMetrics metrics)173         public IntentBuilder setPasswordRequirement(@PasswordComplexity int level,
174                 PasswordMetrics metrics) {
175             mIntent.putExtra(EXTRA_KEY_MIN_COMPLEXITY, level);
176             mIntent.putExtra(EXTRA_KEY_MIN_METRICS, metrics);
177             return this;
178         }
179 
180         /**
181          * Configures the launch such that at the end of the password enrollment, one of its
182          * managed profile (specified by {@code profileId}) will have its lockscreen unified
183          * to the parent user. The profile's current lockscreen credential needs to be specified by
184          * {@code credential}.
185          */
setProfileToUnify(int profileId, LockscreenCredential credential)186         public IntentBuilder setProfileToUnify(int profileId, LockscreenCredential credential) {
187             mIntent.putExtra(EXTRA_KEY_UNIFICATION_PROFILE_ID, profileId);
188             mIntent.putExtra(EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL, credential);
189             return this;
190         }
191 
build()192         public Intent build() {
193             return mIntent;
194         }
195     }
196 
197     @Override
isValidFragment(String fragmentName)198     protected boolean isValidFragment(String fragmentName) {
199         if (ChooseLockPasswordFragment.class.getName().equals(fragmentName)) return true;
200         return false;
201     }
202 
203     @Override
isToolbarEnabled()204     protected boolean isToolbarEnabled() {
205         return false;
206     }
207 
getFragmentClass()208     /* package */ Class<? extends Fragment> getFragmentClass() {
209         return ChooseLockPasswordFragment.class;
210     }
211 
212     @Override
onCreate(Bundle savedInstanceState)213     protected void onCreate(Bundle savedInstanceState) {
214         setTheme(SetupWizardUtils.getTheme(this, getIntent()));
215         ThemeHelper.trySetDynamicColor(this);
216         super.onCreate(savedInstanceState);
217         findViewById(R.id.content_parent).setFitsSystemWindows(false);
218         getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
219     }
220 
221     public static class ChooseLockPasswordFragment extends InstrumentedFragment
222             implements OnEditorActionListener, TextWatcher, SaveAndFinishWorker.Listener {
223         private static final String KEY_FIRST_PASSWORD = "first_password";
224         private static final String KEY_UI_STAGE = "ui_stage";
225         private static final String KEY_CURRENT_CREDENTIAL = "current_credential";
226         private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker";
227 
228         private LockscreenCredential mCurrentCredential;
229         private LockscreenCredential mChosenPassword;
230         private boolean mRequestGatekeeperPassword;
231         private ImeAwareEditText mPasswordEntry;
232         private TextViewInputDisabler mPasswordEntryInputDisabler;
233 
234         // Minimum password metrics enforced by admins.
235         private PasswordMetrics mMinMetrics;
236         private List<PasswordValidationError> mValidationErrors;
237 
238         @PasswordComplexity private int mMinComplexity = PASSWORD_COMPLEXITY_NONE;
239         protected int mUserId;
240         private byte[] mPasswordHistoryHashFactor;
241         private int mUnificationProfileId = UserHandle.USER_NULL;
242 
243         private LockPatternUtils mLockPatternUtils;
244         private SaveAndFinishWorker mSaveAndFinishWorker;
245         private int mPasswordType = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
246         protected Stage mUiStage = Stage.Introduction;
247         private PasswordRequirementAdapter mPasswordRequirementAdapter;
248         private GlifLayout mLayout;
249         protected boolean mForFingerprint;
250         protected boolean mForFace;
251         protected boolean mForBiometrics;
252 
253         private LockscreenCredential mFirstPassword;
254         private RecyclerView mPasswordRestrictionView;
255         protected boolean mIsAlphaMode;
256         protected boolean mIsManagedProfile;
257         protected FooterButton mSkipOrClearButton;
258         private FooterButton mNextButton;
259         private TextView mMessage;
260 
261         private TextChangedHandler mTextChangedHandler;
262 
263         private static final int CONFIRM_EXISTING_REQUEST = 58;
264         static final int RESULT_FINISHED = RESULT_FIRST_USER;
265 
266         /**
267          * Keep track internally of where the user is in choosing a pattern.
268          */
269         protected enum Stage {
270 
271             Introduction(
272                     R.string.lockpassword_choose_your_password_header, // password
273                     SET_WORK_PROFILE_PASSWORD_HEADER,
274                     R.string.lockpassword_choose_your_profile_password_header,
275                     R.string.lockpassword_choose_your_password_header_for_fingerprint,
276                     R.string.lockpassword_choose_your_password_header_for_face,
277                     R.string.lockpassword_choose_your_password_header_for_biometrics,
278                     R.string.lockpassword_choose_your_pin_header, // pin
279                     SET_WORK_PROFILE_PIN_HEADER,
280                     R.string.lockpassword_choose_your_profile_pin_header,
281                     R.string.lockpassword_choose_your_pin_header_for_fingerprint,
282                     R.string.lockpassword_choose_your_pin_header_for_face,
283                     R.string.lockpassword_choose_your_pin_header_for_biometrics,
284                     R.string.lock_settings_picker_biometrics_added_security_message,
285                     R.string.lock_settings_picker_biometrics_added_security_message,
286                     R.string.next_label),
287 
288             NeedToConfirm(
289                     R.string.lockpassword_confirm_your_password_header,
290                     REENTER_WORK_PROFILE_PASSWORD_HEADER,
291                     R.string.lockpassword_reenter_your_profile_password_header,
292                     R.string.lockpassword_confirm_your_password_header,
293                     R.string.lockpassword_confirm_your_password_header,
294                     R.string.lockpassword_confirm_your_password_header,
295                     R.string.lockpassword_confirm_your_pin_header,
296                     REENTER_WORK_PROFILE_PIN_HEADER,
297                     R.string.lockpassword_reenter_your_profile_pin_header,
298                     R.string.lockpassword_confirm_your_pin_header,
299                     R.string.lockpassword_confirm_your_pin_header,
300                     R.string.lockpassword_confirm_your_pin_header,
301                     0,
302                     0,
303                     R.string.lockpassword_confirm_label),
304 
305             ConfirmWrong(
306                     R.string.lockpassword_confirm_passwords_dont_match,
307                     UNDEFINED,
308                     R.string.lockpassword_confirm_passwords_dont_match,
309                     R.string.lockpassword_confirm_passwords_dont_match,
310                     R.string.lockpassword_confirm_passwords_dont_match,
311                     R.string.lockpassword_confirm_passwords_dont_match,
312                     R.string.lockpassword_confirm_pins_dont_match,
313                     UNDEFINED,
314                     R.string.lockpassword_confirm_pins_dont_match,
315                     R.string.lockpassword_confirm_pins_dont_match,
316                     R.string.lockpassword_confirm_pins_dont_match,
317                     R.string.lockpassword_confirm_pins_dont_match,
318                     0,
319                     0,
320                     R.string.lockpassword_confirm_label);
321 
Stage(int hintInAlpha, String hintOverrideInAlphaForProfile, int hintInAlphaForProfile, int hintInAlphaForFingerprint, int hintInAlphaForFace, int hintInAlphaForBiometrics, int hintInNumeric, String hintOverrideInNumericForProfile, int hintInNumericForProfile, int hintInNumericForFingerprint, int hintInNumericForFace, int hintInNumericForBiometrics, int messageInAlphaForBiometrics, int messageInNumericForBiometrics, int nextButtonText)322             Stage(int hintInAlpha,
323                     String hintOverrideInAlphaForProfile,
324                     int hintInAlphaForProfile,
325                     int hintInAlphaForFingerprint,
326                     int hintInAlphaForFace,
327                     int hintInAlphaForBiometrics,
328                     int hintInNumeric,
329                     String hintOverrideInNumericForProfile,
330                     int hintInNumericForProfile,
331                     int hintInNumericForFingerprint,
332                     int hintInNumericForFace,
333                     int hintInNumericForBiometrics,
334                     int messageInAlphaForBiometrics,
335                     int messageInNumericForBiometrics,
336                     int nextButtonText) {
337 
338                 this.alphaHint = hintInAlpha;
339                 this.alphaHintOverrideForProfile = hintOverrideInAlphaForProfile;
340                 this.alphaHintForProfile = hintInAlphaForProfile;
341                 this.alphaHintForFingerprint = hintInAlphaForFingerprint;
342                 this.alphaHintForFace = hintInAlphaForFace;
343                 this.alphaHintForBiometrics = hintInAlphaForBiometrics;
344 
345                 this.numericHint = hintInNumeric;
346                 this.numericHintOverrideForProfile = hintOverrideInNumericForProfile;
347                 this.numericHintForProfile = hintInNumericForProfile;
348                 this.numericHintForFingerprint = hintInNumericForFingerprint;
349                 this.numericHintForFace = hintInNumericForFace;
350                 this.numericHintForBiometrics = hintInNumericForBiometrics;
351 
352                 this.alphaMessageForBiometrics = messageInAlphaForBiometrics;
353                 this.numericMessageForBiometrics = messageInNumericForBiometrics;
354 
355                 this.buttonText = nextButtonText;
356             }
357 
358             public static final int TYPE_NONE = 0;
359             public static final int TYPE_FINGERPRINT = 1;
360             public static final int TYPE_FACE = 2;
361             public static final int TYPE_BIOMETRIC = 3;
362 
363             // Password header
364             public final int alphaHint;
365             public final String alphaHintOverrideForProfile;
366             public final int alphaHintForProfile;
367             public final int alphaHintForFingerprint;
368             public final int alphaHintForFace;
369             public final int alphaHintForBiometrics;
370 
371             // PIN header
372             public final int numericHint;
373             public final String numericHintOverrideForProfile;
374             public final int numericHintForProfile;
375             public final int numericHintForFingerprint;
376             public final int numericHintForFace;
377             public final int numericHintForBiometrics;
378 
379             // Password description
380             public final int alphaMessageForBiometrics;
381 
382             // PIN description
383             public final int numericMessageForBiometrics;
384 
385             public final int buttonText;
386 
getHint(Context context, boolean isAlpha, int type, boolean isProfile)387             public String getHint(Context context, boolean isAlpha, int type, boolean isProfile) {
388                 if (isAlpha) {
389                     if (type == TYPE_FINGERPRINT) {
390                         return context.getString(alphaHintForFingerprint);
391                     } else if (type == TYPE_FACE) {
392                         return context.getString(alphaHintForFace);
393                     } else if (type == TYPE_BIOMETRIC) {
394                         return context.getString(alphaHintForBiometrics);
395                     } else if (isProfile) {
396                         return context.getSystemService(DevicePolicyManager.class).getResources()
397                                 .getString(alphaHintOverrideForProfile,
398                                         () -> context.getString(alphaHintForProfile));
399                     } else {
400                         return context.getString(alphaHint);
401                     }
402                 } else {
403                     if (type == TYPE_FINGERPRINT) {
404                         return context.getString(numericHintForFingerprint);
405                     } else if (type == TYPE_FACE) {
406                         return context.getString(numericHintForFace);
407                     } else if (type == TYPE_BIOMETRIC) {
408                         return context.getString(numericHintForBiometrics);
409                     } else if (isProfile) {
410                         return context.getSystemService(DevicePolicyManager.class).getResources()
411                                 .getString(numericHintOverrideForProfile,
412                                         () -> context.getString(numericHintForProfile));
413                     } else {
414                         return  context.getString(numericHint);
415                     }
416                 }
417             }
418 
getMessage(boolean isAlpha, int type)419             public @StringRes int getMessage(boolean isAlpha, int type) {
420                 switch (type) {
421                     case TYPE_FINGERPRINT:
422                     case TYPE_FACE:
423                     case TYPE_BIOMETRIC:
424                         return isAlpha ? alphaMessageForBiometrics : numericMessageForBiometrics;
425 
426                     case TYPE_NONE:
427                     default:
428                         return 0;
429                 }
430             }
431         }
432 
433         // required constructor for fragments
ChooseLockPasswordFragment()434         public ChooseLockPasswordFragment() {
435 
436         }
437 
438         @Override
onCreate(Bundle savedInstanceState)439         public void onCreate(Bundle savedInstanceState) {
440             super.onCreate(savedInstanceState);
441             mLockPatternUtils = new LockPatternUtils(getActivity());
442             Intent intent = getActivity().getIntent();
443             if (!(getActivity() instanceof ChooseLockPassword)) {
444                 throw new SecurityException("Fragment contained in wrong activity");
445             }
446             // Only take this argument into account if it belongs to the current profile.
447             mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras());
448             mIsManagedProfile = UserManager.get(getActivity()).isManagedProfile(mUserId);
449             mForFingerprint = intent.getBooleanExtra(
450                     ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false);
451             mForFace = intent.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false);
452             mForBiometrics = intent.getBooleanExtra(
453                     ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, false);
454 
455             mPasswordType = intent.getIntExtra(
456                     LockPatternUtils.PASSWORD_TYPE_KEY, PASSWORD_QUALITY_NUMERIC);
457             mUnificationProfileId = intent.getIntExtra(
458                     EXTRA_KEY_UNIFICATION_PROFILE_ID, UserHandle.USER_NULL);
459 
460             mMinComplexity = intent.getIntExtra(EXTRA_KEY_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE);
461             mMinMetrics = intent.getParcelableExtra(EXTRA_KEY_MIN_METRICS);
462             if (mMinMetrics == null) mMinMetrics = new PasswordMetrics(CREDENTIAL_TYPE_NONE);
463 
464             if (intent.getBooleanExtra(
465                     ChooseLockSettingsHelper.EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT, false)) {
466                 SaveAndFinishWorker w = new SaveAndFinishWorker();
467                 final boolean required = getActivity().getIntent().getBooleanExtra(
468                         EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
469                 LockscreenCredential currentCredential = intent.getParcelableExtra(
470                         ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
471 
472                 final LockPatternUtils utils = new LockPatternUtils(getActivity());
473 
474                 w.setBlocking(true);
475                 w.setListener(this);
476                 w.start(utils, required, false /* requestGatekeeperPassword */, currentCredential,
477                         currentCredential, mUserId);
478             }
479             mTextChangedHandler = new TextChangedHandler();
480         }
481 
482         @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)483         public View onCreateView(LayoutInflater inflater, ViewGroup container,
484                 Bundle savedInstanceState) {
485             return inflater.inflate(R.layout.choose_lock_password, container, false);
486         }
487 
488         @Override
onViewCreated(View view, Bundle savedInstanceState)489         public void onViewCreated(View view, Bundle savedInstanceState) {
490             super.onViewCreated(view, savedInstanceState);
491 
492             mLayout = (GlifLayout) view;
493 
494             // Make the password container consume the optical insets so the edit text is aligned
495             // with the sides of the parent visually.
496             ViewGroup container = view.findViewById(R.id.password_container);
497             container.setOpticalInsets(Insets.NONE);
498 
499             final FooterBarMixin mixin = mLayout.getMixin(FooterBarMixin.class);
500             mixin.setSecondaryButton(
501                     new FooterButton.Builder(getActivity())
502                             .setText(R.string.lockpassword_clear_label)
503                             .setListener(this::onSkipOrClearButtonClick)
504                             .setButtonType(FooterButton.ButtonType.SKIP)
505                             .setTheme(R.style.SudGlifButton_Secondary)
506                             .build()
507             );
508             mixin.setPrimaryButton(
509                     new FooterButton.Builder(getActivity())
510                             .setText(R.string.next_label)
511                             .setListener(this::onNextButtonClick)
512                             .setButtonType(FooterButton.ButtonType.NEXT)
513                             .setTheme(R.style.SudGlifButton_Primary)
514                             .build()
515             );
516             mSkipOrClearButton = mixin.getSecondaryButton();
517             mNextButton = mixin.getPrimaryButton();
518 
519             mMessage = view.findViewById(R.id.sud_layout_description);
520             mLayout.setIcon(getActivity().getDrawable(R.drawable.ic_lock));
521 
522             mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mPasswordType
523                     || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mPasswordType
524                     || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mPasswordType;
525 
526             setupPasswordRequirementsView(view);
527 
528             mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
529             mPasswordEntry = view.findViewById(R.id.password_entry);
530             mPasswordEntry.setOnEditorActionListener(this);
531             mPasswordEntry.addTextChangedListener(this);
532             mPasswordEntry.requestFocus();
533             mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry);
534 
535             final Activity activity = getActivity();
536 
537             int currentType = mPasswordEntry.getInputType();
538             mPasswordEntry.setInputType(mIsAlphaMode ? currentType
539                     : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD));
540             if (mIsAlphaMode) {
541                 mPasswordEntry.setContentDescription(
542                         getString(R.string.unlock_set_unlock_password_title));
543             } else {
544                 mPasswordEntry.setContentDescription(
545                         getString(R.string.unlock_set_unlock_pin_title));
546             }
547             // Can't set via XML since setInputType resets the fontFamily to null
548             mPasswordEntry.setTypeface(Typeface.create(
549                     getContext().getString(com.android.internal.R.string.config_headlineFontFamily),
550                     Typeface.NORMAL));
551 
552             Intent intent = getActivity().getIntent();
553             final boolean confirmCredentials = intent.getBooleanExtra(
554                     ChooseLockGeneric.CONFIRM_CREDENTIALS, true);
555             mCurrentCredential = intent.getParcelableExtra(
556                     ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
557             mRequestGatekeeperPassword = intent.getBooleanExtra(
558                     ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false);
559             if (savedInstanceState == null) {
560                 updateStage(Stage.Introduction);
561                 if (confirmCredentials) {
562                     final ChooseLockSettingsHelper.Builder builder =
563                             new ChooseLockSettingsHelper.Builder(getActivity());
564                     builder.setRequestCode(CONFIRM_EXISTING_REQUEST)
565                             .setTitle(getString(R.string.unlock_set_unlock_launch_picker_title))
566                             .setReturnCredentials(true)
567                             .setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
568                             .setUserId(mUserId)
569                             .show();
570                 }
571             } else {
572 
573                 // restore from previous state
574                 mFirstPassword = savedInstanceState.getParcelable(KEY_FIRST_PASSWORD);
575                 final String state = savedInstanceState.getString(KEY_UI_STAGE);
576                 if (state != null) {
577                     mUiStage = Stage.valueOf(state);
578                     updateStage(mUiStage);
579                 }
580 
581                 mCurrentCredential = savedInstanceState.getParcelable(KEY_CURRENT_CREDENTIAL);
582 
583                 // Re-attach to the exiting worker if there is one.
584                 mSaveAndFinishWorker = (SaveAndFinishWorker) getFragmentManager().findFragmentByTag(
585                         FRAGMENT_TAG_SAVE_AND_FINISH);
586             }
587 
588             if (activity instanceof SettingsActivity) {
589                 final SettingsActivity sa = (SettingsActivity) activity;
590                 String title = Stage.Introduction.getHint(
591                         getContext(), mIsAlphaMode, getStageType(), mIsManagedProfile);
592                 sa.setTitle(title);
593                 mLayout.setHeaderText(title);
594             }
595         }
596 
597         @Override
onDestroy()598         public void onDestroy() {
599             super.onDestroy();
600             if (mCurrentCredential != null) {
601                 mCurrentCredential.zeroize();
602             }
603             // Force a garbage collection immediately to remove remnant of user password shards
604             // from memory.
605             System.gc();
606             System.runFinalization();
607             System.gc();
608         }
609 
getStageType()610         protected int getStageType() {
611             if (mForFingerprint) {
612                 return Stage.TYPE_FINGERPRINT;
613             } else if (mForFace) {
614                 return Stage.TYPE_FACE;
615             } else if (mForBiometrics) {
616                 return Stage.TYPE_BIOMETRIC;
617             } else {
618                 return Stage.TYPE_NONE;
619             }
620         }
621 
setupPasswordRequirementsView(View view)622         private void setupPasswordRequirementsView(View view) {
623             mPasswordRestrictionView = view.findViewById(R.id.password_requirements_view);
624             mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
625             mPasswordRequirementAdapter = new PasswordRequirementAdapter();
626             mPasswordRestrictionView.setAdapter(mPasswordRequirementAdapter);
627         }
628 
629         @Override
getMetricsCategory()630         public int getMetricsCategory() {
631             return SettingsEnums.CHOOSE_LOCK_PASSWORD;
632         }
633 
634         @Override
onResume()635         public void onResume() {
636             super.onResume();
637             updateStage(mUiStage);
638             if (mSaveAndFinishWorker != null) {
639                 mSaveAndFinishWorker.setListener(this);
640             } else {
641                 mPasswordEntry.requestFocus();
642                 mPasswordEntry.scheduleShowSoftInput();
643             }
644         }
645 
646         @Override
onPause()647         public void onPause() {
648             if (mSaveAndFinishWorker != null) {
649                 mSaveAndFinishWorker.setListener(null);
650             }
651             super.onPause();
652         }
653 
654         @Override
onSaveInstanceState(Bundle outState)655         public void onSaveInstanceState(Bundle outState) {
656             super.onSaveInstanceState(outState);
657             outState.putString(KEY_UI_STAGE, mUiStage.name());
658             outState.putParcelable(KEY_FIRST_PASSWORD, mFirstPassword);
659             if (mCurrentCredential != null) {
660                 outState.putParcelable(KEY_CURRENT_CREDENTIAL, mCurrentCredential.duplicate());
661             }
662         }
663 
664         @Override
onActivityResult(int requestCode, int resultCode, Intent data)665         public void onActivityResult(int requestCode, int resultCode,
666                 Intent data) {
667             super.onActivityResult(requestCode, resultCode, data);
668             switch (requestCode) {
669                 case CONFIRM_EXISTING_REQUEST:
670                     if (resultCode != Activity.RESULT_OK) {
671                         getActivity().setResult(RESULT_FINISHED);
672                         getActivity().finish();
673                     } else {
674                         mCurrentCredential = data.getParcelableExtra(
675                                 ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
676                     }
677                     break;
678             }
679         }
680 
getRedactionInterstitialIntent(Context context)681         protected Intent getRedactionInterstitialIntent(Context context) {
682             return RedactionInterstitial.createStartIntent(context, mUserId);
683         }
684 
updateStage(Stage stage)685         protected void updateStage(Stage stage) {
686             final Stage previousStage = mUiStage;
687             mUiStage = stage;
688             updateUi();
689 
690             // If the stage changed, announce the header for accessibility. This
691             // is a no-op when accessibility is disabled.
692             if (previousStage != stage) {
693                 mLayout.announceForAccessibility(mLayout.getHeaderText());
694             }
695         }
696 
697         /**
698          * Validates PIN/Password and returns the validation result and updates mValidationErrors
699          * and mPasswordReused to reflect validation results.
700          *
701          * @param credential credential the user typed in.
702          * @return whether password satisfies all the requirements.
703          */
704         @VisibleForTesting
validatePassword(LockscreenCredential credential)705         boolean validatePassword(LockscreenCredential credential) {
706             final byte[] password = credential.getCredential();
707             mValidationErrors = PasswordMetrics.validatePassword(
708                     mMinMetrics, mMinComplexity, !mIsAlphaMode, password);
709             if (mValidationErrors.isEmpty() &&  mLockPatternUtils.checkPasswordHistory(
710                         password, getPasswordHistoryHashFactor(), mUserId)) {
711                 mValidationErrors =
712                         Collections.singletonList(new PasswordValidationError(RECENTLY_USED));
713             }
714             return mValidationErrors.isEmpty();
715         }
716 
717         /**
718          * Lazily compute and return the history hash factor of the current user (mUserId), used for
719          * password history check.
720          */
getPasswordHistoryHashFactor()721         private byte[] getPasswordHistoryHashFactor() {
722             if (mPasswordHistoryHashFactor == null) {
723                 mPasswordHistoryHashFactor = mLockPatternUtils.getPasswordHistoryHashFactor(
724                         mCurrentCredential != null ? mCurrentCredential
725                                 : LockscreenCredential.createNone(), mUserId);
726             }
727             return mPasswordHistoryHashFactor;
728         }
729 
handleNext()730         public void handleNext() {
731             if (mSaveAndFinishWorker != null) return;
732             // TODO(b/120484642): This is a point of entry for passwords from the UI
733             final Editable passwordText = mPasswordEntry.getText();
734             if (TextUtils.isEmpty(passwordText)) {
735                 return;
736             }
737             mChosenPassword = mIsAlphaMode ? LockscreenCredential.createPassword(passwordText)
738                     : LockscreenCredential.createPin(passwordText);
739             if (mUiStage == Stage.Introduction) {
740                 if (validatePassword(mChosenPassword)) {
741                     mFirstPassword = mChosenPassword;
742                     mPasswordEntry.setText("");
743                     updateStage(Stage.NeedToConfirm);
744                 } else {
745                     mChosenPassword.zeroize();
746                 }
747             } else if (mUiStage == Stage.NeedToConfirm) {
748                 if (mChosenPassword.equals(mFirstPassword)) {
749                     startSaveAndFinish();
750                 } else {
751                     CharSequence tmp = mPasswordEntry.getText();
752                     if (tmp != null) {
753                         Selection.setSelection((Spannable) tmp, 0, tmp.length());
754                     }
755                     updateStage(Stage.ConfirmWrong);
756                     mChosenPassword.zeroize();
757                 }
758             }
759         }
760 
setNextEnabled(boolean enabled)761         protected void setNextEnabled(boolean enabled) {
762             mNextButton.setEnabled(enabled);
763         }
764 
setNextText(int text)765         protected void setNextText(int text) {
766             mNextButton.setText(getActivity(), text);
767         }
768 
onSkipOrClearButtonClick(View view)769         protected void onSkipOrClearButtonClick(View view) {
770             mPasswordEntry.setText("");
771         }
772 
onNextButtonClick(View view)773         protected void onNextButtonClick(View view) {
774             handleNext();
775         }
776 
onEditorAction(TextView v, int actionId, KeyEvent event)777         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
778             // Check if this was the result of hitting the enter or "done" key
779             if (actionId == EditorInfo.IME_NULL
780                     || actionId == EditorInfo.IME_ACTION_DONE
781                     || actionId == EditorInfo.IME_ACTION_NEXT) {
782                 handleNext();
783                 return true;
784             }
785             return false;
786         }
787 
788         /**
789          * @param errorCode error code returned from password validation.
790          * @return an array of messages describing the error, important messages come first.
791          */
convertErrorCodeToMessages()792         String[] convertErrorCodeToMessages() {
793             List<String> messages = new ArrayList<>();
794             for (PasswordValidationError error : mValidationErrors) {
795                 switch (error.errorCode) {
796                     case CONTAINS_INVALID_CHARACTERS:
797                         messages.add(getString(R.string.lockpassword_illegal_character));
798                         break;
799                     case NOT_ENOUGH_UPPER_CASE:
800                         messages.add(getResources().getQuantityString(
801                                 R.plurals.lockpassword_password_requires_uppercase,
802                                 error.requirement, error.requirement));
803                         break;
804                     case NOT_ENOUGH_LOWER_CASE:
805                         messages.add(getResources().getQuantityString(
806                                 R.plurals.lockpassword_password_requires_lowercase,
807                                 error.requirement, error.requirement));
808                         break;
809                     case NOT_ENOUGH_LETTERS:
810                         messages.add(getResources().getQuantityString(
811                                 R.plurals.lockpassword_password_requires_letters,
812                                 error.requirement, error.requirement));
813                         break;
814                     case NOT_ENOUGH_DIGITS:
815                         messages.add(getResources().getQuantityString(
816                                 R.plurals.lockpassword_password_requires_numeric,
817                                 error.requirement, error.requirement));
818                         break;
819                     case NOT_ENOUGH_SYMBOLS:
820                         messages.add(getResources().getQuantityString(
821                                 R.plurals.lockpassword_password_requires_symbols,
822                                 error.requirement, error.requirement));
823                         break;
824                     case NOT_ENOUGH_NON_LETTER:
825                         messages.add(getResources().getQuantityString(
826                                 R.plurals.lockpassword_password_requires_nonletter,
827                                 error.requirement, error.requirement));
828                         break;
829                     case NOT_ENOUGH_NON_DIGITS:
830                         messages.add(getResources().getQuantityString(
831                                 R.plurals.lockpassword_password_requires_nonnumerical,
832                                 error.requirement, error.requirement));
833                         break;
834                     case TOO_SHORT:
835                         messages.add(getResources().getQuantityString(
836                                 mIsAlphaMode
837                                         ? R.plurals.lockpassword_password_too_short
838                                         : R.plurals.lockpassword_pin_too_short,
839                                 error.requirement, error.requirement));
840                         break;
841                     case TOO_SHORT_WHEN_ALL_NUMERIC:
842                         messages.add(
843                                 StringUtil.getIcuPluralsString(getContext(), error.requirement,
844                                         R.string.lockpassword_password_too_short_all_numeric));
845                         break;
846                     case TOO_LONG:
847                         messages.add(getResources().getQuantityString(
848                                 mIsAlphaMode
849                                         ? R.plurals.lockpassword_password_too_long
850                                         : R.plurals.lockpassword_pin_too_long,
851                                 error.requirement + 1, error.requirement + 1));
852                         break;
853                     case CONTAINS_SEQUENCE:
854                         messages.add(getString(R.string.lockpassword_pin_no_sequential_digits));
855                         break;
856                     case RECENTLY_USED:
857                         DevicePolicyManager devicePolicyManager =
858                                 getContext().getSystemService(DevicePolicyManager.class);
859                         if (mIsAlphaMode) {
860                             messages.add(devicePolicyManager.getResources().getString(
861                                     PASSWORD_RECENTLY_USED,
862                                     () -> getString(R.string.lockpassword_password_recently_used)));
863                         } else {
864                             messages.add(devicePolicyManager.getResources().getString(
865                                     PIN_RECENTLY_USED,
866                                     () -> getString(R.string.lockpassword_pin_recently_used)));
867                         }
868                         break;
869                     default:
870                         Log.wtf(TAG, "unknown error validating password: " + error);
871                 }
872             }
873 
874             return messages.toArray(new String[0]);
875         }
876 
877         /**
878          * Update the hint based on current Stage and length of password entry
879          */
updateUi()880         protected void updateUi() {
881             final boolean canInput = mSaveAndFinishWorker == null;
882 
883             LockscreenCredential password = mIsAlphaMode
884                     ? LockscreenCredential.createPasswordOrNone(mPasswordEntry.getText())
885                     : LockscreenCredential.createPinOrNone(mPasswordEntry.getText());
886             final int length = password.size();
887             if (mUiStage == Stage.Introduction) {
888                 mPasswordRestrictionView.setVisibility(View.VISIBLE);
889                 final boolean passwordCompliant = validatePassword(password);
890                 String[] messages = convertErrorCodeToMessages();
891                 // Update the fulfillment of requirements.
892                 mPasswordRequirementAdapter.setRequirements(messages);
893                 // Enable/Disable the next button accordingly.
894                 setNextEnabled(passwordCompliant);
895             } else {
896                 // Hide password requirement view when we are just asking user to confirm the pw.
897                 mPasswordRestrictionView.setVisibility(View.GONE);
898                 setHeaderText(mUiStage.getHint(getContext(), mIsAlphaMode, getStageType(),
899                         mIsManagedProfile));
900                 setNextEnabled(canInput && length >= LockPatternUtils.MIN_LOCK_PASSWORD_SIZE);
901                 mSkipOrClearButton.setVisibility(toVisibility(canInput && length > 0));
902             }
903             final int stage = getStageType();
904             if (getStageType() != Stage.TYPE_NONE) {
905                 int message = mUiStage.getMessage(mIsAlphaMode, stage);
906                 if (message != 0) {
907                     mMessage.setVisibility(View.VISIBLE);
908                     mMessage.setText(message);
909                 } else {
910                     mMessage.setVisibility(View.INVISIBLE);
911                 }
912             } else {
913                 mMessage.setVisibility(View.GONE);
914             }
915 
916             setNextText(mUiStage.buttonText);
917             mPasswordEntryInputDisabler.setInputEnabled(canInput);
918             password.zeroize();
919         }
920 
toVisibility(boolean visibleOrGone)921         protected int toVisibility(boolean visibleOrGone) {
922             return visibleOrGone ? View.VISIBLE : View.GONE;
923         }
924 
setHeaderText(String text)925         private void setHeaderText(String text) {
926             // Only set the text if it is different than the existing one to avoid announcing again.
927             if (!TextUtils.isEmpty(mLayout.getHeaderText())
928                     && mLayout.getHeaderText().toString().equals(text)) {
929                 return;
930             }
931             mLayout.setHeaderText(text);
932         }
933 
afterTextChanged(Editable s)934         public void afterTextChanged(Editable s) {
935             // Changing the text while error displayed resets to NeedToConfirm state
936             if (mUiStage == Stage.ConfirmWrong) {
937                 mUiStage = Stage.NeedToConfirm;
938             }
939             // Schedule the UI update.
940             mTextChangedHandler.notifyAfterTextChanged();
941         }
942 
beforeTextChanged(CharSequence s, int start, int count, int after)943         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
944 
945         }
946 
onTextChanged(CharSequence s, int start, int before, int count)947         public void onTextChanged(CharSequence s, int start, int before, int count) {
948 
949         }
950 
startSaveAndFinish()951         private void startSaveAndFinish() {
952             if (mSaveAndFinishWorker != null) {
953                 Log.w(TAG, "startSaveAndFinish with an existing SaveAndFinishWorker.");
954                 return;
955             }
956 
957             mPasswordEntryInputDisabler.setInputEnabled(false);
958             setNextEnabled(false);
959 
960             mSaveAndFinishWorker = new SaveAndFinishWorker();
961             mSaveAndFinishWorker.setListener(this);
962 
963             getFragmentManager().beginTransaction().add(mSaveAndFinishWorker,
964                     FRAGMENT_TAG_SAVE_AND_FINISH).commit();
965             getFragmentManager().executePendingTransactions();
966 
967             final Intent intent = getActivity().getIntent();
968             final boolean required = intent.getBooleanExtra(
969                     EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
970             if (mUnificationProfileId != UserHandle.USER_NULL) {
971                 try (LockscreenCredential profileCredential = (LockscreenCredential)
972                         intent.getParcelableExtra(EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL)) {
973                     mSaveAndFinishWorker.setProfileToUnify(mUnificationProfileId,
974                             profileCredential);
975                 }
976             }
977             mSaveAndFinishWorker.start(mLockPatternUtils, required, mRequestGatekeeperPassword,
978                     mChosenPassword, mCurrentCredential, mUserId);
979         }
980 
981         @Override
onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData)982         public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) {
983             getActivity().setResult(RESULT_FINISHED, resultData);
984 
985             if (mChosenPassword != null) {
986                 mChosenPassword.zeroize();
987             }
988             if (mCurrentCredential != null) {
989                 mCurrentCredential.zeroize();
990             }
991             if (mFirstPassword != null) {
992                 mFirstPassword.zeroize();
993             }
994 
995             mPasswordEntry.setText("");
996 
997             if (!wasSecureBefore) {
998                 Intent intent = getRedactionInterstitialIntent(getActivity());
999                 if (intent != null) {
1000                     startActivity(intent);
1001                 }
1002             }
1003             getActivity().finish();
1004         }
1005 
1006         class TextChangedHandler extends Handler {
1007             private static final int ON_TEXT_CHANGED = 1;
1008             private static final int DELAY_IN_MILLISECOND = 100;
1009 
1010             /**
1011              * With the introduction of delay, we batch processing the text changed event to reduce
1012              * unnecessary UI updates.
1013              */
notifyAfterTextChanged()1014             private void notifyAfterTextChanged() {
1015                 removeMessages(ON_TEXT_CHANGED);
1016                 sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND);
1017             }
1018 
1019             @Override
handleMessage(Message msg)1020             public void handleMessage(Message msg) {
1021                 if (getActivity() == null) {
1022                     return;
1023                 }
1024                 if (msg.what == ON_TEXT_CHANGED) {
1025                     updateUi();
1026                 }
1027             }
1028         }
1029     }
1030 
1031     public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase {
1032 
1033         private LockscreenCredential mChosenPassword;
1034         private LockscreenCredential mCurrentCredential;
1035 
start(LockPatternUtils utils, boolean required, boolean requestGatekeeperPassword, LockscreenCredential chosenPassword, LockscreenCredential currentCredential, int userId)1036         public void start(LockPatternUtils utils, boolean required,
1037                 boolean requestGatekeeperPassword, LockscreenCredential chosenPassword,
1038                 LockscreenCredential currentCredential, int userId) {
1039             prepare(utils, required, requestGatekeeperPassword, userId);
1040 
1041             mChosenPassword = chosenPassword;
1042             mCurrentCredential = currentCredential != null ? currentCredential
1043                     : LockscreenCredential.createNone();
1044             mUserId = userId;
1045 
1046             start();
1047         }
1048 
1049         @Override
saveAndVerifyInBackground()1050         protected Pair<Boolean, Intent> saveAndVerifyInBackground() {
1051             final boolean success = mUtils.setLockCredential(
1052                     mChosenPassword, mCurrentCredential, mUserId);
1053             if (success) {
1054                 unifyProfileCredentialIfRequested();
1055             }
1056             Intent result = null;
1057             if (success && mRequestGatekeeperPassword) {
1058                 // If a Gatekeeper Password was requested, invoke the LockSettingsService code
1059                 // path to return a Gatekeeper Password based on the credential that the user
1060                 // chose. This should only be run if the credential was successfully set.
1061                 final VerifyCredentialResponse response = mUtils.verifyCredential(mChosenPassword,
1062                         mUserId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE);
1063 
1064                 if (!response.isMatched() || !response.containsGatekeeperPasswordHandle()) {
1065                     Log.e(TAG, "critical: bad response or missing GK PW handle for known good"
1066                             + " password: " + response.toString());
1067                 }
1068 
1069                 result = new Intent();
1070                 result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE,
1071                         response.getGatekeeperPasswordHandle());
1072             }
1073             return Pair.create(success, result);
1074         }
1075     }
1076 }
1077