VolumeZen: Introduce internal vs external ringer mode.

Stabilize mapping between ringer-mode=silent and zen=priority
by keeping track of two ringer modes:
 - Internal ringer mode: Used for underlying stream muting
 - External ringer mode: Reported to clients

The mapping between external ringer mode + zen is:
 - normal = all
 - vibrate = all
 - silent = priority (read-write) or none (read)

Changes include:
 - Remove "zen check" from audio service, back to audio
   service having no knowledge of zen.
 - Maintain a new external ringer mode in audio service,
   this is the ringer mode reported through AudioManager
   to callers, also mapped to the change intent.
 - Introduce a "ringer mode delegate" to the local
   audio manager interface, responsible for observing
   external / internal mode changes, and making changes
   if necessary.
 - Internal ringer mode changes are still interesting
   to the volume dialog, wire up a callback through
   the existing IVolumeController interface.
 - On devices without vibration, the mapping is the same
   but since no ringer mode change is possible, disable
   the icon toggle and remove the mute icon when volume=0.
 - On devices with vibration, volume down presses should
   pulse the vibrate icon (and vibrate) as a hint that this
   is as low as the device can go using the keys.  Since
   the mechanics are similar to the existing zen=none hint,
   pull into shared helper.
 - Log ringer mode changes to the zen log, include calling
   package information for issue diagnosis.
 - Include whether vibration is supported in the audio service
   dump.
 - Update the status bar icon policy to use the internal ringer
   mode, not the external mode (for vibrate icon).
 - Update the "Muted by <x>" logic, include current suppressor
   in dumpsys, ensure suppression icon is enabled & !clickable,
   regardless of zen mode.

Bug: 17884168
Bug: 15471679
Bug: 16824970
Change-Id: Ia7d3bb23ce6d1e37b24fb6521d1c1ab9bb8f60c0
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
index 351911c..1fe4698 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
@@ -124,6 +124,8 @@
     private static final int MSG_ZEN_MODE_AVAILABLE_CHANGED = 13;
     private static final int MSG_USER_ACTIVITY = 14;
     private static final int MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED = 15;
+    private static final int MSG_ZEN_MODE_CHANGED = 16;
+    private static final int MSG_INTERNAL_RINGER_MODE_CHANGED = 17;
 
     // Pseudo stream type for master volume
     private static final int STREAM_MASTER = -100;
@@ -169,6 +171,8 @@
     private final ViewGroup mSliderPanel;
     /** The zen mode configuration panel view */
     private ZenModePanel mZenPanel;
+    /** The component currently suppressing notification stream effects */
+    private ComponentName mNotificationEffectsSuppressor;
 
     private Callback mCallback;
 
@@ -178,6 +182,7 @@
     private SparseArray<StreamControl> mStreamControls;
     private final AccessibilityManager mAccessibilityManager;
     private final SecondaryIconTransition mSecondaryIconTransition;
