• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }