Merge "[VolumePanel] Move the Settings' volume panel into a SystemUIDialog" into tm-qpr-dev am: 16219f2ea4

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19482609

Change-Id: I51953caf99f845431d7ceeb7fd102aef308b2837
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Jenhou (Alan) Huang
2022-08-30 05:43:38 +00:00
committed by Automerger Merge Worker
16 changed files with 753 additions and 3 deletions

View File

@@ -58,6 +58,12 @@ public class FeatureFlagUtils {
public static final String SETTINGS_APP_LOCALE_OPT_IN_ENABLED = public static final String SETTINGS_APP_LOCALE_OPT_IN_ENABLED =
"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 */ /** @hide */
public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS = public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS =
@@ -105,6 +111,7 @@ public class FeatureFlagUtils {
DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true"); DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true");
DEFAULT_FLAGS.put("settings_search_always_expand", "true"); DEFAULT_FLAGS.put("settings_search_always_expand", "true");
DEFAULT_FLAGS.put(SETTINGS_APP_LOCALE_OPT_IN_ENABLED, "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_ENABLE_MONITOR_PHANTOM_PROCS, "true");
DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "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"); DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true");

View File

@@ -81,5 +81,6 @@
<permission name="android.permission.READ_DEVICE_CONFIG" /> <permission name="android.permission.READ_DEVICE_CONFIG" />
<permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> <permission name="android.permission.READ_SAFETY_CENTER_STATUS" />
<permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" /> <permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" />
<permission name="android.permission.READ_SEARCH_INDEXABLES" />
</privapp-permissions> </privapp-permissions>
</permissions> </permissions>

View File

@@ -152,6 +152,9 @@
<uses-permission android:name="android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS" /> <uses-permission android:name="android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS" />
<uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" /> <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 --> <!-- Needed for WallpaperManager.clear in ImageWallpaper.updateWallpaperLocked -->
<uses-permission android:name="android.permission.SET_WALLPAPER"/> <uses-permission android:name="android.permission.SET_WALLPAPER"/>
@@ -956,5 +959,13 @@
</intent-filter> </intent-filter>
</receiver> </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> </application>
</manifest> </manifest>

View File

@@ -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>

View File

@@ -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>

View File

@@ -479,6 +479,10 @@
<dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen> <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. --> <!-- 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">42dp</dimen>
<dimen name="volume_ringer_drawer_item_size_half">21dp</dimen> <dimen name="volume_ringer_drawer_item_size_half">21dp</dimen>

View File

@@ -1139,6 +1139,11 @@
<!-- Content description for accessibility: Hint if click will disable. [CHAR LIMIT=NONE] --> <!-- Content description for accessibility: Hint if click will disable. [CHAR LIMIT=NONE] -->
<string name="volume_odi_captions_hint_disable">disable</string> <string name="volume_odi_captions_hint_disable">disable</string>
<!-- Sound and vibration settings dialog title. [CHAR LIMIT=30] -->
<string name="sound_settings">Sound &amp; 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]--> <!-- content description for audio output chooser [CHAR LIMIT=NONE]-->
<!-- Screen pinning dialog title. --> <!-- Screen pinning dialog title. -->

View File

@@ -928,6 +928,10 @@
<item name="rowStyle">@style/SliceRow</item> <item name="rowStyle">@style/SliceRow</item>
</style> </style>
<style name="Widget.SliceView.Panel.Slider">
<item name="rowStyle">@style/SliceRow.Slider</item>
</style>
<style name="SliceRow"> <style name="SliceRow">
<!-- 2dp start padding for the start icon --> <!-- 2dp start padding for the start icon -->
<item name="titleItemStartPadding">2dp</item> <item name="titleItemStartPadding">2dp</item>
@@ -949,6 +953,26 @@
<item name="actionDividerHeight">32dp</item> <item name="actionDividerHeight">32dp</item>
</style> </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"> <style name="TextAppearance.Dialog.Title" parent="@android:style/TextAppearance.DeviceDefault.Large">
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:textSize">24sp</item> <item name="android:textSize">24sp</item>

View File