+    private final IconPulser mIconPulser;
 
     private enum StreamResources {
         BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO,
@@ -188,7 +193,7 @@
         RingerStream(AudioManager.STREAM_RING,
                 R.string.volume_icon_description_ringer,
                 com.android.systemui.R.drawable.ic_ringer_audible,
-                com.android.systemui.R.drawable.ic_ringer_vibrate,
+                com.android.systemui.R.drawable.ic_ringer_mute,
                 false),
         VoiceStream(AudioManager.STREAM_VOICE_CALL,
                 R.string.volume_icon_description_incall,
@@ -208,7 +213,7 @@
         NotificationStream(AudioManager.STREAM_NOTIFICATION,
                 R.string.volume_icon_description_notification,
                 com.android.systemui.R.drawable.ic_ringer_audible,
-                com.android.systemui.R.drawable.ic_ringer_vibrate,
+                com.android.systemui.R.drawable.ic_ringer_mute,
                 true),
         // for now, use media resources for master volume
         MasterStream(STREAM_MASTER,
@@ -268,6 +273,7 @@
     // Synchronize when accessing this
     private ToneGenerator mToneGenerators[];
     private Vibrator mVibrator;
+    private boolean mHasVibrator;
 
     private static AlertDialog sSafetyWarning;
     private static Object sSafetyWarningLock = new Object();
@@ -354,6 +360,7 @@
         mAccessibilityManager = (AccessibilityManager) context.getSystemService(
                 Context.ACCESSIBILITY_SERVICE);
         mSecondaryIconTransition = new SecondaryIconTransition();
+        mIconPulser = new IconPulser(context);
 
         // For now, only show master volume if master volume is supported
         final Resources res = context.getResources();
@@ -435,10 +442,12 @@
 
         mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()];
         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+        mHasVibrator = mVibrator != null && mVibrator.hasVibrator();
         mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable);
 
         if (mZenController != null && !useMasterVolume) {
             mZenModeAvailable = mZenController.isZenAvailable();
+            mNotificationEffectsSuppressor = mZenController.getEffectsSuppressor();
             mZenController.addCallback(mZenCallback);
         }
 
@@ -470,8 +479,10 @@
         pw.print("  mTag="); pw.println(mTag);
         pw.print("  mRingIsSilent="); pw.println(mRingIsSilent);
         pw.print("  mVoiceCapable="); pw.println(mVoiceCapable);
+        pw.print("  mHasVibrator="); pw.println(mHasVibrator);
         pw.print("  mZenModeAvailable="); pw.println(mZenModeAvailable);
         pw.print("  mZenPanelExpanded="); pw.println(mZenPanelExpanded);
+        pw.print("  mNotificationEffectsSuppressor="); pw.println(mNotificationEffectsSuppressor);
         pw.print("  mTimeoutDelay="); pw.println(mTimeoutDelay);
         pw.print("  mDisabledAlpha="); pw.println(mDisabledAlpha);
         pw.print("  mLastRingerMode="); pw.println(mLastRingerMode);
@@ -639,16 +650,19 @@
             sc.iconRes = streamRes.iconRes;
             sc.iconMuteRes = streamRes.iconMuteRes;
             sc.icon.setImageResource(sc.iconRes);
-            sc.icon.setClickable(isNotification);
+            sc.icon.setClickable(isNotification && mHasVibrator);
             if (isNotification) {
-                sc.icon.setSoundEffectsEnabled(false);
-                sc.icon.setOnClickListener(new OnClickListener() {
-                    @Override
-                    public void onClick(View v) {
-                        resetTimeout();
-                        toggle(sc);
-                    }
-                });
+                if (mHasVibrator) {
+                    sc.icon.setSoundEffectsEnabled(false);
+                    sc.iconMuteRes = com.android.systemui.R.drawable.ic_ringer_vibrate;
+                    sc.icon.setOnClickListener(new OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            resetTimeout();
+                            toggleRinger(sc);
+                        }
+                    });
+                }
                 sc.iconSuppressedRes = com.android.systemui.R.drawable.ic_ringer_mute;
             }
             sc.seekbarView = (SeekBar) sc.group.findViewById(com.android.systemui.R.id.seekbar);
@@ -681,12 +695,13 @@
         }
     }
 
