Handle wired headset during voicemail playback.

- Switch to wired headset when plugged in even if playing on speaker
- Remember if wired headset overrode speaker so it can be turned back on
  when the headset is unplugged.
- Ensure proximity sensor is turned on iff the earpiece is the audio
  source.

Bug: 23816959

Change-Id: I952c24ee51139f21a17344acd7698c9ed8f52860
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 26e3965..b7f068e 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -362,6 +362,10 @@
         mAdapter.startCache();
 
         rescheduleDisplayUpdate();
+
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.onResume();
+        }
     }
 
     @Override
diff --git a/src/com/android/dialer/voicemail/VoicemailAudioManager.java b/src/com/android/dialer/voicemail/VoicemailAudioManager.java
index e64e180..267eeca 100644
--- a/src/com/android/dialer/voicemail/VoicemailAudioManager.java
+++ b/src/com/android/dialer/voicemail/VoicemailAudioManager.java
@@ -19,25 +19,36 @@
 import android.content.Context;
 import android.media.AudioManager;
 import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.telecom.CallAudioState;
 import android.util.Log;
 
+import java.util.Objects;
 import java.util.concurrent.RejectedExecutionException;
 
 /**
  * This class manages all audio changes for voicemail playback.
  */
-final class VoicemailAudioManager implements OnAudioFocusChangeListener {
+final class VoicemailAudioManager implements OnAudioFocusChangeListener,
+        WiredHeadsetManager.Listener {
     private static final String TAG = VoicemailAudioManager.class.getSimpleName();
 
     public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
 
     private AudioManager mAudioManager;
     private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+    private WiredHeadsetManager mWiredHeadsetManager;
+    private boolean mWasSpeakerOn;
+    private CallAudioState mCallAudioState;
 
     public VoicemailAudioManager(Context context,
             VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+        mWiredHeadsetManager = new WiredHeadsetManager(context);
+        mWiredHeadsetManager.setListener(this);
+
+        mCallAudioState = getInitialAudioState();
+        Log.i(TAG, "Initial audioState = " + mCallAudioState);
     }
 
     public void requestAudioFocus() {
@@ -60,14 +71,131 @@
         mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN);
     }
 
-    public void turnOnSpeaker(boolean on) {
+    @Override
+    public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+        Log.i(TAG, "wired headset was plugged in changed: " + oldIsPluggedIn
+                + " -> "+ newIsPluggedIn);
+
+        if (oldIsPluggedIn == newIsPluggedIn) {
+            return;
+        }
+
+        int newRoute = mCallAudioState.getRoute();  // start out with existing route
+        if (newIsPluggedIn) {
+            newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            if (mWasSpeakerOn) {
+                newRoute = CallAudioState.ROUTE_SPEAKER;
+            } else {
+                newRoute = CallAudioState.ROUTE_EARPIECE;
+            }
+        }
+
+        mVoicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER);
+
+        // We need to call this every time even if we do not change the route because the supported
+        // routes changed either to include or not include WIRED_HEADSET.
+        setSystemAudioState(
+                new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes()));
+    }
+
+    public void setSpeakerphoneOn(boolean on) {
+        setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE);
+    }
+
+    public boolean isWiredHeadsetPluggedIn() {
+        return mWiredHeadsetManager.isPluggedIn();
+    }
+
+    public void registerReceivers() {
+        // Receivers is plural because we expect to add bluetooth support.
+        mWiredHeadsetManager.registerReceiver();
+    }
+
+    public void unregisterReceivers() {
+        mWiredHeadsetManager.unregisterReceiver();
+    }
+
+    /**
+     * Change the audio route, for example from earpiece to speakerphone.
+     *
+     * @param route The new audio route to use. See {@link CallAudioState}.
+     */
+    void setAudioRoute(int route) {
+        Log.v(TAG, "setAudioRoute, route: " + CallAudioState.audioRouteToString(route));
+
+        // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+        int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());
+
+        // If route is unsupported, do nothing.
+        if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
+            Log.w(TAG, "Asking to set to a route that is unsupported: " + newRoute);
+            return;
+        }
+
+        if (mCallAudioState.getRoute() != 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(new CallAudioState(false /* muted */, newRoute,
+                    mCallAudioState.getSupportedRouteMask()));
+        }
+    }
+
+    private CallAudioState getInitialAudioState() {
+        int supportedRouteMask = calculateSupportedRoutes();
+        int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE,
+                supportedRouteMask);
+        return new CallAudioState(false /* muted */, route, supportedRouteMask);
+    }
+
+    private int calculateSupportedRoutes() {
+        int routeMask = CallAudioState.ROUTE_SPEAKER;
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            routeMask |= CallAudioState.ROUTE_EARPIECE;
+        }
+        return routeMask;
+    }
+
+    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 don't 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(TAG, "One of wired headset or earpiece should always be valid.");
+                // assume earpiece in this case.
+                route = CallAudioState.ROUTE_EARPIECE;
+            }
+        }
+        return route;
+    }
+
+    private void setSystemAudioState(CallAudioState callAudioState) {
+        CallAudioState oldAudioState = mCallAudioState;
+        mCallAudioState = callAudioState;
+
+        Log.i(TAG, "setSystemAudioState: changing from " + oldAudioState + " to "
+                + mCallAudioState);
+
+        // Audio route.
+        if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
+            turnOnSpeaker(true);
+        } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE ||
+                mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
+            // Just handle turning off the speaker, the system will handle switching between wired
+            // headset and earpiece.
+            turnOnSpeaker(false);
+        }
+    }
+
+    private void turnOnSpeaker(boolean on) {
         if (mAudioManager.isSpeakerphoneOn() != on) {
             Log.i(TAG, "turning speaker phone on: " + on);
             mAudioManager.setSpeakerphoneOn(on);
         }
     }
