1/*
2 * Copyright (C) 2008 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.Service;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.AssetFileDescriptor;
23import android.content.res.Resources;
24import android.media.AudioManager;
25import android.media.MediaPlayer;
26import android.media.MediaPlayer.OnErrorListener;
27import android.media.RingtoneManager;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.IBinder;
32import android.os.Message;
33import android.os.Vibrator;
34import android.preference.PreferenceManager;
35import android.telephony.PhoneStateListener;
36import android.telephony.TelephonyManager;
37
38/**
39 * Manages alarms and vibe. Runs as a service so that it can continue to play
40 * if another activity overrides the AlarmAlert dialog.
41 */
42public class AlarmKlaxon extends Service {
43    // Default of 10 minutes until alarm is silenced.
44    private static final String DEFAULT_ALARM_TIMEOUT = "10";
45
46    private static final long[] sVibratePattern = new long[] { 500, 500 };
47
48    private boolean mPlaying = false;
49    private Vibrator mVibrator;
50    private MediaPlayer mMediaPlayer;
51    private Alarm mCurrentAlarm;
52    private long mStartTime;
53    private TelephonyManager mTelephonyManager;
54    private int mInitialCallState;
55
56    // Internal messages
57    private static final int KILLER = 1000;
58    private Handler mHandler = new Handler() {
59        public void handleMessage(Message msg) {
60            switch (msg.what) {
61                case KILLER:
62                    if (Log.LOGV) {
63                        Log.v("*********** Alarm killer triggered ***********");
64                    }
65                    sendKillBroadcast((Alarm) msg.obj);
66                    stopSelf();
67                    break;
68            }
69        }
70    };
71
72    private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
73        @Override
74        public void onCallStateChanged(int state, String ignored) {
75            // The user might already be in a call when the alarm fires. When
76            // we register onCallStateChanged, we get the initial in-call state
77            // which kills the alarm. Check against the initial call state so
78            // we don't kill the alarm during a call.
79            if (state != TelephonyManager.CALL_STATE_IDLE
80                    && state != mInitialCallState) {
81                sendKillBroadcast(mCurrentAlarm);
82                stopSelf();
83            }
84        }
85    };
86
87    @Override
88    public void onCreate() {
89        mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
90        // Listen for incoming calls to kill the alarm.
91        mTelephonyManager =
92                (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
93        mTelephonyManager.listen(
94                mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
95        AlarmAlertWakeLock.acquireCpuWakeLock(this);
96    }
97
98    @Override
99    public void onDestroy() {
100        stop();
101        // Stop listening for incoming calls.
102        mTelephonyManager.listen(mPhoneStateListener, 0);
103        AlarmAlertWakeLock.releaseCpuLock();
104    }
105
106    @Override
107    public IBinder onBind(Intent intent) {
108        return null;
109    }
110
111    @Override
112    public int onStartCommand(Intent intent, int flags, int startId) {
113        // No intent, tell the system not to restart us.
114        if (intent == null) {
115            stopSelf();
116            return START_NOT_STICKY;
117        }
118
119        final Alarm alarm = intent.getParcelableExtra(
120                Alarms.ALARM_INTENT_EXTRA);
121
122        if (alarm == null) {
123            Log.v("AlarmKlaxon failed to parse the alarm from the intent");
124            stopSelf();
125            return START_NOT_STICKY;
126        }
127
128        if (mCurrentAlarm != null) {
129            sendKillBroadcast(mCurrentAlarm);
130        }
131
132        play(alarm);
133        mCurrentAlarm = alarm;
134        // Record the initial call state here so that the new alarm has the
135        // newest state.
136        mInitialCallState = mTelephonyManager.getCallState();
137
138        return START_STICKY;
139    }
140
141    private void sendKillBroadcast(Alarm alarm) {
142        long millis = System.currentTimeMillis() - mStartTime;
143        int minutes = (int) Math.round(millis / 60000.0);
144        Intent alarmKilled = new Intent(Alarms.ALARM_KILLED);
145        alarmKilled.putExtra(Alarms.ALARM_INTENT_EXTRA, alarm);
146        alarmKilled.putExtra(Alarms.ALARM_KILLED_TIMEOUT, minutes);
147        sendBroadcast(alarmKilled);
148    }
149
150    // Volume suggested by media team for in-call alarms.
151    private static final float IN_CALL_VOLUME = 0.125f;
152
153    private void play(Alarm alarm) {
154        // stop() checks to see if we are already playing.
155        stop();
156
157        if (Log.LOGV) {
158            Log.v("AlarmKlaxon.play() " + alarm.id + " alert " + alarm.alert);
159        }
160
161        if (!alarm.silent) {
162            Uri alert = alarm.alert;
163            // Fall back on the default alarm if the database does not have an
164            // alarm stored.
165            if (alert == null) {
166                alert = RingtoneManager.getDefaultUri(
167                        RingtoneManager.TYPE_ALARM);
168                if (Log.LOGV) {
169                    Log.v("Using default alarm: " + alert.toString());
170                }
171            }
172
173            // TODO: Reuse mMediaPlayer instead of creating a new one and/or use
174            // RingtoneManager.
175            mMediaPlayer = new MediaPlayer();
176            mMediaPlayer.setOnErrorListener(new OnErrorListener() {
177                public boolean onError(MediaPlayer mp, int what, int extra) {
178                    Log.e("Error occurred while playing audio.");
179                    mp.stop();
180                    mp.release();
181                    mMediaPlayer = null;
182                    return true;
183                }
184            });
185
186            try {
187                // Check if we are in a call. If we are, use the in-call alarm
188                // resource at a low volume to not disrupt the call.
189                if (mTelephonyManager.getCallState()
190                        != TelephonyManager.CALL_STATE_IDLE) {
191                    Log.v("Using the in-call alarm");
192                    mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
193                    setDataSourceFromResource(getResources(), mMediaPlayer,
194                            R.raw.in_call_alarm);
195                } else {
196                    mMediaPlayer.setDataSource(this, alert);
197                }
198                startAlarm(mMediaPlayer);
199            } catch (Exception ex) {
200                Log.v("Using the fallback ringtone");
201                // The alert may be on the sd card which could be busy right
202                // now. Use the fallback ringtone.
203                try {
204                    // Must reset the media player to clear the error state.
205                    mMediaPlayer.reset();
206                    setDataSourceFromResource(getResources(), mMediaPlayer,
207                            R.raw.fallbackring);
208                    startAlarm(mMediaPlayer);
209                } catch (Exception ex2) {
210                    // At this point we just don't play anything.
211                    Log.e("Failed to play fallback ringtone", ex2);
212                }
213            }
214        }
215
216        /* Start the vibrator after everything is ok with the media player */
217        if (alarm.vibrate) {
218            mVibrator.vibrate(sVibratePattern, 0);
219        } else {
220            mVibrator.cancel();
221        }
222
223        enableKiller(alarm);
224        mPlaying = true;
225        mStartTime = System.currentTimeMillis();
226    }
227
228    // Do the common stuff when starting the alarm.
229    private void startAlarm(MediaPlayer player)
230            throws java.io.IOException, IllegalArgumentException,
231                   IllegalStateException {
232        final AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
233        // do not play alarms if stream volume is 0
234        // (typically because ringer mode is silent).
235        if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
236            player.setAudioStreamType(AudioManager.STREAM_ALARM);
237            player.setLooping(true);
238            player.prepare();
239            player.start();
240        }
241    }
242
243    private void setDataSourceFromResource(Resources resources,
244            MediaPlayer player, int res) throws java.io.IOException {
245        AssetFileDescriptor afd = resources.openRawResourceFd(res);
246        if (afd != null) {
247            player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
248                    afd.getLength());
249            afd.close();
250        }
251    }
252
253    /**
254     * Stops alarm audio and disables alarm if it not snoozed and not
255     * repeating
256     */
257    public void stop() {
258        if (Log.LOGV) Log.v("AlarmKlaxon.stop()");
259        if (mPlaying) {
260            mPlaying = false;
261
262            Intent alarmDone = new Intent(Alarms.ALARM_DONE_ACTION);
263            sendBroadcast(alarmDone);
264
265            // Stop audio playing
266            if (mMediaPlayer != null) {
267                mMediaPlayer.stop();
268                mMediaPlayer.release();
269                mMediaPlayer = null;
270            }
271
272            // Stop vibrator
273            mVibrator.cancel();
274        }
275        disableKiller();
276    }
277
278    /**
279     * Kills alarm audio after ALARM_TIMEOUT_SECONDS, so the alarm
280     * won't run all day.
281     *
282     * This just cancels the audio, but leaves the notification
283     * popped, so the user will know that the alarm tripped.
284     */
285    private void enableKiller(Alarm alarm) {
286        final String autoSnooze =
287                PreferenceManager.getDefaultSharedPreferences(this)
288                .getString(SettingsActivity.KEY_AUTO_SILENCE,
289                        DEFAULT_ALARM_TIMEOUT);
290        int autoSnoozeMinutes = Integer.parseInt(autoSnooze);
291        if (autoSnoozeMinutes != -1) {
292            mHandler.sendMessageDelayed(mHandler.obtainMessage(KILLER, alarm),
293                    1000 * autoSnoozeMinutes * 60);
294        }
295    }
296
297    private void disableKiller() {
298        mHandler.removeMessages(KILLER);
299    }
300
301
302}
303