1 /* 2 * Copyright (C) 2011 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 17 package com.android.cellbroadcastreceiver; 18 19 import android.app.PendingIntent; 20 import android.app.Service; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.res.AssetFileDescriptor; 24 import android.content.res.Resources; 25 import android.media.AudioManager; 26 import android.media.MediaPlayer; 27 import android.media.MediaPlayer.OnErrorListener; 28 import android.media.Ringtone; 29 import android.media.RingtoneManager; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.IBinder; 33 import android.os.Message; 34 import android.os.Vibrator; 35 import android.speech.tts.TextToSpeech; 36 import android.telephony.PhoneStateListener; 37 import android.telephony.TelephonyManager; 38 import android.util.Log; 39 40 import java.util.HashMap; 41 import java.util.Locale; 42 43 import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; 44 45 /** 46 * Manages alert audio and vibration and text-to-speech. Runs as a service so that 47 * it can continue to play if another activity overrides the CellBroadcastListActivity. 48 */ 49 public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnInitListener, 50 TextToSpeech.OnUtteranceCompletedListener { 51 private static final String TAG = "CellBroadcastAlertAudio"; 52 53 /** Action to start playing alert audio/vibration/speech. */ 54 static final String ACTION_START_ALERT_AUDIO = "ACTION_START_ALERT_AUDIO"; 55 56 /** Extra for alert audio duration (from settings). */ 57 public static final String ALERT_AUDIO_DURATION_EXTRA = 58 "com.android.cellbroadcastreceiver.ALERT_AUDIO_DURATION"; 59 60 /** Extra for message body to speak (if speech enabled in settings). */ 61 public static final String ALERT_AUDIO_MESSAGE_BODY = 62 "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_BODY"; 63 64 /** Extra for text-to-speech language (if speech enabled in settings). */ 65 public static final String ALERT_AUDIO_MESSAGE_LANGUAGE = 66 "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_LANGUAGE"; 67 68 /** Extra for alert audio vibration enabled (from settings). */ 69 public static final String ALERT_AUDIO_VIBRATE_EXTRA = 70 "com.android.cellbroadcastreceiver.ALERT_AUDIO_VIBRATE"; 71 72 /** Extra for alert audio ETWS behavior (always vibrate, even in silent mode). */ 73 public static final String ALERT_AUDIO_ETWS_VIBRATE_EXTRA = 74 "com.android.cellbroadcastreceiver.ALERT_AUDIO_ETWS_VIBRATE"; 75 76 private static final String TTS_UTTERANCE_ID = "com.android.cellbroadcastreceiver.UTTERANCE_ID"; 77 78 /** Pause duration between alert sound and alert speech. */ 79 private static final int PAUSE_DURATION_BEFORE_SPEAKING_MSEC = 1000; 80 81 /** Vibration uses the same on/off pattern as the CMAS alert tone */ 82 private static final long[] sVibratePattern = { 0, 2000, 500, 1000, 500, 1000, 500, 83 2000, 500, 1000, 500, 1000}; 84 85 private static final int STATE_IDLE = 0; 86 private static final int STATE_ALERTING = 1; 87 private static final int STATE_PAUSING = 2; 88 private static final int STATE_SPEAKING = 3; 89 90 private int mState; 91 92 private TextToSpeech mTts; 93 private boolean mTtsEngineReady; 94 95 private String mMessageBody; 96 private String mMessageLanguage; 97 private boolean mTtsLanguageSupported; 98 private boolean mEnableVibrate; 99 private boolean mEnableAudio; 100 101 private Vibrator mVibrator; 102 private MediaPlayer mMediaPlayer; 103 private AudioManager mAudioManager; 104 private TelephonyManager mTelephonyManager; 105 private int mInitialCallState; 106 107 private PendingIntent mPlayReminderIntent; 108 109 // Internal messages 110 private static final int ALERT_SOUND_FINISHED = 1000; 111 private static final int ALERT_PAUSE_FINISHED = 1001; 112 private final Handler mHandler = new Handler() { 113 @Override 114 public void handleMessage(Message msg) { 115 switch (msg.what) { 116 case ALERT_SOUND_FINISHED: 117 if (DBG) log("ALERT_SOUND_FINISHED"); 118 stop(); // stop alert sound 119 // if we can speak the message text 120 if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { 121 mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_PAUSE_FINISHED), 122 PAUSE_DURATION_BEFORE_SPEAKING_MSEC); 123 mState = STATE_PAUSING; 124 } else { 125 if (DBG) log("MessageEmpty = " + (mMessageBody == null) + 126 ", mTtsEngineReady = " + mTtsEngineReady + 127 ", mTtsLanguageSupported = " + mTtsLanguageSupported); 128 stopSelf(); 129 mState = STATE_IDLE; 130 } 131 break; 132 133 case ALERT_PAUSE_FINISHED: 134 if (DBG) log("ALERT_PAUSE_FINISHED"); 135 int res = TextToSpeech.ERROR; 136 if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { 137 if (DBG) log("Speaking broadcast text: " + mMessageBody); 138 HashMap<String, String> ttsHashMap = new HashMap<String, String>(); 139 ttsHashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, 140 TTS_UTTERANCE_ID); 141 // Play TTS on notification stream. 142 ttsHashMap.put(TextToSpeech.Engine.KEY_PARAM_STREAM, 143 Integer.toString(AudioManager.STREAM_NOTIFICATION)); 144 145 res = mTts.speak(mMessageBody, TextToSpeech.QUEUE_FLUSH, ttsHashMap); 146 mState = STATE_SPEAKING; 147 } 148 if (res != TextToSpeech.SUCCESS) { 149 loge("TTS engine not ready or language not supported or speak() failed"); 150 stopSelf(); 151 mState = STATE_IDLE; 152 } 153 break; 154 155 default: 156 loge("Handler received unknown message, what=" + msg.what); 157 } 158 } 159 }; 160 161 private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { 162 @Override 163 public void onCallStateChanged(int state, String ignored) { 164 // Stop the alert sound and speech if the call state changes. 165 if (state != TelephonyManager.CALL_STATE_IDLE 166 && state != mInitialCallState) { 167 stopSelf(); 168 } 169 } 170 }; 171 172 /** 173 * Callback from TTS engine after initialization. 174 * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. 175 */ 176 @Override onInit(int status)177 public void onInit(int status) { 178 if (DBG) log("onInit() TTS engine status: " + status); 179 if (status == TextToSpeech.SUCCESS) { 180 mTtsEngineReady = true; 181 mTts.setOnUtteranceCompletedListener(this); 182 // try to set the TTS language to match the broadcast 183 setTtsLanguage(); 184 } else { 185 mTtsEngineReady = false; 186 mTts = null; 187 loge("onInit() TTS engine error: " + status); 188 } 189 } 190 191 /** 192 * Try to set the TTS engine language to the value of mMessageLanguage. 193 * mTtsLanguageSupported will be updated based on the response. 194 */ setTtsLanguage()195 private void setTtsLanguage() { 196 if (mMessageLanguage != null) { 197 if (DBG) log("Setting TTS language to '" + mMessageLanguage + '\''); 198 int result = mTts.setLanguage(new Locale(mMessageLanguage)); 199 // success values are >= 0, failure returns negative value 200 if (DBG) log("TTS setLanguage() returned: " + result); 201 mTtsLanguageSupported = result >= 0; 202 } else { 203 // try to use the default TTS language for broadcasts with no language specified 204 if (DBG) log("No language specified in broadcast: using default"); 205 mTtsLanguageSupported = true; 206 } 207 } 208 209 /** 210 * Callback from TTS engine. 211 * @param utteranceId the identifier of the utterance. 212 */ 213 @Override onUtteranceCompleted(String utteranceId)214 public void onUtteranceCompleted(String utteranceId) { 215 if (utteranceId.equals(TTS_UTTERANCE_ID)) { 216 // When we reach here, it could be TTS completed or TTS was cut due to another 217 // new alert started playing. We don't want to stop the service in the later case. 218 if (mState == STATE_SPEAKING) { 219 stopSelf(); 220 } 221 } 222 } 223 224 @Override onCreate()225 public void onCreate() { 226 mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); 227 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 228 // Listen for incoming calls to kill the alarm. 229 mTelephonyManager = 230 (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 231 mTelephonyManager.listen( 232 mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 233 } 234 235 @Override onDestroy()236 public void onDestroy() { 237 // stop audio, vibration and TTS 238 stop(); 239 // Stop listening for incoming calls. 240 mTelephonyManager.listen(mPhoneStateListener, 0); 241 // shutdown TTS engine 242 if (mTts != null) { 243 try { 244 mTts.shutdown(); 245 } catch (IllegalStateException e) { 246 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 247 loge("exception trying to shutdown text-to-speech"); 248 } 249 } 250 251 if (mEnableAudio) { 252 // Release the audio focus so other audio (e.g. music) can resume. 253 // Do not do this in stop() because stop() is also called when we stop the tone (before 254 // TTS is playing). We only want to release the focus when tone and TTS are played. 255 mAudioManager.abandonAudioFocus(null); 256 } 257 258 // release CPU wake lock acquired by CellBroadcastAlertService 259 CellBroadcastAlertWakeLock.releaseCpuLock(); 260 } 261 262 @Override onBind(Intent intent)263 public IBinder onBind(Intent intent) { 264 return null; 265 } 266 267 @Override onStartCommand(Intent intent, int flags, int startId)268 public int onStartCommand(Intent intent, int flags, int startId) { 269 // No intent, tell the system not to restart us. 270 if (intent == null) { 271 stopSelf(); 272 return START_NOT_STICKY; 273 } 274 275 // This extra should always be provided by CellBroadcastAlertService, 276 // but default to 10.5 seconds just to be safe (CMAS requirement). 277 int duration = intent.getIntExtra(ALERT_AUDIO_DURATION_EXTRA, 10500); 278 279 // Get text to speak (if enabled by user) 280 mMessageBody = intent.getStringExtra(ALERT_AUDIO_MESSAGE_BODY); 281 mMessageLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_LANGUAGE); 282 283 mEnableVibrate = intent.getBooleanExtra(ALERT_AUDIO_VIBRATE_EXTRA, true); 284 if (intent.getBooleanExtra(ALERT_AUDIO_ETWS_VIBRATE_EXTRA, false)) { 285 mEnableVibrate = true; // force enable vibration for ETWS alerts 286 } 287 288 switch (mAudioManager.getRingerMode()) { 289 case AudioManager.RINGER_MODE_SILENT: 290 if (DBG) log("Ringer mode: silent"); 291 mEnableAudio = false; 292 mEnableVibrate = false; 293 break; 294 295 case AudioManager.RINGER_MODE_VIBRATE: 296 if (DBG) log("Ringer mode: vibrate"); 297 mEnableAudio = false; 298 break; 299 300 case AudioManager.RINGER_MODE_NORMAL: 301 default: 302 if (DBG) log("Ringer mode: normal"); 303 mEnableAudio = true; 304 break; 305 } 306 307 if (mMessageBody != null && mEnableAudio) { 308 if (mTts == null) { 309 mTts = new TextToSpeech(this, this); 310 } else if (mTtsEngineReady) { 311 setTtsLanguage(); 312 } 313 } 314 315 if (mEnableAudio || mEnableVibrate) { 316 play(duration); // in milliseconds 317 } else { 318 stopSelf(); 319 return START_NOT_STICKY; 320 } 321 322 // Record the initial call state here so that the new alarm has the 323 // newest state. 324 mInitialCallState = mTelephonyManager.getCallState(); 325 326 return START_STICKY; 327 } 328 329 // Volume suggested by media team for in-call alarms. 330 private static final float IN_CALL_VOLUME = 0.125f; 331 332 /** 333 * Start playing the alert sound, and send delayed message when it's time to stop. 334 * @param duration the alert sound duration in milliseconds 335 */ play(int duration)336 private void play(int duration) { 337 // stop() checks to see if we are already playing. 338 stop(); 339 340 if (DBG) log("play()"); 341 342 // Start the vibration first. 343 if (mEnableVibrate) { 344 mVibrator.vibrate(sVibratePattern, -1); 345 } 346 347 if (mEnableAudio) { 348 // future optimization: reuse media player object 349 mMediaPlayer = new MediaPlayer(); 350 mMediaPlayer.setOnErrorListener(new OnErrorListener() { 351 public boolean onError(MediaPlayer mp, int what, int extra) { 352 loge("Error occurred while playing audio."); 353 mp.stop(); 354 mp.release(); 355 mMediaPlayer = null; 356 return true; 357 } 358 }); 359 360 try { 361 // Check if we are in a call. If we are, play the alert 362 // sound at a low volume to not disrupt the call. 363 if (mTelephonyManager.getCallState() 364 != TelephonyManager.CALL_STATE_IDLE) { 365 log("in call: reducing volume"); 366 mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); 367 } 368 369 // start playing alert audio (unless master volume is vibrate only or silent). 370 setDataSourceFromResource(getResources(), mMediaPlayer, 371 R.raw.attention_signal); 372 mAudioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION, 373 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 374 startAlarm(mMediaPlayer); 375 } catch (Exception ex) { 376 loge("Failed to play alert sound: " + ex); 377 } 378 } 379 380 // stop alert after the specified duration 381 mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED), duration); 382 mState = STATE_ALERTING; 383 } 384 385 // Do the common stuff when starting the alarm. startAlarm(MediaPlayer player)386 private static void startAlarm(MediaPlayer player) 387 throws java.io.IOException, IllegalArgumentException, IllegalStateException { 388 player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); 389 player.setLooping(true); 390 player.prepare(); 391 player.start(); 392 } 393 setDataSourceFromResource(Resources resources, MediaPlayer player, int res)394 private static void setDataSourceFromResource(Resources resources, 395 MediaPlayer player, int res) throws java.io.IOException { 396 AssetFileDescriptor afd = resources.openRawResourceFd(res); 397 if (afd != null) { 398 player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), 399 afd.getLength()); 400 afd.close(); 401 } 402 } 403 playAlertReminderSound()404 private void playAlertReminderSound() { 405 Uri notificationUri = RingtoneManager.getDefaultUri( 406 RingtoneManager.TYPE_NOTIFICATION | RingtoneManager.TYPE_ALARM); 407 if (notificationUri == null) { 408 loge("Can't get URI for alert reminder sound"); 409 return; 410 } 411 Ringtone r = RingtoneManager.getRingtone(this, notificationUri); 412 if (r != null) { 413 log("playing alert reminder sound"); 414 r.play(); 415 } else { 416 loge("can't get Ringtone for alert reminder sound"); 417 } 418 } 419 420 /** 421 * Stops alert audio and speech. 422 */ stop()423 public void stop() { 424 if (DBG) log("stop()"); 425 426 if (mPlayReminderIntent != null) { 427 mPlayReminderIntent.cancel(); 428 mPlayReminderIntent = null; 429 } 430 431 mHandler.removeMessages(ALERT_SOUND_FINISHED); 432 mHandler.removeMessages(ALERT_PAUSE_FINISHED); 433 434 if (mState == STATE_ALERTING) { 435 // Stop audio playing 436 if (mMediaPlayer != null) { 437 try { 438 mMediaPlayer.stop(); 439 mMediaPlayer.release(); 440 } catch (IllegalStateException e) { 441 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 442 loge("exception trying to stop media player"); 443 } 444 mMediaPlayer = null; 445 } 446 447 // Stop vibrator 448 mVibrator.cancel(); 449 } else if (mState == STATE_SPEAKING && mTts != null) { 450 try { 451 mTts.stop(); 452 } catch (IllegalStateException e) { 453 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 454 loge("exception trying to stop text-to-speech"); 455 } 456 } 457 458 mState = STATE_IDLE; 459 } 460 log(String msg)461 private static void log(String msg) { 462 Log.d(TAG, msg); 463 } 464 loge(String msg)465 private static void loge(String msg) { 466 Log.e(TAG, msg); 467 } 468 } 469