• 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 static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG;
20 import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.VDBG;
21 
22 import android.app.PendingIntent;
23 import android.app.Service;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.AssetFileDescriptor;
27 import android.content.res.Resources;
28 import android.media.AudioManager;
29 import android.media.MediaPlayer;
30 import android.media.MediaPlayer.OnCompletionListener;
31 import android.media.MediaPlayer.OnErrorListener;
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.Locale;
43 import java.util.MissingResourceException;
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         // retrieve the vibrate settings from cellbroadcast receiver settings.
305         mEnableVibrate = intent.getBooleanExtra(ALERT_AUDIO_VIBRATE_EXTRA, true);
306 
307         switch (mAudioManager.getRingerMode()) {
308             case AudioManager.RINGER_MODE_SILENT:
309                 if (DBG) log("Ringer mode: silent");
310                 mEnableAudio = false;
311                 // If the device is in silent mode, do not vibrate (except ETWS).
312                 if (!intent.getBooleanExtra(ALERT_AUDIO_ETWS_VIBRATE_EXTRA, false)) {
313                     mEnableVibrate = false;
314                 }
315                 break;
316             case AudioManager.RINGER_MODE_VIBRATE:
317                 if (DBG) log("Ringer mode: vibrate");
318                 mEnableAudio = false;
319                 break;
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         // Vibration duration in milliseconds
367         long vibrateDuration = 0;
368 
369         // Start the vibration first.
370         if (mEnableVibrate) {
371 
372             int[] patternArray = getApplicationContext().getResources().
373                     getIntArray(R.array.default_vibration_pattern);
374             long[] vibrationPattern = new long[patternArray.length];
375 
376             for (int i = 0; i < patternArray.length; i++) {
377                 vibrationPattern[i] = patternArray[i];
378                 vibrateDuration += patternArray[i];
379             }
380 
381             mVibrator.vibrate(vibrationPattern, -1);
382         }
383 
384 
385         if (mEnableAudio) {
386             // future optimization: reuse media player object
387             mMediaPlayer = new MediaPlayer();
388             mMediaPlayer.setOnErrorListener(new OnErrorListener() {
389                 public boolean onError(MediaPlayer mp, int what, int extra) {
390                     loge("Error occurred while playing audio.");
391                     mp.stop();
392                     mp.release();
393                     mMediaPlayer = null;
394                     return true;
395                 }
396             });
397 
398             mMediaPlayer.setOnCompletionListener(new OnCompletionListener() {
399                 public void onCompletion(MediaPlayer mp) {
400                     if (DBG) log("Audio playback complete.");
401                     mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED));
402                     return;
403                 }
404             });
405 
406             try {
407                 // Check if we are in a call. If we are, play the alert
408                 // sound at a low volume to not disrupt the call.
409                 if (mTelephonyManager.getCallState()
410                         != TelephonyManager.CALL_STATE_IDLE) {
411                     log("in call: reducing volume");
412                     mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
413                 }
414 
415                 log("Locale=" + getResources().getConfiguration().getLocales());
416 
417                 // Load the tones based on type
418                 switch (toneType) {
419                     case EARTHQUAKE:
420                         setDataSourceFromResource(getResources(), mMediaPlayer,
421                                 R.raw.etws_earthquake);
422                         break;
423                     case TSUNAMI:
424                         setDataSourceFromResource(getResources(), mMediaPlayer,
425                                 R.raw.etws_tsunami);
426                         break;
427                     case OTHER:
428                         setDataSourceFromResource(getResources(), mMediaPlayer,
429                                 R.raw.etws_other_disaster);
430                         break;
431                     case ETWS_DEFAULT:
432                         setDataSourceFromResource(getResources(), mMediaPlayer,
433                                 R.raw.etws_default);
434                         break;
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