1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.deskclock.timer;
18
19import android.app.Service;
20import android.content.Context;
21import android.content.Intent;
22import android.os.IBinder;
23
24import com.android.deskclock.DeskClock;
25import com.android.deskclock.R;
26import com.android.deskclock.data.DataModel;
27import com.android.deskclock.data.Timer;
28import com.android.deskclock.events.Events;
29import com.android.deskclock.uidata.UiDataModel;
30
31import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
32
33/**
34 * <p>This service exists solely to allow {@link android.app.AlarmManager} and timer notifications
35 * to alter the state of timers without disturbing the notification shade. If an activity were used
36 * instead (even one that is not displayed) the notification manager implicitly closes the
37 * notification shade which clashes with the use case of starting/pausing/resetting timers without
38 * disturbing the notification shade.</p>
39 *
40 * <p>The service has a second benefit. It is used to start heads-up notifications for expired
41 * timers in the foreground. This keeps the entire application in the foreground and thus prevents
42 * the operating system from killing it while expired timers are firing.</p>
43 */
44public final class TimerService extends Service {
45
46    private static final String ACTION_PREFIX = "com.android.deskclock.action.";
47
48    /** Shows the tab with timers; scrolls to a specific timer. */
49    public static final String ACTION_SHOW_TIMER = ACTION_PREFIX + "SHOW_TIMER";
50    /** Pauses running timers; resets expired timers. */
51    public static final String ACTION_PAUSE_TIMER = ACTION_PREFIX + "PAUSE_TIMER";
52    /** Starts the sole timer. */
53    public static final String ACTION_START_TIMER = ACTION_PREFIX + "START_TIMER";
54    /** Resets the timer. */
55    public static final String ACTION_RESET_TIMER = ACTION_PREFIX + "RESET_TIMER";
56    /** Adds an extra minute to the timer. */
57    public static final String ACTION_ADD_MINUTE_TIMER = ACTION_PREFIX + "ADD_MINUTE_TIMER";
58
59    /** Extra for many actions specific to a given timer. */
60    public static final String EXTRA_TIMER_ID = "com.android.deskclock.extra.TIMER_ID";
61
62    private static final String ACTION_TIMER_EXPIRED =
63            ACTION_PREFIX + "TIMER_EXPIRED";
64    private static final String ACTION_UPDATE_NOTIFICATION =
65            ACTION_PREFIX + "UPDATE_NOTIFICATION";
66    private static final String ACTION_RESET_EXPIRED_TIMERS =
67            ACTION_PREFIX + "RESET_EXPIRED_TIMERS";
68    private static final String ACTION_RESET_UNEXPIRED_TIMERS =
69            ACTION_PREFIX + "RESET_UNEXPIRED_TIMERS";
70    private static final String ACTION_RESET_MISSED_TIMERS =
71            ACTION_PREFIX + "RESET_MISSED_TIMERS";
72
73    public static Intent createTimerExpiredIntent(Context context, Timer timer) {
74        final int timerId = timer == null ? -1 : timer.getId();
75        return new Intent(context, TimerService.class)
76                .setAction(ACTION_TIMER_EXPIRED)
77                .putExtra(EXTRA_TIMER_ID, timerId);
78    }
79
80    public static Intent createResetExpiredTimersIntent(Context context) {
81        return new Intent(context, TimerService.class)
82                .setAction(ACTION_RESET_EXPIRED_TIMERS);
83    }
84
85    public static Intent createResetUnexpiredTimersIntent(Context context) {
86        return new Intent(context, TimerService.class)
87                .setAction(ACTION_RESET_UNEXPIRED_TIMERS);
88    }
89
90    public static Intent createResetMissedTimersIntent(Context context) {
91        return new Intent(context, TimerService.class)
92                .setAction(ACTION_RESET_MISSED_TIMERS);
93    }
94
95
96    public static Intent createAddMinuteTimerIntent(Context context, int timerId) {
97        return new Intent(context, TimerService.class)
98                .setAction(ACTION_ADD_MINUTE_TIMER)
99                .putExtra(EXTRA_TIMER_ID, timerId);
100    }
101
102    public static Intent createUpdateNotificationIntent(Context context) {
103        return new Intent(context, TimerService.class)
104                .setAction(ACTION_UPDATE_NOTIFICATION);
105    }
106
107    @Override
108    public IBinder onBind(Intent intent) {
109        return null;
110    }
111
112    @Override
113    public int onStartCommand(Intent intent, int flags, int startId) {
114        try {
115            final String action = intent.getAction();
116            final int label = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent);
117            switch (action) {
118                case ACTION_UPDATE_NOTIFICATION: {
119                    DataModel.getDataModel().updateTimerNotification();
120                    return START_NOT_STICKY;
121                }
122                case ACTION_RESET_EXPIRED_TIMERS: {
123                    DataModel.getDataModel().resetExpiredTimers(label);
124                    return START_NOT_STICKY;
125                }
126                case ACTION_RESET_UNEXPIRED_TIMERS: {
127                    DataModel.getDataModel().resetUnexpiredTimers(label);
128                    return START_NOT_STICKY;
129                }
130                case ACTION_RESET_MISSED_TIMERS: {
131                    DataModel.getDataModel().resetMissedTimers(label);
132                    return START_NOT_STICKY;
133                }
134            }
135
136            // Look up the timer in question.
137            final int timerId = intent.getIntExtra(EXTRA_TIMER_ID, -1);
138            final Timer timer = DataModel.getDataModel().getTimer(timerId);
139
140            // If the timer cannot be located, ignore the action.
141            if (timer == null) {
142                return START_NOT_STICKY;
143            }
144
145            // Perform the action on the timer.
146            switch (action) {
147                case ACTION_SHOW_TIMER: {
148                    Events.sendTimerEvent(R.string.action_show, label);
149
150                    // Change to the timers tab.
151                    UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
152
153                    // Open DeskClock which is now positioned on the timers tab and show the timer
154                    // in question.
155                    final Intent showTimers = new Intent(this, DeskClock.class)
156                            .putExtra(EXTRA_TIMER_ID, timerId)
157                            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
158                    startActivity(showTimers);
159                    break;
160                } case ACTION_START_TIMER: {
161                    Events.sendTimerEvent(R.string.action_start, label);
162                    DataModel.getDataModel().startTimer(this, timer);
163                    break;
164                } case ACTION_PAUSE_TIMER: {
165                    Events.sendTimerEvent(R.string.action_pause, label);
166                    DataModel.getDataModel().pauseTimer(timer);
167                    break;
168                } case ACTION_ADD_MINUTE_TIMER: {
169                    Events.sendTimerEvent(R.string.action_add_minute, label);
170                    DataModel.getDataModel().addTimerMinute(timer);
171                    break;
172                } case ACTION_RESET_TIMER: {
173                    DataModel.getDataModel().resetOrDeleteTimer(timer, label);
174                    break;
175                } case ACTION_TIMER_EXPIRED: {
176                    Events.sendTimerEvent(R.string.action_fire, label);
177                    DataModel.getDataModel().expireTimer(this, timer);
178                    break;
179                }
180            }
181        } finally {
182            // This service is foreground when expired timers exist and stopped when none exist.
183            if (DataModel.getDataModel().getExpiredTimers().isEmpty()) {
184                stopSelf();
185            }
186        }
187
188        return START_NOT_STICKY;
189    }
190}
191