1/*
2 * Copyright (C) 2015 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.os.Looper;
24import android.provider.AlarmClock;
25
26import com.android.deskclock.alarms.AlarmStateManager;
27import com.android.deskclock.provider.Alarm;
28import com.android.deskclock.provider.AlarmInstance;
29
30import java.text.DateFormatSymbols;
31import java.util.ArrayList;
32import java.util.Calendar;
33import java.util.List;
34
35/**
36 * Returns a list of alarms that are specified by the intent
37 * processed by HandleDeskClockApiCalls
38 * if there are more than 1 matching alarms and the SEARCH_MODE is not ALL
39 * we show a picker UI dialog
40 */
41class FetchMatchingAlarmsAction implements Runnable {
42
43    private final Context mContext;
44    private final List<Alarm> mAlarms;
45    private final Intent mIntent;
46    private final List<Alarm> mMatchingAlarms = new ArrayList<>();
47    private final Activity mActivity;
48
49    public FetchMatchingAlarmsAction(Context context, List<Alarm> alarms, Intent intent,
50                                     Activity activity) {
51        mContext = context;
52        // only enabled alarms are passed
53        mAlarms = alarms;
54        mIntent = intent;
55        mActivity = activity;
56    }
57
58    @Override
59    public void run() {
60        // only allow on background thread
61        if (Looper.myLooper() == Looper.getMainLooper()) {
62            throw new IllegalStateException("Must be called on a background thread");
63        }
64
65        final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE);
66        // if search mode isn't specified show all alarms in the UI picker
67        if (searchMode == null) {
68            mMatchingAlarms.addAll(mAlarms);
69            return;
70        }
71
72        final ContentResolver cr = mContext.getContentResolver();
73        switch (searchMode) {
74            case AlarmClock.ALARM_SEARCH_MODE_TIME:
75                // at least one of these has to be specified in this search mode.
76                final int hour = mIntent.getIntExtra(AlarmClock.EXTRA_HOUR, -1);
77                // if minutes weren't specified default to 0
78                final int minutes = mIntent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
79                final Boolean isPm = (Boolean) mIntent.getExtras().get(AlarmClock.EXTRA_IS_PM);
80                boolean badInput = isPm != null && hour > 12 && isPm;
81                badInput |= hour < 0 || hour > 23;
82                badInput |= minutes < 0 || minutes > 59;
83
84                if (badInput) {
85                    final String[] ampm = new DateFormatSymbols().getAmPmStrings();
86                    final String amPm = isPm == null ? "" : (isPm ? ampm[1] : ampm[0]);
87                    final String reason = mContext.getString(R.string.invalid_time, hour, minutes,
88                            amPm);
89                    notifyFailureAndLog(reason, mActivity);
90                    return;
91                }
92
93                final int hour24 = Boolean.TRUE.equals(isPm) && hour < 12 ? (hour + 12) : hour;
94
95                // there might me multiple alarms at the same time
96                for (Alarm alarm : mAlarms) {
97                    if (alarm.hour == hour24 && alarm.minutes == minutes) {
98                        mMatchingAlarms.add(alarm);
99                    }
100                }
101                if (mMatchingAlarms.isEmpty()) {
102                    final String reason = mContext.getString(R.string.no_alarm_at, hour24, minutes);
103                    notifyFailureAndLog(reason, mActivity);
104                    return;
105                }
106                break;
107            case AlarmClock.ALARM_SEARCH_MODE_NEXT:
108                // Match currently firing alarms before scheduled alarms.
109                for (Alarm alarm : mAlarms) {
110                    final AlarmInstance alarmInstance =
111                            AlarmInstance.getNextUpcomingInstanceByAlarmId(cr, alarm.id);
112                    if (alarmInstance != null
113                            && alarmInstance.mAlarmState == AlarmInstance.FIRED_STATE) {
114                        mMatchingAlarms.add(alarm);
115                    }
116                }
117                if (!mMatchingAlarms.isEmpty()) {
118                    // return the matched firing alarms
119                    return;
120                }
121
122                final AlarmInstance nextAlarm = AlarmStateManager.getNextFiringAlarm(mContext);
123                if (nextAlarm == null) {
124                    final String reason = mContext.getString(R.string.no_scheduled_alarms);
125                    notifyFailureAndLog(reason, mActivity);
126                    return;
127                }
128
129                // get time from nextAlarm and see if there are any other alarms matching this time
130                final Calendar nextTime = nextAlarm.getAlarmTime();
131                final List<Alarm> alarmsFiringAtSameTime = getAlarmsByHourMinutes(
132                        nextTime.get(Calendar.HOUR_OF_DAY), nextTime.get(Calendar.MINUTE), cr);
133                // there might me multiple alarms firing next
134                mMatchingAlarms.addAll(alarmsFiringAtSameTime);
135                break;
136            case AlarmClock.ALARM_SEARCH_MODE_ALL:
137                mMatchingAlarms.addAll(mAlarms);
138                break;
139            case AlarmClock.ALARM_SEARCH_MODE_LABEL:
140                // EXTRA_MESSAGE has to be set in this mode
141                final String label = mIntent.getStringExtra(AlarmClock.EXTRA_MESSAGE);
142                if (label == null) {
143                    final String reason = mContext.getString(R.string.no_label_specified);
144                    notifyFailureAndLog(reason, mActivity);
145                    return;
146                }
147
148                // there might me multiple alarms with this label
149                for (Alarm alarm : mAlarms) {
150                    if (alarm.label.contains(label)) {
151                        mMatchingAlarms.add(alarm);
152                    }
153                }
154
155                if (mMatchingAlarms.isEmpty()) {
156                    final String reason = mContext.getString(R.string.no_alarms_with_label);
157                    notifyFailureAndLog(reason, mActivity);
158                    return;
159                }
160                break;
161        }
162    }
163
164    private List<Alarm> getAlarmsByHourMinutes(int hour24, int minutes, ContentResolver cr) {
165        // if we want to dismiss we should only add enabled alarms
166        final String selection = String.format("%s=? AND %s=? AND %s=?",
167                Alarm.HOUR, Alarm.MINUTES, Alarm.ENABLED);
168        final String[] args = { String.valueOf(hour24), String.valueOf(minutes), "1" };
169        return Alarm.getAlarms(cr, selection, args);
170    }
171
172    public List<Alarm> getMatchingAlarms() {
173        return mMatchingAlarms;
174    }
175
176    private void notifyFailureAndLog(String reason, Activity activity) {
177        LogUtils.e(reason);
178        Voice.notifyFailure(activity, reason);
179    }
180}
181