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