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