diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1588cd9657e..3ed7e42fc4e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -5085,6 +5085,10 @@
+
+
+
+
Quad9 Unsecured with ECS DNS
dns12.quad9.net
+
+
+ App volume
+ Per-app volume control
+ Show per-app volume button into volume dialog
diff --git a/res/xml/sound_settings.xml b/res/xml/sound_settings.xml
index 56968f127cf..e3939197685 100644
--- a/res/xml/sound_settings.xml
+++ b/res/xml/sound_settings.xml
@@ -168,6 +168,13 @@
android:ringtoneType="alarm"
android:order="-60"/>
+
+
appVols = new ArrayList<>();
+ for (AppVolume vol : mAudioManager.listAppVolumes()) {
+ if (vol.isActive()) {
+ appVols.add(vol);
+ }
+ }
+ if (appVols.isEmpty()) {
+ Log.d(TAG, "No active tracks");
+ return listBuilder.build();
+ }
+
+ for (AppVolume vol : appVols) {
+ final CharSequence appName = Utils.getApplicationLabel(
+ mContext, vol.getPackageName());
+ IconCompat icon = getApplicationIcon(vol.getPackageName());
+ final SliceAction primarySliceAction = SliceAction.create(
+ getBroadcastIntent(mContext), icon, ListBuilder.ICON_IMAGE, appName);
+ listBuilder.addInputRange(new InputRangeBuilder()
+ .setTitleItem(icon, ListBuilder.ICON_IMAGE)
+ .setTitle(appName)
+ .setInputAction(getSliderInputAction(vol.getPackageName()))
+ .setMax(100)
+ .setValue((int)(vol.getVolume() * 100))
+ .setPrimaryAction(primarySliceAction));
+ }
+ return listBuilder.build();
+ }
+
+ private IconCompat getApplicationIcon(String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
+ Resources resources = pm.getResourcesForApplication(ai);
+ IconCompat icon = IconCompat.createWithResource(resources, packageName, ai.icon);
+ return icon;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Failed to get icon of " + packageName, e);
+ }
+
+ final Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ return IconCompat.createWithBitmap(bitmap);
+ }
+
+
+ private PendingIntent getSliderInputAction(String packageName) {
+ final int requestCode = packageName.hashCode();
+ final Intent intent = new Intent(getUri().toString())
+ .setData(getUri())
+ .putExtra(PACKAGE_NAME, packageName)
+ .setClass(mContext, SliceBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(mContext, requestCode, intent,
+ PendingIntent.FLAG_MUTABLE);
+ }
+
+ @Override
+ public Uri getUri() {
+ return APP_VOLUME_SLICE_URI;
+ }
+
+ @Override
+ public Intent getIntent() {
+ return null;
+ }
+
+ @Override
+ public int getSliceHighlightMenuRes() {
+ return R.string.menu_key_sound;
+ }
+}
diff --git a/src/com/android/settings/panel/AppVolumePanel.java b/src/com/android/settings/panel/AppVolumePanel.java
new file mode 100644
index 00000000000..876a15cf576
--- /dev/null
+++ b/src/com/android/settings/panel/AppVolumePanel.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 Project Kaleidoscope
+ *
+ * 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.panel;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Settings;
+
+import com.android.settings.R;
+import com.android.settings.slices.CustomSliceRegistry;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AppVolumePanel implements PanelContent {
+
+ private final Context mContext;
+
+ public static AppVolumePanel create(Context context) {
+ return new AppVolumePanel(context);
+ }
+
+ private AppVolumePanel(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return mContext.getText(R.string.app_volume);
+ }
+
+ @Override
+ public List getSlices() {
+ final List uris = new ArrayList<>();
+ uris.add(CustomSliceRegistry.APP_VOLUME_SLICE_URI);
+ return uris;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.PANEL_VOLUME;
+ }
+
+ @Override
+ public Intent getSeeMoreIntent() {
+ return new Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+
+}
diff --git a/src/com/android/settings/panel/PanelFeatureProviderImpl.java b/src/com/android/settings/panel/PanelFeatureProviderImpl.java
index a0aeec60856..4d7acea49f3 100644
--- a/src/com/android/settings/panel/PanelFeatureProviderImpl.java
+++ b/src/com/android/settings/panel/PanelFeatureProviderImpl.java
@@ -87,6 +87,8 @@ public class PanelFeatureProviderImpl implements PanelFeatureProvider {
return VolumePanel.create(context);
}
}
+ case Settings.Panel.ACTION_APP_VOLUME:
+ return AppVolumePanel.create(context);
}
throw new IllegalStateException("No matching panel for: " + panelType);
diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java
index 38ca5cde95e..5100ceeadd3 100644
--- a/src/com/android/settings/slices/CustomSliceRegistry.java
+++ b/src/com/android/settings/slices/CustomSliceRegistry.java
@@ -33,6 +33,7 @@ import com.android.settings.homepage.contextualcards.slices.DarkThemeSlice;
import com.android.settings.homepage.contextualcards.slices.FaceSetupSlice;
import com.android.settings.homepage.contextualcards.slices.LowStorageSlice;
import com.android.settings.location.LocationSlice;
+import com.android.settings.media.AppVolumeSlice;
import com.android.settings.media.MediaOutputIndicatorSlice;
import com.android.settings.media.RemoteMediaSlice;
import com.android.settings.network.ProviderModelSlice;
@@ -291,6 +292,16 @@ public class CustomSliceRegistry {
.appendPath("always_on_display")
.build();
+ /**
+ * Backing Uri for the App Volume Slice.
+ */
+ public static Uri APP_VOLUME_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("app_volume")
+ .build();
+
@VisibleForTesting
static final Map> sUriToSlice;
@@ -305,6 +316,7 @@ public class CustomSliceRegistry {
sUriToSlice.put(ALWAYS_ON_SLICE_URI, AlwaysOnDisplaySlice.class);
sUriToSlice.put(MEDIA_OUTPUT_INDICATOR_SLICE_URI, MediaOutputIndicatorSlice.class);
sUriToSlice.put(REMOTE_MEDIA_SLICE_URI, RemoteMediaSlice.class);
+ sUriToSlice.put(APP_VOLUME_SLICE_URI, AppVolumeSlice.class);
// Slices for contextual card.
sUriToSlice.put(FACE_ENROLL_SLICE_URI, FaceSetupSlice.class);