-    private void toggle(StreamControl sc) {
-        if (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL) {
-            mAudioManager.setRingerMode(AudioManager.RINGER_MODE_VIBRATE);
+    private void toggleRinger(StreamControl sc) {
+        if (!mHasVibrator) return;
+        if (mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_NORMAL) {
+            mAudioManager.setRingerModeInternal(AudioManager.RINGER_MODE_VIBRATE);
             postVolumeChanged(sc.streamType, AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE);
         } else {
-            mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+            mAudioManager.setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL);
             postVolumeChanged(sc.streamType, AudioManager.FLAG_PLAY_SOUND);
         }
     }
@@ -710,7 +725,7 @@
 
     private void updateSliderProgress(StreamControl sc, int progress) {
         final boolean isRinger = isNotificationOrRing(sc.streamType);
-        if (isRinger && mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) {
+        if (isRinger && mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) {
             progress = mLastRingerProgress;
         }
         if (progress < 0) {
@@ -723,21 +738,30 @@
     }
 
     private void updateSliderIcon(StreamControl sc, boolean muted) {
+        ComponentName suppressor = null;
         if (isNotificationOrRing(sc.streamType)) {
-            int ringerMode = mAudioManager.getRingerMode();
+            suppressor = mNotificationEffectsSuppressor;
+            int ringerMode = mAudioManager.getRingerModeInternal();
             if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
                 ringerMode = mLastRingerMode;
             } else {
                 mLastRingerMode = ringerMode;
             }
-            muted = ringerMode == AudioManager.RINGER_MODE_VIBRATE;
+            if (mHasVibrator) {
+                muted = ringerMode == AudioManager.RINGER_MODE_VIBRATE;
+            } else {
+                muted = false;
+            }
         }
-        sc.icon.setImageResource(mDemoIcon != 0 ? mDemoIcon : muted ? sc.iconMuteRes : sc.iconRes);
+        sc.icon.setImageResource(mDemoIcon != 0 ? mDemoIcon
+                : suppressor != null ? sc.iconSuppressedRes
+                : muted ? sc.iconMuteRes
+                : sc.iconRes);
     }
 
-    private void updateSliderSupressor(StreamControl sc) {
+    private void updateSliderSuppressor(StreamControl sc) {
         final ComponentName suppressor = isNotificationOrRing(sc.streamType)
-                ? mZenController.getEffectsSuppressor() : null;
+                ? mNotificationEffectsSuppressor : null;
         if (suppressor == null) {
             sc.seekbarView.setVisibility(View.VISIBLE);
             sc.suppressorView.setVisibility(View.GONE);
@@ -746,7 +770,6 @@
             sc.suppressorView.setVisibility(View.VISIBLE);
             sc.suppressorView.setText(mContext.getString(R.string.muted_by,
                     getSuppressorCaption(suppressor)));
-            sc.icon.setImageResource(sc.iconSuppressedRes);
         }
     }
 
@@ -777,7 +800,7 @@
         sc.icon.setImageDrawable(null);
         updateSliderIcon(sc, muted);
         updateSliderEnabled(sc, muted, false);
-        updateSliderSupressor(sc);
+        updateSliderSuppressor(sc);
     }
 
     private void updateSliderEnabled(final StreamControl sc, boolean muted, boolean fixedVolume) {
@@ -787,7 +810,12 @@
             // never disable touch interactions for remote playback, the muting is not tied to
             // the state of the phone.
             sc.seekbarView.setEnabled(!fixedVolume);
-        } else if (isRinger && mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) {
+        } else if (isRinger && mNotificationEffectsSuppressor != null) {
+            sc.icon.setEnabled(true);
+            sc.icon.setAlpha(1f);
+            sc.icon.setClickable(false);
+        } else if (isRinger
+                && mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT) {
             sc.seekbarView.setEnabled(false);
             sc.icon.setEnabled(false);
             sc.icon.setAlpha(mDisabledAlpha);
@@ -805,7 +833,7 @@
         if (isRinger && wasEnabled != sc.seekbarView.isEnabled()) {
             if (sc.seekbarView.isEnabled()) {
                 sc.group.setOnTouchListener(null);
-                sc.icon.setClickable(true);
+                sc.icon.setClickable(mHasVibrator);
             } else {
                 final View.OnTouchListener showHintOnTouch = new View.OnTouchListener() {
                     @Override
@@ -826,6 +854,16 @@
         }
     }
 
+    private void showVibrateHint() {
+        final StreamControl active = mStreamControls.get(mActiveStreamType);
+        if (active != null) {
+            mIconPulser.start(active.icon);
+            if (!hasMessages(MSG_VIBRATE)) {
+                sendEmptyMessageDelayed(MSG_VIBRATE, VIBRATE_DELAY);
+            }
+        }
+    }
+
     private static boolean isNotificationOrRing(int streamType) {
         return streamType == AudioManager.STREAM_RING
                 || streamType == AudioManager.STREAM_NOTIFICATION;
@@ -953,6 +991,19 @@
         obtainMessage(MSG_LAYOUT_DIRECTION, layoutDirection, 0).sendToTarget();
     }
 
+    public void postInternalRingerModeChanged(int mode) {
+        removeMessages(MSG_INTERNAL_RINGER_MODE_CHANGED);
+        obtainMessage(MSG_INTERNAL_RINGER_MODE_CHANGED, mode, 0).sendToTarget();
+    }
+
+    private static String flagsToString(int flags) {
+        return flags == 0 ? "0" : (flags + "=" + AudioManager.flagsToString(flags));
+    }
+
+    private static String streamToString(int stream) {
+        return AudioService.streamToString(stream);
+    }
+
     /**
      * 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
@@ -960,7 +1011,8 @@
      */
     protected void onVolumeChanged(int streamType, int flags) {
 
-        if (LOGD) Log.d(mTag, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")");
+        if (LOGD) Log.d(mTag, "onVolumeChanged(streamType: " + streamToString(streamType)
+                + ", flags: " + flagsToString(flags) + ")");
 
         if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
             synchronized (this) {
@@ -989,7 +1041,8 @@
 
     protected void onMuteChanged(int streamType, int flags) {
 
-        if (LOGD) Log.d(mTag, "onMuteChanged(streamType: " + streamType + ", flags: " + flags + ")");
+        if (LOGD) Log.d(mTag, "onMuteChanged(streamType: " + streamToString(streamType)
+                + ", flags: " + flagsToString(flags) + ")");
 
         StreamControl sc = mStreamControls.get(streamType);
         if (sc != null) {
@@ -1005,8 +1058,8 @@
         mRingIsSilent = false;
 
         if (LOGD) {
-            Log.d(mTag, "onShowVolumeChanged(streamType: " + streamType
-                    + ", flags: " + flags + "), index: " + index);
+            Log.d(mTag, "onShowVolumeChanged(streamType: " + streamToString(streamType)
+                    + ", flags: " + flagsToString(flags) + "), index: " + index);
         }
 
         // get max volume for progress bar
@@ -1017,7 +1070,6 @@
         switch (streamType) {
 
             case AudioManager.STREAM_RING: {
-//                setRingerIcon();
                 Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri(
                         mContext, RingtoneManager.TYPE_RINGTONE);
                 if (ringuri == null) {
@@ -1110,13 +1162,16 @@
                 sc.seekbarView.setMax(max);
             }
             updateSliderProgress(sc, index);
-            updateSliderEnabled(sc, isMuted(streamType),
-                    (flags & AudioManager.FLAG_FIXED_VOLUME) != 0);
-            // check for secondary-icon transition completion
-            if (isNotificationOrRing(streamType) && mSecondaryIconTransition.isRunning()) {
-                mSecondaryIconTransition.cancel();  // safe to reset
-                sc.seekbarView.setAlpha(0); sc.seekbarView.animate().alpha(1);
-                mZenPanel.setAlpha(0); mZenPanel.animate().alpha(1);
+            final boolean muted = isMuted(streamType);
+            updateSliderEnabled(sc, muted, (flags & AudioManager.FLAG_FIXED_VOLUME) != 0);
+            if (isNotificationOrRing(streamType)) {
+                // check for secondary-icon transition completion
+                if (mSecondaryIconTransition.isRunning()) {
+                    mSecondaryIconTransition.cancel();  // safe to reset
+                    sc.seekbarView.setAlpha(0); sc.seekbarView.animate().alpha(1);
+                    mZenPanel.setAlpha(0); mZenPanel.animate().alpha(1);
+                }
+                updateSliderIcon(sc, muted);
             }
         }
 
@@ -1134,15 +1189,20 @@
         // Do a little vibrate if applicable (only when going into vibrate mode)
         if ((streamType != STREAM_REMOTE_MUSIC) &&
                 ((flags & AudioManager.FLAG_VIBRATE) != 0) &&
-                mAudioManager.isStreamAffectedByRingerMode(streamType) &&
-                mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) {
+                isNotificationOrRing(streamType) &&
+                mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE) {
             sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY);
         }
 
-        // Pulse the slider icon if an adjustment was suppressed due to silent mode.
+        // Pulse the zen icon if an adjustment was suppressed due to silent mode.
         if ((flags & AudioManager.FLAG_SHOW_SILENT_HINT) != 0) {
             showSilentHint();
         }
+
+        // Pulse the slider icon & vibrate if an adjustment down was suppressed due to vibrate mode.
+        if ((flags & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) {
+            showVibrateHint();
+        }
     }
 
     private void announceDialogShown() {
@@ -1186,16 +1246,17 @@
     protected void onVibrate() {
 
         // Make sure we ended up in vibrate ringer mode
-        if (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) {
+        if (mAudioManager.getRingerModeInternal() != AudioManager.RINGER_MODE_VIBRATE) {
             return;
         }
-
-        mVibrator.vibrate(VIBRATE_DURATION, VIBRATION_ATTRIBUTES);
+        if (mVibrator != null) {
+            mVibrator.vibrate(VIBRATE_DURATION, VIBRATION_ATTRIBUTES);
+        }
     }
 
     protected void onRemoteVolumeChanged(MediaController controller, int flags) {
-        if (LOGD) Log.d(mTag, "onRemoteVolumeChanged(controller:" + controller + ", flags: " + flags
-                    + ")");
+        if (LOGD) Log.d(mTag, "onRemoteVolumeChanged(controller:" + controller + ", flags: "
+                + flagsToString(flags) + ")");
 
         if (((flags & AudioManager.FLAG_SHOW_UI) != 0) || isShowing()) {
             synchronized (this) {
@@ -1385,7 +1446,9 @@
                 break;
             }
 
+            case MSG_ZEN_MODE_CHANGED:
             case MSG_RINGER_MODE_CHANGED:
+            case MSG_INTERNAL_RINGER_MODE_CHANGED:
             case MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED: {
                 if (isShowing()) {
                     updateStates();
@@ -1491,10 +1554,15 @@
         public void onZenAvailableChanged(boolean available) {
             obtainMessage(MSG_ZEN_MODE_AVAILABLE_CHANGED, available ? 1 : 0, 0).sendToTarget();
         }
+
         @Override
         public void onEffectsSupressorChanged() {
-            obtainMessage(MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED,
-                    mZenController.getEffectsSuppressor()).sendToTarget();
+            mNotificationEffectsSuppressor = mZenController.getEffectsSuppressor();
+            sendEmptyMessage(MSG_NOTIFICATION_EFFECTS_SUPPRESSOR_CHANGED);
+        }
+
+        public void onZenChanged(int zen) {
+            sendEmptyMessage(MSG_ZEN_MODE_CHANGED);
         }
     };