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().resetExpiredTimers(R.string.label_hardware_button); 135 return true; 136 } 137 } 138 return super.dispatchKeyEvent(event); 139 } 140 141 /** 142 * Post the first runnable to update times within the UI. It will reschedule itself as needed. 143 */ 144 private void startUpdatingTime() { 145 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 146 stopUpdatingTime(); 147 mExpiredTimersView.post(mTimeUpdateRunnable); 148 } 149 150 /** 151 * Remove the runnable that updates times within the UI. 152 */ 153 private void stopUpdatingTime() { 154 mExpiredTimersView.removeCallbacks(mTimeUpdateRunnable); 155 } 156 157 /** 158 * Create and add a new view that corresponds with the given {@code timer}. 159 */ 160 private void addTimer(Timer timer) { 161 TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition()); 162 163 final int timerId = timer.getId(); 164 final TimerItem timerItem = (TimerItem) 165 getLayoutInflater().inflate(R.layout.timer_item, mExpiredTimersView, false); 166 // Store the timer id as a tag on the view so it can be located on delete. 167 timerItem.setId(timerId); 168 mExpiredTimersView.addView(timerItem); 169 170 // Hide the label hint for expired timers. 171 final TextView labelView = (TextView) timerItem.findViewById(R.id.timer_label); 172 labelView.setHint(null); 173 labelView.setVisibility(TextUtils.isEmpty(timer.getLabel()) ? View.GONE : View.VISIBLE); 174 175 // Add logic to the "Add 1 Minute" button. 176 final View addMinuteButton = timerItem.findViewById(R.id.reset_add); 177 addMinuteButton.setOnClickListener(new View.OnClickListener() { 178 @Override 179 public void onClick(View v) { 180 final Timer timer = DataModel.getDataModel().getTimer(timerId); 181 DataModel.getDataModel().addTimerMinute(timer); 182 } 183 }); 184 185 // If the first timer was just added, center it. 186 final List<Timer> expiredTimers = getExpiredTimers(); 187 if (expiredTimers.size() == 1) { 188 centerFirstTimer(); 189 } else if (expiredTimers.size() == 2) { 190 uncenterFirstTimer(); 191 } 192 } 193 194 /** 195 * Remove an existing view that corresponds with the given {@code timer}. 196 */ 197 private void removeTimer(Timer timer) { 198 TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition()); 199 200 final int timerId = timer.getId(); 201 final int count = mExpiredTimersView.getChildCount(); 202 for (int i = 0; i < count; ++i) { 203 final View timerView = mExpiredTimersView.getChildAt(i); 204 if (timerView.getId() == timerId) { 205 mExpiredTimersView.removeView(timerView); 206 break; 207 } 208 } 209 210 // If the second last timer was just removed, center the last timer. 211 final List<Timer> expiredTimers = getExpiredTimers(); 212 if (expiredTimers.isEmpty()) { 213 finish(); 214 } else if (expiredTimers.size() == 1) { 215 centerFirstTimer(); 216 } 217 } 218 219 /** 220 * Center the single timer. 221 */ 222 private void centerFirstTimer() { 223 final FrameLayout.LayoutParams lp = 224 (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams(); 225 lp.gravity = Gravity.CENTER; 226 mExpiredTimersView.requestLayout(); 227 } 228 229 /** 230 * Display the multiple timers as a scrollable list. 231 */ 232 private void uncenterFirstTimer() { 233 final FrameLayout.LayoutParams lp = 234 (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams(); 235 lp.gravity = Gravity.NO_GRAVITY; 236 mExpiredTimersView.requestLayout(); 237 } 238 239 private List<Timer> getExpiredTimers() { 240 return DataModel.getDataModel().getExpiredTimers(); 241 } 242 243 /** 244 * Periodically refreshes the state of each timer. 245 */ 246 private class TimeUpdateRunnable implements Runnable { 247 @Override 248 public void run() { 249 final long startTime = SystemClock.elapsedRealtime(); 250 251 final int count = mExpiredTimersView.getChildCount(); 252 for (int i = 0; i < count; ++i) { 253 final TimerItem timerItem = (TimerItem) mExpiredTimersView.getChildAt(i); 254 final Timer timer = DataModel.getDataModel().getTimer(timerItem.getId()); 255 if (timer != null) { 256 timerItem.update(timer); 257 } 258 } 259 260 final long endTime = SystemClock.elapsedRealtime(); 261 262 // Try to maintain a consistent period of time between redraws. 263 final long delay = Math.max(0L, startTime + 20L - endTime); 264 mExpiredTimersView.postDelayed(this, delay); 265 } 266 } 267 268 /** 269 * Clicking the fab resets all expired timers. 270 */ 271 private class FabClickListener implements View.OnClickListener { 272 @Override 273 public void onClick(View v) { 274 stopUpdatingTime(); 275 DataModel.getDataModel().removeTimerListener(mTimerChangeWatcher); 276 DataModel.getDataModel().resetExpiredTimers(R.string.label_deskclock); 277 finish(); 278 } 279 } 280 281 /** 282 * Adds and removes expired timers from this activity based on their state changes. 283 */ 284 private class TimerChangeWatcher implements TimerListener { 285 @Override 286 public void timerAdded(Timer timer) { 287 if (timer.isExpired()) { 288 addTimer(timer); 289 } 290 } 291 292 @Override 293 public void timerUpdated(Timer before, Timer after) { 294 if (!before.isExpired() && after.isExpired()) { 295 addTimer(after); 296 } else if (before.isExpired() && !after.isExpired()) { 297 removeTimer(before); 298 } 299 } 300 301 @Override 302 public void timerRemoved(Timer timer) { 303 if (timer.isExpired()) { 304 removeTimer(timer); 305 } 306 } 307 } 308 } 309