-
-    public boolean isSpeakerphoneOn() {
-        return mAudioManager.isSpeakerphoneOn();
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 7a2bffc..8b8b7c5 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -253,6 +253,7 @@
                 mPosition = 0;
                 // Default to earpiece.
                 setSpeakerphoneOn(false);
+                mVoicemailAudioManager.setSpeakerphoneOn(false);
             } else {
                 // Update the view to the current speakerphone state.
                 mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
@@ -313,9 +314,18 @@
     }
 
     /**
+     * Must be invoked when the parent activity is resumed.
+     */
+    public void onResume() {
+        mVoicemailAudioManager.registerReceivers();
+    }
+
+    /**
      * Must be invoked when the parent activity is paused.
      */
     public void onPause() {
+        mVoicemailAudioManager.unregisterReceivers();
+
         if (mContext != null && mIsPrepared
                 && mInitialOrientation != mContext.getResources().getConfiguration().orientation) {
             // If an orientation change triggers the pause, retain the MediaPlayer.
@@ -329,6 +339,7 @@
         if (mActivity != null) {
             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
         }
+
     }
 
     /**
@@ -637,9 +648,8 @@
                 // Grab audio focus.
                 // Can throw RejectedExecutionException.
                 mVoicemailAudioManager.requestAudioFocus();
-
-                setSpeakerphoneOn(mIsSpeakerphoneOn);
                 mMediaPlayer.start();
+                setSpeakerphoneOn(mIsSpeakerphoneOn);
             } catch (RejectedExecutionException e) {
                 handleError(e);
             }
@@ -725,21 +735,29 @@
         }
     }
 
+    /**
+     * This is for use by UI interactions only. It simplifies UI logic.
+     */
     public void toggleSpeakerphone() {
+        mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
         setSpeakerphoneOn(!mIsSpeakerphoneOn);
     }
 
-    private void setSpeakerphoneOn(boolean on) {
+    /**
+     * This method only handles app-level changes to the speakerphone. Audio layer changes should
+     * be handled separately. This is so that the VoicemailAudioManager can trigger changes to
+     * the presenter without the presenter triggering the audio manager and duplicating actions.
+     */
+    public void setSpeakerphoneOn(boolean on) {
         mView.onSpeakerphoneOn(on);
-        if (mIsSpeakerphoneOn == on) {
-            return;
-        }
 
         mIsSpeakerphoneOn = on;
-        mVoicemailAudioManager.turnOnSpeaker(on);
 
+        // This should run even if speakerphone is not being toggled because we may be switching
+        // from earpiece to headphone and vise versa. Also upon initial setup the default audio
+        // source is the earpiece, so we want to trigger the proximity sensor.
         if (mIsPlaying) {
-            if (on) {
+            if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) {
                 disableProximitySensor(false /* waitForFarState */);
                 if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
                     mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
diff --git a/src/com/android/dialer/voicemail/WiredHeadsetManager.java b/src/com/android/dialer/voicemail/WiredHeadsetManager.java
new file mode 100644
index 0000000..7351f4f
--- /dev/null
+++ b/src/com/android/dialer/voicemail/WiredHeadsetManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.voicemail;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.util.Log;
+
+/** Listens for and caches headset state. */
+class WiredHeadsetManager {
+    private static final String TAG = WiredHeadsetManager.class.getSimpleName();
+
+    interface Listener {
+        void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
+    }
+
+    /** Receiver for wired headset plugged and unplugged events. */
+    private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (AudioManager.ACTION_HEADSET_PLUG.equals(intent.getAction())) {
+                boolean isPluggedIn = intent.getIntExtra("state", 0) == 1;
+                Log.v(TAG, "ACTION_HEADSET_PLUG event, plugged in: " + isPluggedIn);
+                onHeadsetPluggedInChanged(isPluggedIn);
+            }
+        }
+    }
+
+    private final WiredHeadsetBroadcastReceiver mReceiver;
+    private boolean mIsPluggedIn;
+    private Listener mListener;
+    private Context mContext;
+
+    WiredHeadsetManager(Context context) {
+        mContext = context;
+        mReceiver = new WiredHeadsetBroadcastReceiver();
+
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mIsPluggedIn = audioManager.isWiredHeadsetOn();
+
+    }
+
+    void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    boolean isPluggedIn() {
+        return mIsPluggedIn;
+    }
+
+    void registerReceiver() {
+        // Register for misc other intent broadcasts.
+        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+        mContext.registerReceiver(mReceiver, intentFilter);
+    }
+
+    void unregisterReceiver() {
+        mContext.unregisterReceiver(mReceiver);
+    }
+
+    private void onHeadsetPluggedInChanged(boolean isPluggedIn) {
+        if (mIsPluggedIn != isPluggedIn) {
+            Log.v(TAG, "onHeadsetPluggedInChanged, mIsPluggedIn: " + mIsPluggedIn + " -> "
+                    + isPluggedIn);
+            boolean oldIsPluggedIn = mIsPluggedIn;
+            mIsPluggedIn = isPluggedIn;
+            if (mListener != null) {
+                mListener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+            }
+        }
+    }
+}
\ No newline at end of file