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