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;
18
19import android.app.Activity;
20import android.content.Context;
21import android.content.Intent;
22import android.content.SharedPreferences;
23import android.os.AsyncTask;
24import android.os.Bundle;
25import android.preference.PreferenceManager;
26
27import com.android.deskclock.events.Events;
28import com.android.deskclock.stopwatch.StopwatchService;
29import com.android.deskclock.stopwatch.Stopwatches;
30import com.android.deskclock.timer.TimerFullScreenFragment;
31import com.android.deskclock.timer.TimerObj;
32import com.android.deskclock.timer.Timers;
33import com.android.deskclock.worldclock.Cities;
34import com.android.deskclock.worldclock.CitiesActivity;
35import com.android.deskclock.worldclock.CityObj;
36
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.HashSet;
40import java.util.List;
41import java.util.Map;
42import java.util.Set;
43
44public class HandleDeskClockApiCalls extends Activity {
45    private Context mAppContext;
46
47    private static final String ACTION_PREFIX = "com.android.deskclock.action.";
48
49    // shows the tab with world clocks
50    public static final String ACTION_SHOW_CLOCK = ACTION_PREFIX + "SHOW_CLOCK";
51    // add a clock of a selected city, if no city is specified opens the city selection screen
52    public static final String ACTION_ADD_CLOCK = ACTION_PREFIX + "ADD_CLOCK";
53    // delete a clock of a selected city, if no city is specified shows CitiesActivity for the user
54    // to choose a city
55    public static final String ACTION_DELETE_CLOCK = ACTION_PREFIX + "DELETE_CLOCK";
56    // extra for ACTION_ADD_CLOCK and ACTION_DELETE_CLOCK
57    public static final String EXTRA_CITY = "com.android.deskclock.extra.clock.CITY";
58
59    // shows the tab with the stopwatch
60    public static final String ACTION_SHOW_STOPWATCH = ACTION_PREFIX + "SHOW_STOPWATCH";
61    // starts the current stopwatch
62    public static final String ACTION_START_STOPWATCH = ACTION_PREFIX + "START_STOPWATCH";
63    // stops the current stopwatch
64    public static final String ACTION_STOP_STOPWATCH = ACTION_PREFIX + "STOP_STOPWATCH";
65    // laps the stopwatch that's currently running
66    public static final String ACTION_LAP_STOPWATCH = ACTION_PREFIX + "LAP_STOPWATCH";
67    // resets the stopwatch if it's stopped
68    public static final String ACTION_RESET_STOPWATCH = ACTION_PREFIX + "RESET_STOPWATCH";
69
70    // shows the tab with timers
71    public static final String ACTION_SHOW_TIMERS = ACTION_PREFIX + "SHOW_TIMERS";
72    // deletes the topmost timer
73    public static final String ACTION_DELETE_TIMER = ACTION_PREFIX + "DELETE_TIMER";
74    // stops the running timer
75    public static final String ACTION_STOP_TIMER = ACTION_PREFIX + "STOP_TIMER";
76    // starts the topmost timer
77    public static final String ACTION_START_TIMER = ACTION_PREFIX + "START_TIMER";
78    // resets the timer, works for both running and stopped
79    public static final String ACTION_RESET_TIMER = ACTION_PREFIX + "RESET_TIMER";
80
81    @Override
82    protected void onCreate(Bundle icicle) {
83        try {
84            super.onCreate(icicle);
85            mAppContext = getApplicationContext();
86
87            final Intent intent = getIntent();
88            if (intent == null) {
89                return;
90            }
91
92            final String action = intent.getAction();
93            switch (action) {
94                case ACTION_START_STOPWATCH:
95                case ACTION_STOP_STOPWATCH:
96                case ACTION_LAP_STOPWATCH:
97                case ACTION_SHOW_STOPWATCH:
98                case ACTION_RESET_STOPWATCH:
99                    handleStopwatchIntent(action);
100                    break;
101                case ACTION_SHOW_TIMERS:
102                case ACTION_DELETE_TIMER:
103                case ACTION_RESET_TIMER:
104                case ACTION_STOP_TIMER:
105                case ACTION_START_TIMER:
106                    handleTimerIntent(action);
107                    break;
108                case ACTION_SHOW_CLOCK:
109                case ACTION_ADD_CLOCK:
110                case ACTION_DELETE_CLOCK:
111                    handleClockIntent(action);
112                    break;
113            }
114        } finally {
115            finish();
116        }
117    }
118
119    private void handleStopwatchIntent(String action) {
120        // Opens the UI for stopwatch
121        final Intent stopwatchIntent = new Intent(mAppContext, DeskClock.class)
122                .setAction(action)
123                .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
124        startActivity(stopwatchIntent);
125        LogUtils.i("HandleDeskClockApiCalls " + action);
126
127        if (action.equals(ACTION_SHOW_STOPWATCH)) {
128            Events.sendStopwatchEvent(R.string.action_show, R.string.label_intent);
129            return;
130        }
131
132        // checking if the stopwatch is already running
133        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mAppContext);
134        final boolean stopwatchAlreadyRunning =
135                prefs.getBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
136
137        if (stopwatchAlreadyRunning) {
138            // don't fire START_STOPWATCH or RESET_STOPWATCH if a stopwatch is already running
139            if (ACTION_START_STOPWATCH.equals(action)) {
140                final String reason = getString(R.string.stopwatch_already_running);
141                Voice.notifyFailure(this, reason);
142                LogUtils.i(reason);
143                return;
144            } else if (ACTION_RESET_STOPWATCH.equals(action)) { // RESET_STOPWATCH
145                final String reason = getString(R.string.stopwatch_cant_be_reset_because_is_running);
146                Voice.notifyFailure(this, reason);
147                LogUtils.i(reason);
148                return;
149            }
150        } else {
151            // if a stopwatch isn't running, don't try to stop or lap it
152            if (ACTION_STOP_STOPWATCH.equals(action) ||
153                    ACTION_LAP_STOPWATCH.equals(action)) {
154                final String reason = getString(R.string.stopwatch_isnt_running);
155                Voice.notifyFailure(this, reason);
156                LogUtils.i(reason);
157                return;
158            }
159        }
160
161        final String reason;
162        // Events and voice interactor setup
163        switch (action) {
164            case ACTION_START_STOPWATCH:
165                Events.sendStopwatchEvent(R.string.action_start, R.string.label_intent);
166                reason = getString(R.string.stopwatch_started);
167                break;
168            case ACTION_STOP_STOPWATCH:
169                Events.sendStopwatchEvent(R.string.action_stop, R.string.label_intent);
170                reason = getString(R.string.stopwatch_stopped);
171                break;
172            case ACTION_LAP_STOPWATCH:
173                Events.sendStopwatchEvent(R.string.action_lap, R.string.label_intent);
174                reason = getString(R.string.stopwatch_lapped);
175                break;
176            case ACTION_RESET_STOPWATCH:
177                Events.sendStopwatchEvent(R.string.action_reset, R.string.label_intent);
178                reason = getString(R.string.stopwatch_reset);
179                break;
180            default:
181                return;
182        }
183        final Intent intent = new Intent(mAppContext, StopwatchService.class).setAction(action);
184        startService(intent);
185        Voice.notifySuccess(this, reason);
186        LogUtils.i(reason);
187    }
188
189    private void handleTimerIntent(final String action) {
190        // Opens the UI for timers
191        final Intent timerIntent = new Intent(mAppContext, DeskClock.class)
192                .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX)
193                .putExtra(TimerFullScreenFragment.GOTO_SETUP_VIEW, false);
194        startActivity(timerIntent);
195        LogUtils.i("HandleDeskClockApiCalls " + action);
196
197        if (ACTION_SHOW_TIMERS.equals(action)) {
198            Events.sendTimerEvent(R.string.action_show, R.string.label_intent);
199            return;
200        }
201        new HandleTimersAsync(mAppContext, action, this).execute();
202    }
203
204    private void handleClockIntent(final String action) {
205        // Opens the UI for clocks
206        final Intent handleClock = new Intent(mAppContext, DeskClock.class)
207                .setAction(action)
208                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
209                .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.CLOCK_TAB_INDEX);
210        startActivity(handleClock);
211
212        new HandleClockAsync(mAppContext, getIntent(), this).execute();
213    }
214
215    private static class HandleTimersAsync extends AsyncTask<Void, Void, Void> {
216        private final Context mContext;
217        private final String mAction;
218        private final Activity mActivity;
219
220        public HandleTimersAsync(Context context, String action, Activity activity) {
221            mContext = context;
222            mAction = action;
223            mActivity = activity;
224        }
225        // STOP_TIMER and START_TIMER should only be triggered if there is one timer that is
226        // not stopped or not started respectively. This method checks all timers to find only
227        // one that corresponds to that.
228        // Only change the mode of the timer if no disambiguation is necessary
229
230        @Override
231        protected Void doInBackground(Void... parameters) {
232            final List<TimerObj> timers = new ArrayList<>();
233            final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
234            TimerObj.getTimersFromSharedPrefs(prefs, timers);
235            if (timers.isEmpty()) {
236                final String reason = mContext.getString(R.string.no_timer_set);
237                LogUtils.i(reason);
238                Voice.notifyFailure(mActivity, reason);
239                return null;
240            }
241            final TimerObj timer;
242            final String timerAction;
243            switch (mAction) {
244                case ACTION_DELETE_TIMER: {
245                    timerAction = Timers.DELETE_TIMER;
246                    // Delete a timer only if there's one available
247                    if (timers.size() > 1) {
248                        final String reason = mContext.getString(R.string.multiple_timers_available);
249                        LogUtils.i(reason);
250                        Voice.notifyFailure(mActivity, reason);
251                        return null;
252                    }
253
254                    timer = timers.get(0);
255                    timer.deleteFromSharedPref(prefs);
256                    Events.sendTimerEvent(R.string.action_delete, R.string.label_intent);
257                    final String reason = mContext.getString(R.string.timer_deleted);
258                    Voice.notifySuccess(mActivity, reason);
259                    LogUtils.i(reason);
260                    break;
261                }
262                case ACTION_START_TIMER: {
263                    timerAction = Timers.START_TIMER;
264                    timer = getTimerWithStateToIgnore(timers, TimerObj.STATE_RUNNING);
265                    // Only start a timer if there's one non-running timer available
266                    if (timer == null) {
267                        // notifyFailure was already triggered
268                        return null;
269                    }
270                    timer.setState(TimerObj.STATE_RUNNING);
271                    timer.mStartTime = Utils.getTimeNow() - (timer.mSetupLength - timer.mTimeLeft);
272                    timer.writeToSharedPref(prefs);
273                    final String reason = mContext.getString(R.string.timer_started);
274                    Voice.notifySuccess(mActivity, reason);
275                    LogUtils.i(reason);
276                    Events.sendTimerEvent(R.string.action_start, R.string.label_intent);
277                    break;
278                }
279                case ACTION_RESET_TIMER: {
280                    timerAction = Timers.RESET_TIMER;
281                    // Since timer can be reset only if it's stopped
282                    // it's only triggered when there's only one stopped timer
283                    final Set<Integer> statesToInclude = new HashSet<>();
284                    statesToInclude.add(TimerObj.STATE_STOPPED);
285                    timer = getTimerWithStatesToInclude(timers, statesToInclude, mAction);
286                    if (timer == null) {
287                        return null;
288                    }
289                    final String reason = mContext.getString(R.string.timer_was_reset);
290                    Voice.notifySuccess(mActivity, reason);
291                    LogUtils.i(reason);
292                    timer.setState(TimerObj.STATE_RESTART);
293                    timer.mTimeLeft = timer.mSetupLength;
294                    timer.writeToSharedPref(prefs);
295                    Events.sendTimerEvent(R.string.action_reset, R.string.label_intent);
296                    break;
297                }
298                case ACTION_STOP_TIMER: {
299                    timerAction = Timers.STOP_TIMER;
300                    final Set<Integer> statesToInclude = new HashSet<>();
301                    statesToInclude.add(TimerObj.STATE_TIMESUP);
302                    statesToInclude.add(TimerObj.STATE_RUNNING);
303                    // Timer is stopped if there's only one running timer
304                    timer = getTimerWithStatesToInclude(timers, statesToInclude, mAction);
305                    if (timer == null) {
306                        return null;
307                    }
308                    final String reason = mContext.getString(R.string.timer_stopped);
309                    LogUtils.i(reason);
310                    Voice.notifySuccess(mActivity, reason);
311                    if (timer.mState == TimerObj.STATE_RUNNING) {
312                        timer.setState(TimerObj.STATE_STOPPED);
313                    } else {
314                        // if the time is up on the timer
315                        // restart it and reset the length
316                        timer.setState(TimerObj.STATE_RESTART);
317                        timer.mTimeLeft = timer.mSetupLength;
318                    }
319                    timer.writeToSharedPref(prefs);
320                    Events.sendTimerEvent(R.string.action_stop, R.string.label_intent);
321                    break;
322                }
323                default:
324                    return null;
325            }
326            // updating the time for next firing timer
327            final Intent i = new Intent()
328                    .setAction(timerAction)
329                    .putExtra(Timers.TIMER_INTENT_EXTRA, timer.mTimerId)
330                    .putExtra(Timers.UPDATE_NEXT_TIMESUP, true)
331                    // Make sure the receiver is getting the intent ASAP.
332                    .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
333            mContext.sendBroadcast(i);
334            return null;
335        }
336
337        /**
338         * @param timers available to the user
339         * @param stateToIgnore the opposite of the state that the timer should be in
340         * @return a timer only if there's one timer available that is of a state
341         * other than the state that's passed
342         * in all other cases returns null
343         */
344        private TimerObj getTimerWithStateToIgnore(List<TimerObj> timers, int stateToIgnore) {
345            TimerObj soleTimer = null;
346            for (TimerObj timer : timers) {
347                if (timer.mState != stateToIgnore) {
348                    if (soleTimer == null) {
349                        soleTimer = timer;
350                    } else {
351                        // soleTimer has already been set
352                        final String reason = mContext.getString(R.string.multiple_timers_available);
353                        LogUtils.i(reason);
354                        Voice.notifyFailure(mActivity, reason);
355                        return null;
356                    }
357                }
358            }
359            return soleTimer;
360        }
361
362        /**
363         * @param timers available to the user
364         * @param statesToInclude acceptable states of the timer
365         * @return a timer only if there's one timer available that is of the state
366         * that is passed in
367         * in all other cases returns null
368         */
369        private TimerObj getTimerWithStatesToInclude(
370                List<TimerObj> timers, Set<Integer> statesToInclude, String action) {
371            TimerObj soleTimer = null;
372            for (TimerObj timer : timers) {
373                if (statesToInclude.contains(timer.mState)) {
374                    if (soleTimer == null) {
375                        soleTimer = timer;
376                    } else {
377                        // soleTimer has already been set
378                        final String reason = mContext.getString(
379                                R.string.multiple_timers_available);
380                        LogUtils.i(reason);
381                        Voice.notifyFailure(mActivity, reason);
382                        return null;
383                    }
384                }
385            }
386            // if there are no timers of desired property
387            // announce it to the user
388            if (soleTimer == null) {
389                if (action.equals(ACTION_RESET_TIMER)) {
390                    // all timers are running
391                    final String reason = mContext.getString(
392                            R.string.timer_cant_be_reset_because_its_running);
393                    LogUtils.i(reason);
394                    Voice.notifyFailure(mActivity, reason);
395                } else if (action.equals(ACTION_STOP_TIMER)) {
396                    // no running timers
397                    final String reason = mContext.getString(R.string.timer_already_stopped);
398                    LogUtils.i(reason);
399                    Voice.notifyFailure(mActivity, reason);
400                }
401            }
402            return soleTimer;
403        }
404    }
405
406    private static class HandleClockAsync extends AsyncTask<Void, Void, Void> {
407        private final Context mContext;
408        private final Intent mIntent;
409        private final Activity mActivity;
410
411        public HandleClockAsync(Context context, Intent intent, Activity activity) {
412            mContext = context;
413            mIntent = intent;
414            mActivity = activity;
415        }
416
417        @Override
418        protected Void doInBackground(Void... parameters) {
419            final String cityExtra = mIntent.getStringExtra(EXTRA_CITY);
420            final SharedPreferences prefs =
421                    PreferenceManager.getDefaultSharedPreferences(mContext);
422            switch (mIntent.getAction()) {
423                case ACTION_ADD_CLOCK: {
424                    // if a city isn't specified open CitiesActivity to choose a city
425                    if (cityExtra == null) {
426                        final String reason = mContext.getString(R.string.no_city_selected);
427                        Voice.notifyFailure(mActivity, reason);
428                        LogUtils.i(reason);
429                        startCitiesActivity();
430                        Events.sendClockEvent(R.string.action_create, R.string.label_intent);
431                        break;
432                    }
433
434                    // if a city is passed add that city to the list
435                    final Map<String, CityObj> cities = Utils.loadCityMapFromXml(mContext);
436                    final CityObj city = cities.get(cityExtra.toLowerCase());
437                    // check if this city exists in the list of available cities
438                    if (city == null) {
439                        final String reason = mContext.getString(
440                                R.string.the_city_you_specified_is_not_available);
441                        Voice.notifyFailure(mActivity, reason);
442                        LogUtils.i(reason);
443                        break;
444                    }
445
446                    final HashMap<String, CityObj> selectedCities =
447                            Cities.readCitiesFromSharedPrefs(prefs);
448                    // if this city is already added don't add it
449                    if (selectedCities.put(city.mCityId, city) != null) {
450                        final String reason = mContext.getString(R.string.the_city_already_added);
451                        Voice.notifyFailure(mActivity, reason);
452                        LogUtils.i(reason);
453                        break;
454                    }
455
456                    Cities.saveCitiesToSharedPrefs(prefs, selectedCities);
457                    final String reason = mContext.getString(R.string.city_added, city.mCityName);
458                    Voice.notifySuccess(mActivity, reason);
459                    LogUtils.i(reason);
460                    Events.sendClockEvent(R.string.action_start, R.string.label_intent);
461                    break;
462                }
463                case ACTION_DELETE_CLOCK: {
464                    if (cityExtra == null) {
465                        // if a city isn't specified open CitiesActivity to choose a city
466                        final String reason = mContext.getString(R.string.no_city_selected);
467                        Voice.notifyFailure(mActivity, reason);
468                        LogUtils.i(reason);
469                        startCitiesActivity();
470                        Events.sendClockEvent(R.string.action_create, R.string.label_intent);
471                        break;
472                    }
473
474                    // if a city is specified check if it's selected and if so delete it
475                    final Map<String, CityObj> cities = Utils.loadCityMapFromXml(mContext);
476                    // check if this city exists in the list of available cities
477                    final CityObj city = cities.get(cityExtra.toLowerCase());
478                    if (city == null) {
479                        final String reason = mContext.getString(
480                                R.string.the_city_you_specified_is_not_available);
481                        Voice.notifyFailure(mActivity, reason);
482                        LogUtils.i(reason);
483                        break;
484                    }
485
486                    final HashMap<String, CityObj> selectedCities =
487                            Cities.readCitiesFromSharedPrefs(prefs);
488                    if (selectedCities.remove(city.mCityId) != null) {
489                        final String reason = mContext.getString(R.string.city_deleted,
490                                city.mCityName);
491                        Voice.notifySuccess(mActivity, reason);
492                        LogUtils.i(reason);
493                        Cities.saveCitiesToSharedPrefs(prefs, selectedCities);
494                        Events.sendClockEvent(R.string.action_delete, R.string.label_intent);
495                    } else {
496                        // the specified city hasn't been added to the user's list yet
497                        Voice.notifyFailure(mActivity, mContext.getString(
498                                R.string.the_city_you_specified_is_not_available));
499                    }
500                    break;
501                }
502                case ACTION_SHOW_CLOCK:
503                    Events.sendClockEvent(R.string.action_show, R.string.label_intent);
504                    break;
505            }
506            return null;
507        }
508
509        private void startCitiesActivity() {
510            mContext.startActivity(new Intent(mContext, CitiesActivity.class).addFlags(
511                    Intent.FLAG_ACTIVITY_NEW_TASK));
512        }
513    }
514}
515
516