Add audio mode and audio focus support to Telecomm

This CL adds support for:
  - audio focus
  - switching to speaker phone
  - mute
  - wired headset

Bluetooth support is coming in a different CL.

Change-Id: Iaf4013ea747548451cff45b48bcbb5b1dd1b8498
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
index 26987ba..b8d980b 100644
--- a/src/com/android/telecomm/CallAudioManager.java
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -18,176 +18,270 @@
 
 import android.content.Context;
 import android.media.AudioManager;
+import android.telecomm.CallAudioState;
 import android.telecomm.CallState;
 
-import com.google.common.collect.Lists;
-
-import java.util.List;
+import com.google.common.base.Preconditions;
 
 /**
  * This class manages audio modes, streams and other properties.
  */
 final class CallAudioManager extends CallsManagerListenerBase {
-    private AsyncRingtonePlayer mRinger = new AsyncRingtonePlayer();
+    private static final int STREAM_NONE = -1;
 
-    private boolean mHasAudioFocus = false;
+    private final AudioManager mAudioManager;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private CallAudioState mAudioState;
+    private int mAudioFocusStreamType;
+    private boolean mIsRinging;
+    private boolean mWasSpeakerOn;
 
-    /**
-     * Used to keep ordering of unanswered incoming calls. The existence of multiple call services
-     * means that there can easily exist multiple incoming calls and explicit ordering is useful for
-     * maintaining the proper state of the ringer.
-     */
-    private final List<String> mUnansweredCallIds = Lists.newLinkedList();
+    CallAudioManager() {
+        Context context = TelecommApp.getInstance();
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mWiredHeadsetManager = new WiredHeadsetManager(this);
+        mAudioState = getInitialAudioState();
+        mAudioFocusStreamType = STREAM_NONE;
+    }
 
-    /**
-     * Denotes when the ringer is disabled. This is useful in temporarily disabling the ringer when
-     * the a call is answered/rejected by the user, but the call hasn't actually moved out of the
-     * ringing state.
-     */
-    private boolean mIsRingingDisabled = false;
+    CallAudioState getAudioState() {
+        return mAudioState;
+    }
 
     @Override
     public void onCallAdded(Call call) {
-        if (call.getState() == CallState.RINGING) {
-            mUnansweredCallIds.add(call.getId());
+        updateAudioStreamAndMode();
+        if (CallsManager.getInstance().getCalls().size() == 1) {
+            Log.v(this, "first call added, reseting system audio to default state");
+            setInitialAudioState();
+        } else if (!call.isIncoming()) {
+            // Unmute new outgoing call.
+            setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
         }
-        updateAudio();
     }
 
     @Override
     public void onCallRemoved(Call call) {
-        removeFromUnansweredCallIds(call.getId());
-        updateAudio();
+        if (CallsManager.getInstance().getCalls().isEmpty()) {
+            Log.v(this, "all calls removed, reseting system audio to default state");
+            setInitialAudioState();
+        }
+        updateAudioStreamAndMode();
     }
 
     @Override
     public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
-        if (oldState == CallState.RINGING) {
-            removeFromUnansweredCallIds(call.getId());
-        }
-
-        updateAudio();
+        updateAudioStreamAndMode();
     }
 
     @Override
     public void onIncomingCallAnswered(Call call) {
-        mIsRingingDisabled = true;
-        updateAudio();
+        // Unmute new incoming call.
+        setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
     }
 
     @Override
-    public void onIncomingCallRejected(Call call) {
-        mIsRingingDisabled = true;
-        updateAudio();
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+        updateAudioStreamAndMode();
+        // Ensure that the foreground call knows about the latest audio state.
+        updateAudioForForegroundCall();
     }
 
-    /**
-     * Reads the current state of all calls from CallsManager and sets the appropriate audio modes
-     * as well as triggers the start/stop of the ringer.
-     */
-    private void updateAudio() {
-        CallsManager callsManager = CallsManager.getInstance();
+    void mute(boolean shouldMute) {
+        Log.v(this, "mute, shouldMute: %b", shouldMute);
 
-        boolean hasRingingCall = !mIsRingingDisabled && !mUnansweredCallIds.isEmpty();
-        boolean hasLiveCall = callsManager.hasCallWithState(CallState.ACTIVE, CallState.DIALING);
-
-        int mode = hasRingingCall ? AudioManager.MODE_RINGTONE :
-               hasLiveCall ? AudioManager.MODE_IN_CALL :
-               AudioManager.MODE_NORMAL;
-
-        boolean needsFocus = (mode != AudioManager.MODE_NORMAL);
-
-        // Acquiring focus needs to be first, unlike releasing focus, which happens at the end.
-        if (needsFocus) {
-            acquireFocus(hasRingingCall);
-            setMode(mode);
+        // Don't mute if there are any emergency calls.
+        if (CallsManager.getInstance().hasEmergencyCall()) {
+            shouldMute = false;
+            Log.v(this, "ignoring mute for emergency call");
         }
 
-        if (hasRingingCall) {
-            mRinger.play();
-        } else {
-            mRinger.stop();
-        }
-
-        if (!needsFocus && mHasAudioFocus) {
-            setMode(AudioManager.MODE_NORMAL);
-            releaseFocus();
+        if (mAudioState.isMuted != shouldMute) {
+            setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
         }
     }
 
     /**
-     * Acquires audio focus.
+     * Changed the audio route, for example from earpiece to speaker phone.
      *
-     * @param isForRinging True if this focus is for playing the ringer.
+     * @param route The new audio route to use. See {@link CallAudioState}.
      */
-    private void acquireFocus(boolean isForRinging) {
-        if (!mHasAudioFocus) {
-            int stream = isForRinging ? AudioManager.STREAM_RING : AudioManager.STREAM_VOICE_CALL;
+    void setAudioRoute(int route) {
+        Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
 
-            AudioManager audioManager = getAudioManager();
-            audioManager.requestAudioFocusForCall(stream, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-            audioManager.setMicrophoneMute(false);
-            audioManager.setSpeakerphoneOn(false);
-            mHasAudioFocus = true;
+        // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+        int newRoute = selectWiredOrEarpiece(route, mAudioState.supportedRouteMask);
+
+        // If route is unsupported, do nothing.
+        if ((mAudioState.supportedRouteMask | newRoute) == 0) {
+            Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
+            return;
+        }
+
+        if (mAudioState.route != newRoute) {
+            // Remember the new speaker state so it can be restored when the user plugs and unplugs
+            // a headset.
+            mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
+            setSystemAudioState(mAudioState.isMuted, newRoute, mAudioState.supportedRouteMask);
+        }
+    }
+
+    void setIsRinging(boolean isRinging) {
+        if (mIsRinging != isRinging) {
+            Log.v(this, "setIsRinging %b -> %b", mIsRinging, isRinging);
+            mIsRinging = isRinging;
+            updateAudioStreamAndMode();
         }
     }
 
     /**
-     * Releases focus.
-     */
-    void releaseFocus() {
-        if (mHasAudioFocus) {
-            AudioManager audioManager = getAudioManager();
+      * Updates the audio route when the headset plugged in state changes. For example, if audio is
+      * being routed over speakerphone and a headset is plugged in then switch to wired headset.
+      */
+    void onHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+        int newRoute = CallAudioState.ROUTE_EARPIECE;
+        if (newIsPluggedIn) {
+            newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
+        } else if (mWasSpeakerOn) {
+            Call call = CallsManager.getInstance().getForegroundCall();
+            if (call != null && call.isAlive()) {
+                // Restore the speaker state.
+                newRoute = CallAudioState.ROUTE_SPEAKER;
+            }
+        }
+        setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
+    }
 
-            // Reset speakerphone and mute in case they were changed by telecomm.
-            audioManager.setMicrophoneMute(false);
-            audioManager.setSpeakerphoneOn(false);
-            audioManager.abandonAudioFocusForCall();
+    private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
+        CallAudioState oldAudioState = mAudioState;
+        mAudioState = new CallAudioState(isMuted, route, supportedRouteMask);
+        Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
 
-            mHasAudioFocus = false;
-            Log.v(this, "Focus released");
+        // Mute.
+        if (mAudioState.isMuted != mAudioManager.isMicrophoneMute()) {
+            Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted);
+            mAudioManager.setMicrophoneMute(mAudioState.isMuted);
         }
 
+        // Audio route.
+        if (mAudioState.route == CallAudioState.ROUTE_SPEAKER) {
+            if (!mAudioManager.isSpeakerphoneOn()) {
+                Log.i(this, "turning speaker phone on");
+                mAudioManager.setSpeakerphoneOn(true);
+            }
+        } else if (mAudioState.route == CallAudioState.ROUTE_EARPIECE ||
+                mAudioState.route == CallAudioState.ROUTE_WIRED_HEADSET) {
+            // Wired headset and earpiece work the same way
+            if (mAudioManager.isSpeakerphoneOn()) {
+                Log.i(this, "turning speaker phone off");
+                mAudioManager.setSpeakerphoneOn(false);
+            }
+        }
+
+        if (!oldAudioState.equals(mAudioState)) {
+            CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
+            updateAudioForForegroundCall();
+        }
+    }
+
+    private void updateAudioStreamAndMode() {
+        Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b", mIsRinging);
+        if (mIsRinging) {
+            requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
+        } else {
+            Call call = CallsManager.getInstance().getForegroundCall();
+            if (call != null) {
+                int mode = TelephonyUtil.isCurrentlyPSTNCall(call) ?
+                        AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION;
+                requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
+            } else {
+                abandonAudioFocus();
+            }
+        }
+    }
+
+    private void requestAudioFocusAndSetMode(int stream, int mode) {
+        Log.v(this, "setSystemAudioStreamAndMode, stream: %d -> %d", mAudioFocusStreamType, stream);
+        Preconditions.checkState(stream != STREAM_NONE);
+
+        // Only request audio focus once. If the stream type changes there's no need to abandon
+        // and re-request audio focus.  The system doesn't really care about the stream we requested
+        // focus for so just silently switch.
+        if (mAudioFocusStreamType == STREAM_NONE) {
+            Log.v(this, "requesting audio focus for stream: %d", stream);
+            mAudioManager.requestAudioFocusForCall(stream,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        }
+        mAudioFocusStreamType = stream;
+        setMode(mode);
+    }
+
+    private void abandonAudioFocus() {
+        if (mAudioFocusStreamType != STREAM_NONE) {
+            setMode(AudioManager.MODE_NORMAL);
+            Log.v(this, "abandoning audio focus");
+            mAudioManager.abandonAudioFocusForCall();
+            mAudioFocusStreamType = STREAM_NONE;
+        }
     }
 
     /**
      * Sets the audio mode.
      *
-     * @param mode Mode constant from AudioManager.MODE_*.
+     * @param newMode Mode constant from AudioManager.MODE_*.
      */
-    void setMode(int mode) {
-        if (mHasAudioFocus) {
-            AudioManager audioManager = getAudioManager();
-            if (mode != audioManager.getMode()) {
-                Log.v(this, "Audio mode set to %d.", mode);
-                audioManager.setMode(mode);
-                Log.v(this, "Audio mode actually set to %d.", audioManager.getMode());
+    private void setMode(int newMode) {
+        Preconditions.checkState(mAudioFocusStreamType != STREAM_NONE);
+        int oldMode = mAudioManager.getMode();
+        Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
+        if (oldMode != newMode) {
+            mAudioManager.setMode(newMode);
+        }
+    }
+
+    private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
+        // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+        // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
+        // supported before calling setAudioRoute.
+        if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
+            route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
+            if (route == 0) {
+                Log.wtf(this, "One of wired headset or earpiece should always be valid.");
+                // assume earpiece in this case.
+                route = CallAudioState.ROUTE_EARPIECE;
             }
+        }
+        return route;
+    }
+
+    private int calculateSupportedRoutes() {
+        int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
         } else {
-            Log.wtf(this, "Trying to set audio mode to %d without focus.", mode);
+            routeMask |= CallAudioState.ROUTE_EARPIECE;
         }
+
+        return routeMask;
     }
 
-    /**
-     * Removes the specified call from the list of unanswered incoming calls.
-     *
-     * @param callId The ID of the call.
-     */
-    private void removeFromUnansweredCallIds(String callId) {
-        if (!mUnansweredCallIds.isEmpty()) {
-            // If the call is the top-most call, then no longer disable the ringer.
-            if (callId.equals(mUnansweredCallIds.get(0))) {
-                mIsRingingDisabled = false;
-            }
-
-            mUnansweredCallIds.remove(callId);
-        }
+    private CallAudioState getInitialAudioState() {
+        int supportedRouteMask = calculateSupportedRoutes();
+        return new CallAudioState(false,
+                selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask),
+                supportedRouteMask);
     }
 
-    /**
-     * Returns the system audio manager.
-     */
-    private AudioManager getAudioManager() {
-        return (AudioManager) TelecommApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
+    private void setInitialAudioState() {
+        CallAudioState audioState = getInitialAudioState();
+        setSystemAudioState(audioState.isMuted, audioState.route, audioState.supportedRouteMask);
+    }
+
+    private void updateAudioForForegroundCall() {
+        Call call = CallsManager.getInstance().getForegroundCall();
+        if (call != null && call.getCallService() != null) {
+            call.getCallService().onAudioStateChanged(call.getId(), mAudioState);
+        }
     }
 }