1/*
2 * Copyright (C) 2013 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 */
16package com.android.deskclock.alarms;
17
18import android.app.Service;
19import android.content.BroadcastReceiver;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.os.Binder;
25import android.os.IBinder;
26import android.telephony.PhoneStateListener;
27import android.telephony.TelephonyManager;
28
29import com.android.deskclock.AlarmAlertWakeLock;
30import com.android.deskclock.LogUtils;
31import com.android.deskclock.R;
32import com.android.deskclock.events.Events;
33import com.android.deskclock.provider.AlarmInstance;
34
35/**
36 * This service is in charge of starting/stopping the alarm. It will bring up and manage the
37 * {@link AlarmActivity} as well as {@link AlarmKlaxon}.
38 *
39 * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver
40 * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents.
41 */
42public class AlarmService extends Service {
43    /**
44     * AlarmActivity and AlarmService (when unbound) listen for this broadcast intent
45     * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before
46     * ALARM_DONE_ACTION).
47     */
48    public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
49
50    /**
51     * AlarmActivity and AlarmService listen for this broadcast intent so that other
52     * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
53     */
54    public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
55
56    /** A public action sent by AlarmService when the alarm has started. */
57    public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT";
58
59    /** A public action sent by AlarmService when the alarm has stopped for any reason. */
60    public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE";
61
62    /** Private action used to start an alarm with this service. */
63    public static final String START_ALARM_ACTION = "START_ALARM";
64
65    /** Private action used to stop an alarm with this service. */
66    public static final String STOP_ALARM_ACTION = "STOP_ALARM";
67
68    /** Binder given to AlarmActivity */
69    private final IBinder mBinder = new Binder();
70
71    /** Whether the service is currently bound to AlarmActivity */
72    private boolean mIsBound = false;
73
74    /** Whether the receiver is currently registered */
75    private boolean mIsRegistered = false;
76
77    @Override
78    public IBinder onBind(Intent intent) {
79        mIsBound = true;
80        return mBinder;
81    }
82
83    @Override
84    public boolean onUnbind(Intent intent) {
85        mIsBound = false;
86        return super.onUnbind(intent);
87    }
88
89    /**
90     * Utility method to help start alarm properly. If alarm is already firing, it
91     * will mark it as missed and start the new one.
92     *
93     * @param context application context
94     * @param instance to trigger alarm
95     */
96    public static void startAlarm(Context context, AlarmInstance instance) {
97        final Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId)
98                .setAction(START_ALARM_ACTION);
99
100        // Maintain a cpu wake lock until the service can get it
101        AlarmAlertWakeLock.acquireCpuWakeLock(context);
102        context.startService(intent);
103    }
104
105    /**
106     * Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
107     * or using a different instance.
108     *
109     * @param context application context
110     * @param instance you are trying to stop
111     */
112    public static void stopAlarm(Context context, AlarmInstance instance) {
113        final Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId)
114                .setAction(STOP_ALARM_ACTION);
115
116        // We don't need a wake lock here, since we are trying to kill an alarm
117        context.startService(intent);
118    }
119
120    private TelephonyManager mTelephonyManager;
121    private int mInitialCallState;
122    private AlarmInstance mCurrentAlarm = null;
123
124    private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
125        @Override
126        public void onCallStateChanged(int state, String ignored) {
127            // The user might already be in a call when the alarm fires. When
128            // we register onCallStateChanged, we get the initial in-call state
129            // which kills the alarm. Check against the initial call state so
130            // we don't kill the alarm during a call.
131            if (state != TelephonyManager.CALL_STATE_IDLE && state != mInitialCallState) {
132                sendBroadcast(AlarmStateManager.createStateChangeIntent(AlarmService.this,
133                        "AlarmService", mCurrentAlarm, AlarmInstance.MISSED_STATE));
134            }
135        }
136    };
137
138    private void startAlarm(AlarmInstance instance) {
139        LogUtils.v("AlarmService.start with instance: " + instance.mId);
140        if (mCurrentAlarm != null) {
141            AlarmStateManager.setMissedState(this, mCurrentAlarm);
142            stopCurrentAlarm();
143        }
144
145        AlarmAlertWakeLock.acquireCpuWakeLock(this);
146
147        Events.sendEvent(R.string.category_alarm, R.string.action_fire, 0);
148
149        mCurrentAlarm = instance;
150        AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
151        mInitialCallState = mTelephonyManager.getCallState();
152        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
153        AlarmKlaxon.start(this, mCurrentAlarm);
154        sendBroadcast(new Intent(ALARM_ALERT_ACTION));
155    }
156
157    private void stopCurrentAlarm() {
158        if (mCurrentAlarm == null) {
159            LogUtils.v("There is no current alarm to stop");
160            return;
161        }
162
163        LogUtils.v("AlarmService.stop with instance: %s", (Object) mCurrentAlarm.mId);
164        AlarmKlaxon.stop(this);
165        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
166        sendBroadcast(new Intent(ALARM_DONE_ACTION));
167
168        mCurrentAlarm = null;
169        AlarmAlertWakeLock.releaseCpuLock();
170    }
171
172    private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() {
173        @Override
174        public void onReceive(Context context, Intent intent) {
175            final String action = intent.getAction();
176            LogUtils.i("AlarmService received intent %s", action);
177            if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) {
178                LogUtils.i("No valid firing alarm");
179                return;
180            }
181
182            if (mIsBound) {
183                LogUtils.i("AlarmActivity bound; AlarmService no-op");
184                return;
185            }
186
187            switch (action) {
188                case ALARM_SNOOZE_ACTION:
189                    // Set the alarm state to snoozed.
190                    // If this broadcast receiver is handling the snooze intent then AlarmActivity
191                    // must not be showing, so always show snooze toast.
192                    AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */);
193                    Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
194                    break;
195                case ALARM_DISMISS_ACTION:
196                    // Set the alarm state to dismissed.
197                    AlarmStateManager.setDismissState(context, mCurrentAlarm);
198                    Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
199                    break;
200            }
201        }
202    };
203
204    @Override
205    public void onCreate() {
206        super.onCreate();
207        mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
208
209        // Register the broadcast receiver
210        final IntentFilter filter = new IntentFilter(ALARM_SNOOZE_ACTION);
211        filter.addAction(ALARM_DISMISS_ACTION);
212        registerReceiver(mActionsReceiver, filter);
213        mIsRegistered = true;
214    }
215
216    @Override
217    public int onStartCommand(Intent intent, int flags, int startId) {
218        LogUtils.v("AlarmService.onStartCommand() with %s", intent);
219
220        final long instanceId = AlarmInstance.getId(intent.getData());
221        switch (intent.getAction()) {
222            case START_ALARM_ACTION:
223                final ContentResolver cr = this.getContentResolver();
224                final AlarmInstance instance = AlarmInstance.getInstance(cr, instanceId);
225                if (instance == null) {
226                    LogUtils.e("No instance found to start alarm: %d", instanceId);
227                    if (mCurrentAlarm != null) {
228                        // Only release lock if we are not firing alarm
229                        AlarmAlertWakeLock.releaseCpuLock();
230                    }
231                    break;
232                }
233
234                if (mCurrentAlarm != null && mCurrentAlarm.mId == instanceId) {
235                    LogUtils.e("Alarm already started for instance: %d", instanceId);
236                    break;
237                }
238                startAlarm(instance);
239                break;
240            case STOP_ALARM_ACTION:
241                if (mCurrentAlarm != null && mCurrentAlarm.mId != instanceId) {
242                    LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d",
243                            instanceId, mCurrentAlarm.mId);
244                    break;
245                }
246                stopCurrentAlarm();
247                stopSelf();
248        }
249
250        return Service.START_NOT_STICKY;
251    }
252
253    @Override
254    public void onDestroy() {
255        LogUtils.v("AlarmService.onDestroy() called");
256        super.onDestroy();
257        if (mCurrentAlarm != null) {
258            stopCurrentAlarm();
259        }
260
261        if (mIsRegistered) {
262            unregisterReceiver(mActionsReceiver);
263            mIsRegistered = false;
264        }
265    }
266}
267