• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ValueAnimator;
24 import android.app.Activity;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.pm.ActivityInfo;
30 import android.graphics.Color;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.preference.PreferenceManager;
34 import android.support.annotation.NonNull;
35 import android.view.KeyEvent;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.ViewAnimationUtils;
39 import android.view.ViewGroup;
40 import android.view.ViewGroupOverlay;
41 import android.view.WindowManager;
42 import android.view.animation.Interpolator;
43 import android.view.animation.PathInterpolator;
44 import android.widget.ImageView;
45 import android.widget.TextClock;
46 import android.widget.TextView;
47 
48 import com.android.deskclock.AnimatorUtils;
49 import com.android.deskclock.LogUtils;
50 import com.android.deskclock.R;
51 import com.android.deskclock.SettingsActivity;
52 import com.android.deskclock.Utils;
53 import com.android.deskclock.provider.AlarmInstance;
54 
55 public class AlarmActivity extends Activity implements View.OnClickListener, View.OnTouchListener {
56 
57     /**
58      * AlarmActivity listens for this broadcast intent, so that other applications can snooze the
59      * alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
60      */
61     public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
62     /**
63      * AlarmActivity listens for this broadcast intent, so that other applications can dismiss
64      * the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
65      */
66     public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
67 
68     private static final String LOGTAG = AlarmActivity.class.getSimpleName();
69 
70     private static final Interpolator PULSE_INTERPOLATOR =
71             new PathInterpolator(0.4f, 0.0f, 0.2f, 1.0f);
72     private static final Interpolator REVEAL_INTERPOLATOR =
73             new PathInterpolator(0.0f, 0.0f, 0.2f, 1.0f);
74 
75     private static final int PULSE_DURATION_MILLIS = 1000;
76     private static final int ALARM_BOUNCE_DURATION_MILLIS = 500;
77     private static final int ALERT_SOURCE_DURATION_MILLIS = 250;
78     private static final int ALERT_REVEAL_DURATION_MILLIS = 500;
79     private static final int ALERT_FADE_DURATION_MILLIS = 500;
80     private static final int ALERT_DISMISS_DELAY_MILLIS = 2000;
81 
82     private static final float BUTTON_SCALE_DEFAULT = 0.7f;
83     private static final int BUTTON_DRAWABLE_ALPHA_DEFAULT = 165;
84 
85     private final Handler mHandler = new Handler();
86     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
87         @Override
88         public void onReceive(Context context, Intent intent) {
89             final String action = intent.getAction();
90             LogUtils.v(LOGTAG, "Received broadcast: %s", action);
91 
92             if (!mAlarmHandled) {
93                 switch (action) {
94                     case ALARM_SNOOZE_ACTION:
95                         snooze();
96                         break;
97                     case ALARM_DISMISS_ACTION:
98                         dismiss();
99                         break;
100                     case AlarmService.ALARM_DONE_ACTION:
101                         finish();
102                         break;
103                     default:
104                         LogUtils.i(LOGTAG, "Unknown broadcast: %s", action);
105                         break;
106                 }
107             } else {
108                 LogUtils.v(LOGTAG, "Ignored broadcast: %s", action);
109             }
110         }
111     };
112 
113     private AlarmInstance mAlarmInstance;
114     private boolean mAlarmHandled;
115     private String mVolumeBehavior;
116     private int mCurrentHourColor;
117     private boolean mReceiverRegistered;
118 
119     private ViewGroup mContainerView;
120 
121     private ViewGroup mAlertView;
122     private TextView mAlertTitleView;
123     private TextView mAlertInfoView;
124 
125     private ViewGroup mContentView;
126     private ImageView mAlarmButton;
127     private ImageView mSnoozeButton;
128     private ImageView mDismissButton;
129     private TextView mHintView;
130 
131     private ValueAnimator mAlarmAnimator;
132     private ValueAnimator mSnoozeAnimator;
133     private ValueAnimator mDismissAnimator;
134     private ValueAnimator mPulseAnimator;
135 
136     @Override
onCreate(Bundle savedInstanceState)137     protected void onCreate(Bundle savedInstanceState) {
138         super.onCreate(savedInstanceState);
139 
140         final long instanceId = AlarmInstance.getId(getIntent().getData());
141         mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId);
142         if (mAlarmInstance == null) {
143             // The alarm got deleted before the activity got created, so just finish()
144             LogUtils.e(LOGTAG, "Error displaying alarm for intent: %s", getIntent());
145             finish();
146             return;
147         } else if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) {
148             LogUtils.i(LOGTAG, "Skip displaying alarm for instance: %s", mAlarmInstance);
149             finish();
150             return;
151         }
152 
153         LogUtils.i(LOGTAG, "Displaying alarm for instance: %s", mAlarmInstance);
154 
155         // Get the volume/camera button behavior setting
156         mVolumeBehavior = PreferenceManager.getDefaultSharedPreferences(this)
157                 .getString(SettingsActivity.KEY_VOLUME_BEHAVIOR,
158                         SettingsActivity.DEFAULT_VOLUME_BEHAVIOR);
159 
160         getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
161                 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
162                 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
163                 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
164                 | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);
165 
166         // In order to allow tablets to freely rotate and phones to stick
167         // with "nosensor" (use default device orientation) we have to have
168         // the manifest start with an orientation of unspecified" and only limit
169         // to "nosensor" for phones. Otherwise we get behavior like in b/8728671
170         // where tablets start off in their default orientation and then are
171         // able to freely rotate.
172         if (!getResources().getBoolean(R.bool.config_rotateAlarmAlert)) {
173             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
174         }
175 
176         setContentView(R.layout.alarm_activity);
177 
178         mContainerView = (ViewGroup) findViewById(android.R.id.content);
179 
180         mAlertView = (ViewGroup) mContainerView.findViewById(R.id.alert);
181         mAlertTitleView = (TextView) mAlertView.findViewById(R.id.alert_title);
182         mAlertInfoView = (TextView) mAlertView.findViewById(R.id.alert_info);
183 
184         mContentView = (ViewGroup) mContainerView.findViewById(R.id.content);
185         mAlarmButton = (ImageView) mContentView.findViewById(R.id.alarm);
186         mSnoozeButton = (ImageView) mContentView.findViewById(R.id.snooze);
187         mDismissButton = (ImageView) mContentView.findViewById(R.id.dismiss);
188         mHintView = (TextView) mContentView.findViewById(R.id.hint);
189 
190         final TextView titleView = (TextView) mContentView.findViewById(R.id.title);
191         final TextClock digitalClock = (TextClock) mContentView.findViewById(R.id.digital_clock);
192         final View pulseView = mContentView.findViewById(R.id.pulse);
193 
194         titleView.setText(mAlarmInstance.getLabelOrDefault(this));
195         Utils.setTimeFormat(digitalClock,
196                 getResources().getDimensionPixelSize(R.dimen.main_ampm_font_size));
197 
198         mCurrentHourColor = Utils.getCurrentHourColor();
199         mContainerView.setBackgroundColor(mCurrentHourColor);
200 
201         mAlarmButton.setOnTouchListener(this);
202         mSnoozeButton.setOnClickListener(this);
203         mDismissButton.setOnClickListener(this);
204 
205         mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f);
206         mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE);
207         mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor);
208         mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView,
209                 PropertyValuesHolder.ofFloat(View.SCALE_X, 0.0f, 1.0f),
210                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.0f, 1.0f),
211                 PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f));
212         mPulseAnimator.setDuration(PULSE_DURATION_MILLIS);
213         mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR);
214         mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
215         mPulseAnimator.start();
216 
217         // Set the animators to their initial values.
218         setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
219 
220         // Register to get the alarm done/snooze/dismiss intent.
221         final IntentFilter filter = new IntentFilter(AlarmService.ALARM_DONE_ACTION);
222         filter.addAction(ALARM_SNOOZE_ACTION);
223         filter.addAction(ALARM_DISMISS_ACTION);
224         registerReceiver(mReceiver, filter);
225         mReceiverRegistered = true;
226     }
227 
228     @Override
onDestroy()229     public void onDestroy() {
230         super.onDestroy();
231 
232         // Skip if register didn't happen to avoid IllegalArgumentException
233         if (mReceiverRegistered) {
234             unregisterReceiver(mReceiver);
235         }
236     }
237 
238     @Override
dispatchKeyEvent(@onNull KeyEvent keyEvent)239     public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) {
240         // Do this in dispatch to intercept a few of the system keys.
241         LogUtils.v(LOGTAG, "dispatchKeyEvent: %s", keyEvent);
242 
243         switch (keyEvent.getKeyCode()) {
244             // Volume keys and camera keys dismiss the alarm.
245             case KeyEvent.KEYCODE_POWER:
246             case KeyEvent.KEYCODE_VOLUME_UP:
247             case KeyEvent.KEYCODE_VOLUME_DOWN:
248             case KeyEvent.KEYCODE_VOLUME_MUTE:
249             case KeyEvent.KEYCODE_CAMERA:
250             case KeyEvent.KEYCODE_FOCUS:
251                 if (!mAlarmHandled && keyEvent.getAction() == KeyEvent.ACTION_UP) {
252                     switch (mVolumeBehavior) {
253                         case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE:
254                             snooze();
255                             break;
256                         case SettingsActivity.VOLUME_BEHAVIOR_DISMISS:
257                             dismiss();
258                             break;
259                         default:
260                             break;
261                     }
262                 }
263                 return true;
264             default:
265                 return super.dispatchKeyEvent(keyEvent);
266         }
267     }
268 
269     @Override
onBackPressed()270     public void onBackPressed() {
271         // Don't allow back to dismiss.
272     }
273 
274     @Override
onClick(View view)275     public void onClick(View view) {
276         if (mAlarmHandled) {
277             LogUtils.v(LOGTAG, "onClick ignored: %s", view);
278             return;
279         }
280         LogUtils.v(LOGTAG, "onClick: %s", view);
281 
282         final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
283         final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
284         final float translationX = Math.max(view.getLeft() - alarmRight, 0)
285                 + Math.min(view.getRight() - alarmLeft, 0);
286         getAlarmBounceAnimator(translationX, translationX < 0.0f ?
287                 R.string.description_direction_left : R.string.description_direction_right).start();
288     }
289 
290     @Override
onTouch(View view, MotionEvent motionEvent)291     public boolean onTouch(View view, MotionEvent motionEvent) {
292         if (mAlarmHandled) {
293             LogUtils.v(LOGTAG, "onTouch ignored: %s", motionEvent);
294             return false;
295         }
296 
297         final int[] contentLocation = {0, 0};
298         mContentView.getLocationOnScreen(contentLocation);
299 
300         final float x = motionEvent.getRawX() - contentLocation[0];
301         final float y = motionEvent.getRawY() - contentLocation[1];
302 
303         final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
304         final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
305 
306         final float snoozeFraction, dismissFraction;
307         if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
308             snoozeFraction = getFraction(alarmRight, mSnoozeButton.getLeft(), x);
309             dismissFraction = getFraction(alarmLeft, mDismissButton.getRight(), x);
310         } else {
311             snoozeFraction = getFraction(alarmLeft, mSnoozeButton.getRight(), x);
312             dismissFraction = getFraction(alarmRight, mDismissButton.getLeft(), x);
313         }
314         setAnimatedFractions(snoozeFraction, dismissFraction);
315 
316         switch (motionEvent.getActionMasked()) {
317             case MotionEvent.ACTION_DOWN:
318                 LogUtils.v(LOGTAG, "onTouch started: %s", motionEvent);
319 
320                 // Stop the pulse, allowing the last pulse to finish.
321                 mPulseAnimator.setRepeatCount(0);
322                 break;
323             case MotionEvent.ACTION_UP:
324                 LogUtils.v(LOGTAG, "onTouch ended: %s", motionEvent);
325 
326                 if (snoozeFraction == 1.0f) {
327                     snooze();
328                 } else if (dismissFraction == 1.0f) {
329                     dismiss();
330                 } else {
331                     if (snoozeFraction > 0.0f || dismissFraction > 0.0f) {
332                         // Animate back to the initial state.
333                         AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator);
334                     } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) {
335                         // User touched the alarm button, hint the dismiss action.
336                         mDismissButton.performClick();
337                     }
338 
339                     // Restart the pulse.
340                     mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
341                     if (!mPulseAnimator.isStarted()) {
342                         mPulseAnimator.start();
343                     }
344                 }
345                 break;
346             default:
347                 break;
348         }
349 
350         return true;
351     }
352 
snooze()353     private void snooze() {
354         mAlarmHandled = true;
355         LogUtils.v(LOGTAG, "Snoozed: %s", mAlarmInstance);
356 
357         final int alertColor = getResources().getColor(R.color.hot_pink);
358         setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
359 
360         final int snoozeMinutes = AlarmStateManager.getSnoozedMinutes(this);
361         final String infoText = getResources().getQuantityString(
362                 R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes);
363         final String accessibilityText = getResources().getQuantityString(
364                 R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes);
365 
366         getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText,
367                 accessibilityText, alertColor, alertColor).start();
368         AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */);
369     }
370 
dismiss()371     private void dismiss() {
372         mAlarmHandled = true;
373         LogUtils.v(LOGTAG, "Dismissed: %s", mAlarmInstance);
374 
375         setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */);
376         getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */,
377                 getString(R.string.alarm_alert_off_text) /* accessibilityText */,
378                 Color.WHITE, mCurrentHourColor).start();
379         AlarmStateManager.setDismissState(this, mAlarmInstance);
380     }
381 
setAnimatedFractions(float snoozeFraction, float dismissFraction)382     private void setAnimatedFractions(float snoozeFraction, float dismissFraction) {
383         final float alarmFraction = Math.max(snoozeFraction, dismissFraction);
384         mAlarmAnimator.setCurrentFraction(alarmFraction);
385         mSnoozeAnimator.setCurrentFraction(snoozeFraction);
386         mDismissAnimator.setCurrentFraction(dismissFraction);
387     }
388 
getFraction(float x0, float x1, float x)389     private float getFraction(float x0, float x1, float x) {
390         return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f);
391     }
392 
getButtonAnimator(ImageView button, int tintColor)393     private ValueAnimator getButtonAnimator(ImageView button, int tintColor) {
394         return ObjectAnimator.ofPropertyValuesHolder(button,
395                 PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f),
396                 PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f),
397                 PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255),
398                 PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA,
399                         BUTTON_DRAWABLE_ALPHA_DEFAULT, 255),
400                 PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT,
401                         AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor));
402     }
403 
getAlarmBounceAnimator(float translationX, final int hintResId)404     private ValueAnimator getAlarmBounceAnimator(float translationX, final int hintResId) {
405         final ValueAnimator bounceAnimator = ObjectAnimator.ofFloat(mAlarmButton,
406                 View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f);
407         bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR);
408         bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS);
409         bounceAnimator.addListener(new AnimatorListenerAdapter() {
410             @Override
411             public void onAnimationStart(Animator animator) {
412                 mHintView.setText(hintResId);
413                 if (mHintView.getVisibility() != View.VISIBLE) {
414                     mHintView.setVisibility(View.VISIBLE);
415                     ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start();
416                 }
417             }
418         });
419         return bounceAnimator;
420     }
421 
getAlertAnimator(final View source, final int titleResId, final String infoText, final String accessibilityText, final int revealColor, final int backgroundColor)422     private Animator getAlertAnimator(final View source, final int titleResId,
423             final String infoText, final String accessibilityText, final int revealColor,
424             final int backgroundColor) {
425         final ViewGroupOverlay overlay = mContainerView.getOverlay();
426 
427         // Create a transient view for performing the reveal animation.
428         final View revealView = new View(this);
429         revealView.setRight(mContainerView.getWidth());
430         revealView.setBottom(mContainerView.getHeight());
431         revealView.setBackgroundColor(revealColor);
432         overlay.add(revealView);
433 
434         // Add the source to the containerView's overlay so that the animation can occur under the
435         // status bar, the source view will be automatically positioned in the overlay so that
436         // it maintains the same relative position on screen.
437         overlay.add(source);
438 
439         final int centerX = Math.round((source.getLeft() + source.getRight()) / 2.0f);
440         final int centerY = Math.round((source.getTop() + source.getBottom()) / 2.0f);
441         final float startRadius = Math.max(source.getWidth(), source.getHeight()) / 2.0f;
442 
443         final int xMax = Math.max(centerX, mContainerView.getWidth() - centerX);
444         final int yMax = Math.max(centerY, mContainerView.getHeight() - centerY);
445         final float endRadius = (float) Math.sqrt(Math.pow(xMax, 2.0) + Math.pow(yMax, 2.0));
446 
447         final ValueAnimator sourceAnimator = ObjectAnimator.ofFloat(source, View.ALPHA, 0.0f);
448         sourceAnimator.setDuration(ALERT_SOURCE_DURATION_MILLIS);
449         sourceAnimator.addListener(new AnimatorListenerAdapter() {
450             @Override
451             public void onAnimationEnd(Animator animation) {
452                 overlay.remove(source);
453             }
454         });
455 
456         final Animator revealAnimator = ViewAnimationUtils.createCircularReveal(
457                 revealView, centerX, centerY, startRadius, endRadius);
458         revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS);
459         revealAnimator.setInterpolator(REVEAL_INTERPOLATOR);
460         revealAnimator.addListener(new AnimatorListenerAdapter() {
461             @Override
462             public void onAnimationEnd(Animator animator) {
463                 mAlertView.setVisibility(View.VISIBLE);
464                 mAlertTitleView.setText(titleResId);
465 
466                 if (infoText != null) {
467                     mAlertInfoView.setText(infoText);
468                     mAlertInfoView.setVisibility(View.VISIBLE);
469                 }
470                 mAlertView.announceForAccessibility(accessibilityText);
471                 mContentView.setVisibility(View.GONE);
472                 mContainerView.setBackgroundColor(backgroundColor);
473             }
474         });
475 
476         final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
477         fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS);
478         fadeAnimator.addListener(new AnimatorListenerAdapter() {
479             @Override
480             public void onAnimationEnd(Animator animation) {
481                 overlay.remove(revealView);
482             }
483         });
484 
485         final AnimatorSet alertAnimator = new AnimatorSet();
486         alertAnimator.play(revealAnimator).with(sourceAnimator).before(fadeAnimator);
487         alertAnimator.addListener(new AnimatorListenerAdapter() {
488             @Override
489             public void onAnimationEnd(Animator animator) {
490                 mHandler.postDelayed(new Runnable() {
491                     @Override
492                     public void run() {
493                         finish();
494                     }
495                 }, ALERT_DISMISS_DELAY_MILLIS);
496             }
497         });
498 
499         return alertAnimator;
500     }
501 }
502