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.OnCompletionListener; 28 import android.media.MediaPlayer.OnErrorListener; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.os.IBinder; 32 import android.os.Message; 33 import android.os.Vibrator; 34 import android.speech.tts.TextToSpeech; 35 import android.telephony.PhoneStateListener; 36 import android.telephony.TelephonyManager; 37 import android.util.Log; 38 39 import java.util.Locale; 40 import java.util.MissingResourceException; 41 42 import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; 43 import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.VDBG; 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 message body to speak (if speech enabled in settings). */ 57 public static final String ALERT_AUDIO_MESSAGE_BODY = 58 "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_BODY"; 59 60 /** Extra for text-to-speech preferred language (if speech enabled in settings). */ 61 public static final String ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE = 62 "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE"; 63 64 /** Extra for text-to-speech default language when preferred language is 65 not available (if speech enabled in settings). */ 66 public static final String ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE = 67 "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE"; 68 69 /** Extra for alert tone type */ 70 public static final String ALERT_AUDIO_TONE_TYPE = 71 "com.android.cellbroadcastreceiver.ALERT_AUDIO_TONE_TYPE"; 72 73 /** Extra for alert audio vibration enabled (from settings). */ 74 public static final String ALERT_AUDIO_VIBRATE_EXTRA = 75 "com.android.cellbroadcastreceiver.ALERT_AUDIO_VIBRATE"; 76 77 /** Extra for alert audio ETWS behavior (always vibrate, even in silent mode). */ 78 public static final String ALERT_AUDIO_ETWS_VIBRATE_EXTRA = 79 "com.android.cellbroadcastreceiver.ALERT_AUDIO_ETWS_VIBRATE"; 80 81 private static final String TTS_UTTERANCE_ID = "com.android.cellbroadcastreceiver.UTTERANCE_ID"; 82 83 /** Pause duration between alert sound and alert speech. */ 84 private static final int PAUSE_DURATION_BEFORE_SPEAKING_MSEC = 1000; 85 86 private static final int STATE_IDLE = 0; 87 private static final int STATE_ALERTING = 1; 88 private static final int STATE_PAUSING = 2; 89 private static final int STATE_SPEAKING = 3; 90 91 private int mState; 92 93 private TextToSpeech mTts; 94 private boolean mTtsEngineReady; 95 96 private String mMessageBody; 97 private String mMessagePreferredLanguage; 98 private String mMessageDefaultLanguage; 99 private boolean mTtsLanguageSupported; 100 private boolean mEnableVibrate; 101 private boolean mEnableAudio; 102 103 private Vibrator mVibrator; 104 private MediaPlayer mMediaPlayer; 105 private AudioManager mAudioManager; 106 private TelephonyManager mTelephonyManager; 107 private int mInitialCallState; 108 109 private PendingIntent mPlayReminderIntent; 110 111 public enum ToneType { 112 CMAS_DEFAULT, 113 ETWS_DEFAULT, 114 EARTHQUAKE, 115 TSUNAMI, 116 OTHER 117 } 118 119 // Internal messages 120 private static final int ALERT_SOUND_FINISHED = 1000; 121 private static final int ALERT_PAUSE_FINISHED = 1001; 122 private final Handler mHandler = new Handler() { 123 @Override 124 public void handleMessage(Message msg) { 125 switch (msg.what) { 126 case ALERT_SOUND_FINISHED: 127 if (DBG) log("ALERT_SOUND_FINISHED"); 128 stop(); // stop alert sound 129 // if we can speak the message text 130 if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { 131 mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_PAUSE_FINISHED), 132 PAUSE_DURATION_BEFORE_SPEAKING_MSEC); 133 mState = STATE_PAUSING; 134 } else { 135 if (DBG) log("MessageEmpty = " + (mMessageBody == null) + 136 ", mTtsEngineReady = " + mTtsEngineReady + 137 ", mTtsLanguageSupported = " + mTtsLanguageSupported); 138 stopSelf(); 139 mState = STATE_IDLE; 140 } 141 break; 142 143 case ALERT_PAUSE_FINISHED: 144 if (DBG) log("ALERT_PAUSE_FINISHED"); 145 int res = TextToSpeech.ERROR; 146 if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { 147 if (DBG) log("Speaking broadcast text: " + mMessageBody); 148 149 Bundle params = new Bundle(); 150 // Play TTS in notification stream. 151 params.putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, 152 AudioManager.STREAM_NOTIFICATION); 153 // Use the non-public parameter 2 --> TextToSpeech.QUEUE_DESTROY for TTS. 154 // The entire playback queue is purged. This is different from QUEUE_FLUSH 155 // in that all entries are purged, not just entries from a given caller. 156 // This is for emergency so we want to kill all other TTS sessions. 157 res = mTts.speak(mMessageBody, 2, params, TTS_UTTERANCE_ID); 158 mState = STATE_SPEAKING; 159 } 160 if (res != TextToSpeech.SUCCESS) { 161 loge("TTS engine not ready or language not supported or speak() failed"); 162 stopSelf(); 163 mState = STATE_IDLE; 164 } 165 break; 166 167 default: 168 loge("Handler received unknown message, what=" + msg.what); 169 } 170 } 171 }; 172 173 private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { 174 @Override 175 public void onCallStateChanged(int state, String ignored) { 176 // Stop the alert sound and speech if the call state changes. 177 if (state != TelephonyManager.CALL_STATE_IDLE 178 && state != mInitialCallState) { 179 stopSelf(); 180 } 181 } 182 }; 183 184 /** 185 * Callback from TTS engine after initialization. 186 * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. 187 */ 188 @Override onInit(int status)189 public void onInit(int status) { 190 if (VDBG) log("onInit() TTS engine status: " + status); 191 if (status == TextToSpeech.SUCCESS) { 192 mTtsEngineReady = true; 193 mTts.setOnUtteranceCompletedListener(this); 194 // try to set the TTS language to match the broadcast 195 setTtsLanguage(); 196 } else { 197 mTtsEngineReady = false; 198 mTts = null; 199 loge("onInit() TTS engine error: " + status); 200 } 201 } 202 203 /** 204 * Try to set the TTS engine language to the preferred language. If failed, set 205 * it to the default language. mTtsLanguageSupported will be updated based on the response. 206 */ setTtsLanguage()207 private void setTtsLanguage() { 208 209 String language = mMessagePreferredLanguage; 210 if (language == null || language.isEmpty() || 211 TextToSpeech.LANG_AVAILABLE != mTts.isLanguageAvailable(new Locale(language))) { 212 language = mMessageDefaultLanguage; 213 if (language == null || language.isEmpty() || 214 TextToSpeech.LANG_AVAILABLE != mTts.isLanguageAvailable(new Locale(language))) { 215 mTtsLanguageSupported = false; 216 return; 217 } 218 if (DBG) log("Language '" + mMessagePreferredLanguage + "' is not available, using" + 219 "the default language '" + mMessageDefaultLanguage + "'"); 220 } 221 222 if (DBG) log("Setting TTS language to '" + language + '\''); 223 224 try { 225 int result = mTts.setLanguage(new Locale(language)); 226 if (DBG) log("TTS setLanguage() returned: " + result); 227 mTtsLanguageSupported = (result == TextToSpeech.LANG_AVAILABLE); 228 } 229 catch (MissingResourceException e) { 230 mTtsLanguageSupported = false; 231 loge("Language '" + language + "' is not available."); 232 } 233 } 234 235 /** 236 * Callback from TTS engine. 237 * @param utteranceId the identifier of the utterance. 238 */ 239 @Override onUtteranceCompleted(String utteranceId)240 public void onUtteranceCompleted(String utteranceId) { 241 if (utteranceId.equals(TTS_UTTERANCE_ID)) { 242 // When we reach here, it could be TTS completed or TTS was cut due to another 243 // new alert started playing. We don't want to stop the service in the later case. 244 if (mState == STATE_SPEAKING) { 245 stopSelf(); 246 } 247 } 248 } 249 250 @Override onCreate()251 public void onCreate() { 252 mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); 253 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 254 // Listen for incoming calls to kill the alarm. 255 mTelephonyManager = 256 (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 257 mTelephonyManager.listen( 258 mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 259 } 260 261 @Override onDestroy()262 public void onDestroy() { 263 // stop audio, vibration and TTS 264 stop(); 265 // Stop listening for incoming calls. 266 mTelephonyManager.listen(mPhoneStateListener, 0); 267 // shutdown TTS engine 268 if (mTts != null) { 269 try { 270 mTts.shutdown(); 271 } catch (IllegalStateException e) { 272 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 273 loge("exception trying to shutdown text-to-speech"); 274 } 275 } 276 if (mEnableAudio) { 277 // Release the audio focus so other audio (e.g. music) can resume. 278 // Do not do this in stop() because stop() is also called when we stop the tone (before 279 // TTS is playing). We only want to release the focus when tone and TTS are played. 280 mAudioManager.abandonAudioFocus(null); 281 } 282 // release CPU wake lock acquired by CellBroadcastAlertService 283 CellBroadcastAlertWakeLock.releaseCpuLock(); 284 } 285 286 @Override onBind(Intent intent)287 public IBinder onBind(Intent intent) { 288 return null; 289 } 290 291 @Override onStartCommand(Intent intent, int flags, int startId)292 public int onStartCommand(Intent intent, int flags, int startId) { 293 // No intent, tell the system not to restart us. 294 if (intent == null) { 295 stopSelf(); 296 return START_NOT_STICKY; 297 } 298 299 // Get text to speak (if enabled by user) 300 mMessageBody = intent.getStringExtra(ALERT_AUDIO_MESSAGE_BODY); 301 mMessagePreferredLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE); 302 mMessageDefaultLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE); 303 304 mEnableVibrate = intent.getBooleanExtra(ALERT_AUDIO_VIBRATE_EXTRA, true); 305 if (intent.getBooleanExtra(ALERT_AUDIO_ETWS_VIBRATE_EXTRA, false)) { 306 mEnableVibrate = true; // force enable vibration for ETWS alerts 307 } 308 309 switch (mAudioManager.getRingerMode()) { 310 case AudioManager.RINGER_MODE_SILENT: 311 if (DBG) log("Ringer mode: silent"); 312 mEnableAudio = false; 313 mEnableVibrate = false; 314 break; 315 316 case AudioManager.RINGER_MODE_VIBRATE: 317 if (DBG) log("Ringer mode: vibrate"); 318 mEnableAudio = false; 319 break; 320 321 case AudioManager.RINGER_MODE_NORMAL: 322 default: 323 if (DBG) log("Ringer mode: normal"); 324 mEnableAudio = true; 325 break; 326 } 327 328 if (mMessageBody != null && mEnableAudio) { 329 if (mTts == null) { 330 mTts = new TextToSpeech(this, this); 331 } else if (mTtsEngineReady) { 332 setTtsLanguage(); 333 } 334 } 335 336 if (mEnableAudio || mEnableVibrate) { 337 ToneType toneType = ToneType.CMAS_DEFAULT; 338 if (intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE) != null) { 339 toneType = (ToneType) intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE); 340 } 341 playAlertTone(toneType); 342 } else { 343 stopSelf(); 344 return START_NOT_STICKY; 345 } 346 347 // Record the initial call state here so that the new alarm has the 348 // newest state. 349 mInitialCallState = mTelephonyManager.getCallState(); 350 351 return START_STICKY; 352 } 353 354 // Volume suggested by media team for in-call alarms. 355 private static final float IN_CALL_VOLUME = 0.125f; 356 357 /** 358 * Start playing the alert sound. 359 * @param toneType the alert tone type (e.g. default, earthquake, tsunami, etc..) 360 */ playAlertTone(ToneType toneType)361 private void playAlertTone(ToneType toneType) { 362 // stop() checks to see if we are already playing. 363 stop(); 364 365 log("playAlertTone: toneType=" + toneType); 366 367 // Vibration duration in milliseconds 368 long vibrateDuration = 0; 369 370 // Start the vibration first. 371 if (mEnableVibrate) { 372 373 int[] patternArray = getApplicationContext().getResources(). 374 getIntArray(R.array.default_vibration_pattern); 375 long[] vibrationPattern = new long[patternArray.length]; 376 377 for (int i = 0; i < patternArray.length; i++) { 378 vibrationPattern[i] = patternArray[i]; 379 vibrateDuration += patternArray[i]; 380 } 381 382 mVibrator.vibrate(vibrationPattern, -1); 383 } 384 385 386 if (mEnableAudio) { 387 // future optimization: reuse media player object 388 mMediaPlayer = new MediaPlayer(); 389 mMediaPlayer.setOnErrorListener(new OnErrorListener() { 390 public boolean onError(MediaPlayer mp, int what, int extra) { 391 loge("Error occurred while playing audio."); 392 mp.stop(); 393 mp.release(); 394 mMediaPlayer = null; 395 return true; 396 } 397 }); 398 399 mMediaPlayer.setOnCompletionListener(new OnCompletionListener() { 400 public void onCompletion(MediaPlayer mp) { 401 if (DBG) log("Audio playback complete."); 402 mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED)); 403 return; 404 } 405 }); 406 407 try { 408 // Check if we are in a call. If we are, play the alert 409 // sound at a low volume to not disrupt the call. 410 if (mTelephonyManager.getCallState() 411 != TelephonyManager.CALL_STATE_IDLE) { 412 log("in call: reducing volume"); 413 mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); 414 } 415 416 log("Locale=" + getResources().getConfiguration().getLocales()); 417 418 // Load the tones based on type 419 switch (toneType) { 420 case EARTHQUAKE: 421 setDataSourceFromResource(getResources(), mMediaPlayer, 422 R.raw.etws_earthquake); 423 break; 424 case TSUNAMI: 425 setDataSourceFromResource(getResources(), mMediaPlayer, 426 R.raw.etws_tsunami); 427 break; 428 case OTHER: 429 setDataSourceFromResource(getResources(), mMediaPlayer, 430 R.raw.etws_other_disaster); 431 break; 432 case ETWS_DEFAULT: 433 setDataSourceFromResource(getResources(), mMediaPlayer, 434 R.raw.etws_default); 435 case CMAS_DEFAULT: 436 default: 437 setDataSourceFromResource(getResources(), mMediaPlayer, 438 R.raw.cmas_default); 439 } 440 441 // start playing alert audio (unless master volume is vibrate only or silent). 442 mAudioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION, 443 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 444 445 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); 446 mMediaPlayer.setLooping(false); 447 mMediaPlayer.prepare(); 448 mMediaPlayer.start(); 449 450 } catch (Exception ex) { 451 loge("Failed to play alert sound: " + ex); 452 // Immediately move into the next state ALERT_SOUND_FINISHED. 453 mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED)); 454 } 455 } else { 456 // In normal mode (playing tone + vibration), this service will stop after audio 457 // playback is done. However, if the device is in vibrate only mode, we need to stop 458 // the service right after vibration because there won't be any audio complete callback 459 // to stop the service. Unfortunately it's not like MediaPlayer has onCompletion() 460 // callback that we can use, we'll have to use our own timer to stop the service. 461 mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED), 462 vibrateDuration); 463 } 464 465 mState = STATE_ALERTING; 466 } 467 setDataSourceFromResource(Resources resources, MediaPlayer player, int res)468 private static void setDataSourceFromResource(Resources resources, 469 MediaPlayer player, int res) throws java.io.IOException { 470 AssetFileDescriptor afd = resources.openRawResourceFd(res); 471 if (afd != null) { 472 player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), 473 afd.getLength()); 474 afd.close(); 475 } 476 } 477 478 /** 479 * Stops alert audio and speech. 480 */ stop()481 public void stop() { 482 if (DBG) log("stop()"); 483 484 if (mPlayReminderIntent != null) { 485 mPlayReminderIntent.cancel(); 486 mPlayReminderIntent = null; 487 } 488 489 mHandler.removeMessages(ALERT_SOUND_FINISHED); 490 mHandler.removeMessages(ALERT_PAUSE_FINISHED); 491 492 if (mState == STATE_ALERTING) { 493 // Stop audio playing 494 if (mMediaPlayer != null) { 495 try { 496 mMediaPlayer.stop(); 497 mMediaPlayer.release(); 498 } catch (IllegalStateException e) { 499 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 500 loge("exception trying to stop media player"); 501 } 502 mMediaPlayer = null; 503 } 504 505 // Stop vibrator 506 mVibrator.cancel(); 507 } else if (mState == STATE_SPEAKING && mTts != null) { 508 try { 509 mTts.stop(); 510 } catch (IllegalStateException e) { 511 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 512 loge("exception trying to stop text-to-speech"); 513 } 514 } 515 mState = STATE_IDLE; 516 } 517 log(String msg)518 private static void log(String msg) { 519 Log.d(TAG, msg); 520 } 521 loge(String msg)522 private static void loge(String msg) { 523 Log.e(TAG, msg); 524 } 525 } 526