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