Update SoundSettings to extend DashboardFragment.

- initial round of refactoring SoundSettings to use DashboardFragment.
- add controller for Cast, Do not disturb, Alarm volume, Media volume,
  Ring volume and Notification volume.

Bug: 32276590
Test: make RunSettingsRoboTests
Change-Id: I5c02a344bff5117bfce93d7ccac650fccc82d2b0
diff --git a/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java b/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java
new file mode 100644
index 0000000..f513882
--- /dev/null
+++ b/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.support.v7.preference.Preference;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.accounts.AccountRestrictionHelper;
+import com.android.settings.core.PreferenceController;
+import com.android.settingslib.RestrictedPreference;
+
+/**
+ * Base class for preference controller that handles preference that enforce adjust volume
+ * restriction
+ */
+public abstract class AdjustVolumeRestrictedPreferenceController extends PreferenceController {
+
+    private AccountRestrictionHelper mHelper;
+
+    public AdjustVolumeRestrictedPreferenceController(Context context) {
+        this(context, new AccountRestrictionHelper(context));
+    }
+
+    @VisibleForTesting
+    AdjustVolumeRestrictedPreferenceController(Context context, AccountRestrictionHelper helper) {
+        super(context);
+        mHelper = helper;
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        if (!(preference instanceof RestrictedPreference)) {
+            return;
+        }
+        mHelper.enforceRestrictionOnPreference((RestrictedPreference) preference,
+                UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId());
+    }
+
+}
diff --git a/src/com/android/settings/notification/AlarmVolumePreferenceController.java b/src/com/android/settings/notification/AlarmVolumePreferenceController.java
new file mode 100644
index 0000000..e9c4b57
--- /dev/null
+++ b/src/com/android/settings/notification/AlarmVolumePreferenceController.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+import android.media.AudioManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.core.lifecycle.Lifecycle;
+import com.android.settings.notification.VolumeSeekBarPreference.Callback;
+
+public class AlarmVolumePreferenceController extends
+    VolumeSeekBarPreferenceController {
+
+    private static final String KEY_ALARM_VOLUME = "alarm_volume";
+    private AudioHelper mHelper;
+
+    public AlarmVolumePreferenceController(Context context, Callback callback,
+        Lifecycle lifecycle) {
+        this(context, callback, lifecycle, new AudioHelper(context));
+    }
+
+    @VisibleForTesting
+    AlarmVolumePreferenceController(Context context, Callback callback, Lifecycle lifecycle,
+        AudioHelper helper) {
+        super(context, callback, lifecycle);
+        mHelper = helper;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return !mHelper.isSingleVolume();
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY_ALARM_VOLUME;
+    }
+
+    @Override
+    public int getAudioStream() {
+        return AudioManager.STREAM_ALARM;
+    }
+
+    @Override
+    public int getMuteIcon() {
+        return com.android.internal.R.drawable.ic_audio_alarm_mute;
+    }
+
+}
diff --git a/src/com/android/settings/notification/AudioHelper.java b/src/com/android/settings/notification/AudioHelper.java
new file mode 100644
index 0000000..ea5bc4a
--- /dev/null
+++ b/src/com/android/settings/notification/AudioHelper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+import android.media.AudioSystem;
+
+/**
+ * Helper class to wrap API for testing
+ */
+public class AudioHelper {
+
+    private Context mContext;
+
+    public AudioHelper(Context context) {
+        mContext = context;
+    }
+
+    public boolean isSingleVolume() {
+        return AudioSystem.isSingleVolume(mContext);
+    }
+
+}
diff --git a/src/com/android/settings/notification/CastPreferenceController.java b/src/com/android/settings/notification/CastPreferenceController.java
new file mode 100644
index 0000000..2ec7821
--- /dev/null
+++ b/src/com/android/settings/notification/CastPreferenceController.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+import android.support.v7.preference.Preference;
+
+import com.android.settings.Utils;
+import com.android.settings.core.PreferenceController;
+import java.util.List;
+
+public class CastPreferenceController extends PreferenceController {
+
+    private static final String KEY_WIFI_DISPLAY = "wifi_display";
+
+    public CastPreferenceController(Context context) {
+        super(context);
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        return false;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY_WIFI_DISPLAY;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+
+    @Override
+    public void updateNonIndexableKeys(List<String> keys) {
+        if (!Utils.isVoiceCapable(mContext)) {
+            keys.add(KEY_WIFI_DISPLAY);
+        }
+    }
+
+}
diff --git a/src/com/android/settings/notification/MediaVolumePreferenceController.java b/src/com/android/settings/notification/MediaVolumePreferenceController.java
new file mode 100644
index 0000000..fb8dba5
--- /dev/null
+++ b/src/com/android/settings/notification/MediaVolumePreferenceController.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+import android.media.AudioManager;
+import com.android.settings.core.lifecycle.Lifecycle;
+import com.android.settings.notification.VolumeSeekBarPreference.Callback;
+
+public class MediaVolumePreferenceController extends
+    VolumeSeekBarPreferenceController {
+
+    private static final String KEY_MEDIA_VOLUME = "media_volume";
+
+    public MediaVolumePreferenceController(Context context, Callback callback, Lifecycle lifecycle) {
+        super(context, callback, lifecycle);
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY_MEDIA_VOLUME;
+    }
+
+    @Override
+    public int getAudioStream() {
+        return AudioManager.STREAM_MUSIC;
+    }
+
+    @Override
+    public int getMuteIcon() {
+        return com.android.internal.R.drawable.ic_audio_media_mute;
+    }
+
+}
diff --git a/src/com/android/settings/notification/NotificationVolumePreferenceController.java b/src/com/android/settings/notification/NotificationVolumePreferenceController.java
new file mode 100644
index 0000000..e4759d7
--- /dev/null
+++ b/src/com/android/settings/notification/NotificationVolumePreferenceController.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+import android.media.AudioManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.Utils;
+import com.android.settings.core.lifecycle.Lifecycle;
+import com.android.settings.notification.VolumeSeekBarPreference.Callback;
+
+public class NotificationVolumePreferenceController extends
+    RingVolumePreferenceController {
+
+    private static final String KEY_NOTIFICATION_VOLUME = "notification_volume";
+    private AudioHelper mHelper;
+
+    public NotificationVolumePreferenceController(Context context, Callback callback,
+        Lifecycle lifecycle) {
+        this(context, callback, lifecycle, new AudioHelper(context));
+    }
+
+    @VisibleForTesting
+    NotificationVolumePreferenceController(Context context,
+        VolumeSeekBarPreference.Callback callback, Lifecycle lifecycle, AudioHelper helper) {
+        super(context, callback, lifecycle);
+        mHelper = helper;
+    }
+
+
+    @Override
+    public boolean isAvailable() {
+        return !Utils.isVoiceCapable(mContext) && !mHelper.isSingleVolume();
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY_NOTIFICATION_VOLUME;
+    }
+
+    @Override
+    public int getAudioStream() {
+        return AudioManager.STREAM_NOTIFICATION;
+    }
+
+    @Override
+    public int getMuteIcon() {
+        return com.android.internal.R.drawable.ic_audio_ring_notif_mute;
+    }
+
+}
diff --git a/src/com/android/settings/notification/RingVolumePreferenceController.java b/src/com/android/settings/notification/RingVolumePreferenceController.java
new file mode 100644
index 0000000..2297837
--- /dev/null
+++ b/src/com/android/settings/notification/RingVolumePreferenceController.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Vibrator;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.Utils;
+import com.android.settings.core.lifecycle.Lifecycle;
+import com.android.settings.notification.VolumeSeekBarPreference.Callback;
+
+import java.util.Objects;
+
+public class RingVolumePreferenceController extends VolumeSeekBarPreferenceController {
+
+    private static final String TAG = "RingVolumeController";
+    private static final String KEY_RING_VOLUME = "ring_volume";
+
+    private AudioManager mAudioManager;
+    private Vibrator mVibrator;
+    private int mRingerMode = -1;
+    private ComponentName mSuppressor;
+    private final RingReceiver mReceiver = new RingReceiver();
+    private final H mHandler = new H();
+    private AudioHelper mHelper;
+
+    public RingVolumePreferenceController(Context context, Callback callback, Lifecycle lifecycle) {
+        this(context, callback, lifecycle, new AudioHelper(context));
+    }
+
+    @VisibleForTesting
+    RingVolumePreferenceController(Context context, Callback callback, Lifecycle lifecycle,
+        AudioHelper helper) {
+        super(context, callback, lifecycle);
+        mHelper = helper;
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
+        if (mVibrator != null && !mVibrator.hasVibrator()) {
+            mVibrator = null;
+        }
+        updateRingerMode();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mReceiver.register(true);
+        updateEffectsSuppressor();
+        updatePreferenceIcon();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mReceiver.register(false);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY_RING_VOLUME;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return Utils.isVoiceCapable(mContext) && !mHelper.isSingleVolume();
+    }
+
+    @Override
+    public int getAudioStream() {
+        return AudioManager.STREAM_RING;
+    }
+
+    @Override
+    public int getMuteIcon() {
+        return com.android.internal.R.drawable.ic_audio_ring_notif_mute;
+    }
+
+    private void updateRingerMode() {
+        final int ringerMode = mAudioManager.getRingerModeInternal();
+        if (mRingerMode == ringerMode) return;
+        mRingerMode = ringerMode;
+        updatePreferenceIcon();
+    }
+
+    private boolean wasRingerModeVibrate() {
+        return mVibrator != null && mRingerMode == AudioManager.RINGER_MODE_SILENT
+            && mAudioManager.getLastAudibleStreamVolume(AudioManager.STREAM_RING) == 0;
+    }
+
+    private void updateEffectsSuppressor() {
+        final ComponentName suppressor = NotificationManager.from(mContext).getEffectsSuppressor();
+        if (Objects.equals(suppressor, mSuppressor)) return;
+        mSuppressor = suppressor;
+        if (mPreference != null) {
+            final String text = suppressor != null ?
+                mContext.getString(com.android.internal.R.string.muted_by,
+                    getSuppressorCaption(suppressor)) : null;
+            mPreference.setSuppressionText(text);
+        }
+        updatePreferenceIcon();
+    }
+
+    private void updatePreferenceIcon() {
+        if (mPreference != null) {
+            mPreference.showIcon(mSuppressor != null
+                ? com.android.internal.R.drawable.ic_audio_ring_notif_mute
+                : mRingerMode == AudioManager.RINGER_MODE_VIBRATE || wasRingerModeVibrate()
+                    ? com.android.internal.R.drawable.ic_audio_ring_notif_vibrate
+                    : com.android.internal.R.drawable.ic_audio_ring_notif);
+        }
+    }
+
+    private String getSuppressorCaption(ComponentName suppressor) {
+        final PackageManager pm = mContext.getPackageManager();
+        try {
+            final ServiceInfo info = pm.getServiceInfo(suppressor, 0);
+            if (info != null) {
+                final CharSequence seq = info.loadLabel(pm);
+                if (seq != null) {
+                    final String str = seq.toString().trim();
+                    if (str.length() > 0) {
+                        return str;
+                    }
+                }
+            }
+        } catch (Throwable e) {
+            Log.w(TAG, "Error loading suppressor caption", e);
+        }
+        return suppressor.getPackageName();
+    }
+
+    private final class H extends Handler {
+        private static final int UPDATE_EFFECTS_SUPPRESSOR = 1;
+        private static final int UPDATE_RINGER_MODE = 2;
+
+        private H() {
+            super(Looper.getMainLooper());
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case UPDATE_EFFECTS_SUPPRESSOR:
+                    updateEffectsSuppressor();
+                    break;
+                case UPDATE_RINGER_MODE:
+                    updateRingerMode();
+                    break;
+            }
+        }
+    }
+
+    private class RingReceiver extends BroadcastReceiver {
+        private boolean mRegistered;
+
+        public void register(boolean register) {
+            if (mRegistered == register) return;
+            if (register) {
+                final IntentFilter filter = new IntentFilter();
+                filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
+                filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
+                mContext.registerReceiver(this, filter);
+            } else {
+                mContext.unregisterReceiver(this);
+            }
+            mRegistered = register;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED.equals(action)) {
+                mHandler.sendEmptyMessage(H.UPDATE_EFFECTS_SUPPRESSOR);
+            } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) {
+                mHandler.sendEmptyMessage(H.UPDATE_RINGER_MODE);
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/settings/notification/SoundSettings.java b/src/com/android/settings/notification/SoundSettings.java
index 1ec3164..1adfdd8 100644
--- a/src/com/android/settings/notification/SoundSettings.java
+++ b/src/com/android/settings/notification/SoundSettings.java
@@ -20,21 +20,16 @@
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
-import android.app.DialogFragment;
 import android.app.FragmentManager;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
 import android.database.ContentObserver;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.Ringtone;
@@ -47,10 +42,7 @@
 import android.os.Message;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.Vibrator;
 import android.preference.SeekBarVolumizer;
-import android.provider.MediaStore;
-import android.provider.OpenableColumns;
 import android.provider.SearchIndexableResource;
 import android.provider.Settings;
 import android.support.v7.preference.Preference;
@@ -64,38 +56,30 @@
 import com.android.settings.R;
 import com.android.settings.RingtonePreference;
 import com.android.settings.DefaultRingtonePreference;
-import com.android.settings.SettingsPreferenceFragment;
 import com.android.settings.Utils;
+import com.android.settings.core.PreferenceController;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.core.lifecycle.Lifecycle;
+import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.dashboard.SummaryLoader;
 import com.android.settings.search.BaseSearchIndexProvider;
-import com.android.settings.search.Indexable;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedPreference;
 
+import com.android.settingslib.drawer.CategoryKey;
 import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Objects;
 
-import static android.content.ContentProvider.getUriWithoutUserId;
-import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-
-public class SoundSettings extends SettingsPreferenceFragment
-        implements Indexable, OnPreferenceChangeListener {
+public class SoundSettings extends DashboardFragment
+        implements OnPreferenceChangeListener {
     private static final String TAG = "SoundSettings";
 
-    private static final String KEY_MEDIA_VOLUME = "media_volume";
-    private static final String KEY_ALARM_VOLUME = "alarm_volume";
-    private static final String KEY_RING_VOLUME = "ring_volume";
-    private static final String KEY_NOTIFICATION_VOLUME = "notification_volume";
     private static final String KEY_PHONE_RINGTONE = "ringtone";
     private static final String KEY_NOTIFICATION_RINGTONE = "notification_ringtone";
     private static final String KEY_ALARM_RINGTONE = "alarm_ringtone";
     private static final String KEY_VIBRATE_WHEN_RINGING = "vibrate_when_ringing";
-    private static final String KEY_WIFI_DISPLAY = "wifi_display";
-    private static final String KEY_ZEN_MODE = "zen_mode";
     private static final String KEY_CELL_BROADCAST_SETTINGS = "cell_broadcast_settings";
 
     private static final String KEY_WORK_CATEGORY = "sound_work_settings_section";
@@ -107,34 +91,19 @@
     private static final String SELECTED_PREFERENCE_KEY = "selected_preference";
     private static final int REQUEST_CODE = 200;
 
-    private static final String[] RESTRICTED_KEYS = {
-        KEY_MEDIA_VOLUME,
-        KEY_ALARM_VOLUME,
-        KEY_RING_VOLUME,
-        KEY_NOTIFICATION_VOLUME,
-        KEY_ZEN_MODE,
-    };
-
     private static final int SAMPLE_CUTOFF = 2000;  // manually cap sample playback at 2 seconds
 
     private final VolumePreferenceCallback mVolumeCallback = new VolumePreferenceCallback();
     private final H mHandler = new H();
     private final SettingsObserver mSettingsObserver = new SettingsObserver();
-    private final Receiver mReceiver = new Receiver();
-    private final ArrayList<VolumeSeekBarPreference> mVolumePrefs = new ArrayList<>();
 
     private Context mContext;
     private boolean mVoiceCapable;
-    private Vibrator mVibrator;
-    private AudioManager mAudioManager;
-    private VolumeSeekBarPreference mRingOrNotificationPreference;
 
     private Preference mPhoneRingtonePreference;
     private Preference mNotificationRingtonePreference;
     private Preference mAlarmRingtonePreference;
     private TwoStatePreference mVibrateWhenRinging;
-    private ComponentName mSuppressor;
-    private int mRingerMode = -1;
 
     private PreferenceGroup mWorkPreferenceCategory;
     private TwoStatePreference mWorkUsePersonalSounds;
@@ -161,36 +130,6 @@
         mUserManager = UserManager.get(getContext());
         mVoiceCapable = Utils.isVoiceCapable(mContext);
 
-        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
-        mVibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
-        if (mVibrator != null && !mVibrator.hasVibrator()) {
-            mVibrator = null;
-        }
-
-        addPreferencesFromResource(R.xml.sound_settings);
-
-        initVolumePreference(KEY_MEDIA_VOLUME, AudioManager.STREAM_MUSIC,
-                com.android.internal.R.drawable.ic_audio_media_mute);
-        initVolumePreference(KEY_ALARM_VOLUME, AudioManager.STREAM_ALARM,
-                com.android.internal.R.drawable.ic_audio_alarm_mute);
-        if (mVoiceCapable) {
-            mRingOrNotificationPreference =
-                    initVolumePreference(KEY_RING_VOLUME, AudioManager.STREAM_RING,
-                            com.android.internal.R.drawable.ic_audio_ring_notif_mute);
-            removePreference(KEY_NOTIFICATION_VOLUME);
-        } else {
-            mRingOrNotificationPreference =
-                    initVolumePreference(KEY_NOTIFICATION_VOLUME, AudioManager.STREAM_NOTIFICATION,
-                            com.android.internal.R.drawable.ic_audio_ring_notif_mute);
-            removePreference(KEY_RING_VOLUME);
-        }
-
-        if (!shouldShowRingtoneSettings()) {
-            removePreference(KEY_RING_VOLUME);
-            removePreference(KEY_NOTIFICATION_VOLUME);
-            removePreference(KEY_ALARM_VOLUME);
-        }
-
         // Enable link to CMAS app settings depending on the value in config.xml.
         boolean isCellBroadcastAppLinkEnabled = this.getResources().getBoolean(
                 com.android.internal.R.bool.config_cellBroadcastAppLinks);
@@ -211,8 +150,6 @@
         }
         initRingtones();
         initVibrateWhenRinging();
-        updateRingerMode();
-        updateEffectsSuppressor();
 
         if (savedInstanceState != null) {
             String selectedPreference = savedInstanceState.getString(SELECTED_PREFERENCE_KEY, null);
@@ -227,26 +164,7 @@
         super.onResume();
         lookupRingtoneNames();
         mSettingsObserver.register(true);
-        mReceiver.register(true);
-        updateRingOrNotificationPreference();
-        updateEffectsSuppressor();
-        for (VolumeSeekBarPreference volumePref : mVolumePrefs) {
-            volumePref.onActivityResume();
-        }
 
-        final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext,
-                UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId());
-        final boolean hasBaseRestriction = RestrictedLockUtils.hasBaseUserRestriction(mContext,
-                UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId());
-        for (String key : RESTRICTED_KEYS) {
-            Preference pref = findPreference(key);
-            if (pref != null) {
-                pref.setEnabled(!hasBaseRestriction);
-            }
-            if (pref instanceof RestrictedPreference && !hasBaseRestriction) {
-                ((RestrictedPreference) pref).setDisabledByAdmin(admin);
-            }
-        }
         RestrictedPreference broadcastSettingsPref = (RestrictedPreference) findPreference(
                 KEY_CELL_BROADCAST_SETTINGS);
         if (broadcastSettingsPref != null) {
@@ -272,12 +190,8 @@
     @Override
     public void onPause() {
         super.onPause();
-        for (VolumeSeekBarPreference volumePref : mVolumePrefs) {
-            volumePref.onActivityPause();
-        }
         mVolumeCallback.stopSample();
         mSettingsObserver.register(false);
-        mReceiver.register(false);
     }
 
     @Override
@@ -292,6 +206,36 @@
     }
 
     @Override
+    protected String getCategoryKey() {
+        return CategoryKey.CATEGORY_SOUND;
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.sound_settings;
+    }
+
+    @Override
+    protected List<PreferenceController> getPreferenceControllers(Context context) {
+        final List<PreferenceController> controllers = new ArrayList<>();
+        Lifecycle lifecycle = getLifecycle();
+        controllers.add(new CastPreferenceController(context));
+        controllers.add(new ZenModePreferenceController(context));
+        // === Volumes ===
+        controllers.add(new AlarmVolumePreferenceController(context, mVolumeCallback, lifecycle));
+        controllers.add(new MediaVolumePreferenceController(context, mVolumeCallback, lifecycle));
+        controllers.add(
+            new NotificationVolumePreferenceController(context, mVolumeCallback, lifecycle));
+        controllers.add(new RingVolumePreferenceController(context, mVolumeCallback, lifecycle));
+        return controllers;
+    }
+
+    @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (mRequestPreference != null) {
             mRequestPreference.onActivityResult(requestCode, resultCode, data);
@@ -336,68 +280,7 @@
 
     // === Volumes ===
 
-    private VolumeSeekBarPreference initVolumePreference(String key, int stream, int muteIcon) {
-        final VolumeSeekBarPreference volumePref = (VolumeSeekBarPreference) findPreference(key);
-        volumePref.setCallback(mVolumeCallback);
-        volumePref.setStream(stream);
-        mVolumePrefs.add(volumePref);
-        volumePref.setMuteIcon(muteIcon);
-        return volumePref;
-    }
-
-    private void updateRingOrNotificationPreference() {
-        mRingOrNotificationPreference.showIcon(mSuppressor != null
-                ? com.android.internal.R.drawable.ic_audio_ring_notif_mute
-                : mRingerMode == AudioManager.RINGER_MODE_VIBRATE || wasRingerModeVibrate()
-                ? com.android.internal.R.drawable.ic_audio_ring_notif_vibrate
-                : com.android.internal.R.drawable.ic_audio_ring_notif);
-    }
-
-    private boolean wasRingerModeVibrate() {
-        return mVibrator != null && mRingerMode == AudioManager.RINGER_MODE_SILENT
-                && mAudioManager.getLastAudibleStreamVolume(AudioManager.STREAM_RING) == 0;
-    }
-
-    private void updateRingerMode() {
-        final int ringerMode = mAudioManager.getRingerModeInternal();
-        if (mRingerMode == ringerMode) return;
-        mRingerMode = ringerMode;
-        updateRingOrNotificationPreference();
-    }
-
-    private void updateEffectsSuppressor() {
-        final ComponentName suppressor = NotificationManager.from(mContext).getEffectsSuppressor();
-        if (Objects.equals(suppressor, mSuppressor)) return;
-        mSuppressor = suppressor;
-        if (mRingOrNotificationPreference != null) {
-            final String text = suppressor != null ?
-                    mContext.getString(com.android.internal.R.string.muted_by,
-                            getSuppressorCaption(suppressor)) : null;
-            mRingOrNotificationPreference.setSuppressionText(text);
-        }
-        updateRingOrNotificationPreference();
-    }
-
-    private String getSuppressorCaption(ComponentName suppressor) {
-        final PackageManager pm = mContext.getPackageManager();
-        try {
-            final ServiceInfo info = pm.getServiceInfo(suppressor, 0);
-            if (info != null) {
-                final CharSequence seq = info.loadLabel(pm);
-                if (seq != null) {
-                    final String str = seq.toString().trim();
-                    if (str.length() > 0) {
-                        return str;
-                    }
-                }
-            }
-        } catch (Throwable e) {
-            Log.w(TAG, "Error loading suppressor caption", e);
-        }
-        return suppressor.getPackageName();
-    }
-
-    private final class VolumePreferenceCallback implements VolumeSeekBarPreference.Callback {
+    final class VolumePreferenceCallback implements VolumeSeekBarPreference.Callback {
         private SeekBarVolumizer mCurrent;
 
         @Override
@@ -568,12 +451,6 @@
                 case STOP_SAMPLE:
                     mVolumeCallback.stopSample();
                     break;
-                case UPDATE_EFFECTS_SUPPRESSOR:
-                    updateEffectsSuppressor();
-                    break;
-                case UPDATE_RINGER_MODE:
-                    updateRingerMode();
-                    break;
                 case UPDATE_ALARM_RINGTONE:
                     mAlarmRingtonePreference.setSummary((CharSequence) msg.obj);
                     break;
@@ -581,33 +458,6 @@
         }
     }
 
-    private class Receiver extends BroadcastReceiver {
-        private boolean mRegistered;
-
-        public void register(boolean register) {
-            if (mRegistered == register) return;
-            if (register) {
-                final IntentFilter filter = new IntentFilter();
-                filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
-                filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
-                mContext.registerReceiver(this, filter);
-            } else {
-                mContext.unregisterReceiver(this);
-            }
-            mRegistered = register;
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            if (NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED.equals(action)) {
-                mHandler.sendEmptyMessage(H.UPDATE_EFFECTS_SUPPRESSOR);
-            } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) {
-                mHandler.sendEmptyMessage(H.UPDATE_RINGER_MODE);
-            }
-        }
-    }
-
     // === Summary ===
 
     private static class SummaryProvider extends BroadcastReceiver
@@ -684,12 +534,13 @@
 
         public List<String> getNonIndexableKeys(Context context) {
             final ArrayList<String> rt = new ArrayList<String>();
-            if (Utils.isVoiceCapable(context)) {
-                rt.add(KEY_NOTIFICATION_VOLUME);
-            } else {
-                rt.add(KEY_RING_VOLUME);
+            new NotificationVolumePreferenceController(
+                context, null /* Callback */, null /* Lifecycle */).updateNonIndexableKeys(rt);
+            new RingVolumePreferenceController(
+                context, null /* Callback */, null /* Lifecycle */).updateNonIndexableKeys(rt);
+            new CastPreferenceController(context).updateNonIndexableKeys(rt);
+            if (!Utils.isVoiceCapable(context)) {
                 rt.add(KEY_PHONE_RINGTONE);
-                rt.add(KEY_WIFI_DISPLAY);
                 rt.add(KEY_VIBRATE_WHEN_RINGING);
             }
 
diff --git a/src/com/android/settings/notification/VolumeSeekBarPreference.java b/src/com/android/settings/notification/VolumeSeekBarPreference.java
index 7b02cae..5ab1d29 100644
--- a/src/com/android/settings/notification/VolumeSeekBarPreference.java
+++ b/src/com/android/settings/notification/VolumeSeekBarPreference.java
@@ -146,7 +146,9 @@
     @Override
     public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
         super.onProgressChanged(seekBar, progress, fromTouch);
-        mCallback.onStreamValueChanged(mStream, progress);
+        if (mCallback != null) {
+            mCallback.onStreamValueChanged(mStream, progress);
+        }
     }
 
     private void updateIconView() {
diff --git a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java
new file mode 100644
index 0000000..adc86b8
--- /dev/null
+++ b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settings.core.lifecycle.Lifecycle;
+import com.android.settings.core.lifecycle.LifecycleObserver;
+import com.android.settings.core.lifecycle.events.OnPause;
+import com.android.settings.core.lifecycle.events.OnResume;
+import com.android.settings.notification.VolumeSeekBarPreference.Callback;
+
+/**
+ * Base class for preference controller that handles VolumeSeekBarPreference
+ */
+public abstract class VolumeSeekBarPreferenceController extends
+    AdjustVolumeRestrictedPreferenceController implements LifecycleObserver, OnResume, OnPause {
+
+    protected VolumeSeekBarPreference mPreference;
+    protected VolumeSeekBarPreference.Callback mVolumePreferenceCallback;
+
+    public VolumeSeekBarPreferenceController(Context context, Callback callback,
+        Lifecycle lifecycle) {
+        super(context);
+        mVolumePreferenceCallback = callback;
+        if (lifecycle != null) {
+            lifecycle.addObserver(this);
+        }
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        if (isAvailable()) {
+            mPreference = (VolumeSeekBarPreference) screen.findPreference(getPreferenceKey());
+            mPreference.setCallback(mVolumePreferenceCallback);
+            mPreference.setStream(getAudioStream());
+            mPreference.setMuteIcon(getMuteIcon());
+        }
+    }
+
+    @Override
+    public void onResume() {
+        if (mPreference != null) {
+            mPreference.onActivityResume();
+        }
+    }
+
+    @Override
+    public void onPause() {
+        if (mPreference != null) {
+            mPreference.onActivityPause();
+        }
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        return false;
+    }
+
+    protected abstract int getAudioStream();
+
+    protected abstract int getMuteIcon();
+
+}
diff --git a/src/com/android/settings/notification/ZenModePreferenceController.java b/src/com/android/settings/notification/ZenModePreferenceController.java
new file mode 100644
index 0000000..e554732
--- /dev/null
+++ b/src/com/android/settings/notification/ZenModePreferenceController.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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.settings.notification;
+
+import android.content.Context;
+import android.support.v7.preference.Preference;
+
+public class ZenModePreferenceController extends AdjustVolumeRestrictedPreferenceController {
+
+    private static final String KEY_ZEN_MODE = "zen_mode";
+
+    public ZenModePreferenceController(Context context) {
+        super(context);
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        return false;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY_ZEN_MODE;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+
+}