1 /* 2 * Copyright (C) 2015 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.os.Bundle; 18 import android.os.SystemClock; 19 import android.support.annotation.NonNull; 20 import android.transition.AutoTransition; 21 import android.transition.TransitionManager; 22 import android.view.Gravity; 23 import android.view.KeyEvent; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.view.WindowManager; 27 import android.widget.FrameLayout; 28 import android.widget.TextView; 29 30 import com.android.deskclock.BaseActivity; 31 import com.android.deskclock.R; 32 import com.android.deskclock.data.DataModel; 33 import com.android.deskclock.data.Timer; 34 import com.android.deskclock.data.TimerListener; 35 36 import java.util.List; 37 38 /** 39 * This activity is designed to be shown over the lock screen. As such, it displays the expired 40 * timers and a single button to reset them all. Each expired timer can also be reset to one minute 41 * with a button in the user interface. All other timer operations are disabled in this activity. 42 */ 43 public class ExpiredTimersActivity extends BaseActivity { 44 45 /** Scheduled to update the timers while at least one is expired. */ 46 private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable(); 47 48 /** Updates the timers displayed in this activity as the backing data changes. */ 49 private final TimerListener mTimerChangeWatcher = new TimerChangeWatcher(); 50 51 /** The scene root for transitions when expired timers are added/removed from this container. */ 52 private ViewGroup mExpiredTimersScrollView; 53 54 /** Displays the expired timers. */ 55 private ViewGroup mExpiredTimersView; 56 57 @Override 58 protected void onCreate(Bundle savedInstanceState) { 59 super.onCreate(savedInstanceState); 60 61 setContentView(R.layout.expired_timers_activity); 62 63 mExpiredTimersView = (ViewGroup) findViewById(R.id.expired_timers_list); 64 mExpiredTimersScrollView = (ViewGroup) findViewById(R.id.expired_timers_scroll); 65 66 findViewById(R.id.fab).setOnClickListener(new FabClickListener()); 67 68 final View view = findViewById(R.id.expired_timers_activity); 69 view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); 70 71 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 72 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 73 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 74 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD 75 | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON); 76 77 // Create views for each of the expired timers. 78 for (Timer timer : getExpiredTimers()) { 79 addTimer(timer); 80 } 81 82 // Update views in response to timer data changes. 83 DataModel.getDataModel().addTimerListener(mTimerChangeWatcher); 84 } 85 86 @Override 87 protected void onResume() { 88 super.onResume(); 89 startUpdatingTime(); 90 } 91 92 @Override 93 protected void onPause() { 94 super.onPause(); 95 stopUpdatingTime(); 96 } 97 98 @Override 99 public void onDestroy() { 100 super.onDestroy(); 101 DataModel.getDataModel().removeTimerListener(mTimerChangeWatcher); 102 } 103 104 @Override 105 public boolean dispatchKeyEvent(@NonNull KeyEvent event) { 106 if (event.getAction() == KeyEvent.ACTION_UP) { 107 switch (event.getKeyCode()) { 108 case KeyEvent.KEYCODE_VOLUME_UP: 109 case KeyEvent.KEYCODE_VOLUME_DOWN: 110 case KeyEvent.KEYCODE_VOLUME_MUTE: 111 case KeyEvent.KEYCODE_CAMERA: 112 case KeyEvent.KEYCODE_FOCUS: 113 DataModel.getDataModel().resetExpiredTimers(R.string.label_hardware_button); 114 return true; 115 } 116 } 117 return super.dispatchKeyEvent(event); 118 } 119 120 /** 121 * Post the first runnable to update times within the UI. It will reschedule itself as needed. 122 */ 123 private void startUpdatingTime() { 124 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 125 stopUpdatingTime(); 126 mExpiredTimersView.post(mTimeUpdateRunnable); 127 } 128 129 /** 130 * Remove the runnable that updates times within the UI. 131 */ 132 private void stopUpdatingTime() { 133 mExpiredTimersView.removeCallbacks(mTimeUpdateRunnable); 134 } 135 136 /** 137 * Create and add a new view that corresponds with the given {@code timer}. 138 */ 139 private void addTimer(Timer timer) { 140 TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition()); 141 142 final TimerItem timerItem = (TimerItem) 143 getLayoutInflater().inflate(R.layout.timer_item, mExpiredTimersView, false); 144 // Store the timer id as a tag on the view so it can be located on delete. 145 timerItem.setTag(timer.getId()); 146 mExpiredTimersView.addView(timerItem); 147 148 // Hide the label hint for expired timers. 149 final TextView labelView = (TextView) timerItem.findViewById(R.id.timer_label); 150 labelView.setHint(null); 151 152 // Add logic to the "Add 1 Minute" button. 153 final View addMinuteButton = timerItem.findViewById(R.id.reset_add); 154 addMinuteButton.setOnClickListener(new View.OnClickListener() { 155 @Override 156 public void onClick(View v) { 157 final int index = mExpiredTimersView.indexOfChild(timerItem); 158 final Timer timer = getExpiredTimers().get(index); 159 DataModel.getDataModel().addTimerMinute(timer); 160 } 161 }); 162 163 // If the first timer was just added, center it. 164 final List<Timer> expiredTimers = getExpiredTimers(); 165 if (expiredTimers.size() == 1) { 166 centerFirstTimer(); 167 } else if (expiredTimers.size() == 2) { 168 uncenterFirstTimer(); 169 } 170 } 171 172 /** 173 * Remove an existing view that corresponds with the given {@code timer}. 174 */ 175 private void removeTimer(Timer timer) { 176 TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition()); 177 178 final View timerView = mExpiredTimersView.findViewWithTag(timer.getId()); 179 mExpiredTimersView.removeView(timerView); 180 181 // If the second last timer was just removed, center the last timer. 182 final List<Timer> expiredTimers = getExpiredTimers(); 183 if (expiredTimers.isEmpty()) { 184 finish(); 185 } else if (expiredTimers.size() == 1) { 186 centerFirstTimer(); 187 } 188 } 189 190 /** 191 * Center the single timer. 192 */ 193 private void centerFirstTimer() { 194 final FrameLayout.LayoutParams lp = 195 (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams(); 196 lp.gravity = Gravity.CENTER; 197 mExpiredTimersView.requestLayout(); 198 } 199 200 /** 201 * Display the multiple timers as a scrollable list. 202 */ 203 private void uncenterFirstTimer() { 204 final FrameLayout.LayoutParams lp = 205 (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams(); 206 lp.gravity = Gravity.NO_GRAVITY; 207 mExpiredTimersView.requestLayout(); 208 } 209 210 private List<Timer> getExpiredTimers() { 211 return DataModel.getDataModel().getExpiredTimers(); 212 } 213 214 /** 215 * Periodically refreshes the state of each timer. 216 */ 217 private class TimeUpdateRunnable implements Runnable { 218 @Override 219 public void run() { 220 final long startTime = SystemClock.elapsedRealtime(); 221 222 for (int i = 0; i < mExpiredTimersView.getChildCount(); i++) { 223 final TimerItem timerItem = (TimerItem) mExpiredTimersView.getChildAt(i); 224 final Timer timer = getExpiredTimers().get(i); 225 timerItem.update(timer); 226 } 227 228 final long endTime = SystemClock.elapsedRealtime(); 229 230 // Try to maintain a consistent period of time between redraws. 231 final long delay = Math.max(0, startTime + 20 - endTime); 232 mExpiredTimersView.postDelayed(this, delay); 233 } 234 } 235 236 /** 237 * Clicking the fab resets all expired timers. 238 */ 239 private class FabClickListener implements View.OnClickListener { 240 @Override 241 public void onClick(View v) { 242 stopUpdatingTime(); 243 DataModel.getDataModel().removeTimerListener(mTimerChangeWatcher); 244 DataModel.getDataModel().resetExpiredTimers(R.string.label_deskclock); 245 finish(); 246 } 247 } 248 249 /** 250 * Adds and removes expired timers from this activity based on their state changes. 251 */ 252 private class TimerChangeWatcher implements TimerListener { 253 @Override 254 public void timerAdded(Timer timer) { 255 if (timer.isExpired()) { 256 addTimer(timer); 257 } 258 } 259 260 @Override 261 public void timerUpdated(Timer before, Timer after) { 262 if (!before.isExpired() && after.isExpired()) { 263 addTimer(after); 264 } else if (before.isExpired() && !after.isExpired()) { 265 removeTimer(before); 266 } 267 } 268 269 @Override 270 public void timerRemoved(Timer timer) { 271 if (timer.isExpired()) { 272 removeTimer(timer); 273 } 274 } 275 } 276 }