am deb8c897: Move headset hook from Telephony to Telecomm. (1/2)

* commit 'deb8c89707c604d4f9f32e476a58bd10a68293ff':
  Move headset hook from Telephony to Telecomm. (1/2)
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4b6260a..0c46053 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -41,4 +41,12 @@
          The user will be able to send text messages using the phone number.
          [CHAR LIMIT=60] -->
     <string name="notification_missedCall_message">Message</string>
+
+    <!-- Content description of the call muted notification icon for
+         accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_call_muted">Call muted.</string>
+
+    <!-- Content description of the speakerphone enabled notification icon for
+         accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_speakerphone_enabled">Speakerphone enabled.</string>
 </resources>
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
index 43b102b..865e3ca 100644
--- a/src/com/android/telecomm/CallAudioManager.java
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -29,21 +29,23 @@
 final class CallAudioManager extends CallsManagerListenerBase {
     private static final int STREAM_NONE = -1;
 
+    private final StatusBarNotifier mStatusBarNotifier;
     private final AudioManager mAudioManager;
     private final WiredHeadsetManager mWiredHeadsetManager;
     private final BluetoothManager mBluetoothManager;
+
     private CallAudioState mAudioState;
     private int mAudioFocusStreamType;
     private boolean mIsRinging;
     private boolean mIsTonePlaying;
     private boolean mWasSpeakerOn;
 
-    CallAudioManager() {
-        Context context = TelecommApp.getInstance();
+    CallAudioManager(Context context, StatusBarNotifier statusBarNotifier) {
+        mStatusBarNotifier = statusBarNotifier;
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         mWiredHeadsetManager = new WiredHeadsetManager(this);
         mBluetoothManager = new BluetoothManager(context, this);
-        mAudioState = getInitialAudioState(null);
+        saveAudioState(getInitialAudioState(null));
         mAudioFocusStreamType = STREAM_NONE;
     }
 
@@ -100,6 +102,10 @@
         updateAudioForForegroundCall();
     }
 
+    void toggleMute() {
+        mute(!mAudioState.isMuted);
+    }
+
     void mute(boolean shouldMute) {
         Log.v(this, "mute, shouldMute: %b", shouldMute);
 
@@ -206,9 +212,15 @@
         return mBluetoothManager.isBluetoothAvailable();
     }
 
+    private void saveAudioState(CallAudioState audioState) {
+        mAudioState = audioState;
+        mStatusBarNotifier.notifyMute(mAudioState.isMuted);
+        mStatusBarNotifier.notifySpeakerphone(mAudioState.route == CallAudioState.ROUTE_SPEAKER);
+    }
+
     private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
         CallAudioState oldAudioState = mAudioState;
-        mAudioState = new CallAudioState(isMuted, route, supportedRouteMask);
+        saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));
         Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
 
         // Mute.
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index e8b49e5..a2aa045 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -83,6 +83,7 @@
     private final CallAudioManager mCallAudioManager;
     private final Ringer mRinger;
     private final Set<CallsManagerListener> mListeners = new HashSet<>();
