1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.volume; 18 19 import android.bluetooth.BluetoothDevice; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.net.Uri; 24 import android.os.Bundle; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.provider.Settings; 28 import android.provider.SettingsSlicesContract; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.Window; 34 import android.view.WindowManager; 35 import android.widget.Button; 36 37 import androidx.annotation.NonNull; 38 import androidx.lifecycle.Lifecycle; 39 import androidx.lifecycle.LifecycleOwner; 40 import androidx.lifecycle.LifecycleRegistry; 41 import androidx.lifecycle.LiveData; 42 import androidx.recyclerview.widget.LinearLayoutManager; 43 import androidx.recyclerview.widget.RecyclerView; 44 import androidx.slice.Slice; 45 import androidx.slice.SliceMetadata; 46 import androidx.slice.widget.EventInfo; 47 import androidx.slice.widget.SliceLiveData; 48 49 import com.android.settingslib.bluetooth.A2dpProfile; 50 import com.android.settingslib.bluetooth.BluetoothUtils; 51 import com.android.settingslib.bluetooth.LocalBluetoothManager; 52 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 53 import com.android.settingslib.media.MediaOutputConstants; 54 import com.android.systemui.R; 55 import com.android.systemui.plugins.ActivityStarter; 56 import com.android.systemui.statusbar.phone.SystemUIDialog; 57 58 import java.util.ArrayList; 59 import java.util.HashSet; 60 import java.util.LinkedHashMap; 61 import java.util.List; 62 import java.util.Map; 63 64 /** 65 * Visual presentation of the volume panel dialog. 66 */ 67 public class VolumePanelDialog extends SystemUIDialog implements LifecycleOwner { 68 private static final String TAG = "VolumePanelDialog"; 69 70 private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 200; 71 private static final int DEFAULT_SLICE_SIZE = 4; 72 73 private final ActivityStarter mActivityStarter; 74 private RecyclerView mVolumePanelSlices; 75 private VolumePanelSlicesAdapter mVolumePanelSlicesAdapter; 76 private final LifecycleRegistry mLifecycleRegistry; 77 private final Handler mHandler = new Handler(Looper.getMainLooper()); 78 private final Map<Uri, LiveData<Slice>> mSliceLiveData = new LinkedHashMap<>(); 79 private final HashSet<Uri> mLoadedSlices = new HashSet<>(); 80 private boolean mSlicesReadyToLoad; 81 private LocalBluetoothProfileManager mProfileManager; 82 VolumePanelDialog(Context context, ActivityStarter activityStarter, boolean aboveStatusBar)83 public VolumePanelDialog(Context context, 84 ActivityStarter activityStarter, boolean aboveStatusBar) { 85 super(context); 86 mActivityStarter = activityStarter; 87 mLifecycleRegistry = new LifecycleRegistry(this); 88 if (!aboveStatusBar) { 89 getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); 90 } 91 } 92 93 @Override onCreate(Bundle savedInstanceState)94 protected void onCreate(Bundle savedInstanceState) { 95 super.onCreate(savedInstanceState); 96 Log.d(TAG, "onCreate"); 97 98 View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.volume_panel_dialog, 99 null); 100 final Window window = getWindow(); 101 window.setContentView(dialogView); 102 103 Button doneButton = dialogView.findViewById(R.id.done_button); 104 doneButton.setOnClickListener(v -> dismiss()); 105 Button settingsButton = dialogView.findViewById(R.id.settings_button); 106 settingsButton.setOnClickListener(v -> { 107 dismiss(); 108 109 Intent intent = new Intent(Settings.ACTION_SOUND_SETTINGS); 110 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 111 mActivityStarter.startActivity(intent, /* dismissShade= */ true); 112 }); 113 114 LocalBluetoothManager localBluetoothManager = LocalBluetoothManager.getInstance( 115 getContext(), null); 116 if (localBluetoothManager != null) { 117 mProfileManager = localBluetoothManager.getProfileManager(); 118 } 119 120 mVolumePanelSlices = dialogView.findViewById(R.id.volume_panel_parent_layout); 121 mVolumePanelSlices.setLayoutManager(new LinearLayoutManager(getContext())); 122 123 loadAllSlices(); 124 125 mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED); 126 } 127 loadAllSlices()128 private void loadAllSlices() { 129 mSliceLiveData.clear(); 130 mLoadedSlices.clear(); 131 final List<Uri> sliceUris = getSlices(); 132 133 for (Uri uri : sliceUris) { 134 final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getContext(), uri, 135 (int type, Throwable source) -> { 136 if (!removeSliceLiveData(uri)) { 137 mLoadedSlices.add(uri); 138 } 139 }); 140 141 // Add slice first to make it in order. Will remove it later if there's an error. 142 mSliceLiveData.put(uri, sliceLiveData); 143 144 sliceLiveData.observe(this, slice -> { 145 if (mLoadedSlices.contains(uri)) { 146 return; 147 } 148 Log.d(TAG, "received slice: " + (slice == null ? null : slice.getUri())); 149 final SliceMetadata metadata = SliceMetadata.from(getContext(), slice); 150 if (slice == null || metadata.isErrorSlice()) { 151 if (!removeSliceLiveData(uri)) { 152 mLoadedSlices.add(uri); 153 } 154 } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) { 155 mLoadedSlices.add(uri); 156 } else { 157 mHandler.postDelayed(() -> { 158 mLoadedSlices.add(uri); 159 setupAdapterWhenReady(); 160 }, DURATION_SLICE_BINDING_TIMEOUT_MS); 161 } 162 163 setupAdapterWhenReady(); 164 }); 165 } 166 } 167 setupAdapterWhenReady()168 private void setupAdapterWhenReady() { 169 if (mLoadedSlices.size() == mSliceLiveData.size() && !mSlicesReadyToLoad) { 170 mSlicesReadyToLoad = true; 171 mVolumePanelSlicesAdapter = new VolumePanelSlicesAdapter(this, mSliceLiveData); 172 mVolumePanelSlicesAdapter.setOnSliceActionListener((eventInfo, sliceItem) -> { 173 if (eventInfo.actionType == EventInfo.ACTION_TYPE_SLIDER) { 174 return; 175 } 176 this.dismiss(); 177 }); 178 if (mSliceLiveData.size() < DEFAULT_SLICE_SIZE) { 179 mVolumePanelSlices.setMinimumHeight(0); 180 } 181 mVolumePanelSlices.setAdapter(mVolumePanelSlicesAdapter); 182 } 183 } 184 removeSliceLiveData(Uri uri)185 private boolean removeSliceLiveData(Uri uri) { 186 boolean removed = false; 187 // Keeps observe media output slice 188 if (!uri.equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) { 189 Log.d(TAG, "remove uri: " + uri); 190 removed = mSliceLiveData.remove(uri) != null; 191 if (mVolumePanelSlicesAdapter != null) { 192 mVolumePanelSlicesAdapter.updateDataSet(new ArrayList<>(mSliceLiveData.values())); 193 } 194 } 195 return removed; 196 } 197 198 @Override onStart()199 protected void onStart() { 200 super.onStart(); 201 Log.d(TAG, "onStart"); 202 mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED); 203 mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED); 204 } 205 206 @Override onStop()207 protected void onStop() { 208 super.onStop(); 209 Log.d(TAG, "onStop"); 210 mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED); 211 } 212 getSlices()213 private List<Uri> getSlices() { 214 final List<Uri> uris = new ArrayList<>(); 215 uris.add(REMOTE_MEDIA_SLICE_URI); 216 uris.add(VOLUME_MEDIA_URI); 217 Uri controlUri = getExtraControlUri(); 218 if (controlUri != null) { 219 Log.d(TAG, "add extra control slice"); 220 uris.add(controlUri); 221 } 222 uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI); 223 uris.add(VOLUME_CALL_URI); 224 uris.add(VOLUME_RINGER_URI); 225 uris.add(VOLUME_ALARM_URI); 226 return uris; 227 } 228 229 private static final String SETTINGS_SLICE_AUTHORITY = "com.android.settings.slices"; 230 private static final Uri REMOTE_MEDIA_SLICE_URI = new Uri.Builder() 231 .scheme(ContentResolver.SCHEME_CONTENT) 232 .authority(SETTINGS_SLICE_AUTHORITY) 233 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 234 .appendPath(MediaOutputConstants.KEY_REMOTE_MEDIA) 235 .build(); 236 private static final Uri VOLUME_MEDIA_URI = new Uri.Builder() 237 .scheme(ContentResolver.SCHEME_CONTENT) 238 .authority(SETTINGS_SLICE_AUTHORITY) 239 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 240 .appendPath("media_volume") 241 .build(); 242 private static final Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder() 243 .scheme(ContentResolver.SCHEME_CONTENT) 244 .authority(SETTINGS_SLICE_AUTHORITY) 245 .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) 246 .appendPath("media_output_indicator") 247 .build(); 248 private static final Uri VOLUME_CALL_URI = new Uri.Builder() 249 .scheme(ContentResolver.SCHEME_CONTENT) 250 .authority(SETTINGS_SLICE_AUTHORITY) 251 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 252 .appendPath("call_volume") 253 .build(); 254 private static final Uri VOLUME_RINGER_URI = new Uri.Builder() 255 .scheme(ContentResolver.SCHEME_CONTENT) 256 .authority(SETTINGS_SLICE_AUTHORITY) 257 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 258 .appendPath("ring_volume") 259 .build(); 260 private static final Uri VOLUME_ALARM_URI = new Uri.Builder() 261 .scheme(ContentResolver.SCHEME_CONTENT) 262 .authority(SETTINGS_SLICE_AUTHORITY) 263 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 264 .appendPath("alarm_volume") 265 .build(); 266 getExtraControlUri()267 private Uri getExtraControlUri() { 268 Uri controlUri = null; 269 final BluetoothDevice bluetoothDevice = findActiveDevice(); 270 if (bluetoothDevice != null) { 271 // The control slice width = dialog width - horizontal padding of two sides 272 final int dialogWidth = 273 getWindow().getWindowManager().getCurrentWindowMetrics().getBounds().width(); 274 final int controlSliceWidth = dialogWidth 275 - getContext().getResources().getDimensionPixelSize( 276 R.dimen.volume_panel_slice_horizontal_padding) * 2; 277 final String uri = BluetoothUtils.getControlUriMetaData(bluetoothDevice); 278 if (!TextUtils.isEmpty(uri)) { 279 try { 280 controlUri = Uri.parse(uri + controlSliceWidth); 281 } catch (NullPointerException exception) { 282 Log.d(TAG, "unable to parse extra control uri"); 283 controlUri = null; 284 } 285 } 286 } 287 return controlUri; 288 } 289 findActiveDevice()290 private BluetoothDevice findActiveDevice() { 291 if (mProfileManager != null) { 292 final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 293 if (a2dpProfile != null) { 294 return a2dpProfile.getActiveDevice(); 295 } 296 } 297 return null; 298 } 299 300 @NonNull 301 @Override getLifecycle()302 public Lifecycle getLifecycle() { 303 return mLifecycleRegistry; 304 } 305 } 306