Merge "Build management screen for managing blocked numbers." into ub-contactsdialer-a-dev
diff --git a/res/drawable/ic_voicemail_seek_handle_disabled.xml b/res/drawable/ic_voicemail_seek_handle_disabled.xml
new file mode 100644
index 0000000..1262808
--- /dev/null
+++ b/res/drawable/ic_voicemail_seek_handle_disabled.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_handle"
+        android:autoMirrored="true"
+        android:tint="@color/voicemail_icon_disabled_tint" >
+</bitmap>
\ No newline at end of file
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index dbff276..3ca04df 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -158,6 +158,8 @@
      */
     private SmartDialSearchFragment mSmartDialSearchFragment;
 
+    private boolean mIsVisible;
+
     /**
      * Animation that slides in.
      */
@@ -563,6 +565,25 @@
     }
 
     @Override
+    protected void onStart() {
+        super.onStart();
+        mIsVisible = true;
+    }
+
+    @Override
+    protected void onStop() {
+        mIsVisible = false;
+        super.onStop();
+    }
+
+    /**
+     * Returns true when the Activity is currently visible (between onStart and onStop).
+     */
+    /* package */ boolean isVisible() {
+        return mIsVisible;
+    }
+
+    @Override
     protected void onPause() {
         if (mClearSearchOnPause) {
             hideDialpadAndSearchUi();
@@ -576,6 +597,7 @@
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
+        mIsVisible = false;
         super.onSaveInstanceState(outState);
         outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
         outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
@@ -648,6 +670,10 @@
 
     @Override
     public boolean onMenuItemClick(MenuItem item) {
+        if (!isVisible()) {
+            return true;
+        }
+
         switch (item.getItemId()) {
             case R.id.menu_history:
                 // Use explicit CallLogActivity intent instead of ACTION_VIEW +
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/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 14c5473..f86fc55 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -19,6 +19,7 @@
 import android.app.Activity;
 import android.app.Fragment;
 import android.content.Context;
+import android.graphics.drawable.Drawable;
 import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Bundle;
@@ -238,6 +239,8 @@
     private TextView mTotalDurationText;
 
     private PositionUpdater mPositionUpdater;
+    private Drawable mVoicemailSeekHandleEnabled;
+    private Drawable mVoicemailSeekHandleDisabled;
 
     public VoicemailPlaybackLayout(Context context) {
         this(context, null);
@@ -276,9 +279,12 @@
         mDeleteButton.setOnClickListener(mDeleteButtonListener);
 
         mPositionText.setText(formatAsMinutesAndSeconds(0));
-        mPositionText.setVisibility(View.INVISIBLE);
         mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
-        mTotalDurationText.setVisibility(View.INVISIBLE);
+
+        mVoicemailSeekHandleEnabled = getResources().getDrawable(
+                R.drawable.ic_voicemail_seek_handle, mContext.getTheme());
+        mVoicemailSeekHandleDisabled = getResources().getDrawable(
+                R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme());
     }
 
     @Override
@@ -317,6 +323,7 @@
         mStateText.setText(getString(R.string.voicemail_playback_error));
     }
 
+    @Override
     public void onSpeakerphoneOn(boolean on) {
         if (on) {
             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
@@ -354,7 +361,7 @@
 
     @Override
     public void setFetchContentTimeout() {
-        disableUiElements();
+        mStartStopButton.setEnabled(true);
         mStateText.setText(getString(R.string.voicemail_fetching_timout));
     }
 
@@ -366,20 +373,22 @@
     @Override
     public void disableUiElements() {
         mStartStopButton.setEnabled(false);
-        mPlaybackSeek.setProgress(0);
         mPlaybackSeek.setEnabled(false);
-
-        mPositionText.setText(formatAsMinutesAndSeconds(0));
-        mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
+        mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
     }
 
     @Override
     public void enableUiElements() {
         mStartStopButton.setEnabled(true);
         mPlaybackSeek.setEnabled(true);
+        mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
+    }
 
-        mPositionText.setVisibility(View.VISIBLE);
-        mTotalDurationText.setVisibility(View.VISIBLE);
+    @Override
+    public void resetSeekBar() {
+        mPlaybackSeek.setProgress(0);
+        mPlaybackSeek.setEnabled(false);
+        mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
     }
 
     @Override
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 95622bf..8b8b7c5 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -85,6 +85,7 @@
         void setFetchContentTimeout();
         void setIsFetchingContent();
         void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
+        void resetSeekBar();
     }
 
     public interface OnVoicemailDeletedListener {
@@ -101,6 +102,7 @@
 
     private static final String[] HAS_CONTENT_PROJECTION = new String[] {
         VoicemailContract.Voicemails.HAS_CONTENT,
+        VoicemailContract.Voicemails.DURATION
     };
 
     private static final int NUMBER_OF_THREADS_IN_POOL = 2;
@@ -251,19 +253,19 @@
                 mPosition = 0;
                 // Default to earpiece.
                 setSpeakerphoneOn(false);
+                mVoicemailAudioManager.setSpeakerphoneOn(false);
             } else {
                 // Update the view to the current speakerphone state.
                 mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
             }
 
-            mDuration.set(0);
+            checkForContent();
 
             if (startPlayingImmediately) {
                 // Since setPlaybackView can get called during the view binding process, we don't
                 // want to reset mIsPlaying to false if the user is currently playing the
                 // voicemail and the view is rebound.
                 mIsPlaying = startPlayingImmediately;
-                checkForContent();
             }
         }
     }
@@ -272,16 +274,20 @@
      * Reset the presenter for playback back to its original state.
      */
     public void resetAll() {
-        reset();
+        pausePresenter(true);
 
         mView = null;
         mVoicemailUri = null;
     }
 
     /**
-     * Reset the presenter such that it is as if the voicemail has not been played.
+     * When navigating away from voicemail playback, we need to release the media player,
+     * pause the UI and save the position.
+     *
+     * @param reset {@code true} if we want to reset the position of the playback, {@code false} if
+     * we want to retain the current position (in case we return to the voicemail).
      */
-    public void reset() {
+    public void pausePresenter(boolean reset) {
         if (mMediaPlayer != null) {
             mMediaPlayer.release();
             mMediaPlayer = null;
@@ -291,19 +297,35 @@
 
         mIsPrepared = false;
         mIsPlaying = false;
-        mPosition = 0;
-        mDuration.set(0);
+
+        if (reset) {
+            // We want to reset the position whether or not the view is valid.
+            mPosition = 0;
+        }
 
         if (mView != null) {
             mView.onPlaybackStopped();
-            mView.setClipPosition(0, mDuration.get());
+            if (reset) {
+                mView.setClipPosition(0, mDuration.get());
+            } else {
+                mPosition = mView.getDesiredClipPosition();
+            }
         }
     }
 
     /**
+     * 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.
@@ -312,11 +334,12 @@
         }
 
         // Release the media player, otherwise there may be failures.
-        reset();
+        pausePresenter(false);
 
         if (mActivity != null) {
             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
         }
+
     }
 
     /**
@@ -345,8 +368,8 @@
      * voicemail we've been asked to play has any content available.
      * <p>
      * Notify the user that we are fetching the content, then check to see if the content field in
-     * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make
-     * a request to fetch the content asynchronously via {@link #requestContent()}.
+     * the DB is set. If set, we proceed to {@link #prepareContent()} method. We get the duration of
+     * the voicemail from the query and set it if the content is not available.
      */
     private void checkForContent() {
         mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
@@ -360,7 +383,8 @@
                 if (hasContent) {
                     prepareContent();
                 } else {
-                    requestContent();
+                    mView.resetSeekBar();
+                    mView.setClipPosition(0, mDuration.get());
                 }
             }
         });
@@ -373,10 +397,14 @@
 
         ContentResolver contentResolver = mContext.getContentResolver();
         Cursor cursor = contentResolver.query(
-                voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
+                voicemailUri, null, null, null, null);
         try {
             if (cursor != null && cursor.moveToNext()) {
-                return cursor.getInt(cursor.getColumnIndexOrThrow(
+                int duration = cursor.getInt(cursor.getColumnIndex(
+                        VoicemailContract.Voicemails.DURATION));
+                // Convert database duration (seconds) into mDuration (milliseconds)
+                mDuration.set(duration > 0 ? duration * 1000 : 0);
+                return cursor.getInt(cursor.getColumnIndex(
                         VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
             }
         } finally {
@@ -519,7 +547,6 @@
         mIsPrepared = true;
 
         mDuration.set(mMediaPlayer.getDuration());
-        mPosition = mMediaPlayer.getCurrentPosition();
 
         Log.d(TAG, "onPrepared: mPosition=" + mPosition);
         mView.setClipPosition(mPosition, mDuration.get());
@@ -604,7 +631,7 @@
 
         if (!mIsPrepared) {
             // If we haven't downloaded the voicemail yet, attempt to download it.
-            checkForContent();
+            requestContent();
             mIsPlaying = true;
             return;
         }
@@ -614,15 +641,15 @@
         if (!mMediaPlayer.isPlaying()) {
             // Clamp the start position between 0 and the duration.
             mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
+
             mMediaPlayer.seekTo(mPosition);
 
             try {
                 // Grab audio focus.
                 // Can throw RejectedExecutionException.
                 mVoicemailAudioManager.requestAudioFocus();
-
-                setSpeakerphoneOn(mIsSpeakerphoneOn);
                 mMediaPlayer.start();
+                setSpeakerphoneOn(mIsSpeakerphoneOn);
             } catch (RejectedExecutionException e) {
                 handleError(e);
             }
@@ -708,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