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