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