AudioManager: Add support for master mute

Signed-off-by: Mike Lockwood <lockwood@android.com>
diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java
index 193460a..9152cc3 100644
--- a/core/java/android/view/VolumePanel.java
+++ b/core/java/android/view/VolumePanel.java
@@ -91,6 +91,7 @@
     private static final int MSG_VIBRATE = 4;
     private static final int MSG_TIMEOUT = 5;
     private static final int MSG_RINGER_MODE_CHANGED = 6;
+    private static final int MSG_MUTE_CHANGED = 7;
 
     // Pseudo stream type for master volume
     private static final int STREAM_MASTER = -100;
@@ -295,8 +296,7 @@
 
     private boolean isMuted(int streamType) {
         if (streamType == STREAM_MASTER) {
-            // master volume mute not yet supported
-            return false;
+            return mAudioService.isMasterMute();
         } else {
             return mAudioService.isStreamMute(streamType);
         }
@@ -328,8 +328,7 @@
 
     private int getLastAudibleStreamVolume(int streamType) {
         if (streamType == STREAM_MASTER) {
-            // master volume mute not yet supported
-            return getStreamVolume(STREAM_MASTER);
+            return mAudioService.getLastAudibleMasterVolume();
         } else {
             return mAudioService.getLastAudibleStreamVolume(streamType);
         }
@@ -452,6 +451,19 @@
         postVolumeChanged(STREAM_MASTER, flags);
     }
 
+    public void postMuteChanged(int streamType, int flags) {
+        if (hasMessages(MSG_VOLUME_CHANGED)) return;
+        if (mStreamControls == null) {
+            createSliders();
+        }
+        removeMessages(MSG_FREE_RESOURCES);
+        obtainMessage(MSG_MUTE_CHANGED, streamType, flags).sendToTarget();
+    }
+
+    public void postMasterMuteChanged(int flags) {
+        postMuteChanged(STREAM_MASTER, flags);
+    }
+
     /**
      * Override this if you have other work to do when the volume changes (for
      * example, vibrating, playing a sound, etc.). Make sure to call through to
@@ -485,6 +497,18 @@
         resetTimeout();
     }
 
+    protected void onMuteChanged(int streamType, int flags) {
+
+        if (LOGD) Log.d(TAG, "onMuteChanged(streamType: " + streamType + ", flags: " + flags + ")");
+
+        StreamControl sc = mStreamControls.get(streamType);
+        if (sc != null) {
+            sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes);
+        }
+
+        onVolumeChanged(streamType, flags);
+    }
+
     protected void onShowVolumeChanged(int streamType, int flags) {
         int index = isMuted(streamType) ?
                 getLastAudibleStreamVolume(streamType)
@@ -683,6 +707,11 @@
                 break;
             }
 
+            case MSG_MUTE_CHANGED: {
+                onMuteChanged(msg.arg1, msg.arg2);
+                break;
+            }
+
             case MSG_FREE_RESOURCES: {
                 onFreeResources();
                 break;
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index edb2d87..3f5ca69 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -372,11 +372,12 @@
     /**
      * @hide
      */
-    public void preDispatchKeyEvent(int keyCode, int stream) {
+    public void preDispatchKeyEvent(KeyEvent event, int stream) {
         /*
          * If the user hits another key within the play sound delay, then
          * cancel the sound
          */
+        int keyCode = event.getKeyCode();
         if (keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP
                 && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE
                 && mVolumeKeyUpTime + VolumePanel.PLAY_SOUND_DELAY
@@ -397,7 +398,8 @@
     /**
      * @hide
      */
-    public void handleKeyDown(int keyCode, int stream) {
+    public void handleKeyDown(KeyEvent event, int stream) {
+        int keyCode = event.getKeyCode();
         switch (keyCode) {
             case KeyEvent.KEYCODE_VOLUME_UP:
             case KeyEvent.KEYCODE_VOLUME_DOWN:
@@ -426,7 +428,13 @@
                 }
                 break;
             case KeyEvent.KEYCODE_VOLUME_MUTE:
-                // TODO: Actually handle MUTE.
+                if (event.getRepeatCount() == 0) {
+                    if (mUseMasterVolume) {
+                        setMasterMute(!isMasterMute());
+                    } else {
+                        // TODO: Actually handle MUTE.
+                    }
+                }
                 break;
         }
     }
@@ -434,7 +442,8 @@
     /**
      * @hide
      */
-    public void handleKeyUp(int keyCode, int stream) {
+    public void handleKeyUp(KeyEvent event, int stream) {
+        int keyCode = event.getKeyCode();
         switch (keyCode) {
             case KeyEvent.KEYCODE_VOLUME_UP:
             case KeyEvent.KEYCODE_VOLUME_DOWN:
@@ -460,9 +469,6 @@
 
                 mVolumeKeyUpTime = SystemClock.uptimeMillis();
                 break;
-            case KeyEvent.KEYCODE_VOLUME_MUTE:
-                // TODO: Actually handle MUTE.
-                break;
         }
     }
 
@@ -657,7 +663,7 @@
         IAudioService service = getService();
         try {
             if (mUseMasterVolume) {
-                return service.getMasterVolume();
+                return service.getLastAudibleMasterVolume();
             } else {
                 return service.getLastAudibleStreamVolume(streamType);
             }
@@ -784,6 +790,35 @@
     }
 
     /**
+     * set master mute state.
+     *
+     * @hide
+     */
+    public void setMasterMute(boolean state) {
+        IAudioService service = getService();
+        try {
+            service.setMasterMute(state, mICallBack);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in setMasterMute", e);
+        }
+    }
+
+    /**
+     * get master mute state.
+     *
+     * @hide
+     */
+    public boolean isMasterMute() {
+        IAudioService service = getService();
+        try {
+            return service.isMasterMute();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Dead object in isMasterMute", e);
+            return false;
+        }
+    }
+
+    /**
      * forces the stream controlled by hard volume keys
      * specifying streamType == -1 releases control to the
      * logic.
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 73ca607..b858320 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -146,6 +146,7 @@
     private AudioHandler mAudioHandler;
     /** @see VolumeStreamState */
     private VolumeStreamState[] mStreamStates;
+    private MasterMuteState mMasterMuteState;
     private SettingsObserver mSettingsObserver;
 
     private int mMode;
@@ -446,6 +447,7 @@
         for (int i = 0; i < numStreamTypes; i++) {
             streams[i] = new VolumeStreamState(System.VOLUME_SETTINGS[STREAM_VOLUME_ALIAS[i]], i);
         }
+        mMasterMuteState = new MasterMuteState();
 
         // Correct stream index values for streams with aliases
         for (int i = 0; i < numStreamTypes; i++) {
@@ -762,6 +764,16 @@
         return (mStreamStates[streamType].muteCount() != 0);
     }
 
+    /** @see AudioManager#setMasterMute(boolean, IBinder) */
+    public void setMasterMute(boolean state, IBinder cb) {
+        mMasterMuteState.mute(cb, state);
+    }
+
+    /** get master mute state. */
+    public boolean isMasterMute() {
+        return (mMasterMuteState.muteCount() != 0);
+    }
+
     /** @see AudioManager#getStreamVolume(int) */
     public int getStreamVolume(int streamType) {
         ensureValidStreamType(streamType);
@@ -770,7 +782,8 @@
     }
 
     public int getMasterVolume() {
-        return Math.round(AudioSystem.getMasterVolume() * MAX_MASTER_VOLUME);
+        if (isMasterMute()) return 0;
+        return getLastAudibleMasterVolume();
     }
 
     public void setMasterVolume(int volume, int flags) {
@@ -794,6 +807,11 @@
         return (mStreamStates[streamType].getIndex(device, true  /* lastAudible */) + 5) / 10;
     }
 
+    /** Get last audible master volume before it was muted. */
+    public int getLastAudibleMasterVolume() {
+        return Math.round(AudioSystem.getMasterVolume() * MAX_MASTER_VOLUME);
+    }
+
     /** @see AudioManager#getRingerMode() */
     public int getRingerMode() {
         synchronized(mSettingsLock) {
@@ -2231,6 +2249,122 @@
         }
     }
 
+    public class MasterMuteState {
+
+        private ArrayList<MasterMuteDeathHandler> mDeathHandlers;
+
+        private MasterMuteState() {
+            mDeathHandlers = new ArrayList<MasterMuteDeathHandler>();
+        }
+
+        public void mute(IBinder cb, boolean state) {
+            MasterMuteDeathHandler handler = getDeathHandler(cb, state);
+            if (handler == null) {
+                Log.e(TAG, "Could not get client death handler for master volume");
+                return;
+            }
+            handler.mute(state);
+        }
+
+        private class MasterMuteDeathHandler implements IBinder.DeathRecipient {
+            private IBinder mICallback; // To be notified of client's death
+            private int mMuteCount; // Number of active mutes for this client
+
+            MasterMuteDeathHandler(IBinder cb) {
+                mICallback = cb;
+            }
+
+            public void mute(boolean state) {
+                synchronized(mDeathHandlers) {
+                    if (state) {
+                        if (mMuteCount == 0) {
+                            // Register for client death notification
+                            try {
+                                // mICallback can be 0 if muted by AudioService
+                                if (mICallback != null) {
+                                    mICallback.linkToDeath(this, 0);
+                                }
+                                mDeathHandlers.add(this);
+                                // If the stream is not yet muted by any client, set lvel to 0
+                                if (muteCount() == 0) {
+                                    AudioSystem.setMasterMute(true);
+                                    mVolumePanel.postMasterMuteChanged(AudioManager.FLAG_SHOW_UI);
+                                }
+                            } catch (RemoteException e) {
+                                // Client has died!
+                                binderDied();
+                                mDeathHandlers.notify();
+                                return;
+                            }
+                        } else {
+                            Log.w(TAG, "master volume was already muted by this client");
+                        }
+                        mMuteCount++;
+                    } else {
+                        if (mMuteCount == 0) {
+                            Log.e(TAG, "unexpected unmute for master volume");
+                        } else {
+                            mMuteCount--;
+                            if (mMuteCount == 0) {
+                                // Unregistr from client death notification
+                                mDeathHandlers.remove(this);
+                                // mICallback can be 0 if muted by AudioService
+                                if (mICallback != null) {
+                                    mICallback.unlinkToDeath(this, 0);
+                                }
+                                if (muteCount() == 0) {
+                                    AudioSystem.setMasterMute(false);
+                                    mVolumePanel.postMasterMuteChanged(AudioManager.FLAG_SHOW_UI);
+                                }
+                            }
+                        }
+                    }
+                    mDeathHandlers.notify();
+                }
+            }
+
+            public void binderDied() {
+                Log.w(TAG, "Volume service client died for master volume");
+                if (mMuteCount != 0) {
+                    // Reset all active mute requests from this client.
+                    mMuteCount = 1;
+                    mute(false);
+                }
+            }
+        }
+
+        private int muteCount() {
+            int count = 0;
+            int size = mDeathHandlers.size();
+            for (int i = 0; i < size; i++) {
+                count += mDeathHandlers.get(i).mMuteCount;
+            }
+            return count;
+        }
+
+        private MasterMuteDeathHandler getDeathHandler(IBinder cb, boolean state) {
+            synchronized(mDeathHandlers) {
+                MasterMuteDeathHandler handler;
+                int size = mDeathHandlers.size();
+                for (int i = 0; i < size; i++) {
+                    handler = mDeathHandlers.get(i);
+                    if (cb == handler.mICallback) {
+                        return handler;
+                    }
+                }
+                // If this is the first mute request for this client, create a new
+                // client death handler. Otherwise, it is an out of sequence unmute request.
+                if (state) {
+                    handler = new MasterMuteDeathHandler(cb);
+                } else {
+                    Log.w(TAG, "stream was not muted by this client");
+                    handler = null;
+                }
+                return handler;
+            }
+        }
+    }
+
     /** Thread that handles native AudioSystem control. */
     private class AudioSystemThread extends Thread {
         AudioSystemThread() {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 6780c1d..17d8e4d 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -45,6 +45,10 @@
 
     boolean isStreamMute(int streamType);
 
+    void setMasterMute(boolean state, IBinder cb);
+
+    boolean isMasterMute();
+
     int getStreamVolume(int streamType);
 
     int getMasterVolume();
@@ -55,6 +59,8 @@
     
     int getLastAudibleStreamVolume(int streamType);
 
+    int getLastAudibleMasterVolume();
+
     void setRingerMode(int ringerMode);
     
     int getRingerMode();
diff --git a/policy/src/com/android/internal/policy/impl/PhoneFallbackEventHandler.java b/policy/src/com/android/internal/policy/impl/PhoneFallbackEventHandler.java
index abed18f..83f7788 100644
--- a/policy/src/com/android/internal/policy/impl/PhoneFallbackEventHandler.java
+++ b/policy/src/com/android/internal/policy/impl/PhoneFallbackEventHandler.java
@@ -52,8 +52,7 @@
     }
 
     public void preDispatchKeyEvent(KeyEvent event) {
-        getAudioManager().preDispatchKeyEvent(event.getKeyCode(),
-                AudioManager.USE_DEFAULT_STREAM_TYPE);
+        getAudioManager().preDispatchKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE);
     }
 
     public boolean dispatchKeyEvent(KeyEvent event) {
@@ -79,7 +78,7 @@
             case KeyEvent.KEYCODE_VOLUME_UP:
             case KeyEvent.KEYCODE_VOLUME_DOWN:
             case KeyEvent.KEYCODE_VOLUME_MUTE: {
-                getAudioManager().handleKeyDown(keyCode, AudioManager.USE_DEFAULT_STREAM_TYPE);
+                getAudioManager().handleKeyDown(event, AudioManager.USE_DEFAULT_STREAM_TYPE);
                 return true;
             }
 
@@ -197,8 +196,7 @@
                     AudioManager audioManager = (AudioManager)mContext.getSystemService(
                             Context.AUDIO_SERVICE);
                     if (audioManager != null) {
-                        getAudioManager().handleKeyUp(keyCode,
-                                AudioManager.USE_DEFAULT_STREAM_TYPE);
+                        getAudioManager().handleKeyUp(event, AudioManager.USE_DEFAULT_STREAM_TYPE);
                     }
                 }
                 return true;
diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindow.java b/policy/src/com/android/internal/policy/impl/PhoneWindow.java
index f1fe43b..b87b8c3 100644
--- a/policy/src/com/android/internal/policy/impl/PhoneWindow.java
+++ b/policy/src/com/android/internal/policy/impl/PhoneWindow.java
@@ -1416,7 +1416,7 @@
                 // doesn't have one of these.  In this case, we execute it here and
                 // eat the event instead, because we have mVolumeControlStreamType
                 // and they don't.
-                getAudioManager().handleKeyDown(keyCode, mVolumeControlStreamType);
+                getAudioManager().handleKeyDown(event, mVolumeControlStreamType);
                 return true;
             }
 
@@ -1478,7 +1478,7 @@
                 // doesn't have one of these.  In this case, we execute it here and
                 // eat the event instead, because we have mVolumeControlStreamType
                 // and they don't.
-                getAudioManager().handleKeyUp(keyCode, mVolumeControlStreamType);
+                getAudioManager().handleKeyUp(event, mVolumeControlStreamType);
                 return true;
             }