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
17package com.android.cellbroadcastreceiver;
18
19import android.app.PendingIntent;
20import android.app.Service;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.AssetFileDescriptor;
24import android.content.res.Resources;
25import android.media.AudioManager;
26import android.media.MediaPlayer;
27import android.media.MediaPlayer.OnErrorListener;
28import android.media.Ringtone;
29import android.media.RingtoneManager;
30import android.net.Uri;
31import android.os.Handler;
32import android.os.IBinder;
33import android.os.Message;
34import android.os.Vibrator;
35import android.speech.tts.TextToSpeech;
36import android.telephony.PhoneStateListener;
37import android.telephony.TelephonyManager;
38import android.util.Log;
39
40import java.util.HashMap;
41import java.util.Locale;
42
43import 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 */
49public 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
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     */
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
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
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
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
263    public IBinder onBind(Intent intent) {
264        return null;
265    }
266
267    @Override
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     */
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.
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
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
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     */
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
461    private static void log(String msg) {
462        Log.d(TAG, msg);
463    }
464
465    private static void loge(String msg) {
466        Log.e(TAG, msg);
467    }
468}
469