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