• 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 
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
onInit(int status)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      */
setTtsLanguage()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
onUtteranceCompleted(String utteranceId)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
onCreate()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
onDestroy()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
onBind(Intent intent)286     public IBinder onBind(Intent intent) {
287         return null;
288     }
289 
290     @Override
onStartCommand(Intent intent, int flags, int startId)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      */
playAlertTone(ToneType toneType)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 
setDataSourceFromResource(Resources resources, MediaPlayer player, int res)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      */
stop()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 
log(String msg)503     private static void log(String msg) {
504         Log.d(TAG, msg);
505     }
506 
loge(String msg)507     private static void loge(String msg) {
508         Log.e(TAG, msg);
509     }
510 }
511