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
15package com.android.deskclock.timer;
16
17import android.os.Bundle;
18import android.os.SystemClock;
19import android.support.annotation.NonNull;
20import android.transition.AutoTransition;
21import android.transition.TransitionManager;
22import android.view.Gravity;
23import android.view.KeyEvent;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.WindowManager;
27import android.widget.FrameLayout;
28import android.widget.TextView;
29
30import com.android.deskclock.BaseActivity;
31import com.android.deskclock.R;
32import com.android.deskclock.data.DataModel;
33import com.android.deskclock.data.Timer;
34import com.android.deskclock.data.TimerListener;
35
36import 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 */
43public 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}