• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 android.app.Activity;
20 import android.content.Intent;
21 import android.os.AsyncTask;
22 import android.os.Bundle;
23 import android.os.CountDownTimer;
24 import android.os.SystemClock;
25 import android.os.UserManager;
26 import android.os.storage.StorageManager;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.animation.AnimationUtils;
31 import android.view.animation.Interpolator;
32 import android.widget.TextView;
33 
34 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
35 import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
36 import com.android.internal.widget.LockPatternChecker;
37 import com.android.internal.widget.LockPatternUtils;
38 import com.android.internal.widget.LockPatternView;
39 import com.android.internal.widget.LockPatternView.Cell;
40 import com.android.settings.R;
41 import com.android.settingslib.animation.AppearAnimationCreator;
42 import com.android.settingslib.animation.AppearAnimationUtils;
43 import com.android.settingslib.animation.DisappearAnimationUtils;
44 
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.List;
48 
49 /**
50  * Launch this when you want the user to confirm their lock pattern.
51  *
52  * Sets an activity result of {@link Activity#RESULT_OK} when the user
53  * successfully confirmed their pattern.
54  */
55 public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity {
56 
57     public static class InternalActivity extends ConfirmLockPattern {
58     }
59 
60     private enum Stage {
61         NeedToUnlock,
62         NeedToUnlockWrong,
63         LockedOut
64     }
65 
66     @Override
getIntent()67     public Intent getIntent() {
68         Intent modIntent = new Intent(super.getIntent());
69         modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ConfirmLockPatternFragment.class.getName());
70         return modIntent;
71     }
72 
73     @Override
isValidFragment(String fragmentName)74     protected boolean isValidFragment(String fragmentName) {
75         if (ConfirmLockPatternFragment.class.getName().equals(fragmentName)) return true;
76         return false;
77     }
78 
79     public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment
80             implements AppearAnimationCreator<Object>, CredentialCheckResultTracker.Listener {
81 
82         private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result";
83 
84         private LockPatternView mLockPatternView;
85         private AsyncTask<?, ?, ?> mPendingLockCheck;
86         private CredentialCheckResultTracker mCredentialCheckResultTracker;
87         private boolean mDisappearing = false;
88         private CountDownTimer mCountdownTimer;
89 
90         private TextView mHeaderTextView;
91         private TextView mDetailsTextView;
92         private View mLeftSpacerLandscape;
93         private View mRightSpacerLandscape;
94 
95         // caller-supplied text for various prompts
96         private CharSequence mHeaderText;
97         private CharSequence mDetailsText;
98 
99         private AppearAnimationUtils mAppearAnimationUtils;
100         private DisappearAnimationUtils mDisappearAnimationUtils;
101 
102         // required constructor for fragments
ConfirmLockPatternFragment()103         public ConfirmLockPatternFragment() {
104 
105         }
106 
107         @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)108         public View onCreateView(LayoutInflater inflater, ViewGroup container,
109                 Bundle savedInstanceState) {
110             ConfirmLockPattern activity = (ConfirmLockPattern) getActivity();
111             View view = inflater.inflate(
112                     activity.getConfirmCredentialTheme() == ConfirmCredentialTheme.NORMAL
113                             ? R.layout.confirm_lock_pattern_normal
114                             : R.layout.confirm_lock_pattern,
115                     container,
116                     false);
117             mHeaderTextView = (TextView) view.findViewById(R.id.headerText);
118             mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern);
119             mDetailsTextView = (TextView) view.findViewById(R.id.detailsText);
120             mErrorTextView = (TextView) view.findViewById(R.id.errorText);
121             mLeftSpacerLandscape = view.findViewById(R.id.leftSpacer);
122             mRightSpacerLandscape = view.findViewById(R.id.rightSpacer);
123 
124             // make it so unhandled touch events within the unlock screen go to the
125             // lock pattern view.
126             final LinearLayoutWithDefaultTouchRecepient topLayout
127                     = (LinearLayoutWithDefaultTouchRecepient) view.findViewById(R.id.topLayout);
128             topLayout.setDefaultTouchRecepient(mLockPatternView);
129 
130             Intent intent = getActivity().getIntent();
131             if (intent != null) {
132                 mHeaderText = intent.getCharSequenceExtra(
133                         ConfirmDeviceCredentialBaseFragment.HEADER_TEXT);
134                 mDetailsText = intent.getCharSequenceExtra(
135                         ConfirmDeviceCredentialBaseFragment.DETAILS_TEXT);
136             }
137 
138             mLockPatternView.setTactileFeedbackEnabled(
139                     mLockPatternUtils.isTactileFeedbackEnabled());
140             mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(
141                     mEffectiveUserId));
142             mLockPatternView.setOnPatternListener(mConfirmExistingLockPatternListener);
143             updateStage(Stage.NeedToUnlock);
144 
145             if (savedInstanceState == null) {
146                 // on first launch, if no lock pattern is set, then finish with
147                 // success (don't want user to get stuck confirming something that
148                 // doesn't exist).
149                 // Don't do this check for FRP though, because the pattern is not stored
150                 // in a way that isLockPatternEnabled is aware of for that case.
151                 // TODO(roosa): This block should no longer be needed since we removed the
152                 //              ability to disable the pattern in L. Remove this block after
153                 //              ensuring it's safe to do so. (Note that ConfirmLockPassword
154                 //              doesn't have this).
155                 if (!mFrp && !mLockPatternUtils.isLockPatternEnabled(mEffectiveUserId)) {
156                     getActivity().setResult(Activity.RESULT_OK);
157                     getActivity().finish();
158                 }
159             }
160             mAppearAnimationUtils = new AppearAnimationUtils(getContext(),
161                     AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 2f /* translationScale */,
162                     1.3f /* delayScale */, AnimationUtils.loadInterpolator(
163                     getContext(), android.R.interpolator.linear_out_slow_in));
164             mDisappearAnimationUtils = new DisappearAnimationUtils(getContext(),
165                     125, 4f /* translationScale */,
166                     0.3f /* delayScale */, AnimationUtils.loadInterpolator(
167                     getContext(), android.R.interpolator.fast_out_linear_in),
168                     new AppearAnimationUtils.RowTranslationScaler() {
169                         @Override
170                         public float getRowTranslationScale(int row, int numRows) {
171                             return (float)(numRows - row) / numRows;
172                         }
173                     });
174             setAccessibilityTitle(mHeaderTextView.getText());
175 
176             mCredentialCheckResultTracker = (CredentialCheckResultTracker) getFragmentManager()
177                     .findFragmentByTag(FRAGMENT_TAG_CHECK_LOCK_RESULT);
178             if (mCredentialCheckResultTracker == null) {
179                 mCredentialCheckResultTracker = new CredentialCheckResultTracker();
180                 getFragmentManager().beginTransaction().add(mCredentialCheckResultTracker,
181                         FRAGMENT_TAG_CHECK_LOCK_RESULT).commit();
182             }
183             return view;
184         }
185 
186         @Override
onSaveInstanceState(Bundle outState)187         public void onSaveInstanceState(Bundle outState) {
188             // deliberately not calling super since we are managing this in full
189         }
190 
191         @Override
onPause()192         public void onPause() {
193             super.onPause();
194 
195             if (mCountdownTimer != null) {
196                 mCountdownTimer.cancel();
197             }
198             mCredentialCheckResultTracker.setListener(null);
199         }
200 
201         @Override
getMetricsCategory()202         public int getMetricsCategory() {
203             return MetricsEvent.CONFIRM_LOCK_PATTERN;
204         }
205 
206         @Override
onResume()207         public void onResume() {
208             super.onResume();
209 
210             // if the user is currently locked out, enforce it.
211             long deadline = mLockPatternUtils.getLockoutAttemptDeadline(mEffectiveUserId);
212             if (deadline != 0) {
213                 mCredentialCheckResultTracker.clearResult();
214                 handleAttemptLockout(deadline);
215             } else if (!mLockPatternView.isEnabled()) {
216                 // The deadline has passed, but the timer was cancelled. Or the pending lock
217                 // check was cancelled. Need to clean up.
218                 updateStage(Stage.NeedToUnlock);
219             }
220             mCredentialCheckResultTracker.setListener(this);
221         }
222 
223         @Override
onShowError()224         protected void onShowError() {
225         }
226 
227         @Override
prepareEnterAnimation()228         public void prepareEnterAnimation() {
229             super.prepareEnterAnimation();
230             mHeaderTextView.setAlpha(0f);
231             mCancelButton.setAlpha(0f);
232             mLockPatternView.setAlpha(0f);
233             mDetailsTextView.setAlpha(0f);
234             mFingerprintIcon.setAlpha(0f);
235         }
236 
getDefaultDetails()237         private int getDefaultDetails() {
238             if (mFrp) {
239                 return R.string.lockpassword_confirm_your_pattern_details_frp;
240             }
241             final boolean isStrongAuthRequired = isStrongAuthRequired();
242             if (UserManager.get(getActivity()).isManagedProfile(mEffectiveUserId)) {
243                 return isStrongAuthRequired
244                         ? R.string.lockpassword_strong_auth_required_work_pattern
245                         : R.string.lockpassword_confirm_your_pattern_generic_profile;
246             } else {
247                 return isStrongAuthRequired
248                         ? R.string.lockpassword_strong_auth_required_device_pattern
249                         : R.string.lockpassword_confirm_your_pattern_generic;
250             }
251         }
252 
getActiveViews()253         private Object[][] getActiveViews() {
254             ArrayList<ArrayList<Object>> result = new ArrayList<>();
255             result.add(new ArrayList<Object>(Collections.singletonList(mHeaderTextView)));
256             result.add(new ArrayList<Object>(Collections.singletonList(mDetailsTextView)));
257             if (mCancelButton.getVisibility() == View.VISIBLE) {
258                 result.add(new ArrayList<Object>(Collections.singletonList(mCancelButton)));
259             }
260             LockPatternView.CellState[][] cellStates = mLockPatternView.getCellStates();
261             for (int i = 0; i < cellStates.length; i++) {
262                 ArrayList<Object> row = new ArrayList<>();
263                 for (int j = 0; j < cellStates[i].length; j++) {
264                     row.add(cellStates[i][j]);
265                 }
266                 result.add(row);
267             }
268             if (mFingerprintIcon.getVisibility() == View.VISIBLE) {
269                 result.add(new ArrayList<Object>(Collections.singletonList(mFingerprintIcon)));
270             }
271             Object[][] resultArr = new Object[result.size()][cellStates[0].length];
272             for (int i = 0; i < result.size(); i++) {
273                 ArrayList<Object> row = result.get(i);
274                 for (int j = 0; j < row.size(); j++) {
275                     resultArr[i][j] = row.get(j);
276                 }
277             }
278             return resultArr;
279         }
280 
281         @Override
startEnterAnimation()282         public void startEnterAnimation() {
283             super.startEnterAnimation();
284             mLockPatternView.setAlpha(1f);
285             mAppearAnimationUtils.startAnimation2d(getActiveViews(), null, this);
286         }
287 
updateStage(Stage stage)288         private void updateStage(Stage stage) {
289             switch (stage) {
290                 case NeedToUnlock:
291                     if (mHeaderText != null) {
292                         mHeaderTextView.setText(mHeaderText);
293                     } else {
294                         mHeaderTextView.setText(getDefaultHeader());
295                     }
296                     if (mDetailsText != null) {
297                         mDetailsTextView.setText(mDetailsText);
298                     } else {
299                         mDetailsTextView.setText(getDefaultDetails());
300                     }
301                     mErrorTextView.setText("");
302                     updateErrorMessage(
303                             mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId));
304 
305                     mLockPatternView.setEnabled(true);
306                     mLockPatternView.enableInput();
307                     mLockPatternView.clearPattern();
308                     break;
309                 case NeedToUnlockWrong:
310                     showError(R.string.lockpattern_need_to_unlock_wrong,
311                             CLEAR_WRONG_ATTEMPT_TIMEOUT_MS);
312 
313                     mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
314                     mLockPatternView.setEnabled(true);
315                     mLockPatternView.enableInput();
316                     break;
317                 case LockedOut:
318                     mLockPatternView.clearPattern();
319                     // enabled = false means: disable input, and have the
320                     // appearance of being disabled.
321                     mLockPatternView.setEnabled(false); // appearance of being disabled
322                     break;
323             }
324 
325             // Always announce the header for accessibility. This is a no-op
326             // when accessibility is disabled.
327             mHeaderTextView.announceForAccessibility(mHeaderTextView.getText());
328         }
329 
getDefaultHeader()330         private int getDefaultHeader() {
331             return mFrp ? R.string.lockpassword_confirm_your_pattern_header_frp
332                     : R.string.lockpassword_confirm_your_pattern_header;
333         }
334 
335         private Runnable mClearPatternRunnable = new Runnable() {
336             public void run() {
337                 mLockPatternView.clearPattern();
338             }
339         };
340 
341         // clear the wrong pattern unless they have started a new one
342         // already
postClearPatternRunnable()343         private void postClearPatternRunnable() {
344             mLockPatternView.removeCallbacks(mClearPatternRunnable);
345             mLockPatternView.postDelayed(mClearPatternRunnable, CLEAR_WRONG_ATTEMPT_TIMEOUT_MS);
346         }
347 
348         @Override
authenticationSucceeded()349         protected void authenticationSucceeded() {
350             mCredentialCheckResultTracker.setResult(true, new Intent(), 0, mEffectiveUserId);
351         }
352 
startDisappearAnimation(final Intent intent)353         private void startDisappearAnimation(final Intent intent) {
354             if (mDisappearing) {
355                 return;
356             }
357             mDisappearing = true;
358 
359             final ConfirmLockPattern activity = (ConfirmLockPattern) getActivity();
360             // Bail if there is no active activity.
361             if (activity == null || activity.isFinishing()) {
362                 return;
363             }
364             if (activity.getConfirmCredentialTheme() == ConfirmCredentialTheme.DARK) {
365                 mLockPatternView.clearPattern();
366                 mDisappearAnimationUtils.startAnimation2d(getActiveViews(),
367                         () -> {
368                             activity.setResult(RESULT_OK, intent);
369                             activity.finish();
370                             activity.overridePendingTransition(
371                                     R.anim.confirm_credential_close_enter,
372                                     R.anim.confirm_credential_close_exit);
373                         }, this);
374             } else {
375                 activity.setResult(RESULT_OK, intent);
376                 activity.finish();
377             }
378         }
379 
380         @Override
onFingerprintIconVisibilityChanged(boolean visible)381         public void onFingerprintIconVisibilityChanged(boolean visible) {
382             if (mLeftSpacerLandscape != null && mRightSpacerLandscape != null) {
383 
384                 // In landscape, adjust spacing depending on fingerprint icon visibility.
385                 mLeftSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE);
386                 mRightSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE);
387             }
388         }
389 
390         /**
391          * The pattern listener that responds according to a user confirming
392          * an existing lock pattern.
393          */
394         private LockPatternView.OnPatternListener mConfirmExistingLockPatternListener
395                 = new LockPatternView.OnPatternListener()  {
396 
397             public void onPatternStart() {
398                 mLockPatternView.removeCallbacks(mClearPatternRunnable);
399             }
400 
401             public void onPatternCleared() {
402                 mLockPatternView.removeCallbacks(mClearPatternRunnable);
403             }
404 
405             public void onPatternCellAdded(List<Cell> pattern) {
406 
407             }
408 
409             public void onPatternDetected(List<LockPatternView.Cell> pattern) {
410                 if (mPendingLockCheck != null || mDisappearing) {
411                     return;
412                 }
413 
414                 mLockPatternView.setEnabled(false);
415 
416                 final boolean verifyChallenge = getActivity().getIntent().getBooleanExtra(
417                         ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false);
418                 Intent intent = new Intent();
419                 if (verifyChallenge) {
420                     if (isInternalActivity()) {
421                         startVerifyPattern(pattern, intent);
422                         return;
423                     }
424                 } else {
425                     startCheckPattern(pattern, intent);
426                     return;
427                 }
428 
429                 mCredentialCheckResultTracker.setResult(false, intent, 0, mEffectiveUserId);
430             }
431 
432             private boolean isInternalActivity() {
433                 return getActivity() instanceof ConfirmLockPattern.InternalActivity;
434             }
435 
436             private void startVerifyPattern(final List<LockPatternView.Cell> pattern,
437                     final Intent intent) {
438                 final int localEffectiveUserId = mEffectiveUserId;
439                 final int localUserId = mUserId;
440                 long challenge = getActivity().getIntent().getLongExtra(
441                         ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0);
442                 final LockPatternChecker.OnVerifyCallback onVerifyCallback =
443                     new LockPatternChecker.OnVerifyCallback() {
444                         @Override
445                         public void onVerified(byte[] token, int timeoutMs) {
446                             mPendingLockCheck = null;
447                             boolean matched = false;
448                             if (token != null) {
449                                 matched = true;
450                                 if (mReturnCredentials) {
451                                     intent.putExtra(
452                                             ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN,
453                                             token);
454                                 }
455                             }
456                             mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs,
457                                     localEffectiveUserId);
458                         }
459                     };
460                 mPendingLockCheck = (localEffectiveUserId == localUserId)
461                         ? LockPatternChecker.verifyPattern(
462                                 mLockPatternUtils, pattern, challenge, localUserId,
463                                 onVerifyCallback)
464                         : LockPatternChecker.verifyTiedProfileChallenge(
465                                 mLockPatternUtils, LockPatternUtils.patternToString(pattern),
466                                 true, challenge, localUserId, onVerifyCallback);
467             }
468 
469             private void startCheckPattern(final List<LockPatternView.Cell> pattern,
470                     final Intent intent) {
471                 if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
472                     // Pattern size is less than the minimum, do not count it as an fail attempt.
473                     onPatternChecked(false, intent, 0, mEffectiveUserId, false /* newResult */);
474                     return;
475                 }
476 
477                 final int localEffectiveUserId = mEffectiveUserId;
478                 mPendingLockCheck = LockPatternChecker.checkPattern(
479                         mLockPatternUtils,
480                         pattern,
481                         localEffectiveUserId,
482                         new LockPatternChecker.OnCheckCallback() {
483                             @Override
484                             public void onChecked(boolean matched, int timeoutMs) {
485                                 mPendingLockCheck = null;
486                                 if (matched && isInternalActivity() && mReturnCredentials) {
487                                     intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE,
488                                                     StorageManager.CRYPT_TYPE_PATTERN);
489                                     intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD,
490                                                     LockPatternUtils.patternToString(pattern));
491                                 }
492                                 mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs,
493                                         localEffectiveUserId);
494                             }
495                         });
496             }
497         };
498 
onPatternChecked(boolean matched, Intent intent, int timeoutMs, int effectiveUserId, boolean newResult)499         private void onPatternChecked(boolean matched, Intent intent, int timeoutMs,
500                 int effectiveUserId, boolean newResult) {
501             mLockPatternView.setEnabled(true);
502             if (matched) {
503                 if (newResult) {
504                     reportSuccessfulAttempt();
505                 }
506                 startDisappearAnimation(intent);
507                 checkForPendingIntent();
508             } else {
509                 if (timeoutMs > 0) {
510                     refreshLockScreen();
511                     long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
512                             effectiveUserId, timeoutMs);
513                     handleAttemptLockout(deadline);
514                 } else {
515                     updateStage(Stage.NeedToUnlockWrong);
516                     postClearPatternRunnable();
517                 }
518                 if (newResult) {
519                     reportFailedAttempt();
520                 }
521             }
522         }
523 
524         @Override
onCredentialChecked(boolean matched, Intent intent, int timeoutMs, int effectiveUserId, boolean newResult)525         public void onCredentialChecked(boolean matched, Intent intent, int timeoutMs,
526                 int effectiveUserId, boolean newResult) {
527             onPatternChecked(matched, intent, timeoutMs, effectiveUserId, newResult);
528         }
529 
530         @Override
getLastTryErrorMessage(int userType)531         protected int getLastTryErrorMessage(int userType) {
532             switch (userType) {
533                 case USER_TYPE_PRIMARY:
534                     return R.string.lock_last_pattern_attempt_before_wipe_device;
535                 case USER_TYPE_MANAGED_PROFILE:
536                     return R.string.lock_last_pattern_attempt_before_wipe_profile;
537                 case USER_TYPE_SECONDARY:
538                     return R.string.lock_last_pattern_attempt_before_wipe_user;
539                 default:
540                     throw new IllegalArgumentException("Unrecognized user type:" + userType);
541             }
542         }
543 
handleAttemptLockout(long elapsedRealtimeDeadline)544         private void handleAttemptLockout(long elapsedRealtimeDeadline) {
545             updateStage(Stage.LockedOut);
546             long elapsedRealtime = SystemClock.elapsedRealtime();
547             mCountdownTimer = new CountDownTimer(
548                     elapsedRealtimeDeadline - elapsedRealtime,
549                     LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {
550 
551                 @Override
552                 public void onTick(long millisUntilFinished) {
553                     final int secondsCountdown = (int) (millisUntilFinished / 1000);
554                     mErrorTextView.setText(getString(
555                             R.string.lockpattern_too_many_failed_confirmation_attempts,
556                             secondsCountdown));
557                 }
558 
559                 @Override
560                 public void onFinish() {
561                     updateStage(Stage.NeedToUnlock);
562                 }
563             }.start();
564         }
565 
566         @Override
createAnimation(Object obj, long delay, long duration, float translationY, final boolean appearing, Interpolator interpolator, final Runnable finishListener)567         public void createAnimation(Object obj, long delay,
568                 long duration, float translationY, final boolean appearing,
569                 Interpolator interpolator,
570                 final Runnable finishListener) {
571             if (obj instanceof LockPatternView.CellState) {
572                 final LockPatternView.CellState animatedCell = (LockPatternView.CellState) obj;
573                 mLockPatternView.startCellStateAnimation(animatedCell,
574                         1f, appearing ? 1f : 0f, /* alpha */
575                         appearing ? translationY : 0f, /* startTranslation */
576                         appearing ? 0f : translationY, /* endTranslation */
577                         appearing ? 0f : 1f, 1f /* scale */,
578                         delay, duration, interpolator, finishListener);
579             } else {
580                 mAppearAnimationUtils.createAnimation((View) obj, delay, duration, translationY,
581                         appearing, interpolator, finishListener);
582             }
583         }
584     }
585 }
586