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