Home | History | Annotate | Download | only in alarms
      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.accessibilityservice.AccessibilityServiceInfo;
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.AnimatorSet;
     22 import android.animation.ObjectAnimator;
     23 import android.animation.PropertyValuesHolder;
     24 import android.animation.TimeInterpolator;
     25 import android.animation.ValueAnimator;
     26 import android.content.BroadcastReceiver;
     27 import android.content.ComponentName;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.IntentFilter;
     31 import android.content.ServiceConnection;
     32 import android.content.pm.ActivityInfo;
     33 import android.graphics.Color;
     34 import android.graphics.Rect;
     35 import android.graphics.drawable.ColorDrawable;
     36 import android.media.AudioManager;
     37 import android.os.Bundle;
     38 import android.os.Handler;
     39 import android.os.IBinder;
     40 import android.support.annotation.NonNull;
     41 import android.support.v4.graphics.ColorUtils;
     42 import android.support.v4.view.animation.PathInterpolatorCompat;
     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.BaseActivity;
     55 import com.android.deskclock.LogUtils;
     56 import com.android.deskclock.R;
     57 import com.android.deskclock.ThemeUtils;
     58 import com.android.deskclock.Utils;
     59 import com.android.deskclock.data.DataModel;
     60 import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
     61 import com.android.deskclock.events.Events;
     62 import com.android.deskclock.provider.AlarmInstance;
     63 import com.android.deskclock.widget.CircleView;
     64 
     65 import java.util.List;
     66 
     67 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
     68 
     69 public 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