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 17 package com.android.deskclock; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.os.AsyncTask; 24 import android.os.Bundle; 25 import android.preference.PreferenceManager; 26 27 import com.android.deskclock.events.Events; 28 import com.android.deskclock.stopwatch.StopwatchService; 29 import com.android.deskclock.stopwatch.Stopwatches; 30 import com.android.deskclock.timer.TimerFullScreenFragment; 31 import com.android.deskclock.timer.TimerObj; 32 import com.android.deskclock.timer.Timers; 33 import com.android.deskclock.worldclock.Cities; 34 import com.android.deskclock.worldclock.CitiesActivity; 35 import com.android.deskclock.worldclock.CityObj; 36 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Set; 43 44 public 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 onCreate(Bundle icicle)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 handleStopwatchIntent(String action)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 handleTimerIntent(final String action)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 handleClockIntent(final String action)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 HandleTimersAsync(Context context, String action, Activity activity)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 doInBackground(Void... parameters)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 */ getTimerWithStateToIgnore(List<TimerObj> timers, int stateToIgnore)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 */ getTimerWithStatesToInclude( List<TimerObj> timers, Set<Integer> statesToInclude, String action)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 HandleClockAsync(Context context, Intent intent, Activity activity)411 public HandleClockAsync(Context context, Intent intent, Activity activity) { 412 mContext = context; 413 mIntent = intent; 414 mActivity = activity; 415 } 416 417 @Override doInBackground(Void... parameters)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 startCitiesActivity()509 private void startCitiesActivity() { 510 mContext.startActivity(new Intent(mContext, CitiesActivity.class).addFlags( 511 Intent.FLAG_ACTIVITY_NEW_TASK)); 512 } 513 } 514 } 515 516