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