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.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
    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