1 /* 2 * Copyright (C) 2010 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.ContentResolver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.media.RingtoneManager; 25 import android.net.Uri; 26 import android.os.AsyncTask; 27 import android.os.Bundle; 28 import android.os.Looper; 29 import android.os.Parcelable; 30 import android.preference.PreferenceManager; 31 import android.provider.AlarmClock; 32 import android.text.TextUtils; 33 import android.text.format.DateFormat; 34 35 import com.android.deskclock.alarms.AlarmStateManager; 36 import com.android.deskclock.events.Events; 37 import com.android.deskclock.provider.Alarm; 38 import com.android.deskclock.provider.AlarmInstance; 39 import com.android.deskclock.provider.DaysOfWeek; 40 import com.android.deskclock.timer.TimerFullScreenFragment; 41 import com.android.deskclock.timer.TimerObj; 42 import com.android.deskclock.timer.Timers; 43 44 import java.util.ArrayList; 45 import java.util.Calendar; 46 import java.util.Iterator; 47 import java.util.List; 48 49 public class HandleApiCalls extends Activity { 50 51 public static final long TIMER_MIN_LENGTH = 1000; 52 public static final long TIMER_MAX_LENGTH = 24 * 60 * 60 * 1000; 53 54 private Context mAppContext; 55 56 @Override onCreate(Bundle icicle)57 protected void onCreate(Bundle icicle) { 58 try { 59 super.onCreate(icicle); 60 mAppContext = getApplicationContext(); 61 final Intent intent = getIntent(); 62 final String action = intent == null ? null : intent.getAction(); 63 if (action == null) { 64 return; 65 } 66 switch (action) { 67 case AlarmClock.ACTION_SET_ALARM: 68 handleSetAlarm(intent); 69 break; 70 case AlarmClock.ACTION_SHOW_ALARMS: 71 handleShowAlarms(); 72 break; 73 case AlarmClock.ACTION_SET_TIMER: 74 handleSetTimer(intent); 75 break; 76 case AlarmClock.ACTION_DISMISS_ALARM: 77 handleDismissAlarm(intent.getAction()); 78 break; 79 case AlarmClock.ACTION_SNOOZE_ALARM: 80 handleSnoozeAlarm(); 81 } 82 } finally { 83 finish(); 84 } 85 } 86 handleDismissAlarm(final String action)87 private void handleDismissAlarm(final String action) { 88 // Opens the UI for Alarms 89 final Intent alarmIntent = 90 Alarm.createIntent(mAppContext, DeskClock.class, Alarm.INVALID_ID) 91 .setAction(action) 92 .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX); 93 startActivity(alarmIntent); 94 95 final Intent intent = getIntent(); 96 97 new DismissAlarmAsync(mAppContext, intent, this).execute(); 98 } 99 dismissAlarm(Alarm alarm, Context context, Activity activity)100 public static void dismissAlarm(Alarm alarm, Context context, Activity activity) { 101 // only allow on background thread 102 if (Looper.myLooper() == Looper.getMainLooper()) { 103 throw new IllegalStateException("dismissAlarm must be called on a " + 104 "background thread"); 105 } 106 107 final AlarmInstance alarmInstance = AlarmInstance.getNextUpcomingInstanceByAlarmId( 108 context.getContentResolver(), alarm.id); 109 if (alarmInstance == null) { 110 final String reason = context.getString(R.string.no_alarm_scheduled_for_this_time); 111 Voice.notifyFailure(activity, reason); 112 LogUtils.i(reason); 113 return; 114 } 115 116 final String time = DateFormat.getTimeFormat(context).format( 117 alarmInstance.getAlarmTime().getTime()); 118 if (Utils.isAlarmWithin24Hours(alarmInstance)) { 119 AlarmStateManager.setPreDismissState(context, alarmInstance); 120 final String reason = context.getString(R.string.alarm_is_dismissed, time); 121 LogUtils.i(reason); 122 Voice.notifySuccess(activity, reason); 123 Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent); 124 } else { 125 final String reason = context.getString( 126 R.string.alarm_cant_be_dismissed_still_more_than_24_hours_away, time); 127 Voice.notifyFailure(activity, reason); 128 LogUtils.i(reason); 129 } 130 } 131 132 private static class DismissAlarmAsync extends AsyncTask<Void, Void, Void> { 133 134 private final Context mContext; 135 private final Intent mIntent; 136 private final Activity mActivity; 137 DismissAlarmAsync(Context context, Intent intent, Activity activity)138 public DismissAlarmAsync(Context context, Intent intent, Activity activity) { 139 mContext = context; 140 mIntent = intent; 141 mActivity = activity; 142 } 143 144 @Override doInBackground(Void... parameters)145 protected Void doInBackground(Void... parameters) { 146 final List<Alarm> alarms = getEnabledAlarms(mContext); 147 if (alarms.isEmpty()) { 148 final String reason = mContext.getString(R.string.no_scheduled_alarms); 149 LogUtils.i(reason); 150 Voice.notifyFailure(mActivity, reason); 151 return null; 152 } 153 154 // remove Alarms in MISSED, DISMISSED, and PREDISMISSED states 155 for (Iterator<Alarm> i = alarms.iterator(); i.hasNext();) { 156 final AlarmInstance alarmInstance = AlarmInstance.getNextUpcomingInstanceByAlarmId( 157 mContext.getContentResolver(), i.next().id); 158 if (alarmInstance == null || 159 alarmInstance.mAlarmState > AlarmInstance.FIRED_STATE) { 160 i.remove(); 161 } 162 } 163 164 final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE); 165 if (searchMode == null && alarms.size() > 1) { 166 // shows the UI where user picks which alarm they want to DISMISS 167 final Intent pickSelectionIntent = new Intent(mContext, 168 AlarmSelectionActivity.class) 169 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 170 .putExtra(AlarmSelectionActivity.EXTRA_ALARMS, 171 alarms.toArray(new Parcelable[alarms.size()])); 172 mContext.startActivity(pickSelectionIntent); 173 Voice.notifySuccess(mActivity, mContext.getString(R.string.pick_alarm_to_dismiss)); 174 return null; 175 } 176 177 // fetch the alarms that are specified by the intent 178 final FetchMatchingAlarmsAction fmaa = 179 new FetchMatchingAlarmsAction(mContext, alarms, mIntent, mActivity); 180 fmaa.run(); 181 final List<Alarm> matchingAlarms = fmaa.getMatchingAlarms(); 182 183 // If there are multiple matching alarms and it wasn't expected 184 // disambiguate what the user meant 185 if (!AlarmClock.ALARM_SEARCH_MODE_ALL.equals(searchMode) && matchingAlarms.size() > 1) { 186 final Intent pickSelectionIntent = new Intent(mContext, AlarmSelectionActivity.class) 187 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 188 .putExtra(AlarmSelectionActivity.EXTRA_ALARMS, 189 matchingAlarms.toArray(new Parcelable[matchingAlarms.size()])); 190 mContext.startActivity(pickSelectionIntent); 191 Voice.notifySuccess(mActivity, mContext.getString(R.string.pick_alarm_to_dismiss)); 192 return null; 193 } 194 195 // Apply the action to the matching alarms 196 for (Alarm alarm : matchingAlarms) { 197 dismissAlarm(alarm, mContext, mActivity); 198 LogUtils.i("Alarm %s is dismissed", alarm); 199 } 200 return null; 201 } 202 getEnabledAlarms(Context context)203 private static List<Alarm> getEnabledAlarms(Context context) { 204 final String selection = String.format("%s=?", Alarm.ENABLED); 205 final String[] args = { "1" }; 206 return Alarm.getAlarms(context.getContentResolver(), selection, args); 207 } 208 } 209 handleSnoozeAlarm()210 private void handleSnoozeAlarm() { 211 new SnoozeAlarmAsync(mAppContext, this).execute(); 212 } 213 214 private static class SnoozeAlarmAsync extends AsyncTask<Void, Void, Void> { 215 216 private final Context mContext; 217 private final Activity mActivity; 218 SnoozeAlarmAsync(Context context, Activity activity)219 public SnoozeAlarmAsync(Context context, Activity activity) { 220 mContext = context; 221 mActivity = activity; 222 } 223 224 @Override doInBackground(Void... parameters)225 protected Void doInBackground(Void... parameters) { 226 final List<AlarmInstance> alarmInstances = AlarmInstance.getInstancesByState( 227 mContext.getContentResolver(), AlarmInstance.FIRED_STATE); 228 if (alarmInstances.isEmpty()) { 229 final String reason = mContext.getString(R.string.no_firing_alarms); 230 LogUtils.i(reason); 231 Voice.notifyFailure(mActivity, reason); 232 return null; 233 } 234 235 for (AlarmInstance firingAlarmInstance : alarmInstances) { 236 snoozeAlarm(firingAlarmInstance, mContext, mActivity); 237 } 238 return null; 239 } 240 } 241 snoozeAlarm(AlarmInstance alarmInstance, Context context, Activity activity)242 static void snoozeAlarm(AlarmInstance alarmInstance, Context context, Activity activity) { 243 // only allow on background thread 244 if (Looper.myLooper() == Looper.getMainLooper()) { 245 throw new IllegalStateException("snoozeAlarm must be called on a " + 246 "background thread"); 247 } 248 final String time = DateFormat.getTimeFormat(context).format( 249 alarmInstance.getAlarmTime().getTime()); 250 final String reason = context.getString(R.string.alarm_is_snoozed, time); 251 LogUtils.i(reason); 252 Voice.notifySuccess(activity, reason); 253 AlarmStateManager.setSnoozeState(context, alarmInstance, true); 254 LogUtils.i("Snooze %d:%d", alarmInstance.mHour, alarmInstance.mMinute); 255 Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent); 256 } 257 258 /*** 259 * Processes the SET_ALARM intent 260 * @param intent Intent passed to the app 261 */ handleSetAlarm(Intent intent)262 private void handleSetAlarm(Intent intent) { 263 // If not provided or invalid, show UI 264 final int hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, -1); 265 266 // If not provided, use zero. If it is provided, make sure it's valid, otherwise, show UI 267 final int minutes; 268 if (intent.hasExtra(AlarmClock.EXTRA_MINUTES)) { 269 minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, -1); 270 } else { 271 minutes = 0; 272 } 273 if (hour < 0 || hour > 23 || minutes < 0 || minutes > 59) { 274 // Intent has no time or an invalid time, open the alarm creation UI 275 Intent createAlarm = Alarm.createIntent(this, DeskClock.class, Alarm.INVALID_ID); 276 createAlarm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 277 createAlarm.putExtra(AlarmClockFragment.ALARM_CREATE_NEW_INTENT_EXTRA, true); 278 createAlarm.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX); 279 startActivity(createAlarm); 280 Voice.notifyFailure(this, getString(R.string.invalid_time, hour, minutes, " ")); 281 LogUtils.i("HandleApiCalls no/invalid time; opening UI"); 282 return; 283 } 284 285 Events.sendAlarmEvent(R.string.action_create, R.string.label_intent); 286 final boolean skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false); 287 288 final StringBuilder selection = new StringBuilder(); 289 final List<String> args = new ArrayList<>(); 290 setSelectionFromIntent(intent, hour, minutes, selection, args); 291 292 final String message = getMessageFromIntent(intent); 293 final DaysOfWeek daysOfWeek = getDaysFromIntent(intent); 294 final boolean vibrate = intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, false); 295 final String alert = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE); 296 297 Alarm alarm = new Alarm(hour, minutes); 298 alarm.enabled = true; 299 alarm.label = message; 300 alarm.daysOfWeek = daysOfWeek; 301 alarm.vibrate = vibrate; 302 303 if (alert == null) { 304 alarm.alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 305 } else if (AlarmClock.VALUE_RINGTONE_SILENT.equals(alert) || alert.isEmpty()) { 306 alarm.alert = Alarm.NO_RINGTONE_URI; 307 } else { 308 alarm.alert = Uri.parse(alert); 309 } 310 alarm.deleteAfterUse = !daysOfWeek.isRepeating() && skipUi; 311 312 final ContentResolver cr = getContentResolver(); 313 alarm = Alarm.addAlarm(cr, alarm); 314 final AlarmInstance alarmInstance = alarm.createInstanceAfter(Calendar.getInstance()); 315 setupInstance(alarmInstance, skipUi); 316 final String time = DateFormat.getTimeFormat(mAppContext).format( 317 alarmInstance.getAlarmTime().getTime()); 318 Voice.notifySuccess(this, getString(R.string.alarm_is_set, time)); 319 LogUtils.i("HandleApiCalls set up alarm: %s", alarm); 320 } 321 handleShowAlarms()322 private void handleShowAlarms() { 323 startActivity(new Intent(this, DeskClock.class) 324 .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX)); 325 Events.sendAlarmEvent(R.string.action_show, R.string.label_intent); 326 LogUtils.i("HandleApiCalls show alarms"); 327 } 328 handleSetTimer(Intent intent)329 private void handleSetTimer(Intent intent) { 330 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 331 // If no length is supplied, show the timer setup view 332 if (!intent.hasExtra(AlarmClock.EXTRA_LENGTH)) { 333 startActivity(new Intent(this, DeskClock.class) 334 .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX) 335 .putExtra(TimerFullScreenFragment.GOTO_SETUP_VIEW, true)); 336 LogUtils.i("HandleApiCalls showing timer setup"); 337 return; 338 } 339 340 final long length = 1000l * intent.getIntExtra(AlarmClock.EXTRA_LENGTH, 0); 341 if (length < TIMER_MIN_LENGTH || length > TIMER_MAX_LENGTH) { 342 Voice.notifyFailure(this, getString(R.string.invalid_timer_length)); 343 LogUtils.i("Invalid timer length requested: " + length); 344 return; 345 } 346 String label = getMessageFromIntent(intent); 347 348 TimerObj timer = null; 349 // Find an existing matching time 350 final List<TimerObj> timers = new ArrayList<>(); 351 TimerObj.getTimersFromSharedPrefs(prefs, timers); 352 for (TimerObj t : timers) { 353 if (t.mSetupLength == length && (TextUtils.equals(label, t.mLabel)) 354 && t.mState == TimerObj.STATE_RESTART) { 355 timer = t; 356 break; 357 } 358 } 359 360 boolean skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false); 361 if (timer == null) { 362 // Use a new timer 363 timer = new TimerObj(length, label, this /* context */); 364 // Timers set without presenting UI to the user will be deleted after use 365 timer.mDeleteAfterUse = skipUi; 366 367 Events.sendTimerEvent(R.string.action_create, R.string.label_intent); 368 } 369 370 timer.setState(TimerObj.STATE_RUNNING); 371 timer.mStartTime = Utils.getTimeNow(); 372 timer.writeToSharedPref(prefs); 373 374 Events.sendTimerEvent(R.string.action_start, R.string.label_intent); 375 376 // Tell TimerReceiver that the timer was started 377 sendBroadcast(new Intent().setAction(Timers.START_TIMER) 378 .putExtra(Timers.TIMER_INTENT_EXTRA, timer.mTimerId)); 379 380 if (skipUi) { 381 Utils.showInUseNotifications(this); 382 } else { 383 startActivity(new Intent(this, DeskClock.class) 384 .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX) 385 .putExtra(Timers.FIRST_LAUNCH_FROM_API_CALL, true) 386 .putExtra(Timers.SCROLL_TO_TIMER_ID, timer.mTimerId)); 387 } 388 Voice.notifySuccess(this, getString(R.string.timer_created)); 389 LogUtils.i("HandleApiCalls timer created: %s", timer); 390 } 391 setupInstance(AlarmInstance instance, boolean skipUi)392 private void setupInstance(AlarmInstance instance, boolean skipUi) { 393 instance = AlarmInstance.addInstance(this.getContentResolver(), instance); 394 AlarmStateManager.registerInstance(this, instance, true); 395 AlarmUtils.popAlarmSetToast(this, instance.getAlarmTime().getTimeInMillis()); 396 if (!skipUi) { 397 Intent showAlarm = Alarm.createIntent(this, DeskClock.class, instance.mAlarmId); 398 showAlarm.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.ALARM_TAB_INDEX); 399 showAlarm.putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, instance.mAlarmId); 400 showAlarm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 401 startActivity(showAlarm); 402 } 403 } 404 getMessageFromIntent(Intent intent)405 private String getMessageFromIntent(Intent intent) { 406 final String message = intent.getStringExtra(AlarmClock.EXTRA_MESSAGE); 407 return message == null ? "" : message; 408 } 409 getDaysFromIntent(Intent intent)410 private DaysOfWeek getDaysFromIntent(Intent intent) { 411 final DaysOfWeek daysOfWeek = new DaysOfWeek(0); 412 final ArrayList<Integer> days = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS); 413 if (days != null) { 414 final int[] daysArray = new int[days.size()]; 415 for (int i = 0; i < days.size(); i++) { 416 daysArray[i] = days.get(i); 417 } 418 daysOfWeek.setDaysOfWeek(true, daysArray); 419 } else { 420 // API says to use an ArrayList<Integer> but we allow the user to use a int[] too. 421 final int[] daysArray = intent.getIntArrayExtra(AlarmClock.EXTRA_DAYS); 422 if (daysArray != null) { 423 daysOfWeek.setDaysOfWeek(true, daysArray); 424 } 425 } 426 return daysOfWeek; 427 } 428 setSelectionFromIntent( Intent intent, int hour, int minutes, StringBuilder selection, List<String> args)429 private void setSelectionFromIntent( 430 Intent intent, 431 int hour, 432 int minutes, 433 StringBuilder selection, 434 List<String> args) { 435 selection.append(Alarm.HOUR).append("=?"); 436 args.add(String.valueOf(hour)); 437 selection.append(" AND ").append(Alarm.MINUTES).append("=?"); 438 args.add(String.valueOf(minutes)); 439 440 if (intent.hasExtra(AlarmClock.EXTRA_MESSAGE)) { 441 selection.append(" AND ").append(Alarm.LABEL).append("=?"); 442 args.add(getMessageFromIntent(intent)); 443 } 444 445 // Days is treated differently that other fields because if days is not specified, it 446 // explicitly means "not recurring". 447 selection.append(" AND ").append(Alarm.DAYS_OF_WEEK).append("=?"); 448 args.add(String.valueOf(intent.hasExtra(AlarmClock.EXTRA_DAYS) 449 ? getDaysFromIntent(intent).getBitSet() : DaysOfWeek.NO_DAYS_SET)); 450 451 if (intent.hasExtra(AlarmClock.EXTRA_VIBRATE)) { 452 selection.append(" AND ").append(Alarm.VIBRATE).append("=?"); 453 args.add(intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, false) ? "1" : "0"); 454 } 455 456 if (intent.hasExtra(AlarmClock.EXTRA_RINGTONE)) { 457 selection.append(" AND ").append(Alarm.RINGTONE).append("=?"); 458 459 String ringTone = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE); 460 if (ringTone == null) { 461 // If the intent explicitly specified a NULL ringtone, treat it as the default 462 // ringtone. 463 ringTone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM).toString(); 464 } else if (AlarmClock.VALUE_RINGTONE_SILENT.equals(ringTone) || ringTone.isEmpty()) { 465 ringTone = Alarm.NO_RINGTONE; 466 } 467 args.add(ringTone); 468 } 469 } 470 } 471