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  */
16 package com.android.deskclock.alarms;
17 
18 import android.app.Service;
19 import android.content.BroadcastReceiver;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.os.Binder;
25 import android.os.IBinder;
26 import android.telephony.PhoneStateListener;
27 import android.telephony.TelephonyManager;
28 
29 import com.android.deskclock.AlarmAlertWakeLock;
30 import com.android.deskclock.LogUtils;
31 import com.android.deskclock.R;
32 import com.android.deskclock.events.Events;
33 import 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  */
42 public 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 stop an alarm with this service. */
63     public static final String STOP_ALARM_ACTION = "STOP_ALARM";
64 
65     /** Binder given to AlarmActivity */
66     private final IBinder mBinder = new Binder();
67 
68     /** Whether the service is currently bound to AlarmActivity */
69     private boolean mIsBound = false;
70 
71     /** Whether the receiver is currently registered */
72     private boolean mIsRegistered = false;
73 
74     @Override
onBind(Intent intent)75     public IBinder onBind(Intent intent) {
76         mIsBound = true;
77         return mBinder;
78     }
79 
80     @Override
onUnbind(Intent intent)81     public boolean onUnbind(Intent intent) {
82         mIsBound = false;
83         return super.onUnbind(intent);
84     }
85 
86     /**
87      * Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
88      * or using a different instance.
89      *
90      * @param context application context
91      * @param instance you are trying to stop
92      */
stopAlarm(Context context, AlarmInstance instance)93     public static void stopAlarm(Context context, AlarmInstance instance) {
94         final Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId)
95                 .setAction(STOP_ALARM_ACTION);
96 
97         // We don't need a wake lock here, since we are trying to kill an alarm
98         context.startService(intent);
99     }
100 
101     private TelephonyManager mTelephonyManager;
102     private int mInitialCallState;
103     private AlarmInstance mCurrentAlarm = null;
104 
105     private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
106         @Override
107         public void onCallStateChanged(int state, String ignored) {
108             // The user might already be in a call when the alarm fires. When
109             // we register onCallStateChanged, we get the initial in-call state
110             // which kills the alarm. Check against the initial call state so
111             // we don't kill the alarm during a call.
112             if (state != TelephonyManager.CALL_STATE_IDLE && state != mInitialCallState) {
113                 startService(AlarmStateManager.createStateChangeIntent(AlarmService.this,
114                         "AlarmService", mCurrentAlarm, AlarmInstance.MISSED_STATE));
115             }
116         }
117     };
118 
startAlarm(AlarmInstance instance)119     private void startAlarm(AlarmInstance instance) {
120         LogUtils.v("AlarmService.start with instance: " + instance.mId);
121         if (mCurrentAlarm != null) {
122             AlarmStateManager.setMissedState(this, mCurrentAlarm);
123             stopCurrentAlarm();
124         }
125 
126         AlarmAlertWakeLock.acquireCpuWakeLock(this);
127 
128         mCurrentAlarm = instance;
129         AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
130         mInitialCallState = mTelephonyManager.getCallState();
131         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
132         AlarmKlaxon.start(this, mCurrentAlarm);
133         sendBroadcast(new Intent(ALARM_ALERT_ACTION));
134     }
135 
stopCurrentAlarm()136     private void stopCurrentAlarm() {
137         if (mCurrentAlarm == null) {
138             LogUtils.v("There is no current alarm to stop");
139             return;
140         }
141 
142         final long instanceId = mCurrentAlarm.mId;
143         LogUtils.v("AlarmService.stop with instance: %s", instanceId);
144 
145         AlarmKlaxon.stop(this);
146         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
147         sendBroadcast(new Intent(ALARM_DONE_ACTION));
148 
149         // Since we use the same id for all notifications, the system has no way to distinguish the
150         // firing notification we were bound to from other subsequent notifications posted for the
151         // same AlarmInstance (e.g. after snoozing). We workaround the issue by forcing removal of
152         // the notification and re-posting it.
153         stopForeground(true /* removeNotification */);
154         mCurrentAlarm = AlarmInstance.getInstance(getContentResolver(), instanceId);
155         if (mCurrentAlarm != null) {
156             AlarmNotifications.updateNotification(this, mCurrentAlarm);
157         }
158 
159         mCurrentAlarm = null;
160         AlarmAlertWakeLock.releaseCpuLock();
161     }
162 
163     private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() {
164         @Override
165         public void onReceive(Context context, Intent intent) {
166             final String action = intent.getAction();
167             LogUtils.i("AlarmService received intent %s", action);
168             if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) {
169                 LogUtils.i("No valid firing alarm");
170                 return;
171             }
172 
173             if (mIsBound) {
174                 LogUtils.i("AlarmActivity bound; AlarmService no-op");
175                 return;
176             }
177 
178             switch (action) {
179                 case ALARM_SNOOZE_ACTION:
180                     // Set the alarm state to snoozed.
181                     // If this broadcast receiver is handling the snooze intent then AlarmActivity
182                     // must not be showing, so always show snooze toast.
183                     AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */);
184                     Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
185                     break;
186                 case ALARM_DISMISS_ACTION:
187                     // Set the alarm state to dismissed.
188                     AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm);
189                     Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
190                     break;
191             }
192         }
193     };
194 
195     @Override
onCreate()196     public void onCreate() {
197         super.onCreate();
198         mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
199 
200         // Register the broadcast receiver
201         final IntentFilter filter = new IntentFilter(ALARM_SNOOZE_ACTION);
202         filter.addAction(ALARM_DISMISS_ACTION);
203         registerReceiver(mActionsReceiver, filter);
204         mIsRegistered = true;
205     }
206 
207     @Override
onStartCommand(Intent intent, int flags, int startId)208     public int onStartCommand(Intent intent, int flags, int startId) {
209         LogUtils.v("AlarmService.onStartCommand() with %s", intent);
210 
211         final long instanceId = AlarmInstance.getId(intent.getData());
212         switch (intent.getAction()) {
213             case AlarmStateManager.CHANGE_STATE_ACTION:
214                 AlarmStateManager.handleIntent(this, intent);
215 
216                 // If state is changed to firing, actually fire the alarm!
217                 final int alarmState = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1);
218                 if (alarmState == AlarmInstance.FIRED_STATE) {
219                     final ContentResolver cr = this.getContentResolver();
220                     final AlarmInstance instance = AlarmInstance.getInstance(cr, instanceId);
221                     if (instance == null) {
222                         LogUtils.e("No instance found to start alarm: %d", instanceId);
223                         if (mCurrentAlarm != null) {
224                             // Only release lock if we are not firing alarm
225                             AlarmAlertWakeLock.releaseCpuLock();
226                         }
227                         break;
228                     }
229 
230                     if (mCurrentAlarm != null && mCurrentAlarm.mId == instanceId) {
231                         LogUtils.e("Alarm already started for instance: %d", instanceId);
232                         break;
233                     }
234                     startAlarm(instance);
235                 }
236                 break;
237             case STOP_ALARM_ACTION:
238                 if (mCurrentAlarm != null && mCurrentAlarm.mId != instanceId) {
239                     LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d",
240                             instanceId, mCurrentAlarm.mId);
241                     break;
242                 }
243                 stopCurrentAlarm();
244                 stopSelf();
245         }
246 
247         return Service.START_NOT_STICKY;
248     }
249 
250     @Override
onDestroy()251     public void onDestroy() {
252         LogUtils.v("AlarmService.onDestroy() called");
253         super.onDestroy();
254         if (mCurrentAlarm != null) {
255             stopCurrentAlarm();
256         }
257 
258         if (mIsRegistered) {
259             unregisterReceiver(mActionsReceiver);
260             mIsRegistered = false;
261         }
262     }
263 }
264