1
2package com.android.deskclock.stopwatch;
3
4import android.app.Notification;
5import android.app.NotificationManager;
6import android.app.PendingIntent;
7import android.app.Service;
8import android.content.Context;
9import android.content.Intent;
10import android.content.SharedPreferences;
11import android.os.IBinder;
12import android.preference.PreferenceManager;
13import android.view.View;
14import android.widget.RemoteViews;
15
16import com.android.deskclock.CircleTimerView;
17import com.android.deskclock.DeskClock;
18import com.android.deskclock.R;
19import com.android.deskclock.Utils;
20
21/**
22 * TODO: Insert description here. (generated by sblitz)
23 */
24public class StopwatchService extends Service {
25    // Member fields
26    private int mNumLaps;
27    private long mElapsedTime;
28    private long mStartTime;
29    private boolean mLoadApp;
30    private NotificationManager mNotificationManager;
31
32    // Constants for intent information
33    // Make this a large number to avoid the alarm ID's which seem to be 1, 2, ...
34    // Must also be different than TimerReceiver.IN_USE_NOTIFICATION_ID
35    private static final int NOTIFICATION_ID = Integer.MAX_VALUE - 1;
36
37    @Override
38    public IBinder onBind(Intent intent) {
39        return null;
40    }
41
42    @Override
43    public void onCreate() {
44        mNumLaps = 0;
45        mElapsedTime = 0;
46        mStartTime = 0;
47        mLoadApp = false;
48        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
49    }
50
51    @Override
52    public int onStartCommand(Intent intent, int flags, int startId) {
53        if (intent == null) {
54            return Service.START_NOT_STICKY;
55        }
56
57        if (mStartTime == 0 || mElapsedTime == 0 || mNumLaps == 0) {
58            // May not have the most recent values.
59            readFromSharedPrefs();
60        }
61
62        String actionType = intent.getAction();
63        long actionTime = intent.getLongExtra(Stopwatches.MESSAGE_TIME, Utils.getTimeNow());
64        boolean showNotif = intent.getBooleanExtra(Stopwatches.SHOW_NOTIF, true);
65        boolean updateCircle = showNotif; // Don't save updates to the cirle if we're in the app.
66        if (actionType.equals(Stopwatches.START_STOPWATCH)) {
67            mStartTime = actionTime;
68            writeSharedPrefsStarted(mStartTime, updateCircle);
69            if (showNotif) {
70                setNotification(mStartTime - mElapsedTime, true, mNumLaps);
71            } else {
72                saveNotification(mStartTime - mElapsedTime, true, mNumLaps);
73            }
74        } else if (actionType.equals(Stopwatches.LAP_STOPWATCH)) {
75            mNumLaps++;
76            long lapTimeElapsed = actionTime - mStartTime + mElapsedTime;
77            writeSharedPrefsLap(lapTimeElapsed, updateCircle);
78            if (showNotif) {
79                setNotification(mStartTime - mElapsedTime, true, mNumLaps);
80            } else {
81                saveNotification(mStartTime - mElapsedTime, true, mNumLaps);
82            }
83        } else if (actionType.equals(Stopwatches.STOP_STOPWATCH)) {
84            mElapsedTime = mElapsedTime + (actionTime - mStartTime);
85            writeSharedPrefsStopped(mElapsedTime, updateCircle);
86            if (showNotif) {
87                setNotification(actionTime - mElapsedTime, false, mNumLaps);
88            } else {
89                saveNotification(mElapsedTime, false, mNumLaps);
90            }
91        } else if (actionType.equals(Stopwatches.RESET_STOPWATCH)) {
92            mLoadApp = false;
93            writeSharedPrefsReset(updateCircle);
94            clearSavedNotification();
95            stopSelf();
96        } else if (actionType.equals(Stopwatches.RESET_AND_LAUNCH_STOPWATCH)) {
97            mLoadApp = true;
98            writeSharedPrefsReset(updateCircle);
99            clearSavedNotification();
100            closeNotificationShade();
101            stopSelf();
102        } else if (actionType.equals(Stopwatches.SHARE_STOPWATCH)) {
103            closeNotificationShade();
104            Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
105            shareIntent.setType("text/plain");
106            shareIntent.putExtra(
107                    Intent.EXTRA_SUBJECT, Stopwatches.getShareTitle(getApplicationContext()));
108            shareIntent.putExtra(Intent.EXTRA_TEXT, Stopwatches.buildShareResults(
109                    getApplicationContext(), mElapsedTime, readLapsFromPrefs()));
110            Intent chooserIntent = Intent.createChooser(shareIntent, null);
111            chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
112            getApplication().startActivity(chooserIntent);
113        } else if (actionType.equals(Stopwatches.SHOW_NOTIF)) {
114            // SHOW_NOTIF sent from the DeskClock.onPause
115            // If a notification is not displayed, this service's work is over
116            if (!showSavedNotification()) {
117                stopSelf();
118            }
119        } else if (actionType.equals(Stopwatches.KILL_NOTIF)) {
120            mNotificationManager.cancel(NOTIFICATION_ID);
121        }
122
123        // We want this service to continue running until it is explicitly
124        // stopped, so return sticky.
125        return START_STICKY;
126    }
127
128    @Override
129    public void onDestroy() {
130        mNotificationManager.cancel(NOTIFICATION_ID);
131        clearSavedNotification();
132        mNumLaps = 0;
133        mElapsedTime = 0;
134        mStartTime = 0;
135        if (mLoadApp) {
136            Intent activityIntent = new Intent(getApplicationContext(), DeskClock.class);
137            activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
138            activityIntent.putExtra(
139                    DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
140            startActivity(activityIntent);
141            mLoadApp = false;
142        }
143    }
144
145    private void setNotification(long clockBaseTime, boolean clockRunning, int numLaps) {
146        Context context = getApplicationContext();
147        // Intent to load the app for a non-button click.
148        Intent intent = new Intent(context, DeskClock.class);
149        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
150        intent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
151        // add category to distinguish between stopwatch intents and timer intents
152        intent.addCategory("stopwatch");
153        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
154                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
155
156        // Set up remoteviews for the notification.
157        RemoteViews remoteViewsCollapsed = new RemoteViews(getPackageName(),
158                R.layout.stopwatch_notif_collapsed);
159        remoteViewsCollapsed.setOnClickPendingIntent(R.id.swn_collapsed_hitspace, pendingIntent);
160        remoteViewsCollapsed.setChronometer(
161                R.id.swn_collapsed_chronometer, clockBaseTime, null, clockRunning);
162        remoteViewsCollapsed.
163                setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
164        RemoteViews remoteViewsExpanded = new RemoteViews(getPackageName(),
165                R.layout.stopwatch_notif_expanded);
166        remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_expanded_hitspace, pendingIntent);
167        remoteViewsExpanded.setChronometer(
168                R.id.swn_expanded_chronometer, clockBaseTime, null, clockRunning);
169        remoteViewsExpanded.
170                setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
171
172        if (clockRunning) {
173            // Left button: lap
174            remoteViewsExpanded.setTextViewText(
175                    R.id.swn_left_button, getResources().getText(R.string.sw_lap_button));
176            Intent leftButtonIntent = new Intent(context, StopwatchService.class);
177            leftButtonIntent.setAction(Stopwatches.LAP_STOPWATCH);
178            remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_left_button,
179                    PendingIntent.getService(context, 0, leftButtonIntent, 0));
180            remoteViewsExpanded.
181                    setTextViewCompoundDrawablesRelative(R.id.swn_left_button,
182                            R.drawable.ic_notify_lap, 0, 0, 0);
183
184            // Right button: stop clock
185            remoteViewsExpanded.setTextViewText(
186                    R.id.swn_right_button, getResources().getText(R.string.sw_stop_button));
187            Intent rightButtonIntent = new Intent(context, StopwatchService.class);
188            rightButtonIntent.setAction(Stopwatches.STOP_STOPWATCH);
189            remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_right_button,
190                    PendingIntent.getService(context, 0, rightButtonIntent, 0));
191            remoteViewsExpanded.
192                    setTextViewCompoundDrawablesRelative(R.id.swn_right_button,
193                            R.drawable.ic_notify_stop, 0, 0, 0);
194
195            // Show the laps if applicable.
196            if (numLaps > 0) {
197                String lapText = String.format(
198                        context.getString(R.string.sw_notification_lap_number), numLaps);
199                remoteViewsCollapsed.setTextViewText(R.id.swn_collapsed_laps, lapText);
200                remoteViewsCollapsed.setViewVisibility(R.id.swn_collapsed_laps, View.VISIBLE);
201                remoteViewsExpanded.setTextViewText(R.id.swn_expanded_laps, lapText);
202                remoteViewsExpanded.setViewVisibility(R.id.swn_expanded_laps, View.VISIBLE);
203            } else {
204                remoteViewsCollapsed.setViewVisibility(R.id.swn_collapsed_laps, View.GONE);
205                remoteViewsExpanded.setViewVisibility(R.id.swn_expanded_laps, View.GONE);
206            }
207        } else {
208            // Left button: reset clock
209            remoteViewsExpanded.setTextViewText(
210                    R.id.swn_left_button, getResources().getText(R.string.sw_reset_button));
211            Intent leftButtonIntent = new Intent(context, StopwatchService.class);
212            leftButtonIntent.setAction(Stopwatches.RESET_AND_LAUNCH_STOPWATCH);
213            remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_left_button,
214                    PendingIntent.getService(context, 0, leftButtonIntent, 0));
215            remoteViewsExpanded.
216                    setTextViewCompoundDrawablesRelative(R.id.swn_left_button,
217                            R.drawable.ic_notify_reset, 0, 0, 0);
218
219            // Right button: start clock
220            remoteViewsExpanded.setTextViewText(
221                    R.id.swn_right_button, getResources().getText(R.string.sw_start_button));
222            Intent rightButtonIntent = new Intent(context, StopwatchService.class);
223            rightButtonIntent.setAction(Stopwatches.START_STOPWATCH);
224            remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_right_button,
225                    PendingIntent.getService(context, 0, rightButtonIntent, 0));
226            remoteViewsExpanded.
227                    setTextViewCompoundDrawablesRelative(R.id.swn_right_button,
228                            R.drawable.ic_notify_start, 0, 0, 0);
229
230            // Show stopped string.
231            remoteViewsCollapsed.
232                    setTextViewText(R.id.swn_collapsed_laps, getString(R.string.swn_stopped));
233            remoteViewsCollapsed.setViewVisibility(R.id.swn_collapsed_laps, View.VISIBLE);
234            remoteViewsExpanded.
235                    setTextViewText(R.id.swn_expanded_laps, getString(R.string.swn_stopped));
236            remoteViewsExpanded.setViewVisibility(R.id.swn_expanded_laps, View.VISIBLE);
237        }
238
239        Intent dismissIntent = new Intent(context, StopwatchService.class);
240        dismissIntent.setAction(Stopwatches.RESET_STOPWATCH);
241
242        Notification notification = new Notification.Builder(context)
243                .setAutoCancel(!clockRunning)
244                .setContent(remoteViewsCollapsed)
245                .setOngoing(clockRunning)
246                .setDeleteIntent(PendingIntent.getService(context, 0, dismissIntent, 0))
247                .setSmallIcon(R.drawable.ic_tab_stopwatch_activated)
248                .setPriority(Notification.PRIORITY_MAX).build();
249        notification.bigContentView = remoteViewsExpanded;
250        mNotificationManager.notify(NOTIFICATION_ID, notification);
251    }
252
253    /** Save the notification to be shown when the app is closed. **/
254    private void saveNotification(long clockTime, boolean clockRunning, int numLaps) {
255        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
256                getApplicationContext());
257        SharedPreferences.Editor editor = prefs.edit();
258        if (clockRunning) {
259            editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, clockTime);
260            editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, -1);
261            editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, true);
262        } else {
263            editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, clockTime);
264            editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, -1);
265            editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
266        }
267        editor.putBoolean(Stopwatches.PREF_UPDATE_CIRCLE, false);
268        editor.apply();
269    }
270
271    /** Show the most recently saved notification. **/
272    private boolean showSavedNotification() {
273        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
274                getApplicationContext());
275        long clockBaseTime = prefs.getLong(Stopwatches.NOTIF_CLOCK_BASE, -1);
276        long clockElapsedTime = prefs.getLong(Stopwatches.NOTIF_CLOCK_ELAPSED, -1);
277        boolean clockRunning = prefs.getBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
278        int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, -1);
279        if (clockBaseTime == -1) {
280            if (clockElapsedTime == -1) {
281                return false;
282            } else {
283                // We don't have a clock base time, so the clock is stopped.
284                // Use the elapsed time to figure out what time to show.
285                mElapsedTime = clockElapsedTime;
286                clockBaseTime = Utils.getTimeNow() - clockElapsedTime;
287            }
288        }
289        setNotification(clockBaseTime, clockRunning, numLaps);
290        return true;
291    }
292
293    private void clearSavedNotification() {
294        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
295                getApplicationContext());
296        SharedPreferences.Editor editor = prefs.edit();
297        editor.remove(Stopwatches.NOTIF_CLOCK_BASE);
298        editor.remove(Stopwatches.NOTIF_CLOCK_RUNNING);
299        editor.remove(Stopwatches.NOTIF_CLOCK_ELAPSED);
300        editor.apply();
301    }
302
303    private void closeNotificationShade() {
304        Intent intent = new Intent();
305        intent.setAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
306        sendBroadcast(intent);
307    }
308
309    private void readFromSharedPrefs() {
310        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
311                getApplicationContext());
312        mStartTime = prefs.getLong(Stopwatches.PREF_START_TIME, 0);
313        mElapsedTime = prefs.getLong(Stopwatches.PREF_ACCUM_TIME, 0);
314        mNumLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
315    }
316
317    private long[] readLapsFromPrefs() {
318        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
319                getApplicationContext());
320        int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
321        long[] laps = new long[numLaps];
322        long prevLapElapsedTime = 0;
323        for (int lap_i = 0; lap_i < numLaps; lap_i++) {
324            String key = Stopwatches.PREF_LAP_TIME + Integer.toString(lap_i + 1);
325            long lap = prefs.getLong(key, 0);
326            if (lap == prevLapElapsedTime && lap_i == numLaps - 1) {
327                lap = mElapsedTime;
328            }
329            laps[numLaps - lap_i - 1] = lap - prevLapElapsedTime;
330            prevLapElapsedTime = lap;
331        }
332        return laps;
333    }
334
335    private void writeToSharedPrefs(Long startTime, Long lapTimeElapsed, Long elapsedTime,
336            Integer state, boolean updateCircle) {
337        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
338                getApplicationContext());
339        SharedPreferences.Editor editor = prefs.edit();
340        if (startTime != null) {
341            editor.putLong(Stopwatches.PREF_START_TIME, startTime);
342            mStartTime = startTime;
343        }
344        if (lapTimeElapsed != null) {
345            int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, 0);
346            if (numLaps == 0) {
347                mNumLaps++;
348                numLaps++;
349            }
350            editor.putLong(Stopwatches.PREF_LAP_TIME + Integer.toString(numLaps), lapTimeElapsed);
351            numLaps++;
352            editor.putLong(Stopwatches.PREF_LAP_TIME + Integer.toString(numLaps), lapTimeElapsed);
353            editor.putInt(Stopwatches.PREF_LAP_NUM, numLaps);
354        }
355        if (elapsedTime != null) {
356            editor.putLong(Stopwatches.PREF_ACCUM_TIME, elapsedTime);
357            mElapsedTime = elapsedTime;
358        }
359        if (state != null) {
360            if (state == Stopwatches.STOPWATCH_RESET) {
361                editor.putInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_RESET);
362            } else if (state == Stopwatches.STOPWATCH_RUNNING) {
363                editor.putInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_RUNNING);
364            } else if (state == Stopwatches.STOPWATCH_STOPPED) {
365                editor.putInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_STOPPED);
366            }
367        }
368        editor.putBoolean(Stopwatches.PREF_UPDATE_CIRCLE, updateCircle);
369        editor.apply();
370    }
371
372    private void writeSharedPrefsStarted(long startTime, boolean updateCircle) {
373        writeToSharedPrefs(startTime, null, null, Stopwatches.STOPWATCH_RUNNING, updateCircle);
374        if (updateCircle) {
375            long time = Utils.getTimeNow();
376            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
377                    getApplicationContext());
378            long intervalStartTime = prefs.getLong(
379                    Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, -1);
380            if (intervalStartTime != -1) {
381                intervalStartTime = time;
382                SharedPreferences.Editor editor = prefs.edit();
383                editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START,
384                        intervalStartTime);
385                editor.putBoolean(Stopwatches.KEY + CircleTimerView.PREF_CTV_PAUSED, false);
386                editor.apply();
387            }
388        }
389    }
390
391    private void writeSharedPrefsLap(long lapTimeElapsed, boolean updateCircle) {
392        writeToSharedPrefs(null, lapTimeElapsed, null, null, updateCircle);
393        if (updateCircle) {
394            long time = Utils.getTimeNow();
395            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
396                    getApplicationContext());
397            SharedPreferences.Editor editor = prefs.edit();
398            long laps[] = readLapsFromPrefs();
399            int numLaps = laps.length;
400            long lapTime = laps[1];
401            if (numLaps == 2) { // Have only hit lap once.
402                editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL, lapTime);
403            } else {
404                editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_MARKER_TIME, lapTime);
405            }
406            editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_ACCUM_TIME, 0);
407            if (numLaps < Stopwatches.MAX_LAPS) {
408                editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, time);
409                editor.putBoolean(Stopwatches.KEY + CircleTimerView.PREF_CTV_PAUSED, false);
410            } else {
411                editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, -1);
412            }
413            editor.apply();
414        }
415    }
416
417    private void writeSharedPrefsStopped(long elapsedTime, boolean updateCircle) {
418        writeToSharedPrefs(null, null, elapsedTime, Stopwatches.STOPWATCH_STOPPED, updateCircle);
419        if (updateCircle) {
420            long time = Utils.getTimeNow();
421            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
422                    getApplicationContext());
423            long accumulatedTime = prefs.getLong(
424                    Stopwatches.KEY + CircleTimerView.PREF_CTV_ACCUM_TIME, 0);
425            long intervalStartTime = prefs.getLong(
426                    Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, -1);
427            accumulatedTime += time - intervalStartTime;
428            SharedPreferences.Editor editor = prefs.edit();
429            editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_ACCUM_TIME, accumulatedTime);
430            editor.putBoolean(Stopwatches.KEY + CircleTimerView.PREF_CTV_PAUSED, true);
431            editor.putLong(
432                    Stopwatches.KEY + CircleTimerView.PREF_CTV_CURRENT_INTERVAL, accumulatedTime);
433            editor.apply();
434        }
435    }
436
437    private void writeSharedPrefsReset(boolean updateCircle) {
438        writeToSharedPrefs(null, null, null, Stopwatches.STOPWATCH_RESET, updateCircle);
439    }
440}
441