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