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 */
16package com.android.deskclock.alarms;
17
18import android.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.app.Activity;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.pm.ActivityInfo;
30import android.graphics.Color;
31import android.os.Bundle;
32import android.os.Handler;
33import android.preference.PreferenceManager;
34import android.support.annotation.NonNull;
35import android.view.KeyEvent;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.ViewAnimationUtils;
39import android.view.ViewGroup;
40import android.view.ViewGroupOverlay;
41import android.view.WindowManager;
42import android.view.animation.Interpolator;
43import android.view.animation.PathInterpolator;
44import android.widget.ImageButton;
45import android.widget.TextClock;
46import android.widget.TextView;
47
48import com.android.deskclock.AnimatorUtils;
49import com.android.deskclock.LogUtils;
50import com.android.deskclock.R;
51import com.android.deskclock.SettingsActivity;
52import com.android.deskclock.Utils;
53import com.android.deskclock.provider.AlarmInstance;
54
55public 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
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
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
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
265    public void onBackPressed() {
266        // Don't allow back to dismiss.
267    }
268
269    @Override
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
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
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
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
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
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
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
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
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