+    private final HeadsetMediaButton mHeadsetMediaButton;
 
     /**
      * The call the user is currently interacting with. This is the call that should have audio
@@ -101,10 +102,13 @@
     private CallsManager() {
         TelecommApp app = TelecommApp.getInstance();
 
-        mCallAudioManager = new CallAudioManager();
+        StatusBarNotifier statusBarNotifier = new StatusBarNotifier(app, this);
+        mCallAudioManager = new CallAudioManager(app, statusBarNotifier);
         InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager);
         mRinger = new Ringer(mCallAudioManager, this, playerFactory, app);
+        mHeadsetMediaButton = new HeadsetMediaButton(app, this);
 
+        mListeners.add(statusBarNotifier);
         mListeners.add(new CallLogManager(app));
         mListeners.add(new PhoneStateBroadcaster());
         mListeners.add(mInCallController);
@@ -499,6 +503,10 @@
         }
     }
 
+    boolean hasAnyCalls() {
+        return !mCalls.isEmpty();
+    }
+
     boolean hasActiveOrHoldingCall() {
         for (Call call : mCalls) {
             CallState state = call.getState();
@@ -518,6 +526,46 @@
         return false;
     }
 
+    boolean onMediaButton(int type) {
+        if (hasAnyCalls()) {
+            if (HeadsetMediaButton.SHORT_PRESS == type) {
+                Call ringingCall = getFirstCallWithState(CallState.RINGING);
+                if (ringingCall == null) {
+                    mCallAudioManager.toggleMute();
+                    return true;
+                } else {
+                    ringingCall.answer();
+                    return true;
+                }
+            } else if (HeadsetMediaButton.LONG_PRESS == type) {
+                Log.d(this, "handleHeadsetHook: longpress -> hangup");
+                Call callToHangup = getFirstCallWithState(
+                        CallState.RINGING, CallState.DIALING, CallState.ACTIVE, CallState.ON_HOLD);
+                if (callToHangup != null) {
+                    callToHangup.disconnect();
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the first call that it finds with the given states. The states are treated as having
+     * priority order so that any call with the first state will be returned before any call with
+     * states listed later in the parameter list.
+     */
+    private Call getFirstCallWithState(CallState... states) {
+        for (CallState currentState : states) {
+            for (Call call : mCalls) {
+                if (currentState == call.getState()) {
+                    return call;
+                }
+            }
+        }
+        return null;
+    }
+
     /**
      * Adds the specified call to the main list of live calls.
      *
diff --git a/src/com/android/telecomm/HeadsetMediaButton.java b/src/com/android/telecomm/HeadsetMediaButton.java
new file mode 100644
index 0000000..db34f59
--- /dev/null
+++ b/src/com/android/telecomm/HeadsetMediaButton.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2014, 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.telecomm;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.view.KeyEvent;
+
+/**
+ * Static class to handle listening to the headset media buttons.
+ */
+final class HeadsetMediaButton {
+
+    /**
+     * Broadcast receiver for the ACTION_MEDIA_BUTTON broadcast intent.
+     *
+     * This functionality isn't lumped in with the other intents in TelecommBroadcastReceiver
+     * because we instantiate this as a totally separate BroadcastReceiver instance, since we need
+     * to manually adjust its IntentFilter's priority (to make sure we get these intents *before*
+     * the media player.)
+     */
+    private final class MediaButtonBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+            Log.v(this, "MediaButtonBroadcastReceiver.onReceive()...  event = %s.", event);
+            if ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK)) {
+                boolean consumed = handleHeadsetHook(event);
+                Log.v(this, "==> handleHeadsetHook(): consumed = %b.", consumed);
+                if (consumed) {
+                    abortBroadcast();
+                }
+            } else {
+                if (CallsManager.getInstance().hasAnyCalls()) {
+                    // If the phone is anything other than completely idle, then we consume and
+                    // ignore any media key events, otherwise it is too easy to accidentally start
+                    // playing music while a phone call is in progress.
+                    Log.v(this, "MediaButtonBroadcastReceiver: consumed");
+                    abortBroadcast();
+                }
+            }
+        }
+    }
+
+    // Types of media button presses
+    static final int SHORT_PRESS = 1;
+    static final int LONG_PRESS = 2;
+
+    private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() {
+        @Override
+        public void onMediaButtonEvent(Intent intent) {
+            KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+            Log.v(this, "SessionCallback.onMediaButton()...  event = %s.", event);
+            if ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK)) {
+                Log.v(this, "SessionCallback: HEADSETHOOK");
+                boolean consumed = handleHeadsetHook(event);
+                Log.v(this, "==> handleHeadsetHook(): consumed = %b.", consumed);
+            }
+        }
+    };
+
+    private final MediaButtonBroadcastReceiver mMediaButtonReceiver =
+            new MediaButtonBroadcastReceiver();
+
+    private final CallsManager mCallsManager;
+
+    private final MediaSession mSession;
+
+    HeadsetMediaButton(Context context, CallsManager callsManager) {
+        mCallsManager = callsManager;
+
+        // Use a separate receiver (from TelecommBroadcastReceiver) for ACTION_MEDIA_BUTTON
+        // broadcasts, since we need to manually adjust its priority (to make sure we get these
+        // intents *before* the media player.)
+        IntentFilter mediaButtonIntentFilter =
+                new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
+
+        // Make sure we're higher priority than the media player's MediaButtonIntentReceiver (which
+        // currently has the default priority of zero; see apps/Music/AndroidManifest.xml.)
+        mediaButtonIntentFilter.setPriority(1);
+
+        context.registerReceiver(mMediaButtonReceiver, mediaButtonIntentFilter);
+
+        // register the component so it gets priority for calls
+        AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        am.registerMediaButtonEventReceiverForCalls(new ComponentName(context.getPackageName(),
+                MediaButtonBroadcastReceiver.class.getName()));
+
+        // Register a MediaSession but don't enable it yet. This is a
+        // replacement for MediaButtonReceiver
+        MediaSessionManager msm =
+                (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
+        mSession = msm.createSession(HeadsetMediaButton.class.getSimpleName());
+        mSession.addCallback(mSessionCallback);
+        mSession.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY
+                | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
+    }
+
+    /**
+     * Handles the wired headset button while in-call.
+     *
+     * @return true if we consumed the event.
+     */
+    private boolean handleHeadsetHook(KeyEvent event) {
+        Log.d(this, "handleHeadsetHook()...%s %s", event.getAction(), event.getRepeatCount());
+
+        if (event.isLongPress()) {
+            return mCallsManager.onMediaButton(LONG_PRESS);
+        } else if (event.getAction() == KeyEvent.ACTION_UP && event.getRepeatCount() == 0) {
+            return mCallsManager.onMediaButton(SHORT_PRESS);
+        }
+
+        return true;
+    }
+}
diff --git a/src/com/android/telecomm/StatusBarNotifier.java b/src/com/android/telecomm/StatusBarNotifier.java
new file mode 100644
index 0000000..eea187f
--- /dev/null
+++ b/src/com/android/telecomm/StatusBarNotifier.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 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.telecomm;
+
+import android.app.StatusBarManager;
+import android.content.Context;
+
+/**
+ * Manages the special status bar notifications used by the phone app.
+ */
+final class StatusBarNotifier extends CallsManagerListenerBase {
+    private static final String SLOT_MUTE = "mute";
+    private static final String SLOT_SPEAKERPHONE = "speakerphone";
+
+    private final Context mContext;
+    private final CallsManager mCallsManager;
+    private final StatusBarManager mStatusBarManager;
+
+    private boolean mIsShowingMute;
+    private boolean mIsShowingSpeakerphone;
+
+    StatusBarNotifier(Context context, CallsManager callsManager) {
+        mContext = context;
+        mCallsManager = callsManager;
+        mStatusBarManager = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
+    }
+
+    /** ${inheritDoc} */
+    @Override
+    public void onCallRemoved(Call call) {
+        if (!mCallsManager.hasAnyCalls()) {
+            notifyMute(false);
+            notifySpeakerphone(false);
+        }
+    }
+
+    void notifyMute(boolean isMuted) {
+        // Never display anything if there are no calls.
+        if (!mCallsManager.hasAnyCalls()) {
+            isMuted = false;
+        }
+
+        if (mIsShowingMute == isMuted) {
+            return;
+        }
+
+        Log.d(this, "Mute status bar icon being set to %b", isMuted);
+
+        if (isMuted) {
+            mStatusBarManager.setIcon(
+                    SLOT_MUTE,
+                    android.R.drawable.stat_notify_call_mute,
+                    0,  /* iconLevel */
+                    mContext.getString(R.string.accessibility_call_muted));
+        } else {
+            mStatusBarManager.removeIcon(SLOT_MUTE);
+        }
+        mIsShowingMute = isMuted;
+    }
+
+    void notifySpeakerphone(boolean isSpeakerphone) {
+        // Never display anything if there are no calls.
+        if (!mCallsManager.hasAnyCalls()) {
+            isSpeakerphone = false;
+        }
+
+        if (mIsShowingSpeakerphone == isSpeakerphone) {
+            return;
+        }
+
+        Log.d(this, "Speakerphone status bar icon being set to %b", isSpeakerphone);
+
+        if (isSpeakerphone) {
+            mStatusBarManager.setIcon(
+                    SLOT_SPEAKERPHONE,
+                    android.R.drawable.stat_sys_speakerphone,
+                    0,  /* iconLevel */
+                    mContext.getString(R.string.accessibility_speakerphone_enabled));
+        } else {
+            mStatusBarManager.removeIcon(SLOT_SPEAKERPHONE);
+        }
+        mIsShowingSpeakerphone = isSpeakerphone;
+    }
+}