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