Adds extended audio state logs to Android audio.

NOTRY=TRUE

Bug: webrtc:8583
Change-Id: I2e9cb9354cc77c597a308b1f6c519c015a263842
Reviewed-on: https://webrtc-review.googlesource.com/25826
Commit-Queue: Henrik Andreassson <henrika@webrtc.org>
Reviewed-by: Sami Kalliomäki <sakal@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#20934}
diff --git a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioManager.java b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioManager.java
index 12b2fad..08979aa 100644
--- a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioManager.java
+++ b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioManager.java
@@ -89,11 +89,6 @@
 
   private static final int DEFAULT_FRAME_PER_BUFFER = 256;
 
-  // List of possible audio modes.
-  private static final String[] AUDIO_MODES = new String[] {
-      "MODE_NORMAL", "MODE_RINGTONE", "MODE_IN_CALL", "MODE_IN_COMMUNICATION",
-  };
-
   // Private utility class that periodically checks and logs the volume level
   // of the audio stream that is currently controlled by the volume control.
   // A timer triggers logs once every 30 seconds and the timer's associated
@@ -189,7 +184,8 @@
     if (initialized) {
       return true;
     }
-    Logging.d(TAG, "audio mode is: " + AUDIO_MODES[audioManager.getMode()]);
+    Logging.d(TAG, "audio mode is: "
+        + WebRtcAudioUtils.modeToString(audioManager.getMode()));
     initialized = true;
     volumeLogger.start();
     return true;
diff --git a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioRecord.java b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioRecord.java
index f9bdc5d..73cf35d 100644
--- a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioRecord.java
+++ b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioRecord.java
@@ -11,7 +11,6 @@
 package org.webrtc.voiceengine;
 
 import android.annotation.TargetApi;
-import android.content.Context;
 import android.media.AudioFormat;
 import android.media.AudioRecord;
 import android.media.MediaRecorder.AudioSource;
@@ -19,7 +18,6 @@
 import java.lang.System;
 import java.nio.ByteBuffer;
 import java.util.concurrent.TimeUnit;
-import org.webrtc.ContextUtils;
 import org.webrtc.Logging;
 import org.webrtc.ThreadUtils;
 
