• Home
  • History
  • Annotate
  • 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.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