Home | History | Annotate | Download | only in alarms
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.deskclock.alarms;
     17 
     18 import android.animation.Animator;
     19 import android.animation.AnimatorListenerAdapter;
     20 import android.animation.AnimatorSet;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.PropertyValuesHolder;
     23 import android.animation.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
    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