@@ -252,6 +250,7 @@
     audioThread.stopThread();
     if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS)) {
       Logging.e(TAG, "Join of AudioRecordJavaThread timed out");
+      WebRtcAudioUtils.logAudioState(TAG);
     }
     audioThread = null;
     if (effects != null) {
@@ -320,6 +319,7 @@
 
   private void reportWebRtcAudioRecordInitError(String errorMessage) {
     Logging.e(TAG, "Init recording error: " + errorMessage);
+    WebRtcAudioUtils.logAudioState(TAG);
     if (errorCallback != null) {
       errorCallback.onWebRtcAudioRecordInitError(errorMessage);
     }
@@ -328,6 +328,7 @@
   private void reportWebRtcAudioRecordStartError(
       AudioRecordStartErrorCode errorCode, String errorMessage) {
     Logging.e(TAG, "Start recording error: " + errorCode + ". " + errorMessage);
+    WebRtcAudioUtils.logAudioState(TAG);
     if (errorCallback != null) {
       errorCallback.onWebRtcAudioRecordStartError(errorCode, errorMessage);
     }
@@ -335,6 +336,7 @@
 
   private void reportWebRtcAudioRecordError(String errorMessage) {
     Logging.e(TAG, "Run-time recording error: " + errorMessage);
+    WebRtcAudioUtils.logAudioState(TAG);
     if (errorCallback != null) {
       errorCallback.onWebRtcAudioRecordError(errorMessage);
     }
diff --git a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioTrack.java b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioTrack.java
index ae7a16f..1d2f4af 100644
--- a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioTrack.java
+++ b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioTrack.java
@@ -339,6 +339,7 @@
     audioThread.interrupt();
     if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_TRACK_THREAD_JOIN_TIMEOUT_MS)) {
       Logging.e(TAG, "Join of AudioTrackThread timed out.");
+      WebRtcAudioUtils.logAudioState(TAG);
     }
     Logging.d(TAG, "AudioTrackThread has now been stopped.");
     audioThread = null;
@@ -491,6 +492,7 @@
 
   private void reportWebRtcAudioTrackInitError(String errorMessage) {
     Logging.e(TAG, "Init playout error: " + errorMessage);
+    WebRtcAudioUtils.logAudioState(TAG);
     if (errorCallback != null) {
       errorCallbackOld.onWebRtcAudioTrackInitError(errorMessage);
     }
@@ -502,6 +504,7 @@
   private void reportWebRtcAudioTrackStartError(
       AudioTrackStartErrorCode errorCode, String errorMessage) {
     Logging.e(TAG, "Start playout error: "  + errorCode + ". " + errorMessage);
+    WebRtcAudioUtils.logAudioState(TAG);
     if (errorCallback != null) {
       errorCallbackOld.onWebRtcAudioTrackStartError(errorMessage);
     }
@@ -512,6 +515,7 @@
 
   private void reportWebRtcAudioTrackError(String errorMessage) {
     Logging.e(TAG, "Run-time playback error: " + errorMessage);
+    WebRtcAudioUtils.logAudioState(TAG);
     if (errorCallback != null) {
       errorCallbackOld.onWebRtcAudioTrackError(errorMessage);
     }
diff --git a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioUtils.java b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioUtils.java
index 3d66923..da3e1f0 100644
--- a/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioUtils.java
+++ b/modules/audio_device/android/java/src/org/webrtc/voiceengine/WebRtcAudioUtils.java
@@ -10,13 +10,25 @@
 
 package org.webrtc.voiceengine;
 
+import static android.media.AudioManager.MODE_IN_CALL;
+import static android.media.AudioManager.MODE_IN_COMMUNICATION;
+import static android.media.AudioManager.MODE_NORMAL;
+import static android.media.AudioManager.MODE_RINGTONE;
+
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioRecordingConfiguration;
+import android.media.MediaRecorder.AudioSource;
 import android.os.Build;
 import android.os.Process;
 import java.lang.Thread;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
+import org.webrtc.ContextUtils;
 import org.webrtc.Logging;
 
 public final class WebRtcAudioUtils {
@@ -196,7 +208,7 @@
   }
 
   // Information about the current build, taken from system properties.
-  public static void logDeviceInfo(String tag) {
+  static void logDeviceInfo(String tag) {
     Logging.d(tag, "Android SDK: " + Build.VERSION.SDK_INT + ", "
             + "Release: " + Build.VERSION.RELEASE + ", "
             + "Brand: " + Build.BRAND + ", "
@@ -207,4 +219,194 @@
             + "Model: " + Build.MODEL + ", "
             + "Product: " + Build.PRODUCT);
   }
+
+  // Logs information about the current audio state. The idea is to call this
+  // method when errors are detected to log under what conditions the error
+  // occurred. Hopefully it will provide clues to what might be the root cause.
+  static void logAudioState(String tag) {
+    logDeviceInfo(tag);
+    final Context context = ContextUtils.getApplicationContext();
+    final AudioManager audioManager =
+        (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+    logAudioStateBasic(tag, audioManager);
+    logAudioStateVolume(tag, audioManager);
+    logAudioDeviceInfo(tag, audioManager);
+  }
+
+  // Reports basic audio statistics.
+  private static void logAudioStateBasic(String tag, AudioManager audioManager) {
+    Logging.d(tag, "Audio State: "
+            + "audio mode: " + modeToString(audioManager.getMode()) + ", "
+            + "has mic: " + hasMicrophone() + ", "
+            + "mic muted: " + audioManager.isMicrophoneMute() + ", "
+            + "music active: " + audioManager.isMusicActive() + ", "
+            + "speakerphone: " + audioManager.isSpeakerphoneOn() + ", "
+            + "BT SCO: " + audioManager.isBluetoothScoOn());
+  }
+
+  // Adds volume information for all possible stream types.
+  private static void logAudioStateVolume(String tag, AudioManager audioManager) {
+    final int[] streams = {
+        AudioManager.STREAM_VOICE_CALL,
+        AudioManager.STREAM_MUSIC,
+        AudioManager.STREAM_RING,
+        AudioManager.STREAM_ALARM,
+        AudioManager.STREAM_NOTIFICATION,
+        AudioManager.STREAM_SYSTEM
+    };
+    Logging.d(tag, "Audio State: ");
+    boolean fixedVolume = false;
+    if (WebRtcAudioUtils.runningOnLollipopOrHigher()) {
+      fixedVolume = audioManager.isVolumeFixed();
+      // Some devices may not have volume controls and might use a fixed volume.
+      Logging.d(tag, "  fixed volume=" + fixedVolume);
+    }
+    if (!fixedVolume) {
+      for (int stream : streams) {
+        StringBuilder info = new StringBuilder();
+        info.append("  " + streamTypeToString(stream) + ": ");
+        info.append("volume=").append(audioManager.getStreamVolume(stream));
+        info.append(", max=").append(audioManager.getStreamMaxVolume(stream));
+        logIsStreamMute(tag, audioManager, stream, info);
+        Logging.d(tag, info.toString());
+      }
+    }
+  }
+
+  @TargetApi(23)
+  private static void logIsStreamMute(
+      String tag, AudioManager audioManager, int stream, StringBuilder info) {
+    if (WebRtcAudioUtils.runningOnMarshmallowOrHigher()) {
+      info.append(", muted=").append(audioManager.isStreamMute(stream));
+    }
+  }
+
+  @TargetApi(23)
+  private static void logAudioDeviceInfo(String tag, AudioManager audioManager) {
+    if (!WebRtcAudioUtils.runningOnMarshmallowOrHigher()) {
+      return;
+    }
+    final AudioDeviceInfo[] devices =
+        audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+    if (devices.length == 0) {
+      return;
+    }
+    Logging.d(tag, "Audio Devices: ");
+    for (AudioDeviceInfo device : devices) {
+      StringBuilder info = new StringBuilder();
+      info.append("  ").append(deviceTypeToString(device.getType()));
+      info.append(device.isSource() ? "(in): " : "(out): ");
+      // An empty array indicates that the device supports arbitrary channel counts.
+      if (device.getChannelCounts().length > 0) {
+        info.append("channels=").append(Arrays.toString(device.getChannelCounts()));
+        info.append(", ");
+      }
+      if (device.getEncodings().length > 0) {
+        // Examples: ENCODING_PCM_16BIT = 2, ENCODING_PCM_FLOAT = 4.
+        info.append("encodings=").append(Arrays.toString(device.getEncodings()));
+        info.append(", ");
+      }
+      if (device.getSampleRates().length > 0) {
+        info.append("sample rates=").append(Arrays.toString(device.getSampleRates()));
+        info.append(", ");
+      }
+      info.append("id=").append(device.getId());
+      Logging.d(tag, info.toString());
+    }
+  }
+
+  // Converts media.AudioManager modes into local string representation.
+  static String modeToString(int mode) {
+    switch (mode) {
+      case MODE_IN_CALL:
+        return "MODE_IN_CALL";
+      case MODE_IN_COMMUNICATION:
+        return "MODE_IN_COMMUNICATION";
+      case MODE_NORMAL:
+        return "MODE_NORMAL";
+      case MODE_RINGTONE:
+        return "MODE_RINGTONE";
+      default:
+        return "MODE_INVALID";
+    }
+  }
+
+  private static String streamTypeToString(int stream) {
+    switch(stream) {
+      case AudioManager.STREAM_VOICE_CALL:
+        return "STREAM_VOICE_CALL";
+      case AudioManager.STREAM_MUSIC:
+        return "STREAM_MUSIC";
+      case AudioManager.STREAM_RING:
+        return "STREAM_RING";
+      case AudioManager.STREAM_ALARM:
+        return "STREAM_ALARM";
+      case AudioManager.STREAM_NOTIFICATION:
+        return "STREAM_NOTIFICATION";
+      case AudioManager.STREAM_SYSTEM:
+        return "STREAM_SYSTEM";
+      default:
+        return "STREAM_INVALID";
+    }
+  }
+
+  // Converts AudioDeviceInfo types to local string representation.
+  private static String deviceTypeToString(int type) {
+    switch (type) {
+      case AudioDeviceInfo.TYPE_UNKNOWN:
+        return "TYPE_UNKNOWN";
+      case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
+        return "TYPE_BUILTIN_EARPIECE";
+      case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
+        return "TYPE_BUILTIN_SPEAKER";
+      case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+        return "TYPE_WIRED_HEADSET";
+      case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
+        return "TYPE_WIRED_HEADPHONES";
+      case AudioDeviceInfo.TYPE_LINE_ANALOG:
+        return "TYPE_LINE_ANALOG";
+      case AudioDeviceInfo.TYPE_LINE_DIGITAL:
+        return "TYPE_LINE_DIGITAL";
+      case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
+        return "TYPE_BLUETOOTH_SCO";
+      case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
+        return "TYPE_BLUETOOTH_A2DP";
+      case AudioDeviceInfo.TYPE_HDMI:
+        return "TYPE_HDMI";
+      case AudioDeviceInfo.TYPE_HDMI_ARC:
+        return "TYPE_HDMI_ARC";
+      case AudioDeviceInfo.TYPE_USB_DEVICE:
+        return "TYPE_USB_DEVICE";
+      case AudioDeviceInfo.TYPE_USB_ACCESSORY:
+        return "TYPE_USB_ACCESSORY";
+      case AudioDeviceInfo.TYPE_DOCK:
+        return "TYPE_DOCK";
+      case AudioDeviceInfo.TYPE_FM:
+        return "TYPE_FM";
+      case AudioDeviceInfo.TYPE_BUILTIN_MIC:
+        return "TYPE_BUILTIN_MIC";
+      case AudioDeviceInfo.TYPE_FM_TUNER:
+        return "TYPE_FM_TUNER";
+      case AudioDeviceInfo.TYPE_TV_TUNER:
+        return "TYPE_TV_TUNER";
+      case AudioDeviceInfo.TYPE_TELEPHONY:
+        return "TYPE_TELEPHONY";
+      case AudioDeviceInfo.TYPE_AUX_LINE:
+        return "TYPE_AUX_LINE";
+      case AudioDeviceInfo.TYPE_IP:
+        return "TYPE_IP";
+      case AudioDeviceInfo.TYPE_BUS:
+        return "TYPE_BUS";
+      case AudioDeviceInfo.TYPE_USB_HEADSET:
+        return "TYPE_USB_HEADSET";
+      default:
+        return "TYPE_UNKNOWN";
+    }
+  }
+
+  // Returns true if the device can record audio via a microphone.
+  private static boolean hasMicrophone() {
+    return ContextUtils.getApplicationContext().getPackageManager().hasSystemFeature(
+        PackageManager.FEATURE_MICROPHONE);
+  }
 }