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