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 }