Home | History | Annotate | Download | only in deskclock
      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
     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 
     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 
    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 
    140         public DismissAlarmAsync(Context context, Intent intent, Activity activity) {
    141             mContext = context;
    142             mIntent = intent;
    143             mActivity = activity;
    144         }
    145 
    146         @Override
    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 
    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 
    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 
    221         public SnoozeAlarmAsync(Context context, Activity activity) {
    222             mContext = context;
    223             mActivity = activity;
    224         }
    225 
    226         @Override
    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 
    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      */
    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 
    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 
    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 
    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 
    393     private static String getMessageFromIntent(Intent intent) {
    394         final String message = intent.getStringExtra(AlarmClock.EXTRA_MESSAGE);
    395         return message == null ? "" : message;
    396     }
    397 
    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 
    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