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