[VolumePanel] Move the Settings' volume panel into a SystemUIDialog
Move most of the logic of the Settings' volume panel to a SystemUIDialog
to make the dialogs consistent with other system dialog.
Bug: 202262476
Test: manual build and launch the new dialog.
Change-Id: Ic27dcca77072dee2b78827e1eb58c28022b47265
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index e0e41d0..976a3e4 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -58,6 +58,12 @@
public static final String SETTINGS_APP_LOCALE_OPT_IN_ENABLED =
"settings_app_locale_opt_in_enabled";
+ /**
+ * Launch the Volume panel in SystemUI.
+ * @hide
+ */
+ public static final String SETTINGS_VOLUME_PANEL_IN_SYSTEMUI =
+ "settings_volume_panel_in_systemui";
/** @hide */
public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS =
@@ -105,6 +111,7 @@
DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true");
DEFAULT_FLAGS.put("settings_search_always_expand", "true");
DEFAULT_FLAGS.put(SETTINGS_APP_LOCALE_OPT_IN_ENABLED, "true");
+ DEFAULT_FLAGS.put(SETTINGS_VOLUME_PANEL_IN_SYSTEMUI, "false");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true");
DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true");
DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true");
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index f030d80..e0e13f5 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -81,5 +81,6 @@
<permission name="android.permission.READ_DEVICE_CONFIG" />
<permission name="android.permission.READ_SAFETY_CENTER_STATUS" />
<permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" />
+ <permission name="android.permission.READ_SEARCH_INDEXABLES" />
</privapp-permissions>
</permissions>
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 78dea89..62def48 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -152,6 +152,9 @@
<uses-permission android:name="android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS" />
<uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" />
+ <!-- For auto-grant the access to the Settings' slice preferences, e.g. volume slices. -->
+ <uses-permission android:name="android.permission.READ_SEARCH_INDEXABLES" />
+
<!-- Needed for WallpaperManager.clear in ImageWallpaper.updateWallpaperLocked -->
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
@@ -956,5 +959,13 @@
</intent-filter>
</receiver>
+ <receiver android:name=".volume.VolumePanelDialogReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.settings.panel.action.VOLUME" />
+ <action android:name="com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG" />
+ <action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" />
+ </intent-filter>
+ </receiver>
</application>
</manifest>
diff --git a/packages/SystemUI/res/layout/volume_panel_dialog.xml b/packages/SystemUI/res/layout/volume_panel_dialog.xml
new file mode 100644
index 0000000..99a1b5c
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_panel_dialog.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/volume_panel_dialog"
+ android:layout_width="@dimen/large_dialog_width"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/Widget.SliceView.Panel"
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_marginTop="@dimen/dialog_top_padding"
+ android:layout_marginBottom="@dimen/dialog_bottom_padding"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/volume_panel_dialog_title"
+ android:ellipsize="end"
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sound_settings"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"/>
+ </LinearLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/volume_panel_parent_layout"
+ android:scrollbars="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:minHeight="304dp"
+ android:layout_weight="1"
+ android:overScrollMode="never"/>
+
+ <LinearLayout
+ android:id="@+id/button_layout"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/dialog_button_vertical_padding"
+ android:layout_marginStart="@dimen/dialog_side_padding"
+ android:layout_marginEnd="@dimen/dialog_side_padding"
+ android:layout_marginBottom="@dimen/dialog_bottom_padding"
+ android:baselineAligned="false"
+ android:clickable="false"
+ android:focusable="false">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="start|center_vertical"
+ android:orientation="vertical">
+ <Button
+ android:id="@+id/settings_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/volume_panel_dialog_settings_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/Widget.Dialog.Button.BorderButton"
+ android:clickable="true"
+ android:focusable="true"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/dialog_button_horizontal_padding"
+ android:layout_gravity="end|center_vertical">
+ <Button
+ android:id="@+id/done_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/inline_done_button"
+ style="@style/Widget.Dialog.Button"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:clickable="true"
+ android:focusable="true"/>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml b/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml
new file mode 100644
index 0000000..d1303ed
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/slice_slider_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <androidx.slice.widget.SliceView
+ android:id="@+id/slice_view"
+ style="@style/Widget.SliceView.Panel.Slider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/volume_panel_slice_vertical_padding"
+ android:paddingHorizontal="@dimen/volume_panel_slice_horizontal_padding"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c9776dd..c939504 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -479,6 +479,10 @@
<dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen>
+ <!-- Volume panel slices dimensions -->
+ <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
+ <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
+
<!-- Size of each item in the ringer selector drawer. -->
<dimen name="volume_ringer_drawer_item_size">42dp</dimen>
<dimen name="volume_ringer_drawer_item_size_half">21dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e4fefc7..2d0fa53 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1139,6 +1139,11 @@
<!-- Content description for accessibility: Hint if click will disable. [CHAR LIMIT=NONE] -->
<string name="volume_odi_captions_hint_disable">disable</string>
+ <!-- Sound and vibration settings dialog title. [CHAR LIMIT=30] -->
+ <string name="sound_settings">Sound & vibration</string>
+ <!-- Label for button to go to sound settings screen [CHAR_LIMIT=30] -->
+ <string name="volume_panel_dialog_settings_button">Settings</string>
+
<!-- content description for audio output chooser [CHAR LIMIT=NONE]-->
<!-- Screen pinning dialog title. -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 112d903..6b2ff37 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -928,6 +928,10 @@
<item name="rowStyle">@style/SliceRow</item>
</style>
+ <style name="Widget.SliceView.Panel.Slider">
+ <item name="rowStyle">@style/SliceRow.Slider</item>
+ </style>
+
<style name="SliceRow">
<!-- 2dp start padding for the start icon -->
<item name="titleItemStartPadding">2dp</item>
@@ -949,6 +953,26 @@
<item name="actionDividerHeight">32dp</item>
</style>
+ <style name="SliceRow.Slider">
+ <!-- Padding between content and the start icon is 5dp -->
+ <item name="contentStartPadding">5dp</item>
+ <item name="contentEndPadding">0dp</item>
+
+ <!-- 0dp start padding for the end item -->
+ <item name="endItemStartPadding">0dp</item>
+ <!-- 8dp end padding for the end item -->
+ <item name="endItemEndPadding">8dp</item>
+
+ <item name="titleSize">20sp</item>
+ <!-- Align text with slider -->
+ <item name="titleStartPadding">11dp</item>
+ <item name="subContentStartPadding">11dp</item>
+
+ <!-- Padding for indeterminate progress bar -->
+ <item name="progressBarStartPadding">12dp</item>
+ <item name="progressBarEndPadding">16dp</item>
+ </style>
+
<style name="TextAppearance.Dialog.Title" parent="@android:style/TextAppearance.DeviceDefault.Large">
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:textSize">24sp</item>
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
index 8ba6f1c..d60a222 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
@@ -26,6 +26,7 @@
import com.android.systemui.screenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.DeleteScreenshotReceiver;
import com.android.systemui.screenshot.SmartActionsReceiver;
+import com.android.systemui.volume.VolumePanelDialogReceiver;
import dagger.Binds;
import dagger.Module;
@@ -78,6 +79,15 @@
*/
@Binds
@IntoMap
+ @ClassKey(VolumePanelDialogReceiver.class)
+ public abstract BroadcastReceiver bindVolumePanelDialogReceiver(
+ VolumePanelDialogReceiver broadcastReceiver);
+
+ /**
+ *
+ */
+ @Binds
+ @IntoMap
@ClassKey(PeopleSpaceWidgetPinnedReceiver.class)
public abstract BroadcastReceiver bindPeopleSpaceWidgetPinnedReceiver(
PeopleSpaceWidgetPinnedReceiver broadcastReceiver);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index c94a915..e6a3e74c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -48,7 +48,6 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -248,6 +247,7 @@
private final ConfigurationController mConfigurationController;
private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+ private final VolumePanelFactory mVolumePanelFactory;
private final ActivityStarter mActivityStarter;
private boolean mShowing;
@@ -279,6 +279,7 @@
DeviceProvisionedController deviceProvisionedController,
ConfigurationController configurationController,
MediaOutputDialogFactory mediaOutputDialogFactory,
+ VolumePanelFactory volumePanelFactory,
ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor) {
mContext =
@@ -290,6 +291,7 @@
mDeviceProvisionedController = deviceProvisionedController;
mConfigurationController = configurationController;
mMediaOutputDialogFactory = mediaOutputDialogFactory;
+ mVolumePanelFactory = volumePanelFactory;
mActivityStarter = activityStarter;
mShowActiveStreamOnly = showActiveStreamOnly();
mHasSeenODICaptionsTooltip =
@@ -1043,10 +1045,9 @@
if (mSettingsIcon != null) {
mSettingsIcon.setOnClickListener(v -> {
Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
- Intent intent = new Intent(Settings.Panel.ACTION_VOLUME);
dismissH(DISMISS_REASON_SETTINGS_CLICKED);
mMediaOutputDialogFactory.dismiss();
- mActivityStarter.startActivity(intent, true /* dismissShade */);
+ mVolumePanelFactory.create(true /* aboveStatusBar */, null);
});
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java
new file mode 100644
index 0000000..2c74fb9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2022 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.systemui.volume;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.provider.SettingsSlicesContract;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.slice.Slice;
+import androidx.slice.SliceMetadata;
+import androidx.slice.widget.EventInfo;
+import androidx.slice.widget.SliceLiveData;
+
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.media.MediaOutputConstants;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Visual presentation of the volume panel dialog.
+ */
+public class VolumePanelDialog extends SystemUIDialog implements LifecycleOwner {
+ private static final String TAG = "VolumePanelDialog";
+
+ private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 200;
+ private static final int DEFAULT_SLICE_SIZE = 4;
+
+ private RecyclerView mVolumePanelSlices;
+ private VolumePanelSlicesAdapter mVolumePanelSlicesAdapter;
+ private final LifecycleRegistry mLifecycleRegistry;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Map<Uri, LiveData<Slice>> mSliceLiveData = new LinkedHashMap<>();
+ private final HashSet<Uri> mLoadedSlices = new HashSet<>();
+ private boolean mSlicesReadyToLoad;
+ private LocalBluetoothProfileManager mProfileManager;
+
+ public VolumePanelDialog(Context context, boolean aboveStatusBar) {
+ super(context);
+ mLifecycleRegistry = new LifecycleRegistry(this);
+ if (!aboveStatusBar) {
+ getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate");
+
+ View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.volume_panel_dialog,
+ null);
+ final Window window = getWindow();
+ window.setContentView(dialogView);
+
+ Button doneButton = dialogView.findViewById(R.id.done_button);
+ doneButton.setOnClickListener(v -> dismiss());
+ Button settingsButton = dialogView.findViewById(R.id.settings_button);
+ settingsButton.setOnClickListener(v -> {
+ getContext().startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK));
+ dismiss();
+ });
+
+ LocalBluetoothManager localBluetoothManager = LocalBluetoothManager.getInstance(
+ getContext(), null);
+ if (localBluetoothManager != null) {
+ mProfileManager = localBluetoothManager.getProfileManager();
+ }
+
+ mVolumePanelSlices = dialogView.findViewById(R.id.volume_panel_parent_layout);
+ mVolumePanelSlices.setLayoutManager(new LinearLayoutManager(getContext()));
+
+ loadAllSlices();
+
+ mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
+ }
+
+ private void loadAllSlices() {
+ mSliceLiveData.clear();
+ mLoadedSlices.clear();
+ final List<Uri> sliceUris = getSlices();
+
+ for (Uri uri : sliceUris) {
+ final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getContext(), uri,
+ (int type, Throwable source) -> {
+ if (!removeSliceLiveData(uri)) {
+ mLoadedSlices.add(uri);
+ }
+ });
+
+ // Add slice first to make it in order. Will remove it later if there's an error.
+ mSliceLiveData.put(uri, sliceLiveData);
+
+ sliceLiveData.observe(this, slice -> {
+ if (mLoadedSlices.contains(uri)) {
+ return;
+ }
+ Log.d(TAG, "received slice: " + (slice == null ? null : slice.getUri()));
+ final SliceMetadata metadata = SliceMetadata.from(getContext(), slice);
+ if (slice == null || metadata.isErrorSlice()) {
+ if (!removeSliceLiveData(uri)) {
+ mLoadedSlices.add(uri);
+ }
+ } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) {
+ mLoadedSlices.add(uri);
+ } else {
+ mHandler.postDelayed(() -> {
+ mLoadedSlices.add(uri);
+ setupAdapterWhenReady();
+ }, DURATION_SLICE_BINDING_TIMEOUT_MS);
+ }
+
+ setupAdapterWhenReady();
+ });
+ }
+ }
+
+ private void setupAdapterWhenReady() {
+ if (mLoadedSlices.size() == mSliceLiveData.size() && !mSlicesReadyToLoad) {
+ mSlicesReadyToLoad = true;
+ mVolumePanelSlicesAdapter = new VolumePanelSlicesAdapter(this, mSliceLiveData);
+ mVolumePanelSlicesAdapter.setOnSliceActionListener((eventInfo, sliceItem) -> {
+ if (eventInfo.actionType == EventInfo.ACTION_TYPE_SLIDER) {
+ return;
+ }
+ this.dismiss();
+ });
+ if (mSliceLiveData.size() < DEFAULT_SLICE_SIZE) {
+ mVolumePanelSlices.setMinimumHeight(0);
+ }
+ mVolumePanelSlices.setAdapter(mVolumePanelSlicesAdapter);
+ }
+ }
+
+ private boolean removeSliceLiveData(Uri uri) {
+ boolean removed = false;
+ // Keeps observe media output slice
+ if (!uri.equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) {
+ Log.d(TAG, "remove uri: " + uri);
+ removed = mSliceLiveData.remove(uri) != null;
+ if (mVolumePanelSlicesAdapter != null) {
+ mVolumePanelSlicesAdapter.updateDataSet(new ArrayList<>(mSliceLiveData.values()));
+ }
+ }
+ return removed;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Log.d(TAG, "onStart");
+ mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
+ mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.d(TAG, "onStop");
+ mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+ }
+
+ private List<Uri> getSlices() {
+ final List<Uri> uris = new ArrayList<>();
+ uris.add(REMOTE_MEDIA_SLICE_URI);
+ uris.add(VOLUME_MEDIA_URI);
+ Uri controlUri = getExtraControlUri();
+ if (controlUri != null) {
+ Log.d(TAG, "add extra control slice");
+ uris.add(controlUri);
+ }
+ uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI);
+ uris.add(VOLUME_CALL_URI);
+ uris.add(VOLUME_RINGER_URI);
+ uris.add(VOLUME_ALARM_URI);
+ return uris;
+ }
+
+ private static final String SETTINGS_SLICE_AUTHORITY = "com.android.settings.slices";
+ private static final Uri REMOTE_MEDIA_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SETTINGS_SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath(MediaOutputConstants.KEY_REMOTE_MEDIA)
+ .build();
+ private static final Uri VOLUME_MEDIA_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SETTINGS_SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("media_volume")
+ .build();
+ private static final Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SETTINGS_SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT)
+ .appendPath("media_output_indicator")
+ .build();
+ private static final Uri VOLUME_CALL_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SETTINGS_SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("call_volume")
+ .build();
+ private static final Uri VOLUME_RINGER_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SETTINGS_SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("ring_volume")
+ .build();
+ private static final Uri VOLUME_ALARM_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SETTINGS_SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("alarm_volume")
+ .build();
+
+ private Uri getExtraControlUri() {
+ Uri controlUri = null;
+ final BluetoothDevice bluetoothDevice = findActiveDevice();
+ if (bluetoothDevice != null) {
+ // The control slice width = dialog width - horizontal padding of two sides
+ final int dialogWidth =
+ getWindow().getWindowManager().getCurrentWindowMetrics().getBounds().width();
+ final int controlSliceWidth = dialogWidth
+ - getContext().getResources().getDimensionPixelSize(
+ R.dimen.volume_panel_slice_horizontal_padding) * 2;
+ final String uri = BluetoothUtils.getControlUriMetaData(bluetoothDevice);
+ if (!TextUtils.isEmpty(uri)) {
+ try {
+ controlUri = Uri.parse(uri + controlSliceWidth);
+ } catch (NullPointerException exception) {
+ Log.d(TAG, "unable to parse extra control uri");
+ controlUri = null;
+ }
+ }
+ }
+ return controlUri;
+ }
+
+ private BluetoothDevice findActiveDevice() {
+ if (mProfileManager != null) {
+ final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+ if (a2dpProfile != null) {
+ return a2dpProfile.getActiveDevice();
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycleRegistry;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt
new file mode 100644
index 0000000..f11d5d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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.systemui.volume
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import android.text.TextUtils
+import android.util.Log
+import javax.inject.Inject
+
+private const val TAG = "VolumePanelDialogReceiver"
+private const val LAUNCH_ACTION = "com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG"
+private const val DISMISS_ACTION = "com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG"
+
+/**
+ * BroadcastReceiver for handling volume panel dialog intent
+ */
+class VolumePanelDialogReceiver @Inject constructor(
+ private val volumePanelFactory: VolumePanelFactory
+) : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "onReceive intent" + intent.action)
+ if (TextUtils.equals(LAUNCH_ACTION, intent.action) ||
+ TextUtils.equals(Settings.Panel.ACTION_VOLUME, intent.action)) {
+ volumePanelFactory.create(true, null)
+ } else if (TextUtils.equals(DISMISS_ACTION, intent.action)) {
+ volumePanelFactory.dismiss()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt
new file mode 100644
index 0000000..c2fafbf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 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.systemui.volume
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+private const val TAG = "VolumePanelFactory"
+private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+
+/**
+ * Factory to create [VolumePanelDialog] objects. This is the dialog that allows the user to adjust
+ * multiple streams with sliders.
+ */
+@SysUISingleton
+class VolumePanelFactory @Inject constructor(
+ private val context: Context,
+ private val dialogLaunchAnimator: DialogLaunchAnimator
+) {
+ companion object {
+ var volumePanelDialog: VolumePanelDialog? = null
+ }
+
+ /** Creates a [VolumePanelDialog]. The dialog will be animated from [view] if it is not null. */
+ fun create(aboveStatusBar: Boolean, view: View? = null) {
+ if (volumePanelDialog?.isShowing == true) {
+ return
+ }
+
+ val dialog = VolumePanelDialog(context, aboveStatusBar)
+ volumePanelDialog = dialog
+
+ // Show the dialog.
+ if (view != null) {
+ dialogLaunchAnimator.showFromView(dialog, view, animateBackgroundBoundsChange = true)
+ } else {
+ dialog.show()
+ }
+ }
+
+ /** Dismiss [VolumePanelDialog] if exist. */
+ fun dismiss() {
+ if (DEBUG) {
+ Log.d(TAG, "dismiss dialog")
+ }
+ volumePanelDialog?.dismiss()
+ volumePanelDialog = null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java
new file mode 100644
index 0000000..2371402
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2022 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.systemui.volume;
+
+import static android.app.slice.Slice.HINT_ERROR;
+import static android.app.slice.SliceItem.FORMAT_SLICE;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.slice.Slice;
+import androidx.slice.SliceItem;
+import androidx.slice.widget.SliceView;
+
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * RecyclerView adapter for Slices in Settings Panels.
+ */
+public class VolumePanelSlicesAdapter extends
+ RecyclerView.Adapter<VolumePanelSlicesAdapter.SliceRowViewHolder> {
+
+ private final List<LiveData<Slice>> mSliceLiveData;
+ private final LifecycleOwner mLifecycleOwner;
+ private SliceView.OnSliceActionListener mOnSliceActionListener;
+
+ public VolumePanelSlicesAdapter(LifecycleOwner lifecycleOwner,
+ Map<Uri, LiveData<Slice>> sliceLiveData) {
+ mLifecycleOwner = lifecycleOwner;
+ mSliceLiveData = new ArrayList<>(sliceLiveData.values());
+ }
+
+ @NonNull
+ @Override
+ public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+ final Context context = viewGroup.getContext();
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ View view = inflater.inflate(R.layout.volume_panel_slice_slider_row, viewGroup, false);
+ return new SliceRowViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) {
+ sliceRowViewHolder.onBind(mSliceLiveData.get(position), position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSliceLiveData.size();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position;
+ }
+
+ void setOnSliceActionListener(SliceView.OnSliceActionListener listener) {
+ mOnSliceActionListener = listener;
+ }
+
+ void updateDataSet(ArrayList<LiveData<Slice>> list) {
+ mSliceLiveData.clear();
+ mSliceLiveData.addAll(list);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * ViewHolder for binding Slices to SliceViews.
+ */
+ public class SliceRowViewHolder extends RecyclerView.ViewHolder {
+
+ private final SliceView mSliceView;
+
+ public SliceRowViewHolder(View view) {
+ super(view);
+ mSliceView = view.findViewById(R.id.slice_view);
+ mSliceView.setMode(SliceView.MODE_LARGE);
+ mSliceView.setShowTitleItems(true);
+ mSliceView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mSliceView.setOnSliceActionListener(mOnSliceActionListener);
+ }
+
+ /**
+ * Called when the view is displayed.
+ */
+ public void onBind(LiveData<Slice> sliceLiveData, int position) {
+ sliceLiveData.observe(mLifecycleOwner, mSliceView);
+
+ // Do not show the divider above media devices switcher slice per request
+ final Slice slice = sliceLiveData.getValue();
+
+ // Hides slice which reports with error hint or not contain any slice sub-item.
+ if (slice == null || !isValidSlice(slice)) {
+ mSliceView.setVisibility(View.GONE);
+ } else {
+ mSliceView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private boolean isValidSlice(Slice slice) {
+ if (slice.getHints().contains(HINT_ERROR)) {
+ return false;
+ }
+ for (SliceItem item : slice.getItems()) {
+ if (item.getFormat().equals(FORMAT_SLICE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index f3855bd..c5792b9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -30,6 +30,7 @@
import com.android.systemui.volume.VolumeComponent;
import com.android.systemui.volume.VolumeDialogComponent;
import com.android.systemui.volume.VolumeDialogImpl;
+import com.android.systemui.volume.VolumePanelFactory;
import dagger.Binds;
import dagger.Module;
@@ -52,6 +53,7 @@
DeviceProvisionedController deviceProvisionedController,
ConfigurationController configurationController,
MediaOutputDialogFactory mediaOutputDialogFactory,
+ VolumePanelFactory volumePanelFactory,
ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor) {
VolumeDialogImpl impl = new VolumeDialogImpl(
@@ -61,6 +63,7 @@
deviceProvisionedController,
configurationController,
mediaOutputDialogFactory,
+ volumePanelFactory,
activityStarter,
interactionJankMonitor);
impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 312db2d..2e74bf5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -85,6 +85,8 @@
@Mock
MediaOutputDialogFactory mMediaOutputDialogFactory;
@Mock
+ VolumePanelFactory mVolumePanelFactory;
+ @Mock
ActivityStarter mActivityStarter;
@Mock
InteractionJankMonitor mInteractionJankMonitor;
@@ -102,6 +104,7 @@
mDeviceProvisionedController,
mConfigurationController,
mMediaOutputDialogFactory,
+ mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor);
mDialog.init(0, null);