1 /*
2  * Copyright (C) 2014 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 package com.android.deskclock.alarms;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.AnimatorSet;
21 import android.animation.ObjectAnimator;
22 import android.animation.PropertyValuesHolder;
23 import android.animation.TimeInterpolator;
24 import android.animation.ValueAnimator;
25 import android.content.BroadcastReceiver;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.ServiceConnection;
31 import android.content.pm.ActivityInfo;
32 import android.graphics.Color;
33 import android.graphics.Rect;
34 import android.graphics.drawable.ColorDrawable;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.IBinder;
38 import android.preference.PreferenceManager;
39 import android.support.annotation.NonNull;
40 import android.support.v4.graphics.ColorUtils;
41 import android.support.v4.view.animation.PathInterpolatorCompat;
42 import android.support.v7.app.AppCompatActivity;
43 import android.view.KeyEvent;
44 import android.view.MotionEvent;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.view.WindowManager;
48 import android.view.accessibility.AccessibilityManager;
49 import android.widget.ImageView;
50 import android.widget.TextClock;
51 import android.widget.TextView;
52 
53 import com.android.deskclock.AnimatorUtils;
54 import com.android.deskclock.LogUtils;
55 import com.android.deskclock.R;
56 import com.android.deskclock.Utils;
57 import com.android.deskclock.events.Events;
58 import com.android.deskclock.provider.AlarmInstance;
59 import com.android.deskclock.settings.SettingsActivity;
60 import com.android.deskclock.widget.CircleView;
61 
62 public class AlarmActivity extends AppCompatActivity
63         implements View.OnClickListener, View.OnTouchListener {
64 
65     private static final String LOGTAG = AlarmActivity.class.getSimpleName();
66 
67     private static final TimeInterpolator PULSE_INTERPOLATOR =
68             PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f);
69     private static final TimeInterpolator REVEAL_INTERPOLATOR =
70             PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f);
71 
72     private static final int PULSE_DURATION_MILLIS = 1000;
73     private static final int ALARM_BOUNCE_DURATION_MILLIS = 500;
74     private static final int ALERT_REVEAL_DURATION_MILLIS = 500;
75     private static final int ALERT_FADE_DURATION_MILLIS = 500;
76     private static final int ALERT_DISMISS_DELAY_MILLIS = 2000;
77 
78     private static final float BUTTON_SCALE_DEFAULT = 0.7f;
79     private static final int BUTTON_DRAWABLE_ALPHA_DEFAULT = 165;
80 
81     private final Handler mHandler = new Handler();
82     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
83         @Override
84         public void onReceive(Context context, Intent intent) {
85             final String action = intent.getAction();
86             LogUtils.v(LOGTAG, "Received broadcast: %s", action);
87 
88             if (!mAlarmHandled) {
89                 switch (action) {
90                     case AlarmService.ALARM_SNOOZE_ACTION:
91                         snooze();
92                         break;
93                     case AlarmService.ALARM_DISMISS_ACTION:
94                         dismiss();
95                         break;
96                     case AlarmService.ALARM_DONE_ACTION:
97                         finish();
98                         break;
99                     default:
100                         LogUtils.i(LOGTAG, "Unknown broadcast: %s", action);
101                         break;
102                 }
103             } else {
104                 LogUtils.v(LOGTAG, "Ignored broadcast: %s", action);
105             }
106         }
107     };
108 
109     private final ServiceConnection mConnection = new ServiceConnection() {
110         @Override
111         public void onServiceConnected(ComponentName name, IBinder service) {
112             LogUtils.i("Finished binding to AlarmService");
113         }
114 
115         @Override
116         public void onServiceDisconnected(ComponentName name) {
117             LogUtils.i("Disconnected from AlarmService");
118         }
119     };
120 
121     private AlarmInstance mAlarmInstance;
122     private boolean mAlarmHandled;
123     private String mVolumeBehavior;
124     private int mCurrentHourColor;
125     private boolean mReceiverRegistered;
126     /** Whether the AlarmService is currently bound */
127     private boolean mServiceBound;
128 
129     private AccessibilityManager mAccessibilityManager;
130 
131     private ViewGroup mAlertView;
132     private TextView mAlertTitleView;
133     private TextView mAlertInfoView;
134 
135     private ViewGroup mContentView;
136     private ImageView mAlarmButton;
137     private ImageView mSnoozeButton;
138     private ImageView mDismissButton;
139     private TextView mHintView;
140 
141     private ValueAnimator mAlarmAnimator;
142     private ValueAnimator mSnoozeAnimator;
143     private ValueAnimator mDismissAnimator;
144     private ValueAnimator mPulseAnimator;
145 
146     @Override
onCreate(Bundle savedInstanceState)147     protected void onCreate(Bundle savedInstanceState) {
148         super.onCreate(savedInstanceState);
149 
150         final long instanceId = AlarmInstance.getId(getIntent().getData());
151         mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId);
152         if (mAlarmInstance == null) {
153             // The alarm was deleted before the activity got created, so just finish()
154             LogUtils.e(LOGTAG, "Error displaying alarm for intent: %s", getIntent());
155             finish();
156             return;
157         } else if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) {
158             LogUtils.i(LOGTAG, "Skip displaying alarm for instance: %s", mAlarmInstance);
159             finish();
160             return;
161         }
162 
163         LogUtils.i(LOGTAG, "Displaying alarm for instance: %s", mAlarmInstance);
164 
165         // Get the volume/camera button behavior setting
166         mVolumeBehavior = Utils.getDefaultSharedPreferences(this)
167                 .getString(SettingsActivity.KEY_VOLUME_BUTTONS,
168                         SettingsActivity.DEFAULT_VOLUME_BEHAVIOR);
169 
170         getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
171                 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
172                 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
173                 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
174                 | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);
175 
176         // Hide navigation bar to minimize accidental tap on Home key
177         hideNavigationBar();
178 
179         // Close dialogs and window shade, so this is fully visible
180         sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
181 
182         // In order to allow tablets to freely rotate and phones to stick
183         // with "nosensor" (use default device orientation) we have to have
184         // the manifest start with an orientation of unspecified" and only limit
185         // to "nosensor" for phones. Otherwise we get behavior like in b/8728671
186         // where tablets start off in their default orientation and then are
187         // able to freely rotate.
188         if (!getResources().getBoolean(R.bool.config_rotateAlarmAlert)) {
189             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
190         }
191 
192         mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
193 
194         setContentView(R.layout.alarm_activity);
195 
196         mAlertView = (ViewGroup) findViewById(R.id.alert);
197         mAlertTitleView = (TextView) mAlertView.findViewById(R.id.alert_title);
198         mAlertInfoView = (TextView) mAlertView.findViewById(R.id.alert_info);
199 
200         mContentView = (ViewGroup) findViewById(R.id.content);
201         mAlarmButton = (ImageView) mContentView.findViewById(R.id.alarm);
202         mSnoozeButton = (ImageView) mContentView.findViewById(R.id.snooze);
203         mDismissButton = (ImageView) mContentView.findViewById(R.id.dismiss);
204         mHintView = (TextView) mContentView.findViewById(R.id.hint);
205 
206         final TextView titleView = (TextView) mContentView.findViewById(R.id.title);
207         final TextClock digitalClock = (TextClock) mContentView.findViewById(R.id.digital_clock);
208         final CircleView pulseView = (CircleView) mContentView.findViewById(R.id.pulse);
209 
210         titleView.setText(mAlarmInstance.getLabelOrDefault(this));
211         Utils.setTimeFormat(this, digitalClock);
212 
213         mCurrentHourColor = Utils.getCurrentHourColor();
214         getWindow().setBackgroundDrawable(new ColorDrawable(mCurrentHourColor));
215 
216         mAlarmButton.setOnTouchListener(this);
217         mSnoozeButton.setOnClickListener(this);
218         mDismissButton.setOnClickListener(this);
219 
220         mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f);
221         mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE);
222         mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor);
223         mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView,
224                 PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.getRadius()),
225                 PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR,
226                         ColorUtils.setAlphaComponent(pulseView.getFillColor(), 0)));
227         mPulseAnimator.setDuration(PULSE_DURATION_MILLIS);
228         mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR);
229         mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
230         mPulseAnimator.start();
231     }
232 
233     @Override
onResume()234     protected void onResume() {
235         super.onResume();
236 
237         // Re-query for AlarmInstance in case the state has changed externally
238         final long instanceId = AlarmInstance.getId(getIntent().getData());
239         mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId);
240 
241         if (mAlarmInstance == null) {
242             LogUtils.i(LOGTAG, "No alarm instance for instanceId: %d", instanceId);
243             finish();
244             return;
245         }
246 
247         // Verify that the alarm is still firing before showing the activity
248         if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) {
249             LogUtils.i(LOGTAG, "Skip displaying alarm for instance: %s", mAlarmInstance);
250             finish();
251             return;
252         }
253 
254         if (!mReceiverRegistered) {
255             // Register to get the alarm done/snooze/dismiss intent.
256             final IntentFilter filter = new IntentFilter(AlarmService.ALARM_DONE_ACTION);
257             filter.addAction(AlarmService.ALARM_SNOOZE_ACTION);
258             filter.addAction(AlarmService.ALARM_DISMISS_ACTION);
259             registerReceiver(mReceiver, filter);
260             mReceiverRegistered = true;
261         }
262 
263         bindAlarmService();
264 
265         resetAnimations();
266     }
267 
268     @Override
onPause()269     protected void onPause() {
270         super.onPause();
271 
272         unbindAlarmService();
273 
274         // Skip if register didn't happen to avoid IllegalArgumentException
275         if (mReceiverRegistered) {
276             unregisterReceiver(mReceiver);
277             mReceiverRegistered = false;
278         }
279     }
280 
281     @Override
dispatchKeyEvent(@onNull KeyEvent keyEvent)282     public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) {
283         // Do this in dispatch to intercept a few of the system keys.
284         LogUtils.v(LOGTAG, "dispatchKeyEvent: %s", keyEvent);
285 
286         switch (keyEvent.getKeyCode()) {
287             // Volume keys and camera keys dismiss the alarm.
288             case KeyEvent.KEYCODE_POWER:
289             case KeyEvent.KEYCODE_VOLUME_UP:
290             case KeyEvent.KEYCODE_VOLUME_DOWN:
291             case KeyEvent.KEYCODE_VOLUME_MUTE:
292             case KeyEvent.KEYCODE_CAMERA:
293             case KeyEvent.KEYCODE_FOCUS:
294                 if (!mAlarmHandled && keyEvent.getAction() == KeyEvent.ACTION_UP) {
295                     switch (mVolumeBehavior) {
296                         case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE:
297                             snooze();
298                             break;
299                         case SettingsActivity.VOLUME_BEHAVIOR_DISMISS:
300                             dismiss();
301                             break;
302                         default:
303                             break;
304                     }
305                 }
306                 return true;
307             default:
308                 return super.dispatchKeyEvent(keyEvent);
309         }
310     }
311 
312     @Override
onBackPressed()313     public void onBackPressed() {
314         // Don't allow back to dismiss.
315     }
316 
317     @Override
onClick(View view)318     public void onClick(View view) {
319         if (mAlarmHandled) {
320             LogUtils.v(LOGTAG, "onClick ignored: %s", view);
321             return;
322         }
323         LogUtils.v(LOGTAG, "onClick: %s", view);
324 
325         // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons.
326         if (mAccessibilityManager != null && mAccessibilityManager.isTouchExplorationEnabled()) {
327             if (view == mSnoozeButton) {
328                 snooze();
329             } else if (view == mDismissButton) {
330                 dismiss();
331             }
332             return;
333         }
334 
335         if (view == mSnoozeButton) {
336             hintSnooze();
337         } else if (view == mDismissButton) {
338             hintDismiss();
339         }
340     }
341 
342     @Override
onTouch(View view, MotionEvent motionEvent)343     public boolean onTouch(View view, MotionEvent motionEvent) {
344         if (mAlarmHandled) {
345             LogUtils.v(LOGTAG, "onTouch ignored: %s", motionEvent);
346             return false;
347         }
348 
349         final int[] contentLocation = {0, 0};
350         mContentView.getLocationOnScreen(contentLocation);
351 
352         final float x = motionEvent.getRawX() - contentLocation[0];
353         final float y = motionEvent.getRawY() - contentLocation[1];
354 
355         final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
356         final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
357 
358         final float snoozeFraction, dismissFraction;
359         if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
360             snoozeFraction = getFraction(alarmRight, mSnoozeButton.getLeft(), x);
361             dismissFraction = getFraction(alarmLeft, mDismissButton.getRight(), x);
362         } else {
363             snoozeFraction = getFraction(alarmLeft, mSnoozeButton.getRight(), x);
364             dismissFraction = getFraction(alarmRight, mDismissButton.getLeft(), x);
365         }
366         setAnimatedFractions(snoozeFraction, dismissFraction);
367 
368         switch (motionEvent.getActionMasked()) {
369             case MotionEvent.ACTION_DOWN:
370                 LogUtils.v(LOGTAG, "onTouch started: %s", motionEvent);
371 
372                 // Stop the pulse, allowing the last pulse to finish.
373                 mPulseAnimator.setRepeatCount(0);
374                 break;
375             case MotionEvent.ACTION_UP:
376                 LogUtils.v(LOGTAG, "onTouch ended: %s", motionEvent);
377 
378                 if (snoozeFraction == 1.0f) {
379                     snooze();
380                 } else if (dismissFraction == 1.0f) {
381                     dismiss();
382                 } else {
383                     if (snoozeFraction > 0.0f || dismissFraction > 0.0f) {
384                         // Animate back to the initial state.
385                         AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator);
386                     } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) {
387                         // User touched the alarm button, hint the dismiss action
388                         hintDismiss();
389                     }
390 
391                     // Restart the pulse.
392                     mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
393                     if (!mPulseAnimator.isStarted()) {
394                         mPulseAnimator.start();
395                     }
396                 }
397                 break;
398             case MotionEvent.ACTION_CANCEL:
399                 resetAnimations();
400                 break;
401             default:
402                 break;
403         }
404 
405         return true;
406     }
407 
hideNavigationBar()408     private void hideNavigationBar() {
409         getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
410                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
411                 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
412     }
413 
hintSnooze()414     private void hintSnooze() {
415         final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
416         final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
417         final float translationX = Math.max(mSnoozeButton.getLeft() - alarmRight, 0)
418                 + Math.min(mSnoozeButton.getRight() - alarmLeft, 0);
419         getAlarmBounceAnimator(translationX, translationX < 0.0f ?
420                 R.string.description_direction_left : R.string.description_direction_right).start();
421     }
422 
hintDismiss()423     private void hintDismiss() {
424         final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
425         final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
426         final float translationX = Math.max(mDismissButton.getLeft() - alarmRight, 0)
427                 + Math.min(mDismissButton.getRight() - alarmLeft, 0);
428         getAlarmBounceAnimator(translationX, translationX < 0.0f ?
429                 R.string.description_direction_left : R.string.description_direction_right).start();
430     }
431 
432     /**
433      * Set animators to initial values and restart pulse on alarm button.
434      */
resetAnimations()435     private void resetAnimations() {
436         // Set the animators to their initial values.
437         setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
438         // Restart the pulse.
439         mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
440         if (!mPulseAnimator.isStarted()) {
441             mPulseAnimator.start();
442         }
443     }
444 
445     /**
446      * Perform snooze animation and send snooze intent.
447      */
snooze()448     private void snooze() {
449         mAlarmHandled = true;
450         LogUtils.v(LOGTAG, "Snoozed: %s", mAlarmInstance);
451 
452         final int accentColor = Utils.obtainStyledColor(this, R.attr.colorAccent, Color.RED);
453         setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
454 
455         final int snoozeMinutes = AlarmStateManager.getSnoozedMinutes(this);
456         final String infoText = getResources().getQuantityString(
457                 R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes);
458         final String accessibilityText = getResources().getQuantityString(
459                 R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes);
460 
461         getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText,
462                 accessibilityText, accentColor, accentColor).start();
463 
464         AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */);
465 
466         Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock);
467 
468         // Unbind here, otherwise alarm will keep ringing until activity finishes.
469         unbindAlarmService();
470     }
471 
472     /**
473      * Perform dismiss animation and send dismiss intent.
474      */
dismiss()475     private void dismiss() {
476         mAlarmHandled = true;
477         LogUtils.v(LOGTAG, "Dismissed: %s", mAlarmInstance);
478 
479         setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */);
480 
481         getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */,
482                 getString(R.string.alarm_alert_off_text) /* accessibilityText */,
483                 Color.WHITE, mCurrentHourColor).start();
484 
485         AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance);
486 
487         Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock);
488 
489         // Unbind here, otherwise alarm will keep ringing until activity finishes.
490         unbindAlarmService();
491     }
492 
493     /**
494      * Bind AlarmService if not yet bound.
495      */
bindAlarmService()496     private void bindAlarmService() {
497         if (!mServiceBound) {
498             final Intent intent = new Intent(this, AlarmService.class);
499             bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
500             mServiceBound = true;
501         }
502     }
503 
504     /**
505      * Unbind AlarmService if bound.
506      */
unbindAlarmService()507     private void unbindAlarmService() {
508         if (mServiceBound) {
509             unbindService(mConnection);
510             mServiceBound = false;
511         }
512     }
513 
setAnimatedFractions(float snoozeFraction, float dismissFraction)514     private void setAnimatedFractions(float snoozeFraction, float dismissFraction) {
515         final float alarmFraction = Math.max(snoozeFraction, dismissFraction);
516         AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction);
517         AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction);
518         AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction);
519     }
520 
getFraction(float x0, float x1, float x)521     private float getFraction(float x0, float x1, float x) {
522         return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f);
523     }
524 
getButtonAnimator(ImageView button, int tintColor)525     private ValueAnimator getButtonAnimator(ImageView button, int tintColor) {
526         return ObjectAnimator.ofPropertyValuesHolder(button,
527                 PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f),
528                 PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f),
529                 PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255),
530                 PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA,
531                         BUTTON_DRAWABLE_ALPHA_DEFAULT, 255),
532                 PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT,
533                         AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor));
534     }
535 
getAlarmBounceAnimator(float translationX, final int hintResId)536     private ValueAnimator getAlarmBounceAnimator(float translationX, final int hintResId) {
537         final ValueAnimator bounceAnimator = ObjectAnimator.ofFloat(mAlarmButton,
538                 View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f);
539         bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR);
540         bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS);
541         bounceAnimator.addListener(new AnimatorListenerAdapter() {
542             @Override
543             public void onAnimationStart(Animator animator) {
544                 mHintView.setText(hintResId);
545                 if (mHintView.getVisibility() != View.VISIBLE) {
546                     mHintView.setVisibility(View.VISIBLE);
547                     ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start();
548                 }
549             }
550         });
551         return bounceAnimator;
552     }
553 
getAlertAnimator(final View source, final int titleResId, final String infoText, final String accessibilityText, final int revealColor, final int backgroundColor)554     private Animator getAlertAnimator(final View source, final int titleResId,
555             final String infoText, final String accessibilityText, final int revealColor,
556             final int backgroundColor) {
557         final ViewGroup containerView = (ViewGroup) findViewById(android.R.id.content);
558 
559         final Rect sourceBounds = new Rect(0, 0, source.getHeight(), source.getWidth());
560         containerView.offsetDescendantRectToMyCoords(source, sourceBounds);
561 
562         final int centerX = sourceBounds.centerX();
563         final int centerY = sourceBounds.centerY();
564 
565         final int xMax = Math.max(centerX, containerView.getWidth() - centerX);
566         final int yMax = Math.max(centerY, containerView.getHeight() - centerY);
567 
568         final float startRadius = Math.max(sourceBounds.width(), sourceBounds.height()) / 2.0f;
569         final float endRadius = (float) Math.sqrt(xMax * xMax + yMax * yMax);
570 
571         final CircleView revealView = new CircleView(this)
572                 .setCenterX(centerX)
573                 .setCenterY(centerY)
574                 .setFillColor(revealColor);
575         containerView.addView(revealView);
576 
577         // TODO: Fade out source icon over the reveal (like LOLLIPOP version).
578 
579         final Animator revealAnimator = ObjectAnimator.ofFloat(
580                 revealView, CircleView.RADIUS, startRadius, endRadius);
581         revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS);
582         revealAnimator.setInterpolator(REVEAL_INTERPOLATOR);
583         revealAnimator.addListener(new AnimatorListenerAdapter() {
584             @Override
585             public void onAnimationEnd(Animator animator) {
586                 mAlertView.setVisibility(View.VISIBLE);
587                 mAlertTitleView.setText(titleResId);
588 
589                 if (infoText != null) {
590                     mAlertInfoView.setText(infoText);
591                     mAlertInfoView.setVisibility(View.VISIBLE);
592                 }
593                 mContentView.setVisibility(View.GONE);
594 
595                 getWindow().setBackgroundDrawable(new ColorDrawable(backgroundColor));
596             }
597         });
598 
599         final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
600         fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS);
601         fadeAnimator.addListener(new AnimatorListenerAdapter() {
602             @Override
603             public void onAnimationEnd(Animator animation) {
604                 containerView.removeView(revealView);
605             }
606         });
607 
608         final AnimatorSet alertAnimator = new AnimatorSet();
609         alertAnimator.play(revealAnimator).before(fadeAnimator);
610         alertAnimator.addListener(new AnimatorListenerAdapter() {
611             @Override
612             public void onAnimationEnd(Animator animator) {
613                 mAlertView.announceForAccessibility(accessibilityText);
614                 mHandler.postDelayed(new Runnable() {
615                     @Override
616                     public void run() {
617                         finish();
618                     }
619                 }, ALERT_DISMISS_DELAY_MILLIS);
620             }
621         });
622 
623         return alertAnimator;
624     }
625 }
626