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 static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; 20 import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.VDBG; 21 22 import android.app.Service; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.res.AssetFileDescriptor; 26 import android.content.res.Resources; 27 import android.media.AudioAttributes; 28 import android.media.AudioDeviceInfo; 29 import android.media.AudioManager; 30 import android.media.MediaPlayer; 31 import android.media.MediaPlayer.OnCompletionListener; 32 import android.media.MediaPlayer.OnErrorListener; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.IBinder; 36 import android.os.Message; 37 import android.os.Vibrator; 38 import android.preference.PreferenceManager; 39 import android.speech.tts.TextToSpeech; 40 import android.telephony.PhoneStateListener; 41 import android.telephony.TelephonyManager; 42 import android.util.Log; 43 44 import com.android.cellbroadcastreceiver.CellBroadcastAlertService.AlertType; 45 46 import java.util.Locale; 47 import java.util.MissingResourceException; 48 49 /** 50 * Manages alert audio and vibration and text-to-speech. Runs as a service so that 51 * it can continue to play if another activity overrides the CellBroadcastListActivity. 52 */ 53 public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnInitListener, 54 TextToSpeech.OnUtteranceCompletedListener { 55 private static final String TAG = "CellBroadcastAlertAudio"; 56 57 /** Action to start playing alert audio/vibration/speech. */ 58 static final String ACTION_START_ALERT_AUDIO = "ACTION_START_ALERT_AUDIO"; 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 preferred language (if speech enabled in settings). */ 65 public static final String ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE = 66 "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_PREFERRED_LANGUAGE"; 67 68 /** Extra for text-to-speech default language when preferred language is 69 not available (if speech enabled in settings). */ 70 public static final String ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE = 71 "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_DEFAULT_LANGUAGE"; 72 73 /** Extra for alert tone type */ 74 public static final String ALERT_AUDIO_TONE_TYPE = 75 "com.android.cellbroadcastreceiver.ALERT_AUDIO_TONE_TYPE"; 76 77 /** Extra for alert audio vibration enabled (from settings). */ 78 public static final String ALERT_AUDIO_VIBRATE_EXTRA = 79 "com.android.cellbroadcastreceiver.ALERT_AUDIO_VIBRATE"; 80 81 /** Extra for alert audio ETWS behavior (always vibrate, even in silent mode). */ 82 public static final String ALERT_AUDIO_ETWS_VIBRATE_EXTRA = 83 "com.android.cellbroadcastreceiver.ALERT_AUDIO_ETWS_VIBRATE"; 84 85 private static final String TTS_UTTERANCE_ID = "com.android.cellbroadcastreceiver.UTTERANCE_ID"; 86 87 /** Pause duration between alert sound and alert speech. */ 88 private static final int PAUSE_DURATION_BEFORE_SPEAKING_MSEC = 1000; 89 90 private static final int STATE_IDLE = 0; 91 private static final int STATE_ALERTING = 1; 92 private static final int STATE_PAUSING = 2; 93 private static final int STATE_SPEAKING = 3; 94 95 private int mState; 96 97 private TextToSpeech mTts; 98 private boolean mTtsEngineReady; 99 100 private String mMessageBody; 101 private String mMessagePreferredLanguage; 102 private String mMessageDefaultLanguage; 103 private boolean mTtsLanguageSupported; 104 private boolean mEnableVibrate; 105 private boolean mEnableAudio; 106 private boolean mUseFullVolume; 107 private boolean mResetAlarmVolumeNeeded; 108 private int mUserSetAlarmVolume; 109 110 private Vibrator mVibrator; 111 private MediaPlayer mMediaPlayer; 112 private AudioManager mAudioManager; 113 private TelephonyManager mTelephonyManager; 114 private int mInitialCallState; 115 116 // Internal messages 117 private static final int ALERT_SOUND_FINISHED = 1000; 118 private static final int ALERT_PAUSE_FINISHED = 1001; 119 private final Handler mHandler = new Handler() { 120 @Override 121 public void handleMessage(Message msg) { 122 switch (msg.what) { 123 case ALERT_SOUND_FINISHED: 124 if (DBG) log("ALERT_SOUND_FINISHED"); 125 stop(); // stop alert sound 126 // if we can speak the message text 127 if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { 128 mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_PAUSE_FINISHED), 129 PAUSE_DURATION_BEFORE_SPEAKING_MSEC); 130 mState = STATE_PAUSING; 131 } else { 132 if (DBG) log("MessageEmpty = " + (mMessageBody == null) + 133 ", mTtsEngineReady = " + mTtsEngineReady + 134 ", mTtsLanguageSupported = " + mTtsLanguageSupported); 135 stopSelf(); 136 mState = STATE_IDLE; 137 } 138 // Set alert reminder depending on user preference 139 CellBroadcastAlertReminder.queueAlertReminder(getApplicationContext(), true); 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 onInit(int status)188 public void onInit(int status) { 189 if (VDBG) 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 */ setTtsLanguage()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 onUtteranceCompleted(String utteranceId)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 onCreate()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 onDestroy()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 the screen bright wakelock acquired by CellBroadcastAlertService 282 CellBroadcastAlertWakeLock.releaseScreenBrightWakeLock(); 283 } 284 285 @Override onBind(Intent intent)286 public IBinder onBind(Intent intent) { 287 return null; 288 } 289 290 @Override onStartCommand(Intent intent, int flags, int startId)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 // Get config of whether to always sound CBS alerts at full volume. 304 mUseFullVolume = PreferenceManager.getDefaultSharedPreferences(this) 305 .getBoolean(CellBroadcastSettings.KEY_USE_FULL_VOLUME, false); 306 307 // retrieve the vibrate settings from cellbroadcast receiver settings. 308 mEnableVibrate = intent.getBooleanExtra(ALERT_AUDIO_VIBRATE_EXTRA, true); 309 switch (mAudioManager.getRingerMode()) { 310 case AudioManager.RINGER_MODE_SILENT: 311 if (DBG) log("Ringer mode: silent"); 312 mEnableAudio = false; 313 // If the device is in silent mode, do not vibrate (except ETWS). 314 if (!intent.getBooleanExtra(ALERT_AUDIO_ETWS_VIBRATE_EXTRA, false)) { 315 mEnableVibrate = false; 316 } 317 break; 318 case AudioManager.RINGER_MODE_VIBRATE: 319 if (DBG) log("Ringer mode: vibrate"); 320 mEnableAudio = false; 321 break; 322 case AudioManager.RINGER_MODE_NORMAL: 323 default: 324 if (DBG) log("Ringer mode: normal"); 325 mEnableAudio = true; 326 break; 327 } 328 329 if (mUseFullVolume) { 330 mEnableAudio = true; 331 } 332 333 if (mMessageBody != null && mEnableAudio) { 334 if (mTts == null) { 335 mTts = new TextToSpeech(this, this); 336 } else if (mTtsEngineReady) { 337 setTtsLanguage(); 338 } 339 } 340 341 if (mEnableAudio || mEnableVibrate) { 342 AlertType alertType = AlertType.CMAS_DEFAULT; 343 if (intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE) != null) { 344 alertType = (AlertType) intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE); 345 } 346 playAlertTone(alertType); 347 } else { 348 stopSelf(); 349 return START_NOT_STICKY; 350 } 351 352 // Record the initial call state here so that the new alarm has the 353 // newest state. 354 mInitialCallState = mTelephonyManager.getCallState(); 355 356 return START_STICKY; 357 } 358 359 // Volume suggested by media team for in-call alarms. 360 private static final float IN_CALL_VOLUME = 0.125f; 361 362 /** 363 * Start playing the alert sound. 364 * @param alertType the alert type (e.g. default, earthquake, tsunami, etc..) 365 */ playAlertTone(AlertType alertType)366 private void playAlertTone(AlertType alertType) { 367 // stop() checks to see if we are already playing. 368 stop(); 369 370 log("playAlertTone: alertType=" + alertType); 371 372 // Vibration duration in milliseconds 373 long vibrateDuration = 0; 374 375 int customAlertDuration = getResources().getInteger(R.integer.alert_duration); 376 377 // Start the vibration first. 378 if (mEnableVibrate) { 379 380 int[] patternArray = getApplicationContext().getResources(). 381 getIntArray(R.array.default_vibration_pattern); 382 long[] vibrationPattern = new long[patternArray.length]; 383 384 for (int i = 0; i < patternArray.length; i++) { 385 vibrationPattern[i] = patternArray[i]; 386 vibrateDuration += patternArray[i]; 387 } 388 mVibrator.vibrate(vibrationPattern, 0); 389 } 390 391 392 if (mEnableAudio) { 393 // future optimization: reuse media player object 394 mMediaPlayer = new MediaPlayer(); 395 mMediaPlayer.setOnErrorListener(new OnErrorListener() { 396 public boolean onError(MediaPlayer mp, int what, int extra) { 397 loge("Error occurred while playing audio."); 398 mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED)); 399 return true; 400 } 401 }); 402 403 // If the duration is specified by the config, use the specified duration. Otherwise, 404 // just play the alert tone with the tone's duration. 405 if (customAlertDuration >= 0) { 406 mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED), 407 customAlertDuration); 408 } else { 409 mMediaPlayer.setOnCompletionListener(new OnCompletionListener() { 410 public void onCompletion(MediaPlayer mp) { 411 if (DBG) log("Audio playback complete."); 412 mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED)); 413 return; 414 } 415 }); 416 } 417 418 try { 419 log("Locale=" + getResources().getConfiguration().getLocales()); 420 421 // Load the tones based on type 422 switch (alertType) { 423 case EARTHQUAKE: 424 setDataSourceFromResource(getResources(), mMediaPlayer, 425 R.raw.etws_earthquake); 426 break; 427 case TSUNAMI: 428 setDataSourceFromResource(getResources(), mMediaPlayer, 429 R.raw.etws_tsunami); 430 break; 431 case OTHER: 432 setDataSourceFromResource(getResources(), mMediaPlayer, 433 R.raw.etws_other_disaster); 434 break; 435 case ETWS_DEFAULT: 436 setDataSourceFromResource(getResources(), mMediaPlayer, 437 R.raw.etws_default); 438 break; 439 case CMAS_DEFAULT: 440 default: 441 setDataSourceFromResource(getResources(), mMediaPlayer, 442 R.raw.cmas_default); 443 } 444 445 // start playing alert audio (unless master volume is vibrate only or silent). 446 mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM, 447 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 448 449 setAlertAudioAttributes(); 450 setAlertVolume(); 451 452 // If we are using the custom alert duration, set looping to true so we can repeat 453 // the alert. The tone playing will stop when ALERT_SOUND_FINISHED arrives. 454 // Otherwise we just play the alert tone once. 455 mMediaPlayer.setLooping(customAlertDuration >= 0); 456 mMediaPlayer.prepare(); 457 mMediaPlayer.start(); 458 459 } catch (Exception ex) { 460 loge("Failed to play alert sound: " + ex); 461 // Immediately move into the next state ALERT_SOUND_FINISHED. 462 mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED)); 463 } 464 } else { 465 // In normal mode (playing tone + vibration), this service will stop after audio 466 // playback is done. However, if the device is in vibrate only mode, we need to stop 467 // the service right after vibration because there won't be any audio complete callback 468 // to stop the service. Unfortunately it's not like MediaPlayer has onCompletion() 469 // callback that we can use, we'll have to use our own timer to stop the service. 470 mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED), 471 customAlertDuration >= 0 ? customAlertDuration : vibrateDuration); 472 } 473 474 mState = STATE_ALERTING; 475 } 476 setDataSourceFromResource(Resources resources, MediaPlayer player, int res)477 private static void setDataSourceFromResource(Resources resources, 478 MediaPlayer player, int res) throws java.io.IOException { 479 AssetFileDescriptor afd = resources.openRawResourceFd(res); 480 if (afd != null) { 481 player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), 482 afd.getLength()); 483 afd.close(); 484 } 485 } 486 487 /** 488 * Stops alert audio and speech. 489 */ stop()490 public void stop() { 491 if (DBG) log("stop()"); 492 493 mHandler.removeMessages(ALERT_SOUND_FINISHED); 494 mHandler.removeMessages(ALERT_PAUSE_FINISHED); 495 496 resetAlarmStreamVolume(); 497 498 if (mState == STATE_ALERTING) { 499 // Stop audio playing 500 if (mMediaPlayer != null) { 501 try { 502 mMediaPlayer.stop(); 503 mMediaPlayer.release(); 504 } catch (IllegalStateException e) { 505 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 506 loge("exception trying to stop media player"); 507 } 508 mMediaPlayer = null; 509 } 510 511 // Stop vibrator 512 mVibrator.cancel(); 513 } else if (mState == STATE_SPEAKING && mTts != null) { 514 try { 515 mTts.stop(); 516 } catch (IllegalStateException e) { 517 // catch "Unable to retrieve AudioTrack pointer for stop()" exception 518 loge("exception trying to stop text-to-speech"); 519 } 520 } 521 mState = STATE_IDLE; 522 } 523 524 /** 525 * Set AudioAttributes for mMediaPlayer. Replacement of deprecated 526 * mMediaPlayer.setAudioStreamType. 527 */ setAlertAudioAttributes()528 private void setAlertAudioAttributes() { 529 AudioAttributes.Builder builder = new AudioAttributes.Builder(); 530 531 builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION); 532 builder.setUsage(AudioAttributes.USAGE_ALARM); 533 if (mUseFullVolume) { 534 // Set FLAG_BYPASS_INTERRUPTION_POLICY and FLAG_BYPASS_MUTE so that it enables 535 // audio in any DnD mode, even in total silence DnD mode (requires MODIFY_PHONE_STATE). 536 builder.setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY 537 | AudioAttributes.FLAG_BYPASS_MUTE); 538 } 539 540 mMediaPlayer.setAudioAttributes(builder.build()); 541 } 542 543 /** 544 * Set volume for alerts. 545 */ setAlertVolume()546 private void setAlertVolume() { 547 if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE 548 || isOnEarphone()) { 549 // If we are in a call, play the alert 550 // sound at a low volume to not disrupt the call. 551 log("in call: reducing volume"); 552 mMediaPlayer.setVolume(IN_CALL_VOLUME); 553 } else if (mUseFullVolume) { 554 // If use_full_volume is configured, 555 // we overwrite volume setting of STREAM_ALARM to full, play at 556 // max possible volume, and reset it after it's finished. 557 setAlarmStreamVolumeToFull(); 558 } 559 } 560 isOnEarphone()561 private boolean isOnEarphone() { 562 AudioDeviceInfo[] deviceList = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); 563 564 for (AudioDeviceInfo devInfo : deviceList) { 565 int type = devInfo.getType(); 566 if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET 567 || type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES 568 || type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO 569 || type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) { 570 return true; 571 } 572 } 573 574 return false; 575 } 576 577 /** 578 * Set volume of STREAM_ALARM to full. 579 */ setAlarmStreamVolumeToFull()580 private void setAlarmStreamVolumeToFull() { 581 log("setting alarm volume to full for cell broadcast alerts."); 582 mUserSetAlarmVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM); 583 mResetAlarmVolumeNeeded = true; 584 mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, 585 mAudioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), 586 0); 587 } 588 589 /** 590 * Reset volume of STREAM_ALARM, if needed. 591 */ resetAlarmStreamVolume()592 private void resetAlarmStreamVolume() { 593 if (mResetAlarmVolumeNeeded) { 594 log("resetting alarm volume to back to " + mUserSetAlarmVolume); 595 mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, mUserSetAlarmVolume, 0); 596 mResetAlarmVolumeNeeded = false; 597 } 598 } 599 log(String msg)600 private static void log(String msg) { 601 Log.d(TAG, msg); 602 } 603 loge(String msg)604 private static void loge(String msg) { 605 Log.e(TAG, msg); 606 } 607 } 608