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