@@ -26,6 +26,7 @@ import com.android.systemui.people.widget.PeopleSpaceWidgetProvider;
import com.android.systemui.screenshot.ActionProxyReceiver; import com.android.systemui.screenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.DeleteScreenshotReceiver; import com.android.systemui.screenshot.DeleteScreenshotReceiver;
import com.android.systemui.screenshot.SmartActionsReceiver; import com.android.systemui.screenshot.SmartActionsReceiver;
import com.android.systemui.volume.VolumePanelDialogReceiver;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
@@ -73,6 +74,15 @@ public abstract class DefaultBroadcastReceiverBinder {
public abstract BroadcastReceiver bindMediaOutputDialogReceiver( public abstract BroadcastReceiver bindMediaOutputDialogReceiver(
MediaOutputDialogReceiver broadcastReceiver); MediaOutputDialogReceiver broadcastReceiver);
/**
*
*/
@Binds
@IntoMap
@ClassKey(VolumePanelDialogReceiver.class)
public abstract BroadcastReceiver bindVolumePanelDialogReceiver(
VolumePanelDialogReceiver broadcastReceiver);
/** /**
* *
*/ */

View File

@@ -48,7 +48,6 @@ import android.app.KeyguardManager;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.content.res.Configuration; import android.content.res.Configuration;
@@ -248,6 +247,7 @@ public class VolumeDialogImpl implements VolumeDialog,
private final ConfigurationController mConfigurationController; private final ConfigurationController mConfigurationController;
private final MediaOutputDialogFactory mMediaOutputDialogFactory; private final MediaOutputDialogFactory mMediaOutputDialogFactory;
private final VolumePanelFactory mVolumePanelFactory;
private final ActivityStarter mActivityStarter; private final ActivityStarter mActivityStarter;
private boolean mShowing; private boolean mShowing;
@@ -279,6 +279,7 @@ public class VolumeDialogImpl implements VolumeDialog,
DeviceProvisionedController deviceProvisionedController, DeviceProvisionedController deviceProvisionedController,
ConfigurationController configurationController, ConfigurationController configurationController,
MediaOutputDialogFactory mediaOutputDialogFactory, MediaOutputDialogFactory mediaOutputDialogFactory,
VolumePanelFactory volumePanelFactory,
ActivityStarter activityStarter, ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor) { InteractionJankMonitor interactionJankMonitor) {
mContext = mContext =
@@ -290,6 +291,7 @@ public class VolumeDialogImpl implements VolumeDialog,
mDeviceProvisionedController = deviceProvisionedController; mDeviceProvisionedController = deviceProvisionedController;
mConfigurationController = configurationController; mConfigurationController = configurationController;
mMediaOutputDialogFactory = mediaOutputDialogFactory; mMediaOutputDialogFactory = mediaOutputDialogFactory;
mVolumePanelFactory = volumePanelFactory;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
mShowActiveStreamOnly = showActiveStreamOnly(); mShowActiveStreamOnly = showActiveStreamOnly();
mHasSeenODICaptionsTooltip = mHasSeenODICaptionsTooltip =
@@ -1045,10 +1047,9 @@ public class VolumeDialogImpl implements VolumeDialog,
if (mSettingsIcon != null) { if (mSettingsIcon != null) {
mSettingsIcon.setOnClickListener(v -> { mSettingsIcon.setOnClickListener(v -> {
Events.writeEvent(Events.EVENT_SETTINGS_CLICK); Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
Intent intent = new Intent(Settings.Panel.ACTION_VOLUME);
dismissH(DISMISS_REASON_SETTINGS_CLICKED); dismissH(DISMISS_REASON_SETTINGS_CLICKED);
mMediaOutputDialogFactory.dismiss(); mMediaOutputDialogFactory.dismiss();
mActivityStarter.startActivity(intent, true /* dismissShade */); mVolumePanelFactory.create(true /* aboveStatusBar */, null);
}); });
} }
} }

View File

@@ -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;
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -30,6 +30,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.volume.VolumeComponent; import com.android.systemui.volume.VolumeComponent;
import com.android.systemui.volume.VolumeDialogComponent; import com.android.systemui.volume.VolumeDialogComponent;
import com.android.systemui.volume.VolumeDialogImpl; import com.android.systemui.volume.VolumeDialogImpl;
import com.android.systemui.volume.VolumePanelFactory;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
@@ -52,6 +53,7 @@ public interface VolumeModule {
DeviceProvisionedController deviceProvisionedController, DeviceProvisionedController deviceProvisionedController,
ConfigurationController configurationController, ConfigurationController configurationController,
MediaOutputDialogFactory mediaOutputDialogFactory, MediaOutputDialogFactory mediaOutputDialogFactory,
VolumePanelFactory volumePanelFactory,
ActivityStarter activityStarter, ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor) { InteractionJankMonitor interactionJankMonitor) {
VolumeDialogImpl impl = new VolumeDialogImpl( VolumeDialogImpl impl = new VolumeDialogImpl(
@@ -61,6 +63,7 @@ public interface VolumeModule {
deviceProvisionedController, deviceProvisionedController,
configurationController, configurationController,
mediaOutputDialogFactory, mediaOutputDialogFactory,
volumePanelFactory,
activityStarter, activityStarter,
interactionJankMonitor); interactionJankMonitor);
impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);

View File

@@ -85,6 +85,8 @@ public class VolumeDialogImplTest extends SysuiTestCase {
@Mock @Mock
MediaOutputDialogFactory mMediaOutputDialogFactory; MediaOutputDialogFactory mMediaOutputDialogFactory;
@Mock @Mock
VolumePanelFactory mVolumePanelFactory;
@Mock
ActivityStarter mActivityStarter; ActivityStarter mActivityStarter;
@Mock @Mock
InteractionJankMonitor mInteractionJankMonitor; InteractionJankMonitor mInteractionJankMonitor;
@@ -102,6 +104,7 @@ public class VolumeDialogImplTest extends SysuiTestCase {
mDeviceProvisionedController, mDeviceProvisionedController,
mConfigurationController, mConfigurationController,
mMediaOutputDialogFactory, mMediaOutputDialogFactory,
mVolumePanelFactory,
mActivityStarter, mActivityStarter,
mInteractionJankMonitor); mInteractionJankMonitor);
mDialog.init(0, null); mDialog.init(0, null);