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 static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG;
20import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.VDBG;
21
22import android.app.Service;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.AssetFileDescriptor;
26import android.content.res.Resources;
27import android.media.AudioAttributes;
28import android.media.AudioDeviceInfo;
29import android.media.AudioManager;
30import android.media.MediaPlayer;
31import android.media.MediaPlayer.OnCompletionListener;
32import android.media.MediaPlayer.OnErrorListener;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.IBinder;
36import android.os.Message;
37import android.os.Vibrator;
38import android.preference.PreferenceManager;
39import android.provider.Settings;
40import android.speech.tts.TextToSpeech;
41import android.telephony.PhoneStateListener;
42import android.telephony.TelephonyManager;
43import android.util.Log;
44
45import com.android.cellbroadcastreceiver.CellBroadcastAlertService.AlertType;
46
47import java.util.Locale;
48import java.util.MissingResourceException;
49
50/**
51 * Manages alert audio and vibration and text-to-speech. Runs as a service so that
52 * it can continue to play if another activity overrides the CellBroadcastListActivity.
53 */
54public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnInitListener,
55        TextToSpeech.OnUtteranceCompletedListener {
56    private static final String TAG = "CellBroadcastAlertAudio";
57
58    /** Action to start playing alert audio/vibration/speech. */
59    static final String ACTION_START_ALERT_AUDIO = "ACTION_START_ALERT_AUDIO";
60
61    /** Extra for message body to speak (if speech enabled in settings). */
62    public static final String ALERT_AUDIO_MESSAGE_BODY =
63            "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_BODY";
64
65    /** Extra for text-to-speech preferred language (if speech enabled in settings). */
66    public static final String ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE =
67            "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE";
68
69    /** Extra for text-to-speech default language when preferred language is
70        not available (if speech enabled in settings). */
71    public static final String ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE =
72            "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE";
73
74    /** Extra for alert tone type */
75    public static final String ALERT_AUDIO_TONE_TYPE =
76            "com.android.cellbroadcastreceiver.ALERT_AUDIO_TONE_TYPE";
77
78    /** Extra for alert audio vibration enabled (from settings). */
79    public static final String ALERT_AUDIO_VIBRATE_EXTRA =
80            "com.android.cellbroadcastreceiver.ALERT_AUDIO_VIBRATE";
81
82    /** Extra for alert vibration pattern (unless master volume is silent). */
83    public static final String ALERT_AUDIO_VIBRATION_PATTERN_EXTRA =
84            "com.android.cellbroadcastreceiver.ALERT_VIBRATION_PATTERN";
85
86    private static final String TTS_UTTERANCE_ID = "com.android.cellbroadcastreceiver.UTTERANCE_ID";
87
88    /** Pause duration between alert sound and alert speech. */
89    private static final int PAUSE_DURATION_BEFORE_SPEAKING_MSEC = 1000;
90
91    private static final int STATE_IDLE = 0;
92    private static final int STATE_ALERTING = 1;
93    private static final int STATE_PAUSING = 2;
94    private static final int STATE_SPEAKING = 3;
95
96    private int mState;
97
98    private TextToSpeech mTts;
99    private boolean mTtsEngineReady;
100
101    private String mMessageBody;
102    private String mMessagePreferredLanguage;
103    private String mMessageDefaultLanguage;
104    private boolean mTtsLanguageSupported;
105    private boolean mEnableVibrate;
106    private boolean mEnableAudio;
107    private boolean mUseFullVolume;
108    private boolean mResetAlarmVolumeNeeded;
109    private int mUserSetAlarmVolume;
110    private int[] mVibrationPattern;
111
112    private Vibrator mVibrator;
113    private MediaPlayer mMediaPlayer;
114    private AudioManager mAudioManager;
115    private TelephonyManager mTelephonyManager;
116    private int mInitialCallState;
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                    // Set alert reminder depending on user preference
141                    CellBroadcastAlertReminder.queueAlertReminder(getApplicationContext(), true);
142                    break;
143
144                case ALERT_PAUSE_FINISHED:
145                    if (DBG) log("ALERT_PAUSE_FINISHED");
146                    int res = TextToSpeech.ERROR;
147                    if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) {
148                        if (DBG) log("Speaking broadcast text: " + mMessageBody);
149
150                        Bundle params = new Bundle();
151                        // Play TTS in the alarm stream, which we use for playing alert tones as
152                        // well.
153                        params.putInt(TextToSpeech.Engine.KEY_PARAM_STREAM,
154                                AudioManager.STREAM_ALARM);
155                        // Use the non-public parameter 2 --> TextToSpeech.QUEUE_DESTROY for TTS.
156                        // The entire playback queue is purged. This is different from QUEUE_FLUSH
157                        // in that all entries are purged, not just entries from a given caller.
158                        // This is for emergency so we want to kill all other TTS sessions.
159                        res = mTts.speak(mMessageBody, 2, params, TTS_UTTERANCE_ID);
160                        mState = STATE_SPEAKING;
161                    }
162                    if (res != TextToSpeech.SUCCESS) {
163                        loge("TTS engine not ready or language not supported or speak() failed");
164                        stopSelf();
165                        mState = STATE_IDLE;
166                    }
167                    break;
168
169                default:
170                    loge("Handler received unknown message, what=" + msg.what);
171            }
172        }
173    };
174
175    private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
176        @Override
177        public void onCallStateChanged(int state, String ignored) {
178            // Stop the alert sound and speech if the call state changes.
179            if (state != TelephonyManager.CALL_STATE_IDLE
180                    && state != mInitialCallState) {
181                stopSelf();
182            }
183        }
184    };
185
186    /**
187     * Callback from TTS engine after initialization.
188     * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}.
189     */
190    @Override
191    public void onInit(int status) {
192        if (VDBG) log("onInit() TTS engine status: " + status);
193        if (status == TextToSpeech.SUCCESS) {
194            mTtsEngineReady = true;
195            mTts.setOnUtteranceCompletedListener(this);
196            // try to set the TTS language to match the broadcast
197            setTtsLanguage();
198        } else {
199            mTtsEngineReady = false;
200            mTts = null;
201            loge("onInit() TTS engine error: " + status);
202        }
203    }
204
205    /**
206     * Try to set the TTS engine language to the preferred language. If failed, set
207     * it to the default language. mTtsLanguageSupported will be updated based on the response.
208     */
209    private void setTtsLanguage() {
210
211        String language = mMessagePreferredLanguage;
212        if (language == null || language.isEmpty() ||
213                TextToSpeech.LANG_AVAILABLE != mTts.isLanguageAvailable(new Locale(language))) {
214            language = mMessageDefaultLanguage;
215            if (language == null || language.isEmpty() ||
216                    TextToSpeech.LANG_AVAILABLE != mTts.isLanguageAvailable(new Locale(language))) {
217                mTtsLanguageSupported = false;
218                return;
219            }
220            if (DBG) log("Language '" + mMessagePreferredLanguage + "' is not available, using" +
221                    "the default language '" + mMessageDefaultLanguage + "'");
222        }
223
224        if (DBG) log("Setting TTS language to '" + language + '\'');
225
226        try {
227            int result = mTts.setLanguage(new Locale(language));
228            if (DBG) log("TTS setLanguage() returned: " + result);
229            mTtsLanguageSupported = (result == TextToSpeech.LANG_AVAILABLE);
230        }
231        catch (MissingResourceException e) {
232            mTtsLanguageSupported = false;
233            loge("Language '" + language + "' is not available.");
234        }
235    }
236
237    /**
238     * Callback from TTS engine.
239     * @param utteranceId the identifier of the utterance.
240     */
241    @Override
242    public void onUtteranceCompleted(String utteranceId) {
243        if (utteranceId.equals(TTS_UTTERANCE_ID)) {
244            // When we reach here, it could be TTS completed or TTS was cut due to another
245            // new alert started playing. We don't want to stop the service in the later case.
246            if (mState == STATE_SPEAKING) {
247                stopSelf();
248            }
249        }
250    }
251
252    @Override
253    public void onCreate() {
254        mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
255        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
256        // Listen for incoming calls to kill the alarm.
257        mTelephonyManager =
258                (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
259        mTelephonyManager.listen(
260                mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
261    }
262
263    @Override
264    public void onDestroy() {
265        // stop audio, vibration and TTS
266        stop();
267        // Stop listening for incoming calls.
268        mTelephonyManager.listen(mPhoneStateListener, 0);
269        // shutdown TTS engine
270        if (mTts != null) {
271            try {
272                mTts.shutdown();
273            } catch (IllegalStateException e) {
274                // catch "Unable to retrieve AudioTrack pointer for stop()" exception
275                loge("exception trying to shutdown text-to-speech");
276            }
277        }
278        if (mEnableAudio) {
279            // Release the audio focus so other audio (e.g. music) can resume.
280            // Do not do this in stop() because stop() is also called when we stop the tone (before
281            // TTS is playing). We only want to release the focus when tone and TTS are played.
282            mAudioManager.abandonAudioFocus(null);
283        }
284        // release the screen bright wakelock acquired by CellBroadcastAlertService
285        CellBroadcastAlertWakeLock.releaseScreenBrightWakeLock();
286    }
287
288    @Override
289    public IBinder onBind(Intent intent) {
290        return null;
291    }
292
293    @Override
294    public int onStartCommand(Intent intent, int flags, int startId) {
295        // No intent, tell the system not to restart us.
296        if (intent == null) {
297            stopSelf();
298            return START_NOT_STICKY;
299        }
300
301        // Get text to speak (if enabled by user)
302        mMessageBody = intent.getStringExtra(ALERT_AUDIO_MESSAGE_BODY);
303        mMessagePreferredLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE);
304        mMessageDefaultLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE);
305
306        // Get config of whether to always sound CBS alerts at full volume.
307        mUseFullVolume = PreferenceManager.getDefaultSharedPreferences(this)
308                .getBoolean(CellBroadcastSettings.KEY_USE_FULL_VOLUME, false);
309
310        // retrieve the vibrate settings from cellbroadcast receiver settings.
311        mEnableVibrate = intent.getBooleanExtra(ALERT_AUDIO_VIBRATE_EXTRA, true);
312        // retrieve the vibration patterns
313        mVibrationPattern = intent.getIntArrayExtra(ALERT_AUDIO_VIBRATION_PATTERN_EXTRA);
314
315        switch (mAudioManager.getRingerMode()) {
316            case AudioManager.RINGER_MODE_SILENT:
317                if (DBG) log("Ringer mode: silent");
318                mEnableAudio = false;
319                mEnableVibrate = false;
320                break;
321            case AudioManager.RINGER_MODE_VIBRATE:
322                if (DBG) log("Ringer mode: vibrate");
323                mEnableAudio = false;
324                break;
325            case AudioManager.RINGER_MODE_NORMAL:
326            default:
327                if (DBG) log("Ringer mode: normal");
328                mEnableAudio = true;
329                break;
330        }
331
332        if (mUseFullVolume) {
333            mEnableAudio = true;
334        }
335
336        if (mMessageBody != null && mEnableAudio) {
337            if (mTts == null) {
338                mTts = new TextToSpeech(this, this);
339            } else if (mTtsEngineReady) {
340                setTtsLanguage();
341            }
342        }
343
344        if (mEnableAudio || mEnableVibrate) {
345            AlertType alertType = AlertType.DEFAULT;
346            if (intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE) != null) {
347                alertType = (AlertType) intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE);
348            }
349            playAlertTone(alertType, mVibrationPattern);
350        } else {
351            stopSelf();
352            return START_NOT_STICKY;
353        }
354
355        // Record the initial call state here so that the new alarm has the
356        // newest state.
357        mInitialCallState = mTelephonyManager.getCallState();
358
359        return START_STICKY;
360    }
361
362    // Volume suggested by media team for in-call alarms.
363    private static final float IN_CALL_VOLUME = 0.125f;
364
365    /**
366     * Start playing the alert sound.
367     * @param alertType the alert type (e.g. default, earthquake, tsunami, etc..)
368     * @param patternArray the alert vibration pattern
369     */
370    private void playAlertTone(AlertType alertType, int[] patternArray) {
371        // stop() checks to see if we are already playing.
372        stop();
373
374        log("playAlertTone: alertType=" + alertType);
375
376        // Vibration duration in milliseconds
377        long vibrateDuration = 0;
378
379        int customAlertDuration = getResources().getInteger(R.integer.alert_duration);
380
381        // Start the vibration first.
382        if (mEnableVibrate) {
383            long[] vibrationPattern = new long[patternArray.length];
384
385            for (int i = 0; i < patternArray.length; i++) {
386                vibrationPattern[i] = patternArray[i];
387                vibrateDuration += patternArray[i];
388            }
389            mVibrator.vibrate(vibrationPattern, 0);
390        }
391
392
393        if (mEnableAudio) {
394            // future optimization: reuse media player object
395            mMediaPlayer = new MediaPlayer();
396            mMediaPlayer.setOnErrorListener(new OnErrorListener() {
397                public boolean onError(MediaPlayer mp, int what, int extra) {
398                    loge("Error occurred while playing audio.");
399                    mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED));
400                    return true;
401                }
402            });
403
404            // If the duration is specified by the config, use the specified duration. Otherwise,
405            // just play the alert tone with the tone's duration.
406            if (customAlertDuration >= 0) {
407                mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED),
408                        customAlertDuration);
409            } else {
410                mMediaPlayer.setOnCompletionListener(new OnCompletionListener() {
411                    public void onCompletion(MediaPlayer mp) {
412                        if (DBG) log("Audio playback complete.");
413                        mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED));
414                        return;
415                    }
416                });
417            }
418
419            try {
420                log("Locale=" + getResources().getConfiguration().getLocales()
421                        + ", alertType=" + alertType);
422
423                // Load the tones based on type
424                switch (alertType) {
425                    case ETWS_EARTHQUAKE:
426                        setDataSourceFromResource(getResources(), mMediaPlayer,
427                                R.raw.etws_earthquake);
428                        break;
429                    case ETWS_TSUNAMI:
430                        setDataSourceFromResource(getResources(), mMediaPlayer,
431                                R.raw.etws_tsunami);
432                        break;
433                    case OTHER:
434                        setDataSourceFromResource(getResources(), mMediaPlayer,
435                                R.raw.etws_other_disaster);
436                        break;
437                    case ETWS_DEFAULT:
438                        setDataSourceFromResource(getResources(), mMediaPlayer,
439                                R.raw.etws_default);
440                        break;
441                    case INFO:
442                        // for non-emergency alerts, we are using system default notification sound.
443                        String sound = Settings.System.getString(
444                                getApplicationContext().getContentResolver(),
445                                Settings.System.NOTIFICATION_SOUND);
446                        mMediaPlayer.setDataSource(sound);
447                        break;
448                    case TEST:
449                    case DEFAULT:
450                    default:
451                        setDataSourceFromResource(getResources(), mMediaPlayer,
452                                R.raw.default_tone);
453                }
454
455                // Request audio focus (though we're going to play even if we don't get it)
456                mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM,
457                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
458                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
459
460                setAlertAudioAttributes();
461                setAlertVolume();
462
463                // If we are using the custom alert duration, set looping to true so we can repeat
464                // the alert. The tone playing will stop when ALERT_SOUND_FINISHED arrives.
465                // Otherwise we just play the alert tone once.
466                mMediaPlayer.setLooping(customAlertDuration >= 0);
467                mMediaPlayer.prepare();
468                mMediaPlayer.start();
469
470            } catch (Exception ex) {
471                loge("Failed to play alert sound: " + ex);
472                // Immediately move into the next state ALERT_SOUND_FINISHED.
473                mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED));
474            }
475        } else {
476            // In normal mode (playing tone + vibration), this service will stop after audio
477            // playback is done. However, if the device is in vibrate only mode, we need to stop
478            // the service right after vibration because there won't be any audio complete callback
479            // to stop the service. Unfortunately it's not like MediaPlayer has onCompletion()
480            // callback that we can use, we'll have to use our own timer to stop the service.
481            mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED),
482                    customAlertDuration >= 0 ? customAlertDuration : vibrateDuration);
483        }
484
485        mState = STATE_ALERTING;
486    }
487
488    private static void setDataSourceFromResource(Resources resources,
489            MediaPlayer player, int res) throws java.io.IOException {
490        AssetFileDescriptor afd = resources.openRawResourceFd(res);
491        if (afd != null) {
492            player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
493                    afd.getLength());
494            afd.close();
495        }
496    }
497
498    /**
499     * Stops alert audio and speech.
500     */
501    public void stop() {
502        if (DBG) log("stop()");
503
504        mHandler.removeMessages(ALERT_SOUND_FINISHED);
505        mHandler.removeMessages(ALERT_PAUSE_FINISHED);
506
507        resetAlarmStreamVolume();
508
509        if (mState == STATE_ALERTING) {
510            // Stop audio playing
511            if (mMediaPlayer != null) {
512                try {
513                    mMediaPlayer.stop();
514                    mMediaPlayer.release();
515                } catch (IllegalStateException e) {
516                    // catch "Unable to retrieve AudioTrack pointer for stop()" exception
517                    loge("exception trying to stop media player");
518                }
519                mMediaPlayer = null;
520            }
521
522            // Stop vibrator
523            mVibrator.cancel();
524        } else if (mState == STATE_SPEAKING && mTts != null) {
525            try {
526                mTts.stop();
527            } catch (IllegalStateException e) {
528                // catch "Unable to retrieve AudioTrack pointer for stop()" exception
529                loge("exception trying to stop text-to-speech");
530            }
531        }
532        mState = STATE_IDLE;
533    }
534
535    /**
536     * Set AudioAttributes for mMediaPlayer. Replacement of deprecated
537     * mMediaPlayer.setAudioStreamType.
538     */
539    private void setAlertAudioAttributes() {
540        AudioAttributes.Builder builder = new AudioAttributes.Builder();
541
542        builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION);
543        builder.setUsage(AudioAttributes.USAGE_ALARM);
544        if (mUseFullVolume) {
545            // Set FLAG_BYPASS_INTERRUPTION_POLICY and FLAG_BYPASS_MUTE so that it enables
546            // audio in any DnD mode, even in total silence DnD mode (requires MODIFY_PHONE_STATE).
547            builder.setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
548                    | AudioAttributes.FLAG_BYPASS_MUTE);
549        }
550
551        mMediaPlayer.setAudioAttributes(builder.build());
552    }
553
554    /**
555     * Set volume for alerts.
556     */
557    private void setAlertVolume() {
558        if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE
559                || isOnEarphone()) {
560            // If we are in a call, play the alert
561            // sound at a low volume to not disrupt the call.
562            log("in call: reducing volume");
563            mMediaPlayer.setVolume(IN_CALL_VOLUME);
564        } else if (mUseFullVolume) {
565            // If use_full_volume is configured,
566            // we overwrite volume setting of STREAM_ALARM to full, play at
567            // max possible volume, and reset it after it's finished.
568            setAlarmStreamVolumeToFull();
569        }
570    }
571
572    private boolean isOnEarphone() {
573        AudioDeviceInfo[] deviceList = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
574
575        for (AudioDeviceInfo devInfo : deviceList) {
576            int type = devInfo.getType();
577            if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET
578                    || type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
579                    || type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
580                    || type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
581                return true;
582            }
583        }
584
585        return false;
586    }
587
588    /**
589     * Set volume of STREAM_ALARM to full.
590     */
591    private void setAlarmStreamVolumeToFull() {
592        log("setting alarm volume to full for cell broadcast alerts.");
593        mUserSetAlarmVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM);
594        mResetAlarmVolumeNeeded = true;
595        mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM,
596                mAudioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM),
597                0);
598    }
599
600    /**
601     * Reset volume of STREAM_ALARM, if needed.
602     */
603    private void resetAlarmStreamVolume() {
604        if (mResetAlarmVolumeNeeded) {
605            log("resetting alarm volume to back to " + mUserSetAlarmVolume);
606            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, mUserSetAlarmVolume, 0);
607            mResetAlarmVolumeNeeded = false;
608        }
609    }
610
611    private static void log(String msg) {
612        Log.d(TAG, msg);
613    }
614
615    private static void loge(String msg) {
616        Log.e(TAG, msg);
617    }
618}
619