1 /* 2 * Copyright (C) 2020 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 17 package com.android.deskclock.alarms 18 19 import android.accessibilityservice.AccessibilityServiceInfo 20 import android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC 21 import android.animation.Animator 22 import android.animation.AnimatorListenerAdapter 23 import android.animation.AnimatorSet 24 import android.animation.ObjectAnimator 25 import android.animation.PropertyValuesHolder 26 import android.animation.TimeInterpolator 27 import android.animation.ValueAnimator 28 import android.content.BroadcastReceiver 29 import android.content.ComponentName 30 import android.content.Context 31 import android.content.Intent 32 import android.content.IntentFilter 33 import android.content.ServiceConnection 34 import android.content.pm.ActivityInfo 35 import android.graphics.Color 36 import android.graphics.Rect 37 import android.graphics.drawable.ColorDrawable 38 import android.media.AudioManager 39 import android.os.Bundle 40 import android.os.Handler 41 import android.os.IBinder 42 import android.os.Looper 43 import android.view.KeyEvent 44 import android.view.MotionEvent 45 import android.view.View 46 import android.view.ViewGroup 47 import android.view.WindowManager 48 import android.view.accessibility.AccessibilityManager 49 import android.widget.ImageView 50 import android.widget.TextClock 51 import android.widget.TextView 52 import androidx.core.graphics.ColorUtils 53 import androidx.core.view.animation.PathInterpolatorCompat 54 55 import com.android.deskclock.AnimatorUtils 56 import com.android.deskclock.BaseActivity 57 import com.android.deskclock.LogUtils 58 import com.android.deskclock.data.DataModel 59 import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior 60 import com.android.deskclock.events.Events 61 import com.android.deskclock.provider.AlarmInstance 62 import com.android.deskclock.provider.ClockContract.InstancesColumns 63 import com.android.deskclock.R 64 import com.android.deskclock.ThemeUtils 65 import com.android.deskclock.Utils 66 import com.android.deskclock.widget.CircleView 67 68 import kotlin.math.max 69 import kotlin.math.sqrt 70 71 class AlarmActivity : BaseActivity(), View.OnClickListener, View.OnTouchListener { 72 private val mHandler: Handler = Handler(Looper.myLooper()!!) 73 74 private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() { onReceivenull75 override fun onReceive(context: Context?, intent: Intent) { 76 val action: String? = intent.getAction() 77 LOGGER.v("Received broadcast: %s", action) 78 79 if (!mAlarmHandled) { 80 when (action) { 81 AlarmService.ALARM_SNOOZE_ACTION -> snooze() 82 AlarmService.ALARM_DISMISS_ACTION -> dismiss() 83 AlarmService.ALARM_DONE_ACTION -> finish() 84 else -> LOGGER.i("Unknown broadcast: %s", action) 85 } 86 } else { 87 LOGGER.v("Ignored broadcast: %s", action) 88 } 89 } 90 } 91 92 private val mConnection: ServiceConnection = object : ServiceConnection { onServiceConnectednull93 override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 94 LOGGER.i("Finished binding to AlarmService") 95 } 96 onServiceDisconnectednull97 override fun onServiceDisconnected(name: ComponentName?) { 98 LOGGER.i("Disconnected from AlarmService") 99 } 100 } 101 102 private var mAlarmInstance: AlarmInstance? = null 103 private var mAlarmHandled = false 104 private var mVolumeBehavior: AlarmVolumeButtonBehavior? = null 105 private var mCurrentHourColor = 0 106 private var mReceiverRegistered = false 107 /** Whether the AlarmService is currently bound */ 108 private var mServiceBound = false 109 110 private var mAccessibilityManager: AccessibilityManager? = null 111 112 private lateinit var mAlertView: ViewGroup 113 private lateinit var mAlertTitleView: TextView 114 private lateinit var mAlertInfoView: TextView 115 116 private lateinit var mContentView: ViewGroup 117 private lateinit var mAlarmButton: ImageView 118 private lateinit var mSnoozeButton: ImageView 119 private lateinit var mDismissButton: ImageView 120 private lateinit var mHintView: TextView 121 122 private lateinit var mAlarmAnimator: ValueAnimator 123 private lateinit var mSnoozeAnimator: ValueAnimator 124 private lateinit var mDismissAnimator: ValueAnimator 125 private lateinit var mPulseAnimator: ValueAnimator 126 127 private var mInitialPointerIndex: Int = MotionEvent.INVALID_POINTER_ID 128 onCreatenull129 override fun onCreate(savedInstanceState: Bundle?) { 130 super.onCreate(savedInstanceState) 131 132 setVolumeControlStream(AudioManager.STREAM_ALARM) 133 val instanceId = AlarmInstance.getId(getIntent().getData()!!) 134 mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId) 135 if (mAlarmInstance == null) { 136 // The alarm was deleted before the activity got created, so just finish() 137 LOGGER.e("Error displaying alarm for intent: %s", getIntent()) 138 finish() 139 return 140 } else if (mAlarmInstance!!.mAlarmState != InstancesColumns.FIRED_STATE) { 141 LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance) 142 finish() 143 return 144 } 145 146 LOGGER.i("Displaying alarm for instance: %s", mAlarmInstance) 147 148 // Get the volume/camera button behavior setting 149 mVolumeBehavior = DataModel.dataModel.alarmVolumeButtonBehavior 150 151 if (Utils.isOOrLater) { 152 setShowWhenLocked(true) 153 setTurnScreenOn(true) 154 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 155 or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON) 156 } else { 157 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 158 or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD 159 or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 160 or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 161 or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON) 162 } 163 164 // Hide navigation bar to minimize accidental tap on Home key 165 hideNavigationBar() 166 167 // Close dialogs and window shade, so this is fully visible 168 sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) 169 170 // Honor rotation on tablets; fix the orientation on phones. 171 if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) { 172 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) 173 } 174 175 mAccessibilityManager = getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager? 176 177 setContentView(R.layout.alarm_activity) 178 179 mAlertView = findViewById(R.id.alert) as ViewGroup 180 mAlertTitleView = mAlertView.findViewById(R.id.alert_title) as TextView 181 mAlertInfoView = mAlertView.findViewById(R.id.alert_info) as TextView 182 183 mContentView = findViewById(R.id.content) as ViewGroup 184 mAlarmButton = mContentView.findViewById(R.id.alarm) as ImageView 185 mSnoozeButton = mContentView.findViewById(R.id.snooze) as ImageView 186 mDismissButton = mContentView.findViewById(R.id.dismiss) as ImageView 187 mHintView = mContentView.findViewById(R.id.hint) as TextView 188 189 val titleView: TextView = mContentView.findViewById(R.id.title) as TextView 190 val digitalClock: TextClock = mContentView.findViewById(R.id.digital_clock) as TextClock 191 val pulseView = mContentView.findViewById(R.id.pulse) as CircleView 192 193 titleView.setText(mAlarmInstance!!.getLabelOrDefault(this)) 194 Utils.setTimeFormat(digitalClock, false) 195 196 mCurrentHourColor = ThemeUtils.resolveColor(this, android.R.attr.windowBackground) 197 getWindow().setBackgroundDrawable(ColorDrawable(mCurrentHourColor)) 198 199 mAlarmButton.setOnTouchListener(this) 200 mSnoozeButton.setOnClickListener(this) 201 mDismissButton.setOnClickListener(this) 202 203 mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f) 204 mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE) 205 mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor) 206 mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView, 207 PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.radius), 208 PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR, 209 ColorUtils.setAlphaComponent(pulseView.fillColor, 0))) 210 mPulseAnimator.setDuration(PULSE_DURATION_MILLIS.toLong()) 211 mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR) 212 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE) 213 mPulseAnimator.start() 214 } 215 onResumenull216 override fun onResume() { 217 super.onResume() 218 219 // Re-query for AlarmInstance in case the state has changed externally 220 val instanceId = AlarmInstance.getId(getIntent().getData()!!) 221 mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId) 222 223 if (mAlarmInstance == null) { 224 LOGGER.i("No alarm instance for instanceId: %d", instanceId) 225 finish() 226 return 227 } 228 229 // Verify that the alarm is still firing before showing the activity 230 if (mAlarmInstance!!.mAlarmState != InstancesColumns.FIRED_STATE) { 231 LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance) 232 finish() 233 return 234 } 235 236 if (!mReceiverRegistered) { 237 // Register to get the alarm done/snooze/dismiss intent. 238 val filter = IntentFilter(AlarmService.ALARM_DONE_ACTION) 239 filter.addAction(AlarmService.ALARM_SNOOZE_ACTION) 240 filter.addAction(AlarmService.ALARM_DISMISS_ACTION) 241 registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED) 242 mReceiverRegistered = true 243 } 244 bindAlarmService() 245 resetAnimations() 246 } 247 onPausenull248 override fun onPause() { 249 super.onPause() 250 unbindAlarmService() 251 252 // Skip if register didn't happen to avoid IllegalArgumentException 253 if (mReceiverRegistered) { 254 unregisterReceiver(mReceiver) 255 mReceiverRegistered = false 256 } 257 } 258 dispatchKeyEventnull259 override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean { 260 // Do this in dispatch to intercept a few of the system keys. 261 LOGGER.v("dispatchKeyEvent: %s", keyEvent) 262 263 val keyCode: Int = keyEvent.getKeyCode() 264 when (keyCode) { 265 KeyEvent.KEYCODE_VOLUME_UP, 266 KeyEvent.KEYCODE_VOLUME_DOWN, 267 KeyEvent.KEYCODE_VOLUME_MUTE, 268 KeyEvent.KEYCODE_HEADSETHOOK, 269 KeyEvent.KEYCODE_CAMERA, 270 KeyEvent.KEYCODE_FOCUS -> if (!mAlarmHandled) { 271 when (mVolumeBehavior) { 272 AlarmVolumeButtonBehavior.SNOOZE -> { 273 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 274 snooze() 275 } 276 return true 277 } 278 AlarmVolumeButtonBehavior.DISMISS -> { 279 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 280 dismiss() 281 } 282 return true 283 } 284 AlarmVolumeButtonBehavior.NOTHING -> { 285 } 286 null -> { } 287 } 288 } 289 } 290 return super.dispatchKeyEvent(keyEvent) 291 } 292 onBackPressednull293 override fun onBackPressed() { 294 // Don't allow back to dismiss. 295 } 296 onClicknull297 override fun onClick(view: View) { 298 if (mAlarmHandled) { 299 LOGGER.v("onClick ignored: %s", view) 300 return 301 } 302 LOGGER.v("onClick: %s", view) 303 304 // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons. 305 if (isAccessibilityEnabled) { 306 if (view == mSnoozeButton) { 307 snooze() 308 } else if (view == mDismissButton) { 309 dismiss() 310 } 311 return 312 } 313 314 if (view == mSnoozeButton) { 315 hintSnooze() 316 } else if (view == mDismissButton) { 317 hintDismiss() 318 } 319 } 320 onTouchnull321 override fun onTouch(view: View?, event: MotionEvent): Boolean { 322 if (mAlarmHandled) { 323 LOGGER.v("onTouch ignored: %s", event) 324 return false 325 } 326 327 val action: Int = event.getActionMasked() 328 if (action == MotionEvent.ACTION_DOWN) { 329 LOGGER.v("onTouch started: %s", event) 330 331 // Track the pointer that initiated the touch sequence. 332 mInitialPointerIndex = event.getPointerId(event.getActionIndex()) 333 334 // Stop the pulse, allowing the last pulse to finish. 335 mPulseAnimator.setRepeatCount(0) 336 } else if (action == MotionEvent.ACTION_CANCEL) { 337 LOGGER.v("onTouch canceled: %s", event) 338 339 // Clear the pointer index. 340 mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID 341 342 // Reset everything. 343 resetAnimations() 344 } 345 346 val actionIndex: Int = event.getActionIndex() 347 if (mInitialPointerIndex == MotionEvent.INVALID_POINTER_ID || 348 mInitialPointerIndex != event.getPointerId(actionIndex)) { 349 // Ignore any pointers other than the initial one, bail early. 350 return true 351 } 352 353 val contentLocation = intArrayOf(0, 0) 354 mContentView.getLocationOnScreen(contentLocation) 355 356 val x: Float = event.getRawX() - contentLocation[0] 357 val y: Float = event.getRawY() - contentLocation[1] 358 359 val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft() 360 val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight() 361 362 val snoozeFraction: Float 363 val dismissFraction: Float 364 if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 365 snoozeFraction = 366 getFraction(alarmRight.toFloat(), mSnoozeButton.getLeft().toFloat(), x) 367 dismissFraction = 368 getFraction(alarmLeft.toFloat(), mDismissButton.getRight().toFloat(), x) 369 } else { 370 snoozeFraction = getFraction(alarmLeft.toFloat(), mSnoozeButton.getRight().toFloat(), x) 371 dismissFraction = 372 getFraction(alarmRight.toFloat(), mDismissButton.getLeft().toFloat(), x) 373 } 374 setAnimatedFractions(snoozeFraction, dismissFraction) 375 376 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { 377 LOGGER.v("onTouch ended: %s", event) 378 379 mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID 380 if (snoozeFraction == 1.0f) { 381 snooze() 382 } else if (dismissFraction == 1.0f) { 383 dismiss() 384 } else { 385 if (snoozeFraction > 0.0f || dismissFraction > 0.0f) { 386 // Animate back to the initial state. 387 AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator) 388 } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) { 389 // User touched the alarm button, hint the dismiss action. 390 hintDismiss() 391 } 392 393 // Restart the pulse. 394 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE) 395 if (!mPulseAnimator.isStarted()) { 396 mPulseAnimator.start() 397 } 398 } 399 } 400 401 return true 402 } 403 hideNavigationBarnull404 private fun hideNavigationBar() { 405 getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 406 or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 407 or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) 408 } 409 410 /** 411 * Returns `true` if accessibility is enabled, to enable alternate behavior for click 412 * handling, etc. 413 */ 414 private val isAccessibilityEnabled: Boolean 415 get() { 416 if (mAccessibilityManager == null || !mAccessibilityManager!!.isEnabled()) { 417 // Accessibility is unavailable or disabled. 418 return false 419 } else if (mAccessibilityManager!!.isTouchExplorationEnabled()) { 420 // TalkBack's touch exploration mode is enabled. 421 return true 422 } 423 424 // Check if "Switch Access" is enabled. 425 val enabledAccessibilityServices: List<AccessibilityServiceInfo> = 426 mAccessibilityManager!!.getEnabledAccessibilityServiceList(FEEDBACK_GENERIC) 427 return !enabledAccessibilityServices.isEmpty() 428 } 429 hintSnoozenull430 private fun hintSnooze() { 431 val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft() 432 val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight() 433 val translationX = (Math.max(mSnoozeButton.getLeft() - alarmRight, 0) + 434 Math.min(mSnoozeButton.getRight() - alarmLeft, 0)).toFloat() 435 getAlarmBounceAnimator(translationX, if (translationX < 0.0f) { 436 R.string.description_direction_left 437 } else { 438 R.string.description_direction_right 439 }).start() 440 } 441 hintDismissnull442 private fun hintDismiss() { 443 val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft() 444 val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight() 445 val translationX = (Math.max(mDismissButton.getLeft() - alarmRight, 0) + 446 Math.min(mDismissButton.getRight() - alarmLeft, 0)).toFloat() 447 getAlarmBounceAnimator(translationX, if (translationX < 0.0f) { 448 R.string.description_direction_left 449 } else { 450 R.string.description_direction_right 451 }).start() 452 } 453 454 /** 455 * Set animators to initial values and restart pulse on alarm button. 456 */ resetAnimationsnull457 private fun resetAnimations() { 458 // Set the animators to their initial values. 459 setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */) 460 // Restart the pulse. 461 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE) 462 if (!mPulseAnimator.isStarted()) { 463 mPulseAnimator.start() 464 } 465 } 466 467 /** 468 * Perform snooze animation and send snooze intent. 469 */ snoozenull470 private fun snooze() { 471 mAlarmHandled = true 472 LOGGER.v("Snoozed: %s", mAlarmInstance) 473 474 val colorAccent = ThemeUtils.resolveColor(this, R.attr.colorAccent) 475 setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */) 476 477 val snoozeMinutes = DataModel.dataModel.snoozeLength 478 val infoText: String = getResources().getQuantityString( 479 R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes) 480 val accessibilityText: String = getResources().getQuantityString( 481 R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes) 482 483 getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText, 484 accessibilityText, colorAccent, colorAccent).start() 485 486 AlarmStateManager.setSnoozeState(this, mAlarmInstance!!, false /* showToast */) 487 488 Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock) 489 490 // Unbind here, otherwise alarm will keep ringing until activity finishes. 491 unbindAlarmService() 492 } 493 494 /** 495 * Perform dismiss animation and send dismiss intent. 496 */ dismissnull497 private fun dismiss() { 498 mAlarmHandled = true 499 LOGGER.v("Dismissed: %s", mAlarmInstance) 500 501 setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */) 502 503 getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */, 504 getString(R.string.alarm_alert_off_text) /* accessibilityText */, 505 Color.WHITE, mCurrentHourColor).start() 506 507 AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance!!) 508 509 Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock) 510 511 // Unbind here, otherwise alarm will keep ringing until activity finishes. 512 unbindAlarmService() 513 } 514 515 /** 516 * Bind AlarmService if not yet bound. 517 */ bindAlarmServicenull518 private fun bindAlarmService() { 519 if (!mServiceBound) { 520 val intent = Intent(this, AlarmService::class.java) 521 bindService(intent, mConnection, Context.BIND_AUTO_CREATE) 522 mServiceBound = true 523 } 524 } 525 526 /** 527 * Unbind AlarmService if bound. 528 */ unbindAlarmServicenull529 private fun unbindAlarmService() { 530 if (mServiceBound) { 531 unbindService(mConnection) 532 mServiceBound = false 533 } 534 } 535 setAnimatedFractionsnull536 private fun setAnimatedFractions(snoozeFraction: Float, dismissFraction: Float) { 537 val alarmFraction = Math.max(snoozeFraction, dismissFraction) 538 AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction) 539 AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction) 540 AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction) 541 } 542 getFractionnull543 private fun getFraction(x0: Float, x1: Float, x: Float): Float { 544 return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f) 545 } 546 getButtonAnimatornull547 private fun getButtonAnimator(button: ImageView?, tintColor: Int): ValueAnimator { 548 return ObjectAnimator.ofPropertyValuesHolder(button, 549 PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f), 550 PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f), 551 PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255), 552 PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA, 553 BUTTON_DRAWABLE_ALPHA_DEFAULT, 255), 554 PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT, 555 AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor)) 556 } 557 getAlarmBounceAnimatornull558 private fun getAlarmBounceAnimator(translationX: Float, hintResId: Int): ValueAnimator { 559 val bounceAnimator: ValueAnimator = ObjectAnimator.ofFloat(mAlarmButton, 560 View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f) 561 bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR) 562 bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS.toLong()) 563 bounceAnimator.addListener(object : AnimatorListenerAdapter() { 564 override fun onAnimationStart(animator: Animator) { 565 mHintView.setText(hintResId) 566 if (mHintView.getVisibility() != View.VISIBLE) { 567 mHintView.setVisibility(View.VISIBLE) 568 ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start() 569 } 570 } 571 }) 572 return bounceAnimator 573 } 574 getAlertAnimatornull575 private fun getAlertAnimator( 576 source: View, 577 titleResId: Int, 578 infoText: String?, 579 accessibilityText: String, 580 revealColor: Int, 581 backgroundColor: Int 582 ): Animator { 583 val containerView: ViewGroup = findViewById(android.R.id.content) as ViewGroup 584 585 val sourceBounds = Rect(0, 0, source.getHeight(), source.getWidth()) 586 containerView.offsetDescendantRectToMyCoords(source, sourceBounds) 587 588 val centerX: Int = sourceBounds.centerX() 589 val centerY: Int = sourceBounds.centerY() 590 591 val xMax = max(centerX, containerView.getWidth() - centerX) 592 val yMax = max(centerY, containerView.getHeight() - centerY) 593 594 val startRadius: Float = max(sourceBounds.width(), sourceBounds.height()) / 2.0f 595 val endRadius = sqrt(xMax * xMax + yMax * yMax.toDouble()).toFloat() 596 597 val revealView = CircleView(this) 598 .setCenterX(centerX.toFloat()) 599 .setCenterY(centerY.toFloat()) 600 .setFillColor(revealColor) 601 containerView.addView(revealView) 602 603 // TODO: Fade out source icon over the reveal (like LOLLIPOP version). 604 605 val revealAnimator: Animator = ObjectAnimator.ofFloat( 606 revealView, CircleView.RADIUS, startRadius, endRadius) 607 revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS.toLong()) 608 revealAnimator.setInterpolator(REVEAL_INTERPOLATOR) 609 revealAnimator.addListener(object : AnimatorListenerAdapter() { 610 override fun onAnimationEnd(animator: Animator) { 611 mAlertView.setVisibility(View.VISIBLE) 612 mAlertTitleView.setText(titleResId) 613 if (infoText != null) { 614 mAlertInfoView.setText(infoText) 615 mAlertInfoView.setVisibility(View.VISIBLE) 616 } 617 mContentView.setVisibility(View.GONE) 618 getWindow().setBackgroundDrawable(ColorDrawable(backgroundColor)) 619 } 620 }) 621 622 val fadeAnimator: ValueAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f) 623 fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS.toLong()) 624 fadeAnimator.addListener(object : AnimatorListenerAdapter() { 625 override fun onAnimationEnd(animation: Animator) { 626 containerView.removeView(revealView) 627 } 628 }) 629 630 val alertAnimator = AnimatorSet() 631 alertAnimator.play(revealAnimator).before(fadeAnimator) 632 alertAnimator.addListener(object : AnimatorListenerAdapter() { 633 override fun onAnimationEnd(animator: Animator) { 634 mAlertView.announceForAccessibility(accessibilityText) 635 mHandler.postDelayed(Runnable { finish() }, ALERT_DISMISS_DELAY_MILLIS.toLong()) 636 } 637 }) 638 639 return alertAnimator 640 } 641 642 companion object { 643 private val LOGGER = LogUtils.Logger("AlarmActivity") 644 645 private val PULSE_INTERPOLATOR: TimeInterpolator = 646 PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f) 647 private val REVEAL_INTERPOLATOR: TimeInterpolator = 648 PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f) 649 650 private const val PULSE_DURATION_MILLIS = 1000 651 private const val ALARM_BOUNCE_DURATION_MILLIS = 500 652 private const val ALERT_REVEAL_DURATION_MILLIS = 500 653 private const val ALERT_FADE_DURATION_MILLIS = 500 654 private const val ALERT_DISMISS_DELAY_MILLIS = 2000 655 656 private const val BUTTON_SCALE_DEFAULT = 0.7f 657 private const val BUTTON_DRAWABLE_ALPHA_DEFAULT = 165 658 } 659 }