/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.cellbroadcastreceiver; import android.app.PendingIntent; 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.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Vibrator; import android.speech.tts.TextToSpeech; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import java.util.HashMap; import java.util.Locale; import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; /** * Manages alert audio and vibration and text-to-speech. Runs as a service so that * it can continue to play if another activity overrides the CellBroadcastListActivity. */ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnInitListener, TextToSpeech.OnUtteranceCompletedListener { private static final String TAG = "CellBroadcastAlertAudio"; /** Action to start playing alert audio/vibration/speech. */ static final String ACTION_START_ALERT_AUDIO = "ACTION_START_ALERT_AUDIO"; /** Extra for alert audio duration (from settings). */ public static final String ALERT_AUDIO_DURATION_EXTRA = "com.android.cellbroadcastreceiver.ALERT_AUDIO_DURATION"; /** Extra for message body to speak (if speech enabled in settings). */ public static final String ALERT_AUDIO_MESSAGE_BODY = "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_BODY"; /** Extra for text-to-speech language (if speech enabled in settings). */ public static final String ALERT_AUDIO_MESSAGE_LANGUAGE = "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_LANGUAGE"; /** Extra for alert audio vibration enabled (from settings). */ public static final String ALERT_AUDIO_VIBRATE_EXTRA = "com.android.cellbroadcastreceiver.ALERT_AUDIO_VIBRATE"; /** Extra for alert audio ETWS behavior (always vibrate, even in silent mode). */ public static final String ALERT_AUDIO_ETWS_VIBRATE_EXTRA = "com.android.cellbroadcastreceiver.ALERT_AUDIO_ETWS_VIBRATE"; private static final String TTS_UTTERANCE_ID = "com.android.cellbroadcastreceiver.UTTERANCE_ID"; /** Pause duration between alert sound and alert speech. */ private static final int PAUSE_DURATION_BEFORE_SPEAKING_MSEC = 1000; /** Vibration uses the same on/off pattern as the CMAS alert tone */ private static final long[] sVibratePattern = { 0, 2000, 500, 1000, 500, 1000, 500, 2000, 500, 1000, 500, 1000}; private static final int STATE_IDLE = 0; private static final int STATE_ALERTING = 1; private static final int STATE_PAUSING = 2; private static final int STATE_SPEAKING = 3; private int mState; private TextToSpeech mTts; private boolean mTtsEngineReady; private String mMessageBody; private String mMessageLanguage; private boolean mTtsLanguageSupported; private boolean mEnableVibrate; private boolean mEnableAudio; private Vibrator mVibrator; private MediaPlayer mMediaPlayer; private AudioManager mAudioManager; private TelephonyManager mTelephonyManager; private int mInitialCallState; private PendingIntent mPlayReminderIntent; // Internal messages private static final int ALERT_SOUND_FINISHED = 1000; private static final int ALERT_PAUSE_FINISHED = 1001; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case ALERT_SOUND_FINISHED: if (DBG) log("ALERT_SOUND_FINISHED"); stop(); // stop alert sound // if we can speak the message text if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_PAUSE_FINISHED), PAUSE_DURATION_BEFORE_SPEAKING_MSEC); mState = STATE_PAUSING; } else { if (DBG) log("MessageEmpty = " + (mMessageBody == null) + ", mTtsEngineReady = " + mTtsEngineReady + ", mTtsLanguageSupported = " + mTtsLanguageSupported); stopSelf(); mState = STATE_IDLE; } break; case ALERT_PAUSE_FINISHED: if (DBG) log("ALERT_PAUSE_FINISHED"); int res = TextToSpeech.ERROR; if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { if (DBG) log("Speaking broadcast text: " + mMessageBody); HashMap ttsHashMap = new HashMap(); ttsHashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, TTS_UTTERANCE_ID); // Play TTS on notification stream. ttsHashMap.put(TextToSpeech.Engine.KEY_PARAM_STREAM, Integer.toString(AudioManager.STREAM_NOTIFICATION)); res = mTts.speak(mMessageBody, TextToSpeech.QUEUE_FLUSH, ttsHashMap); mState = STATE_SPEAKING; } if (res != TextToSpeech.SUCCESS) { loge("TTS engine not ready or language not supported or speak() failed"); stopSelf(); mState = STATE_IDLE; } break; default: loge("Handler received unknown message, what=" + msg.what); } } }; private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String ignored) { // Stop the alert sound and speech if the call state changes. if (state != TelephonyManager.CALL_STATE_IDLE && state != mInitialCallState) { stopSelf(); } } }; /** * Callback from TTS engine after initialization. * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. */ @Override public void onInit(int status) { if (DBG) log("onInit() TTS engine status: " + status); if (status == TextToSpeech.SUCCESS) { mTtsEngineReady = true; mTts.setOnUtteranceCompletedListener(this); // try to set the TTS language to match the broadcast setTtsLanguage(); } else { mTtsEngineReady = false; mTts = null; loge("onInit() TTS engine error: " + status); } } /** * Try to set the TTS engine language to the value of mMessageLanguage. * mTtsLanguageSupported will be updated based on the response. */ private void setTtsLanguage() { if (mMessageLanguage != null) { if (DBG) log("Setting TTS language to '" + mMessageLanguage + '\''); int result = mTts.setLanguage(new Locale(mMessageLanguage)); // success values are >= 0, failure returns negative value if (DBG) log("TTS setLanguage() returned: " + result); mTtsLanguageSupported = result >= 0; } else { // try to use the default TTS language for broadcasts with no language specified if (DBG) log("No language specified in broadcast: using default"); mTtsLanguageSupported = true; } } /** * Callback from TTS engine. * @param utteranceId the identifier of the utterance. */ @Override public void onUtteranceCompleted(String utteranceId) { if (utteranceId.equals(TTS_UTTERANCE_ID)) { // When we reach here, it could be TTS completed or TTS was cut due to another // new alert started playing. We don't want to stop the service in the later case. if (mState == STATE_SPEAKING) { stopSelf(); } } } @Override public void onCreate() { mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); // Listen for incoming calls to kill the alarm. mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); mTelephonyManager.listen( mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } @Override public void onDestroy() { // stop audio, vibration and TTS stop(); // Stop listening for incoming calls. mTelephonyManager.listen(mPhoneStateListener, 0); // shutdown TTS engine if (mTts != null) { try { mTts.shutdown(); } catch (IllegalStateException e) { // catch "Unable to retrieve AudioTrack pointer for stop()" exception loge("exception trying to shutdown text-to-speech"); } } if (mEnableAudio) { // Release the audio focus so other audio (e.g. music) can resume. // Do not do this in stop() because stop() is also called when we stop the tone (before // TTS is playing). We only want to release the focus when tone and TTS are played. mAudioManager.abandonAudioFocus(null); } // release CPU wake lock acquired by CellBroadcastAlertService CellBroadcastAlertWakeLock.releaseCpuLock(); } @Override public IBinder onBind(Intent intent) { return null; } @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; } // This extra should always be provided by CellBroadcastAlertService, // but default to 10.5 seconds just to be safe (CMAS requirement). int duration = intent.getIntExtra(ALERT_AUDIO_DURATION_EXTRA, 10500); if (DBG) log(TAG + "Recvd intent = " + intent + " hasExtras = " + intent.hasExtra(ALERT_AUDIO_DURATION_EXTRA) + duration); // Get text to speak (if enabled by user) mMessageBody = intent.getStringExtra(ALERT_AUDIO_MESSAGE_BODY); mMessageLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_LANGUAGE); mEnableVibrate = intent.getBooleanExtra(ALERT_AUDIO_VIBRATE_EXTRA, true); if (intent.getBooleanExtra(ALERT_AUDIO_ETWS_VIBRATE_EXTRA, false)) { mEnableVibrate = true; // force enable vibration for ETWS alerts } switch (mAudioManager.getRingerMode()) { case AudioManager.RINGER_MODE_SILENT: if (DBG) log("Ringer mode: silent"); mEnableAudio = false; mEnableVibrate = false; break; case AudioManager.RINGER_MODE_VIBRATE: if (DBG) log("Ringer mode: vibrate"); mEnableAudio = false; break; case AudioManager.RINGER_MODE_NORMAL: default: if (DBG) log("Ringer mode: normal"); mEnableAudio = true; break; } if (mMessageBody != null && mEnableAudio) { if (mTts == null) { mTts = new TextToSpeech(this, this); } else if (mTtsEngineReady) { setTtsLanguage(); } } if (mEnableAudio || mEnableVibrate) { play(duration); // in milliseconds } else { stopSelf(); return START_NOT_STICKY; } // Record the initial call state here so that the new alarm has the // newest state. mInitialCallState = mTelephonyManager.getCallState(); return START_STICKY; } // Volume suggested by media team for in-call alarms. private static final float IN_CALL_VOLUME = 0.125f; /** * Start playing the alert sound, and send delayed message when it's time to stop. * @param duration the alert sound duration in milliseconds */ private void play(int duration) { // stop() checks to see if we are already playing. stop(); if (DBG) log("play()"); // Start the vibration first, can be set to 0 = "Till Alert Dialog Dismissed" if (mEnableVibrate) { mVibrator.vibrate(sVibratePattern, (duration!=0 ? -1 : 0)); } if (mEnableAudio) { // future optimization: reuse media player object mMediaPlayer = new MediaPlayer(); mMediaPlayer.setOnErrorListener(new OnErrorListener() { public boolean onError(MediaPlayer mp, int what, int extra) { loge("Error occurred while playing audio."); mp.stop(); mp.release(); mMediaPlayer = null; return true; } }); try { // Check if we are in a call. If we are, play the alert // sound at a low volume to not disrupt the call. if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) { log("in call: reducing volume"); mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); } // start playing alert audio (unless master volume is vibrate only or silent). setDataSourceFromResource(getResources(), mMediaPlayer, R.raw.attention_signal); mAudioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); startAlarm(mMediaPlayer); } catch (Exception ex) { loge("Failed to play alert sound: " + ex); } } // stop alert after the specified duration, can be set to 0 = "Till Alert Dialog Dismissed" if (DBG) log(TAG + " play() alert duration = " + duration); if(duration!=0) mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED), duration); mState = STATE_ALERTING; } // Do the common stuff when starting the alarm. private static void startAlarm(MediaPlayer player) throws java.io.IOException, IllegalArgumentException, IllegalStateException { player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); player.setLooping(true); player.prepare(); player.start(); } private static void setDataSourceFromResource(Resources resources, MediaPlayer player, int res) throws java.io.IOException { AssetFileDescriptor afd = resources.openRawResourceFd(res); if (afd != null) { player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); afd.close(); } } private void playAlertReminderSound() { Uri notificationUri = RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION | RingtoneManager.TYPE_ALARM); if (notificationUri == null) { loge("Can't get URI for alert reminder sound"); return; } Ringtone r = RingtoneManager.getRingtone(this, notificationUri); if (r != null) { log("playing alert reminder sound"); r.play(); } else { loge("can't get Ringtone for alert reminder sound"); } } /** * Stops alert audio and speech. */ public void stop() { if (DBG) log("stop() state = " + mState); if (mPlayReminderIntent != null) { mPlayReminderIntent.cancel(); mPlayReminderIntent = null; } mHandler.removeMessages(ALERT_SOUND_FINISHED); mHandler.removeMessages(ALERT_PAUSE_FINISHED); if (mState == STATE_ALERTING) { // Stop audio playing if (mMediaPlayer != null) { try { mMediaPlayer.stop(); mMediaPlayer.release(); } catch (IllegalStateException e) { // catch "Unable to retrieve AudioTrack pointer for stop()" exception loge("exception trying to stop media player"); } mMediaPlayer = null; } // Stop vibrator mVibrator.cancel(); } else if (mState == STATE_SPEAKING && mTts != null) { try { mTts.stop(); } catch (IllegalStateException e) { // catch "Unable to retrieve AudioTrack pointer for stop()" exception loge("exception trying to stop text-to-speech"); } // Stop vibrator mVibrator.cancel(); } mState = STATE_IDLE; } private static void log(String msg) { Log.d(TAG, msg); } private static void loge(String msg) { Log.e(TAG, msg); } }