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