• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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