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