Merge "Sound + Output Switcher on Volume Slice"

This commit is contained in:
TreeHugger Robot
2019-03-19 22:49:04 +00:00
committed by Android (Google) Code Review
7 changed files with 550 additions and 1 deletions

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2019 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.media;
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;
import android.annotation.ColorInt;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.slices.CustomSliceable;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settingslib.media.MediaOutputSliceConstants;
public class MediaOutputIndicatorSlice implements CustomSliceable {
private Context mContext;
@VisibleForTesting
MediaOutputIndicatorWorker mWorker;
public MediaOutputIndicatorSlice(Context context) {
mContext = context;
}
@Override
public Slice getSlice() {
if (!getWorker().isVisible()) {
return null;
}
final IconCompat icon = IconCompat.createWithResource(mContext,
com.android.internal.R.drawable.ic_settings_bluetooth);
final CharSequence title = mContext.getText(R.string.media_output_title);
final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext,
0 /* requestCode */, getMediaOutputSliceIntent(), 0 /* flags */);
final SliceAction primarySliceAction = SliceAction.createDeeplink(
primaryActionIntent, icon, ListBuilder.ICON_IMAGE, title);
@ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
final ListBuilder listBuilder = new ListBuilder(mContext,
MEDIA_OUTPUT_INDICATOR_SLICE_URI,
ListBuilder.INFINITY)
.setAccentColor(color)
.addRow(new ListBuilder.RowBuilder()
.setTitle(title)
.setSubtitle(getWorker().findActiveDeviceName())
.setPrimaryAction(primarySliceAction));
return listBuilder.build();
}
private MediaOutputIndicatorWorker getWorker() {
if (mWorker == null) {
mWorker = (MediaOutputIndicatorWorker) SliceBackgroundWorker.getInstance(getUri());
}
return mWorker;
}
private Intent getMediaOutputSliceIntent() {
final Intent intent = new Intent()
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
return intent;
}
@Override
public Uri getUri() {
return MEDIA_OUTPUT_INDICATOR_SLICE_URI;
}
@Override
public Intent getIntent() {
// This Slice reflects active media device information and launch MediaOutputSlice. It does
// not contain its owned Slice data
return null;
}
@Override
public Class getBackgroundWorkerClass() {
return MediaOutputIndicatorWorker.class;
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (C) 2019 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.media;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import com.android.internal.util.CollectionUtils;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingAidProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Listener for background change from {@code BluetoothCallback} to update media output indicator.
*/
public class MediaOutputIndicatorWorker extends SliceBackgroundWorker implements BluetoothCallback {
private static final String TAG = "MediaOutputIndicatorWorker";
private LocalBluetoothManager mLocalBluetoothManager;
private LocalBluetoothProfileManager mProfileManager;
public MediaOutputIndicatorWorker(Context context, Uri uri) {
super(context, uri);
}
@Override
protected void onSlicePinned() {
LocalBluetoothManager mLocalBluetoothManager = Utils.getLocalBtManager(getContext());
if (mLocalBluetoothManager == null) {
Log.e(TAG, "Bluetooth is not supported on this device");
return;
}
mProfileManager = mLocalBluetoothManager.getProfileManager();
mLocalBluetoothManager.getEventManager().registerCallback(this);
}
@Override
protected void onSliceUnpinned() {
if (mLocalBluetoothManager == null) {
Log.e(TAG, "Bluetooth is not supported on this device");
return;
}
mLocalBluetoothManager.getEventManager().unregisterCallback(this);
}
@Override
public void close() throws IOException {
mLocalBluetoothManager = null;
mProfileManager = null;
}
@Override
public void onBluetoothStateChanged(int bluetoothState) {
// To handle the case that Bluetooth on and no connected devices
notifySliceChange();
}
@Override
public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
if (bluetoothProfile == BluetoothProfile.A2DP) {
notifySliceChange();
}
}
/**
* To decide Slice's visibility.
*
* @return true if device is connected or previously connected, false for other cases.
*/
public boolean isVisible() {
return !CollectionUtils.isEmpty(getConnectableA2dpDevices())
|| !CollectionUtils.isEmpty(getConnectableHearingAidDevices());
}
private List<BluetoothDevice> getConnectableA2dpDevices() {
// get A2dp devices on all states
// (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING)
final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
if (a2dpProfile == null) {
return new ArrayList<>();
}
return a2dpProfile.getConnectableDevices();
}
private List<BluetoothDevice> getConnectableHearingAidDevices() {
// get hearing aid profile devices on all states
// (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING)
final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
if (hapProfile == null) {
return new ArrayList<>();
}
return hapProfile.getConnectableDevices();
}
/**
* Get active devices name.
*
* @return active Bluetooth device alias, or default summary if no active device.
*/
public CharSequence findActiveDeviceName() {
// Return Hearing Aid device name if it is active
BluetoothDevice activeDevice = findActiveHearingAidDevice();
if (activeDevice != null) {
return activeDevice.getAliasName();
}
// Return A2DP device name if it is active
final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
if (a2dpProfile != null) {
activeDevice = a2dpProfile.getActiveDevice();
if (activeDevice != null) {
return activeDevice.getAliasName();
}
}
// No active device, return default summary
return getContext().getText(R.string.media_output_default_summary);
}
private BluetoothDevice findActiveHearingAidDevice() {
final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
if (hearingAidProfile == null) {
return null;
}
final List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices();
for (BluetoothDevice btDevice : activeDevices) {
if (btDevice != null) {
return btDevice;
}
}
return null;
}
}

