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 androidx.annotation.NonNull;
41 import androidx.core.graphics.ColorUtils;
42 import androidx.core.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
onCreate(Bundle savedInstanceState)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
onResume()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
onPause()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
dispatchKeyEvent(@onNull KeyEvent keyEvent)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
onBackPressed()317     public void onBackPressed() {
318         // Don't allow back to dismiss.
319     }
320 
321     @Override
onClick(View view)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
onTouch(View view, MotionEvent event)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 
hideNavigationBar()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      */
isAccessibilityEnabled()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 
hintSnooze()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 
hintDismiss()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      */
resetAnimations()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      */
snooze()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      */
dismiss()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      */
bindAlarmService()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      */
unbindAlarmService()544     private void unbindAlarmService() {
545         if (mServiceBound) {
546             unbindService(mConnection);
547             mServiceBound = false;
548         }
549     }
550 
setAnimatedFractions(float snoozeFraction, float dismissFraction)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 
getFraction(float x0, float x1, float x)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 
getButtonAnimator(ImageView button, int tintColor)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 
getAlarmBounceAnimator(float translationX, final int hintResId)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 
getAlertAnimator(final View source, final int titleResId, final String infoText, final String accessibilityText, final int revealColor, final int backgroundColor)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