1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.android.deskclock.timer 16 17 import android.content.Intent 18 import android.content.pm.ActivityInfo 19 import android.os.Bundle 20 import android.os.SystemClock 21 import android.text.TextUtils 22 import android.transition.AutoTransition 23 import android.transition.TransitionManager 24 import android.widget.FrameLayout 25 import android.widget.TextView 26 import android.view.Gravity 27 import android.view.KeyEvent 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.WindowManager 31 32 import com.android.deskclock.BaseActivity 33 import com.android.deskclock.LogUtils 34 import com.android.deskclock.R 35 import com.android.deskclock.data.DataModel 36 import com.android.deskclock.data.Timer 37 import com.android.deskclock.data.TimerListener 38 39 /** 40 * This activity is designed to be shown over the lock screen. As such, it displays the expired 41 * timers and a single button to reset them all. Each expired timer can also be reset to one minute 42 * with a button in the user interface. All other timer operations are disabled in this activity. 43 */ 44 class ExpiredTimersActivity : BaseActivity() { 45 /** Scheduled to update the timers while at least one is expired. */ 46 private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable() 47 48 /** Updates the timers displayed in this activity as the backing data changes. */ 49 private val mTimerChangeWatcher: TimerListener = TimerChangeWatcher() 50 51 /** The scene root for transitions when expired timers are added/removed from this container. */ 52 private lateinit var mExpiredTimersScrollView: ViewGroup 53 54 /** Displays the expired timers. */ 55 private lateinit var mExpiredTimersView: ViewGroup 56 onCreatenull57 override fun onCreate(savedInstanceState: Bundle?) { 58 super.onCreate(savedInstanceState) 59 60 val expiredTimers = expiredTimers 61 62 // If no expired timers, finish 63 if (expiredTimers.size == 0) { 64 LogUtils.i("No expired timers, skipping display.") 65 finish() 66 return 67 } 68 69 setContentView(R.layout.expired_timers_activity) 70 71 mExpiredTimersView = findViewById(R.id.expired_timers_list) as ViewGroup 72 mExpiredTimersScrollView = findViewById(R.id.expired_timers_scroll) as ViewGroup 73 74 (findViewById(R.id.fab) as View).setOnClickListener(FabClickListener()) 75 76 val view: View = findViewById(R.id.expired_timers_activity) 77 view.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE 78 79 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 80 or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON) 81 82 setTurnScreenOn(true) 83 setShowWhenLocked(true) 84 85 // Close dialogs and window shade, so this is fully visible 86 sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) 87 88 // Honor rotation on tablets; fix the orientation on phones. 89 if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) { 90 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) 91 } 92 93 // Create views for each of the expired timers. 94 for (timer in expiredTimers) { 95 addTimer(timer) 96 } 97 98 // Update views in response to timer data changes. 99 DataModel.dataModel.addTimerListener(mTimerChangeWatcher) 100 } 101 onResumenull102 override fun onResume() { 103 super.onResume() 104 startUpdatingTime() 105 } 106 onPausenull107 override fun onPause() { 108 super.onPause() 109 stopUpdatingTime() 110 } 111 onDestroynull112 override fun onDestroy() { 113 super.onDestroy() 114 DataModel.dataModel.removeTimerListener(mTimerChangeWatcher) 115 } 116 dispatchKeyEventnull117 override fun dispatchKeyEvent(event: KeyEvent): Boolean { 118 if (event.action == KeyEvent.ACTION_UP) { 119 when (event.keyCode) { 120 KeyEvent.KEYCODE_VOLUME_UP, 121 KeyEvent.KEYCODE_VOLUME_DOWN, 122 KeyEvent.KEYCODE_VOLUME_MUTE, 123 KeyEvent.KEYCODE_CAMERA, 124 KeyEvent.KEYCODE_FOCUS -> { 125 DataModel.dataModel.resetOrDeleteExpiredTimers(R.string.label_hardware_button) 126 return true 127 } 128 } 129 } 130 return super.dispatchKeyEvent(event) 131 } 132 133 /** 134 * Post the first runnable to update times within the UI. It will reschedule itself as needed. 135 */ startUpdatingTimenull136 private fun startUpdatingTime() { 137 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 138 stopUpdatingTime() 139 mExpiredTimersView.post(mTimeUpdateRunnable) 140 } 141 142 /** 143 * Remove the runnable that updates times within the UI. 144 */ stopUpdatingTimenull145 private fun stopUpdatingTime() { 146 mExpiredTimersView.removeCallbacks(mTimeUpdateRunnable) 147 } 148 149 /** 150 * Create and add a new view that corresponds with the given `timer`. 151 */ addTimernull152 private fun addTimer(timer: Timer) { 153 TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, AutoTransition()) 154 155 val timerId: Int = timer.id 156 val timerItem = getLayoutInflater() 157 .inflate(R.layout.timer_item, mExpiredTimersView, false) as TimerItem 158 // Store the timer id as a tag on the view so it can be located on delete. 159 timerItem.id = timerId 160 mExpiredTimersView.addView(timerItem) 161 162 // Hide the label hint for expired timers. 163 val labelView = timerItem.findViewById<View>(R.id.timer_label) as TextView 164 labelView.hint = null 165 labelView.visibility = if (TextUtils.isEmpty(timer.label)) View.GONE else View.VISIBLE 166 167 // Add logic to the "Add 1 Minute" button. 168 val addMinuteButton = timerItem.findViewById<View>(R.id.reset_add) 169 addMinuteButton.setOnClickListener { 170 val timer: Timer = DataModel.dataModel.getTimer(timerId)!! 171 DataModel.dataModel.addTimerMinute(timer) 172 } 173 174 // If the first timer was just added, center it. 175 val expiredTimers = expiredTimers 176 if (expiredTimers.size == 1) { 177 centerFirstTimer() 178 } else if (expiredTimers.size == 2) { 179 uncenterFirstTimer() 180 } 181 } 182 183 /** 184 * Remove an existing view that corresponds with the given `timer`. 185 */ removeTimernull186 private fun removeTimer(timer: Timer) { 187 TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, AutoTransition()) 188 189 val timerId: Int = timer.id 190 val count = mExpiredTimersView.childCount 191 for (i in 0 until count) { 192 val timerView = mExpiredTimersView.getChildAt(i) 193 if (timerView.id == timerId) { 194 mExpiredTimersView.removeView(timerView) 195 break 196 } 197 } 198 199 // If the second last timer was just removed, center the last timer. 200 val expiredTimers = expiredTimers 201 if (expiredTimers.isEmpty()) { 202 finish() 203 } else if (expiredTimers.size == 1) { 204 centerFirstTimer() 205 } 206 } 207 208 /** 209 * Center the single timer. 210 */ centerFirstTimernull211 private fun centerFirstTimer() { 212 val lp = mExpiredTimersView.layoutParams as FrameLayout.LayoutParams 213 lp.gravity = Gravity.CENTER 214 mExpiredTimersView.requestLayout() 215 } 216 217 /** 218 * Display the multiple timers as a scrollable list. 219 */ uncenterFirstTimernull220 private fun uncenterFirstTimer() { 221 val lp = mExpiredTimersView.layoutParams as FrameLayout.LayoutParams 222 lp.gravity = Gravity.NO_GRAVITY 223 mExpiredTimersView.requestLayout() 224 } 225 226 private val expiredTimers: List<Timer> 227 get() = DataModel.dataModel.expiredTimers 228 229 /** 230 * Periodically refreshes the state of each timer. 231 */ 232 private inner class TimeUpdateRunnable : Runnable { runnull233 override fun run() { 234 val startTime = SystemClock.elapsedRealtime() 235 236 val count = mExpiredTimersView.childCount 237 for (i in 0 until count) { 238 val timerItem = mExpiredTimersView.getChildAt(i) as TimerItem 239 val timer: Timer? = DataModel.dataModel.getTimer(timerItem.id) 240 if (timer != null) { 241 timerItem.update(timer) 242 } 243 } 244 245 val endTime = SystemClock.elapsedRealtime() 246 247 // Try to maintain a consistent period of time between redraws. 248 val delay = Math.max(0L, startTime + 20L - endTime) 249 mExpiredTimersView.postDelayed(this, delay) 250 } 251 } 252 253 /** 254 * Clicking the fab resets all expired timers. 255 */ 256 private inner class FabClickListener : View.OnClickListener { onClicknull257 override fun onClick(v: View) { 258 stopUpdatingTime() 259 DataModel.dataModel.removeTimerListener(mTimerChangeWatcher) 260 DataModel.dataModel.resetOrDeleteExpiredTimers(R.string.label_deskclock) 261 finish() 262 } 263 } 264 265 /** 266 * Adds and removes expired timers from this activity based on their state changes. 267 */ 268 private inner class TimerChangeWatcher : TimerListener { timerAddednull269 override fun timerAdded(timer: Timer) { 270 if (timer.isExpired) { 271 addTimer(timer) 272 } 273 } 274 timerUpdatednull275 override fun timerUpdated(before: Timer, after: Timer) { 276 if (!before.isExpired && after.isExpired) { 277 addTimer(after) 278 } else if (before.isExpired && !after.isExpired) { 279 removeTimer(before) 280 } 281 } 282 timerRemovednull283 override fun timerRemoved(timer: Timer) { 284 if (timer.isExpired) { 285 removeTimer(timer) 286 } 287 } 288 } 289 }