View File

@@ -16,6 +16,7 @@
package com.android.settings.panel;
import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_ALARM_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI;
import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI;
@@ -55,6 +56,7 @@ public class VolumePanel implements PanelContent {
final List<Uri> uris = new ArrayList<>();
uris.add(VOLUME_REMOTE_MEDIA_URI);
uris.add(VOLUME_MEDIA_URI);
uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI);
uris.add(VOLUME_CALL_URI);
uris.add(VOLUME_RINGER_URI);
uris.add(VOLUME_ALARM_URI);

View File

@@ -39,6 +39,7 @@ import com.android.settings.homepage.contextualcards.slices.BluetoothDevicesSlic
import com.android.settings.homepage.contextualcards.slices.LowStorageSlice;
import com.android.settings.homepage.contextualcards.slices.NotificationChannelSlice;
import com.android.settings.location.LocationSlice;
import com.android.settings.media.MediaOutputIndicatorSlice;
import com.android.settings.media.MediaOutputSlice;
import com.android.settings.network.telephony.MobileDataSlice;
import com.android.settings.wifi.calling.WifiCallingSliceHelper;
@@ -299,6 +300,16 @@ public class CustomSliceRegistry {
.appendPath(MediaOutputSliceConstants.KEY_MEDIA_OUTPUT)
.build();
/**
* Backing Uri for the Media output indicator Slice.
*/
public static Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(SettingsSliceProvider.SLICE_AUTHORITY)
.appendPath(SettingsSlicesContract.PATH_SETTING_INTENT)
.appendPath("media_output_indicator")
.build();
@VisibleForTesting
static final Map<Uri, Class<? extends CustomSliceable>> sUriToSlice;
@@ -319,6 +330,7 @@ public class CustomSliceRegistry {
sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class);
sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class);
sUriToSlice.put(MEDIA_OUTPUT_SLICE_URI, MediaOutputSlice.class);
sUriToSlice.put(MEDIA_OUTPUT_INDICATOR_SLICE_URI, MediaOutputIndicatorSlice.class);
}
public static Class<? extends CustomSliceable> getSliceClassByUri(Uri uri) {
@@ -344,5 +356,4 @@ public class CustomSliceRegistry {
public static boolean isValidAction(String action) {
return isValidUri(Uri.parse(action));
}
}