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) 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 } 287 } 288 } 289 return super.dispatchKeyEvent(keyEvent) 290 } 291 onBackPressednull292 override fun onBackPressed() { 293 // Don't allow back to dismiss. 294 } 295 onClicknull296 override fun onClick(view: View) { 297 if (mAlarmHandled) { 298 LOGGER.v("onClick ignored: %s", view) 299 return 300 } 301 LOGGER.v("onClick: %s", view) 302 303 // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons. 304 if (isAccessibilityEnabled) { 305 if (view == mSnoozeButton) { 306 snooze() 307 } else if (view == mDismissButton) { 308 dismiss() 309 } 310 return 311 } 312 313 if (view == mSnoozeButton) { 314 hintSnooze() 315 } else if (view == mDismissButton) { 316 hintDismiss() 317 } 318 } 319 onTouchnull320 override fun onTouch(view: View?, event: MotionEvent): Boolean { 321 if (mAlarmHandled) { 322 LOGGER.v("onTouch ignored: %s", event) 323 return false 324 } 325 326 val action: Int = event.getActionMasked() 327 if (action == MotionEvent.ACTION_DOWN) { 328 LOGGER.v("onTouch started: %s", event) 329 330 // Track the pointer that initiated the touch sequence. 331 mInitialPointerIndex = event.getPointerId(event.getActionIndex()) 332 333 // Stop the pulse, allowing the last pulse to finish. 334 mPulseAnimator.setRepeatCount(0) 335 } else if (action == MotionEvent.ACTION_CANCEL) { 336 LOGGER.v("onTouch canceled: %s", event) 337 338 // Clear the pointer index. 339 mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID 340 341 // Reset everything. 342 resetAnimations() 343 } 344 345 val actionIndex: Int = event.getActionIndex() 346 if (mInitialPointerIndex == MotionEvent.INVALID_POINTER_ID || 347 mInitialPointerIndex != event.getPointerId(actionIndex)) { 348 // Ignore any pointers other than the initial one, bail early. 349 return true 350 } 351 352 val contentLocation = intArrayOf(0, 0) 353 mContentView.getLocationOnScreen(contentLocation) 354 355 val x: Float = event.getRawX() - contentLocation[0] 356 val y: Float = event.getRawY() - contentLocation[1] 357 358 val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft() 359 val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight() 360 361 val snoozeFraction: Float 362 val dismissFraction: Float 363 if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 364 snoozeFraction = 365 getFraction(alarmRight.toFloat(), mSnoozeButton.getLeft().toFloat(), x) 366 dismissFraction = 367 getFraction(alarmLeft.toFloat(), mDismissButton.getRight().toFloat(), x) 368 } else { 369 snoozeFraction = getFraction(alarmLeft.toFloat(), mSnoozeButton.getRight().toFloat(), x) 370 dismissFraction = 371 getFraction(alarmRight.toFloat(), mDismissButton.getLeft().toFloat(), x) 372 } 373 setAnimatedFractions(snoozeFraction, dismissFraction) 374 375 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { 376 LOGGER.v("onTouch ended: %s", event) 377 378 mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID 379 if (snoozeFraction == 1.0f) { 380 snooze() 381 } else if (dismissFraction == 1.0f) { 382 dismiss() 383 } else { 384 if (snoozeFraction > 0.0f || dismissFraction > 0.0f) { 385 // Animate back to the initial state. 386 AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator) 387 } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) { 388 // User touched the alarm button, hint the dismiss action. 389 hintDismiss() 390 } 391 392 // Restart the pulse. 393 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE) 394 if (!mPulseAnimator.isStarted()) { 395 mPulseAnimator.start() 396 } 397 } 398 } 399 400 return true 401 } 402 hideNavigationBarnull403 private fun hideNavigationBar() { 404 getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 405 or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 406 or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) 407 } 408 409 /** 410 * Returns `true` if accessibility is enabled, to enable alternate behavior for click 411 * handling, etc. 412 */ 413 private val isAccessibilityEnabled: Boolean 414 get() { 415 if (mAccessibilityManager == null || !mAccessibilityManager!!.isEnabled()) { 416 // Accessibility is unavailable or disabled. 417 return false 418 } else if (mAccessibilityManager!!.isTouchExplorationEnabled()) { 419 // TalkBack's touch exploration mode is enabled. 420 return true 421 } 422 423 // Check if "Switch Access" is enabled. 424 val enabledAccessibilityServices: List<AccessibilityServiceInfo> = 425 mAccessibilityManager!!.getEnabledAccessibilityServiceList(FEEDBACK_GENERIC) 426 return !enabledAccessibilityServices.isEmpty() 427 } 428 hintSnoozenull429 private fun hintSnooze() { 430 val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft() 431 val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight() 432 val translationX = (Math.max(mSnoozeButton.getLeft() - alarmRight, 0) + 433 Math.min(mSnoozeButton.getRight() - alarmLeft, 0)).toFloat() 434 getAlarmBounceAnimator(translationX, if (translationX < 0.0f) { 435 R.string.description_direction_left 436 } else { 437 R.string.description_direction_right 438 }).start() 439 } 440 hintDismissnull441 private fun hintDismiss() { 442 val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft() 443 val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight() 444 val translationX = (Math.max(mDismissButton.getLeft() - alarmRight, 0) + 445 Math.min(mDismissButton.getRight() - alarmLeft, 0)).toFloat() 446 getAlarmBounceAnimator(translationX, if (translationX < 0.0f) { 447 R.string.description_direction_left 448 } else { 449 R.string.description_direction_right 450 }).start() 451 } 452 453 /** 454 * Set animators to initial values and restart pulse on alarm button. 455 */ resetAnimationsnull456 private fun resetAnimations() { 457 // Set the animators to their initial values. 458 setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */) 459 // Restart the pulse. 460 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE) 461 if (!mPulseAnimator.isStarted()) { 462 mPulseAnimator.start() 463 } 464 } 465 466 /** 467 * Perform snooze animation and send snooze intent. 468 */ snoozenull469 private fun snooze() { 470 mAlarmHandled = true 471 LOGGER.v("Snoozed: %s", mAlarmInstance) 472 473 val colorAccent = ThemeUtils.resolveColor(this, R.attr.colorAccent) 474 setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */) 475 476 val snoozeMinutes = DataModel.dataModel.snoozeLength 477 val infoText: String = getResources().getQuantityString( 478 R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes) 479 val accessibilityText: String = getResources().getQuantityString( 480 R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes) 481 482 getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText, 483 accessibilityText, colorAccent, colorAccent).start() 484 485 AlarmStateManager.setSnoozeState(this, mAlarmInstance!!, false /* showToast */) 486 487 Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock) 488 489 // Unbind here, otherwise alarm will keep ringing until activity finishes. 490 unbindAlarmService() 491 } 492 493 /** 494 * Perform dismiss animation and send dismiss intent. 495 */ dismissnull496 private fun dismiss() { 497 mAlarmHandled = true 498 LOGGER.v("Dismissed: %s", mAlarmInstance) 499 500 setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */) 501 502 getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */, 503 getString(R.string.alarm_alert_off_text) /* accessibilityText */, 504 Color.WHITE, mCurrentHourColor).start() 505 506 AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance!!) 507 508 Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock) 509 510 // Unbind here, otherwise alarm will keep ringing until activity finishes. 511 unbindAlarmService() 512 } 513 514 /** 515 * Bind AlarmService if not yet bound. 516 */ bindAlarmServicenull517 private fun bindAlarmService() { 518 if (!mServiceBound) { 519 val intent = Intent(this, AlarmService::class.java) 520 bindService(intent, mConnection, Context.BIND_AUTO_CREATE) 521 mServiceBound = true 522 } 523 } 524 525 /** 526 * Unbind AlarmService if bound. 527 */ unbindAlarmServicenull528 private fun unbindAlarmService() { 529 if (mServiceBound) { 530 unbindService(mConnection) 531 mServiceBound = false 532 } 533 } 534 setAnimatedFractionsnull535 private fun setAnimatedFractions(snoozeFraction: Float, dismissFraction: Float) { 536 val alarmFraction = Math.max(snoozeFraction, dismissFraction) 537 AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction) 538 AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction) 539 AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction) 540 } 541 getFractionnull542 private fun getFraction(x0: Float, x1: Float, x: Float): Float { 543 return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f) 544 } 545 getButtonAnimatornull546 private fun getButtonAnimator(button: ImageView?, tintColor: Int): ValueAnimator { 547 return ObjectAnimator.ofPropertyValuesHolder(button, 548 PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f), 549 PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f), 550 PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255), 551 PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA, 552 BUTTON_DRAWABLE_ALPHA_DEFAULT, 255), 553 PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT, 554 AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor)) 555 } 556 getAlarmBounceAnimatornull557 private fun getAlarmBounceAnimator(translationX: Float, hintResId: Int): ValueAnimator { 558 val bounceAnimator: ValueAnimator = ObjectAnimator.ofFloat(mAlarmButton, 559 View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f) 560 bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR) 561 bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS.toLong()) 562 bounceAnimator.addListener(object : AnimatorListenerAdapter() { 563 override fun onAnimationStart(animator: Animator?) { 564 mHintView.setText(hintResId) 565 if (mHintView.getVisibility() != View.VISIBLE) { 566 mHintView.setVisibility(View.VISIBLE) 567 ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start() 568 } 569 } 570 }) 571 return bounceAnimator 572 } 573 getAlertAnimatornull574 private fun getAlertAnimator( 575 source: View, 576 titleResId: Int, 577 infoText: String?, 578 accessibilityText: String, 579 revealColor: Int, 580 backgroundColor: Int 581 ): Animator { 582 val containerView: ViewGroup = findViewById(android.R.id.content) as ViewGroup 583 584 val sourceBounds = Rect(0, 0, source.getHeight(), source.getWidth()) 585 containerView.offsetDescendantRectToMyCoords(source, sourceBounds) 586 587 val centerX: Int = sourceBounds.centerX() 588 val centerY: Int = sourceBounds.centerY() 589 590 val xMax = max(centerX, containerView.getWidth() - centerX) 591 val yMax = max(centerY, containerView.getHeight() - centerY) 592 593 val startRadius: Float = max(sourceBounds.width(), sourceBounds.height()) / 2.0f 594 val endRadius = sqrt(xMax * xMax + yMax * yMax.toDouble()).toFloat() 595 596 val revealView = CircleView(this) 597 .setCenterX(centerX.toFloat()) 598 .setCenterY(centerY.toFloat()) 599 .setFillColor(revealColor) 600 containerView.addView(revealView) 601 602 // TODO: Fade out source icon over the reveal (like LOLLIPOP version). 603 604 val revealAnimator: Animator = ObjectAnimator.ofFloat( 605 revealView, CircleView.RADIUS, startRadius, endRadius) 606 revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS.toLong()) 607 revealAnimator.setInterpolator(REVEAL_INTERPOLATOR) 608 revealAnimator.addListener(object : AnimatorListenerAdapter() { 609 override fun onAnimationEnd(animator: Animator?) { 610 mAlertView.setVisibility(View.VISIBLE) 611 mAlertTitleView.setText(titleResId) 612 if (infoText != null) { 613 mAlertInfoView.setText(infoText) 614 mAlertInfoView.setVisibility(View.VISIBLE) 615 } 616 mContentView.setVisibility(View.GONE) 617 getWindow().setBackgroundDrawable(ColorDrawable(backgroundColor)) 618 } 619 }) 620 621 val fadeAnimator: ValueAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f) 622 fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS.toLong()) 623 fadeAnimator.addListener(object : AnimatorListenerAdapter() { 624 override fun onAnimationEnd(animation: Animator?) { 625 containerView.removeView(revealView) 626 } 627 }) 628 629 val alertAnimator = AnimatorSet() 630 alertAnimator.play(revealAnimator).before(fadeAnimator) 631 alertAnimator.addListener(object : AnimatorListenerAdapter() { 632 override fun onAnimationEnd(animator: Animator?) { 633 mAlertView.announceForAccessibility(accessibilityText) 634 mHandler.postDelayed(Runnable { finish() }, ALERT_DISMISS_DELAY_MILLIS.toLong()) 635 } 636 }) 637 638 return alertAnimator 639 } 640 641 companion object { 642 private val LOGGER = LogUtils.Logger("AlarmActivity") 643 644 private val PULSE_INTERPOLATOR: TimeInterpolator = 645 PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f) 646 private val REVEAL_INTERPOLATOR: TimeInterpolator = 647 PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f) 648 649 private const val PULSE_DURATION_MILLIS = 1000 650 private const val ALARM_BOUNCE_DURATION_MILLIS = 500 651 private const val ALERT_REVEAL_DURATION_MILLIS = 500 652 private const val ALERT_FADE_DURATION_MILLIS = 500 653 private const val ALERT_DISMISS_DELAY_MILLIS = 2000 654 655 private const val BUTTON_SCALE_DEFAULT = 0.7f 656 private const val BUTTON_DRAWABLE_ALPHA_DEFAULT = 165 657 } 658 }