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