package com.android.alarmclock;
-import android.content.ContentResolver;
+import android.app.Service;
import android.content.Context;
+import android.content.Intent;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnErrorListener;
+import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
import android.os.Vibrator;
+import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
/**
- * Manages alarms and vibe. Singleton, so it can be initiated in
- * AlarmReceiver and shut down in the AlarmAlert activity
+ * Manages alarms and vibe. Runs as a service so that it can continue to play
+ * if another activity overrides the AlarmAlert dialog.
*/
-class AlarmKlaxon implements Alarms.AlarmSettings {
-
- interface KillerCallback {
- public void onKilled();
- }
+public class AlarmKlaxon extends Service {
/** Play alarm up to 10 minutes before silencing */
- final static int ALARM_TIMEOUT_SECONDS = 10 * 60;
-
- private static long[] sVibratePattern = new long[] { 500, 500 };
+ private static final int ALARM_TIMEOUT_SECONDS = 10 * 60;
- private static AlarmKlaxon sInstance;
-
- private int mAlarmId;
- private String mAlert;
- private Alarms.DaysOfWeek mDaysOfWeek;
- private boolean mVibrate;
+ private static final long[] sVibratePattern = new long[] { 500, 500 };
private boolean mPlaying = false;
-
private Vibrator mVibrator;
private MediaPlayer mMediaPlayer;
+ private Alarm mCurrentAlarm;
+ private long mStartTime;
+ private TelephonyManager mTelephonyManager;
+ private int mInitialCallState;
+
+ // Internal messages
+ private static final int KILLER = 1000;
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case KILLER:
+ if (Log.LOGV) {
+ Log.v("*********** Alarm killer triggered ***********");
+ }
+ sendKillBroadcast((Alarm) msg.obj);
+ stopSelf();
+ break;
+ }
+ }
+ };
+
+ private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+ @Override
+ public void onCallStateChanged(int state, String ignored) {
+ // The user might already be in a call when the alarm fires. When
+ // we register onCallStateChanged, we get the initial in-call state
+ // which kills the alarm. Check against the initial call state so
+ // we don't kill the alarm during a call.
+ if (state != TelephonyManager.CALL_STATE_IDLE
+ && state != mInitialCallState) {
+ sendKillBroadcast(mCurrentAlarm);
+ stopSelf();
+ }
+ }
+ };
- private Handler mTimeout;
- private KillerCallback mKillerCallback;
+ @Override
+ public void onCreate() {
+ mVibrator = new Vibrator();
+ // Listen for incoming calls to kill the alarm.
+ mTelephonyManager =
+ (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+ mTelephonyManager.listen(
+ mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+ AlarmAlertWakeLock.acquireCpuWakeLock(this);
+ }
+ @Override
+ public void onDestroy() {
+ stop();
+ // Stop listening for incoming calls.
+ mTelephonyManager.listen(mPhoneStateListener, 0);
+ AlarmAlertWakeLock.releaseCpuLock();
+ }
- static synchronized AlarmKlaxon getInstance() {
- if (sInstance == null) sInstance = new AlarmKlaxon();
- return sInstance;
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
}
- private AlarmKlaxon() {
- mVibrator = new Vibrator();
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // No intent, tell the system not to restart us.
+ if (intent == null) {
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ final Alarm alarm = intent.getParcelableExtra(
+ Alarms.ALARM_INTENT_EXTRA);
+
+ if (alarm == null) {
+ Log.v("AlarmKlaxon failed to parse the alarm from the intent");
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ if (mCurrentAlarm != null) {
+ sendKillBroadcast(mCurrentAlarm);
+ }
+
+ play(alarm);
+ mCurrentAlarm = alarm;
+ // Record the initial call state here so that the new alarm has the
+ // newest state.
+ mInitialCallState = mTelephonyManager.getCallState();
+
+ return START_STICKY;
}
- public void reportAlarm(
- int idx, boolean enabled, int hour, int minutes,
- Alarms.DaysOfWeek daysOfWeek, boolean vibrate, String message,
- String alert) {
- if (Log.LOGV) Log.v("AlarmKlaxon.reportAlarm: " + idx + " " + hour +
- " " + minutes + " dow " + daysOfWeek);
- mAlert = alert;
- mDaysOfWeek = daysOfWeek;
- mVibrate = vibrate;
+ private void sendKillBroadcast(Alarm alarm) {
+ long millis = System.currentTimeMillis() - mStartTime;
+ int minutes = (int) Math.round(millis / 60000.0);
+ Intent alarmKilled = new Intent(Alarms.ALARM_KILLED);
+ alarmKilled.putExtra(Alarms.ALARM_INTENT_EXTRA, alarm);
+ alarmKilled.putExtra(Alarms.ALARM_KILLED_TIMEOUT, minutes);
+ sendBroadcast(alarmKilled);
}
- // Volume suggested by media team for in-call alarms.
+ // Volume suggested by media team for in-call alarms.
private static final float IN_CALL_VOLUME = 0.125f;
- synchronized void play(Context context, int alarmId) {
- ContentResolver contentResolver = context.getContentResolver();
-
- if (mPlaying) stop(context, false);
+ private void play(Alarm alarm) {
+ // stop() checks to see if we are already playing.
+ stop();
- mAlarmId = alarmId;
+ if (Log.LOGV) {
+ Log.v("AlarmKlaxon.play() " + alarm.id + " alert " + alarm.alert);
+ }
- /* this will call reportAlarm() callback */
- Alarms.getAlarm(contentResolver, this, mAlarmId);
+ if (!alarm.silent) {
+ Uri alert = alarm.alert;
+ // Fall back on the default alarm if the database does not have an
+ // alarm stored.
+ if (alert == null) {
+ alert = RingtoneManager.getDefaultUri(
+ RingtoneManager.TYPE_ALARM);
+ if (Log.LOGV) {
+ Log.v("Using default alarm: " + alert.toString());
+ }
+ }
- if (Log.LOGV) Log.v("AlarmKlaxon.play() " + mAlarmId + " alert " + mAlert);
+ // TODO: Reuse mMediaPlayer instead of creating a new one and/or use
+ // RingtoneManager.
+ mMediaPlayer = new MediaPlayer();
+ mMediaPlayer.setOnErrorListener(new OnErrorListener() {
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ Log.e("Error occurred while playing audio.");
+ mp.stop();
+ mp.release();
+ mMediaPlayer = null;
+ return true;
+ }
+ });
- // TODO: Reuse mMediaPlayer instead of creating a new one and/or use
- // RingtoneManager.
- mMediaPlayer = new MediaPlayer();
- mMediaPlayer.setOnErrorListener(new OnErrorListener() {
- public boolean onError(MediaPlayer mp, int what, int extra) {
- Log.e("Error occurred while playing audio.");
- mp.stop();
- mp.release();
- mMediaPlayer = null;
- return true;
- }
- });
-
- try {
- TelephonyManager tm = (TelephonyManager) context.getSystemService(
- Context.TELEPHONY_SERVICE);
- // Check if we are in a call. If we are, use the in-call alarm
- // resource at a low volume to not disrupt the call.
- if (tm.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
- Log.v("Using the in-call alarm");
- mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
- setDataSourceFromResource(context.getResources(),
- mMediaPlayer, R.raw.in_call_alarm);
- } else {
- mMediaPlayer.setDataSource(context, Uri.parse(mAlert));
- }
- } catch (Exception ex) {
- Log.v("Using the fallback ringtone");
- // The alert may be on the sd card which could be busy right now.
- // Use the fallback ringtone.
try {
- setDataSourceFromResource(context.getResources(), mMediaPlayer,
- com.android.internal.R.raw.fallbackring);
- } catch (Exception ex2) {
- // At this point we just don't play anything.
- Log.e("Failed to play fallback ringtone", ex2);
+ // Check if we are in a call. If we are, use the in-call alarm
+ // resource at a low volume to not disrupt the call.
+ if (mTelephonyManager.getCallState()
+ != TelephonyManager.CALL_STATE_IDLE) {
+ Log.v("Using the in-call alarm");
+ mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
+ setDataSourceFromResource(getResources(), mMediaPlayer,
+ R.raw.in_call_alarm);
+ } else {
+ mMediaPlayer.setDataSource(this, alert);
+ }
+ startAlarm(mMediaPlayer);
+ } catch (Exception ex) {
+ Log.v("Using the fallback ringtone");
+ // The alert may be on the sd card which could be busy right
+ // now. Use the fallback ringtone.
+ try {
+ // Must reset the media player to clear the error state.
+ mMediaPlayer.reset();
+ setDataSourceFromResource(getResources(), mMediaPlayer,
+ com.android.internal.R.raw.fallbackring);
+ startAlarm(mMediaPlayer);
+ } catch (Exception ex2) {
+ // At this point we just don't play anything.
+ Log.e("Failed to play fallback ringtone", ex2);
+ }
}
}
- /* Now try to play the alert. */
- try {
- mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
- mMediaPlayer.setLooping(true);
- mMediaPlayer.prepare();
- mMediaPlayer.start();
- } catch (Exception ex) {
- Log.e("Error playing alarm: " + mAlert, ex);
- }
/* Start the vibrator after everything is ok with the media player */
- if (mVibrate) {
+ if (alarm.vibrate) {
mVibrator.vibrate(sVibratePattern, 0);
} else {
mVibrator.cancel();
}
- enableKiller();
+ enableKiller(alarm);
mPlaying = true;
+ mStartTime = System.currentTimeMillis();
+ }
+
+ // Do the common stuff when starting the alarm.
+ private void startAlarm(MediaPlayer player)
+ throws java.io.IOException, IllegalArgumentException,
+ IllegalStateException {
+ final AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
+ // do not play alarms if stream volume is 0
+ // (typically because ringer mode is silent).
+ if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
+ player.setAudioStreamType(AudioManager.STREAM_ALARM);
+ player.setLooping(true);
+ player.prepare();
+ player.start();
+ }
}
private void setDataSourceFromResource(Resources resources,
* Stops alarm audio and disables alarm if it not snoozed and not
* repeating
*/
- synchronized void stop(Context context, boolean snoozed) {
- if (Log.LOGV) Log.v("AlarmKlaxon.stop() " + mAlarmId);
+ public void stop() {
+ if (Log.LOGV) Log.v("AlarmKlaxon.stop()");
if (mPlaying) {
mPlaying = false;
// Stop vibrator
mVibrator.cancel();
-
- /* disable alarm only if it is not set to repeat */
- if (!snoozed && ((mDaysOfWeek == null || !mDaysOfWeek.isRepeatSet()))) {
- Alarms.enableAlarm(context, mAlarmId, false);
- }
}
disableKiller();
}
/**
- * This callback called when alarm killer times out unattended
- * alarm
- */
- void setKillerCallback(KillerCallback killerCallback) {
- mKillerCallback = killerCallback;
- }
-
- /**
* Kills alarm audio after ALARM_TIMEOUT_SECONDS, so the alarm
* won't run all day.
*
* This just cancels the audio, but leaves the notification
* popped, so the user will know that the alarm tripped.
*/
- private void enableKiller() {
- mTimeout = new Handler();
- mTimeout.postDelayed(new Runnable() {
- public void run() {
- if (Log.LOGV) Log.v("*********** Alarm killer triggered *************");
- if (mKillerCallback != null) mKillerCallback.onKilled();
- }
- }, 1000 * ALARM_TIMEOUT_SECONDS);
+ private void enableKiller(Alarm alarm) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(KILLER, alarm),
+ 1000 * ALARM_TIMEOUT_SECONDS);
}
private void disableKiller() {
- if (mTimeout != null) {
- mTimeout.removeCallbacksAndMessages(null);
- mTimeout = null;
- }
+ mHandler.removeMessages(KILLER);
}