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