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.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.animation.TimeInterpolator;
24import android.animation.ValueAnimator;
25import android.content.BroadcastReceiver;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.ServiceConnection;
31import android.content.pm.ActivityInfo;
32import android.graphics.Color;
33import android.graphics.Rect;
34import android.graphics.drawable.ColorDrawable;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.IBinder;
38import android.preference.PreferenceManager;
39import android.support.annotation.NonNull;
40import android.support.v4.graphics.ColorUtils;
41import android.support.v4.view.animation.PathInterpolatorCompat;
42import android.support.v7.app.AppCompatActivity;
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.LogUtils;
55import com.android.deskclock.R;
56import com.android.deskclock.Utils;
57import com.android.deskclock.events.Events;
58import com.android.deskclock.provider.AlarmInstance;
59import com.android.deskclock.settings.SettingsActivity;
60import com.android.deskclock.widget.CircleView;
61
62public 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
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
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
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
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
313    public void onBackPressed() {
314        // Don't allow back to dismiss.
315    }
316
317    @Override
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
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
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
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
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     */
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     */
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     */
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     */
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     */
507    private void unbindAlarmService() {
508        if (mServiceBound) {
509            unbindService(mConnection);
510            mServiceBound = false;
511        }
512    }
513
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
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
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
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
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