Merge Android 24Q2 Release (ab/11526283) to aosp-main-future

Bug: 337098550
Merged-In: I96574a79eba581db95d387f0d9c9fde2e004c41c
Change-Id: Ib9f2c742f8aa72651ef9eca80a716dd94b9041ea
This commit is contained in:
Xin Li
2024-05-23 14:12:07 -07:00
1413 changed files with 69959 additions and 31812 deletions

View File

@@ -17,18 +17,17 @@ package com.android.settings.connecteddevice;
import static com.android.settingslib.Utils.isAudioModeOngoingCall;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
@@ -38,138 +37,66 @@ import com.android.settings.accessibility.HearingAidUtils;
import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import com.android.settingslib.core.lifecycle.Lifecycle;
/**
* Controller to maintain the {@link androidx.preference.PreferenceGroup} for all available media
* devices. It uses {@link DevicePreferenceCallback} to add/remove {@link Preference}
*/
public class AvailableMediaDeviceGroupController extends BasePreferenceController
implements LifecycleObserver, OnStart, OnStop, DevicePreferenceCallback, BluetoothCallback {
implements DefaultLifecycleObserver, DevicePreferenceCallback, BluetoothCallback {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "AvailableMediaDeviceGroupController";
private static final String KEY = "available_device_list";
@VisibleForTesting PreferenceGroup mPreferenceGroup;
@VisibleForTesting @Nullable PreferenceGroup mPreferenceGroup;
@VisibleForTesting LocalBluetoothManager mLocalBluetoothManager;
private final Executor mExecutor;
private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
private FragmentManager mFragmentManager;
private BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
new BluetoothLeBroadcastAssistant.Callback() {
@Override
public void onSearchStarted(int reason) {}
@Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
@Nullable private FragmentManager mFragmentManager;
@Override
public void onSearchStartFailed(int reason) {}
@Override
public void onSearchStopped(int reason) {}
@Override
public void onSearchStopFailed(int reason) {}
@Override
public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
@Override
public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
mBluetoothDeviceUpdater.forceUpdate();
}
@Override
public void onSourceAddFailed(
@NonNull BluetoothDevice sink,
@NonNull BluetoothLeBroadcastMetadata source,
int reason) {}
@Override
public void onSourceModified(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceModifyFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceRemoved(
@NonNull BluetoothDevice sink, int sourceId, int reason) {
mBluetoothDeviceUpdater.forceUpdate();
}
@Override
public void onSourceRemoveFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onReceiveStateChanged(
BluetoothDevice sink,
int sourceId,
BluetoothLeBroadcastReceiveState state) {}
};
public AvailableMediaDeviceGroupController(Context context) {
public AvailableMediaDeviceGroupController(
Context context,
@Nullable DashboardFragment fragment,
@Nullable Lifecycle lifecycle) {
super(context, KEY);
if (fragment != null) {
init(fragment);
}
if (lifecycle != null) {
lifecycle.addObserver(this);
}
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
mExecutor = Executors.newSingleThreadExecutor();
}
@Override
public void onStart() {
public void onStart(@NonNull LifecycleOwner owner) {
if (mLocalBluetoothManager == null) {
Log.e(TAG, "onStart() Bluetooth is not supported on this device");
return;
}
if (AudioSharingUtils.isFeatureEnabled()) {
LocalBluetoothLeBroadcastAssistant assistant =
mLocalBluetoothManager
.getProfileManager()
.getLeAudioBroadcastAssistantProfile();
if (assistant != null) {
if (DEBUG) {
Log.d(TAG, "onStart() Register callbacks for assistant.");
}
assistant.registerServiceCallBack(mExecutor, mAssistantCallback);
}
}
mBluetoothDeviceUpdater.registerCallback();
mLocalBluetoothManager.getEventManager().registerCallback(this);
mBluetoothDeviceUpdater.refreshPreference();
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.registerCallback();
mBluetoothDeviceUpdater.refreshPreference();
}
}
@Override
public void onStop() {
public void onStop(@NonNull LifecycleOwner owner) {
if (mLocalBluetoothManager == null) {
Log.e(TAG, "onStop() Bluetooth is not supported on this device");
return;
}
if (AudioSharingUtils.isFeatureEnabled()) {
LocalBluetoothLeBroadcastAssistant assistant =
mLocalBluetoothManager
.getProfileManager()
.getLeAudioBroadcastAssistantProfile();
if (assistant != null) {
if (DEBUG) {
Log.d(TAG, "onStop() Register callbacks for assistant.");
}
assistant.unregisterServiceCallBack(mAssistantCallback);
}
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.unregisterCallback();
}
mBluetoothDeviceUpdater.unregisterCallback();
mLocalBluetoothManager.getEventManager().unregisterCallback(this);
}
@@ -178,12 +105,16 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
super.displayPreference(screen);
mPreferenceGroup = screen.findPreference(KEY);
mPreferenceGroup.setVisible(false);
if (mPreferenceGroup != null) {
mPreferenceGroup.setVisible(false);
}
if (isAvailable()) {
updateTitle();
mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
mBluetoothDeviceUpdater.forceUpdate();
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
mBluetoothDeviceUpdater.forceUpdate();
}
}
}
@@ -201,17 +132,21 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
@Override
public void onDeviceAdded(Preference preference) {
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(true);
if (mPreferenceGroup != null) {
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(true);
}
mPreferenceGroup.addPreference(preference);
}
mPreferenceGroup.addPreference(preference);
}
@Override
public void onDeviceRemoved(Preference preference) {
mPreferenceGroup.removePreference(preference);
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(false);
if (mPreferenceGroup != null) {
mPreferenceGroup.removePreference(preference);
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(false);
}
}
}
@@ -253,14 +188,16 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
}
private void updateTitle() {
if (isAudioModeOngoingCall(mContext)) {
// in phone call
mPreferenceGroup.setTitle(
mContext.getString(R.string.connected_device_call_device_title));
} else {
// without phone call
mPreferenceGroup.setTitle(
mContext.getString(R.string.connected_device_media_device_title));
if (mPreferenceGroup != null) {
if (isAudioModeOngoingCall(mContext)) {
// in phone call
mPreferenceGroup.setTitle(
mContext.getString(R.string.connected_device_call_device_title));
} else {
// without phone call
mPreferenceGroup.setTitle(
mContext.getString(R.string.connected_device_media_device_title));
}
}
}
}

View File

@@ -22,13 +22,12 @@ import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingDevicePreferenceController;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
@@ -36,8 +35,13 @@ import com.android.settings.overlay.SurveyFeatureProvider;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.slices.SlicePreferenceController;
import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.search.SearchIndexable;
import java.util.ArrayList;
import java.util.List;
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
public class ConnectedDeviceDashboardFragment extends DashboardFragment {
@@ -87,10 +91,6 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
+ ", action : "
+ action);
}
if (AudioSharingUtils.isFeatureEnabled()) {
use(AudioSharingDevicePreferenceController.class).init(this);
}
use(AvailableMediaDeviceGroupController.class).init(this);
use(ConnectedDeviceGroupController.class).init(this);
use(PreviouslyConnectedDevicePreferenceController.class).init(this);
use(SlicePreferenceController.class)
@@ -112,6 +112,31 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
}
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
return buildPreferenceControllers(context, /* fragment= */ this, getSettingsLifecycle());
}
private static List<AbstractPreferenceController> buildPreferenceControllers(
Context context,
@Nullable ConnectedDeviceDashboardFragment fragment,
@Nullable Lifecycle lifecycle) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
AbstractPreferenceController availableMediaController =
FeatureFactory.getFeatureFactory()
.getAudioSharingFeatureProvider()
.createAvailableMediaDeviceGroupController(context, fragment, lifecycle);
controllers.add(availableMediaController);
AbstractPreferenceController audioSharingController =
FeatureFactory.getFeatureFactory()
.getAudioSharingFeatureProvider()
.createAudioSharingDevicePreferenceController(context, fragment, lifecycle);
if (audioSharingController != null) {
controllers.add(audioSharingController);
}
return controllers;
}
@VisibleForTesting
boolean isAlwaysDiscoverable(String callingAppPackageName, String action) {
return TextUtils.equals(SLICE_ACTION, action)
@@ -122,5 +147,12 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
/** For Search. */
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.connected_devices);
new BaseSearchIndexProvider(R.xml.connected_devices) {
@Override
public List<AbstractPreferenceController> createPreferenceControllers(
Context context) {
return buildPreferenceControllers(
context, /* fragment= */ null, /* lifecycle= */ null);
}
};
}

View File

@@ -17,6 +17,7 @@ package com.android.settings.connecteddevice;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -36,13 +37,16 @@ import com.android.settings.bluetooth.SavedBluetoothDeviceUpdater;
import com.android.settings.connecteddevice.dock.DockUpdater;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PreviouslyConnectedDevicePreferenceController extends BasePreferenceController
implements LifecycleObserver, OnStart, OnStop, DevicePreferenceCallback {
@@ -56,11 +60,12 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
private final List<Preference> mDevicesList = new ArrayList<>();
private final List<Preference> mDockDevicesList = new ArrayList<>();
private final Map<BluetoothDevice, Preference> mDevicePreferenceMap = new HashMap<>();
private final BluetoothAdapter mBluetoothAdapter;
private PreferenceGroup mPreferenceGroup;
private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
private DockUpdater mSavedDockUpdater;
private BluetoothAdapter mBluetoothAdapter;
@VisibleForTesting
Preference mSeeAllPreference;
@@ -81,7 +86,11 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
mSavedDockUpdater = FeatureFactory.getFeatureFactory().getDockUpdaterFeatureProvider()
.getSavedDockUpdater(context, this);
mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (Flags.enableSavedDevicesOrderByRecency()) {
mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
} else {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
}
@Override
@@ -114,6 +123,9 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
mContext.registerReceiver(mReceiver, mIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mBluetoothDeviceUpdater.refreshPreference();
if (Flags.enableSavedDevicesOrderByRecency()) {
updatePreferenceGroup();
}
}
@Override
@@ -131,19 +143,37 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
@Override
public void onDeviceAdded(Preference preference) {
final List<BluetoothDevice> bluetoothDevices =
mBluetoothAdapter.getMostRecentlyConnectedDevices();
final int index = preference instanceof BluetoothDevicePreference
? bluetoothDevices.indexOf(((BluetoothDevicePreference) preference)
.getBluetoothDevice().getDevice()) : DOCK_DEVICE_INDEX;
if (DEBUG) {
Log.d(TAG, "onDeviceAdded() " + preference.getTitle() + ", index of : " + index);
for (BluetoothDevice device : bluetoothDevices) {
Log.d(TAG, "onDeviceAdded() most recently device : " + device.getName());
if (Flags.enableSavedDevicesOrderByRecency()) {
if (preference instanceof BluetoothDevicePreference) {
mDevicePreferenceMap.put(
((BluetoothDevicePreference) preference).getBluetoothDevice().getDevice(),
preference);
} else {
mDockDevicesList.add(preference);
}
if (DEBUG) {
Log.d(TAG, "onDeviceAdded() " + preference.getTitle());
}
updatePreferenceGroup();
} else {
final List<BluetoothDevice> bluetoothDevices =
mBluetoothAdapter.getMostRecentlyConnectedDevices();
final int index =
preference instanceof BluetoothDevicePreference
? bluetoothDevices.indexOf(
((BluetoothDevicePreference) preference)
.getBluetoothDevice()
.getDevice())
: DOCK_DEVICE_INDEX;
if (DEBUG) {
Log.d(TAG, "onDeviceAdded() " + preference.getTitle() + ", index of : " + index);
for (BluetoothDevice device : bluetoothDevices) {
Log.d(TAG, "onDeviceAdded() most recently device : " + device.getName());
}
}
addPreference(index, preference);
updatePreferenceVisibility();
}
addPreference(index, preference);
updatePreferenceVisibility();
}
private void addPreference(int index, Preference preference) {
@@ -194,13 +224,57 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
@Override
public void onDeviceRemoved(Preference preference) {
if (preference instanceof BluetoothDevicePreference) {
mDevicesList.remove(preference);
if (Flags.enableSavedDevicesOrderByRecency()) {
if (preference instanceof BluetoothDevicePreference) {
mDevicePreferenceMap.remove(
((BluetoothDevicePreference) preference).getBluetoothDevice().getDevice(),
preference);
} else {
mDockDevicesList.remove(preference);
}
if (DEBUG) {
Log.d(TAG, "onDeviceRemoved() " + preference.getTitle());
}
updatePreferenceGroup();
} else {
mDockDevicesList.remove(preference);
}
if (preference instanceof BluetoothDevicePreference) {
mDevicesList.remove(preference);
} else {
mDockDevicesList.remove(preference);
}
addPreference();
addPreference();
updatePreferenceVisibility();
}
}
/** Sort the preferenceGroup by most recently used. */
public void updatePreferenceGroup() {
mPreferenceGroup.removeAll();
mPreferenceGroup.addPreference(mSeeAllPreference);
if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) {
// Bluetooth is supported
int order = 0;
for (BluetoothDevice device : mBluetoothAdapter.getMostRecentlyConnectedDevices()) {
Preference preference = mDevicePreferenceMap.getOrDefault(device, null);
if (preference != null) {
preference.setOrder(order);
mPreferenceGroup.addPreference(preference);
order += 1;
}
if (order == MAX_DEVICE_NUM) {
break;
}
}
for (Preference preference : mDockDevicesList) {
if (order == MAX_DEVICE_NUM) {
break;
}
preference.setOrder(order);
mPreferenceGroup.addPreference(preference);
order += 1;
}
}
updatePreferenceVisibility();
}

View File

@@ -15,6 +15,9 @@
*/
package com.android.settings.connecteddevice;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -23,18 +26,25 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.BluetoothDevicePreference;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.SavedBluetoothDeviceUpdater;
import com.android.settings.connecteddevice.dock.DockUpdater;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags;
import com.android.settings.overlay.DockUpdaterFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Controller to maintain the {@link PreferenceGroup} for all
* saved devices. It uses {@link DevicePreferenceCallback} to add/remove {@link Preference}
@@ -45,6 +55,10 @@ public class SavedDeviceGroupController extends BasePreferenceController
private static final String KEY = "saved_device_list";
private final Map<BluetoothDevice, Preference> mDevicePreferenceMap = new HashMap<>();
private final List<Preference> mDockDevicesList = new ArrayList<>();
private final BluetoothAdapter mBluetoothAdapter;
@VisibleForTesting
PreferenceGroup mPreferenceGroup;
private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
@@ -57,6 +71,7 @@ public class SavedDeviceGroupController extends BasePreferenceController
FeatureFactory.getFeatureFactory().getDockUpdaterFeatureProvider();
mSavedDockUpdater =
dockUpdaterFeatureProvider.getSavedDockUpdater(context, this);
mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
}
@Override
@@ -64,6 +79,9 @@ public class SavedDeviceGroupController extends BasePreferenceController
mBluetoothDeviceUpdater.registerCallback();
mSavedDockUpdater.registerCallback();
mBluetoothDeviceUpdater.refreshPreference();
if (Flags.enableSavedDevicesOrderByRecency()) {
updatePreferenceGroup();
}
}
@Override
@@ -101,17 +119,63 @@ public class SavedDeviceGroupController extends BasePreferenceController
@Override
public void onDeviceAdded(Preference preference) {
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(true);
if (Flags.enableSavedDevicesOrderByRecency()) {
mPreferenceGroup.addPreference(preference);
if (preference instanceof BluetoothDevicePreference) {
mDevicePreferenceMap.put(
((BluetoothDevicePreference) preference).getBluetoothDevice().getDevice(),
preference);
} else {
mDockDevicesList.add(preference);
}
updatePreferenceGroup();
} else {
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(true);
}
mPreferenceGroup.addPreference(preference);
}
mPreferenceGroup.addPreference(preference);
}
@Override
public void onDeviceRemoved(Preference preference) {
mPreferenceGroup.removePreference(preference);
if (mPreferenceGroup.getPreferenceCount() == 0) {
if (Flags.enableSavedDevicesOrderByRecency()) {
mPreferenceGroup.removePreference(preference);
if (preference instanceof BluetoothDevicePreference) {
mDevicePreferenceMap.remove(
((BluetoothDevicePreference) preference).getBluetoothDevice().getDevice(),
preference);
} else {
mDockDevicesList.remove(preference);
}
updatePreferenceGroup();
} else {
mPreferenceGroup.removePreference(preference);
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(false);
}
}
}
/** Sort the preferenceGroup by most recently used. */
public void updatePreferenceGroup() {
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
// Bluetooth is unsupported or disabled
mPreferenceGroup.setVisible(false);
} else {
mPreferenceGroup.setVisible(true);
int order = 0;
for (BluetoothDevice device : mBluetoothAdapter.getMostRecentlyConnectedDevices()) {
Preference preference = mDevicePreferenceMap.getOrDefault(device, null);
if (preference != null) {
preference.setOrder(order);
order += 1;
}
}
for (Preference preference : mDockDevicesList) {
preference.setOrder(order);
order += 1;
}
}
}
@@ -130,4 +194,9 @@ public class SavedDeviceGroupController extends BasePreferenceController
public void setSavedDockUpdater(DockUpdater savedDockUpdater) {
mSavedDockUpdater = savedDockUpdater;
}
@VisibleForTesting
void setPreferenceGroup(PreferenceGroup preferenceGroup) {
mPreferenceGroup = preferenceGroup;
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
public abstract class AudioSharingBasePreferenceController extends BasePreferenceController
implements DefaultLifecycleObserver {
private final BluetoothAdapter mBluetoothAdapter;
private final LocalBluetoothManager mBtManager;
protected final LocalBluetoothLeBroadcast mBroadcast;
protected Preference mPreference;
public AudioSharingBasePreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBtManager = Utils.getLocalBtManager(context);
mBroadcast =
mBtManager == null
? null
: mBtManager.getProfileManager().getLeAudioBroadcastProfile();
}
@Override
public int getAvailabilityStatus() {
return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
if (isAvailable()) {
updateVisibility();
}
}
/** Update the visibility of the preference. */
protected void updateVisibility() {
if (mPreference != null) {
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
boolean isVisible = isBroadcasting() && isBluetoothStateOn();
ThreadUtils.postOnMainThread(
() -> mPreference.setVisible(isVisible));
});
}
}
protected boolean isBroadcasting() {
return mBroadcast != null && mBroadcast.isEnabled(null);
}
protected boolean isBluetoothStateOn() {
return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
}
}

View File

@@ -1,91 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.content.Context;
import android.util.Log;
import androidx.preference.Preference;
import com.android.settings.bluetooth.BluetoothDevicePreference;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater
implements Preference.OnPreferenceClickListener {
private static final String TAG = "AudioSharingBluetoothDeviceUpdater";
private static final String PREF_KEY = "audio_sharing_bt";
private LocalBluetoothManager mLocalBluetoothManager;
public AudioSharingBluetoothDeviceUpdater(
Context context,
DevicePreferenceCallback devicePreferenceCallback,
int metricsCategory) {
super(context, devicePreferenceCallback, metricsCategory);
mLocalBluetoothManager = Utils.getLocalBluetoothManager(context);
}
@Override
public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
boolean isFilterMatched = false;
if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) {
// If device is LE audio device and has a broadcast source,
// it would show in audio sharing devices group.
if (cachedDevice.isConnectedLeAudioDevice()
&& AudioSharingUtils.hasBroadcastSource(cachedDevice, mLocalBluetoothManager)) {
isFilterMatched = true;
}
}
Log.d(
TAG,
"isFilterMatched() device : "
+ cachedDevice.getName()
+ ", isFilterMatched : "
+ isFilterMatched);
return isFilterMatched;
}
@Override
public boolean onPreferenceClick(Preference preference) {
mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory);
final CachedBluetoothDevice device =
((BluetoothDevicePreference) preference).getBluetoothDevice();
return device.setActive();
}
@Override
protected String getPreferenceKey() {
return PREF_KEY;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
super.update(cachedBluetoothDevice);
Log.d(TAG, "Map : " + mPreferenceMap);
}
}

View File

@@ -1,106 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.SettingsMainSwitchBar;
public class AudioSharingDashboardFragment extends DashboardFragment
implements AudioSharingSwitchBarController.OnSwitchBarChangedListener {
private static final String TAG = "AudioSharingDashboardFrag";
SettingsMainSwitchBar mMainSwitchBar;
private AudioSharingSwitchBarController mSwitchBarController;
private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
private CallsAndAlarmsPreferenceController mCallsAndAlarmsPreferenceController;
private AudioSharingNamePreferenceController mAudioSharingNamePreferenceController;
private AudioStreamsCategoryController mAudioStreamsCategoryController;
public AudioSharingDashboardFragment() {
super();
}
@Override
public int getMetricsCategory() {
return SettingsEnums.AUDIO_SHARING_SETTINGS;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
public int getHelpResource() {
return R.string.help_url_audio_sharing;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.bluetooth_audio_sharing;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mAudioSharingDeviceVolumeGroupController =
use(AudioSharingDeviceVolumeGroupController.class);
mAudioSharingDeviceVolumeGroupController.init(this);
mCallsAndAlarmsPreferenceController = use(CallsAndAlarmsPreferenceController.class);
mCallsAndAlarmsPreferenceController.init(this);
mAudioSharingNamePreferenceController = use(AudioSharingNamePreferenceController.class);
mAudioStreamsCategoryController = use(AudioStreamsCategoryController.class);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Assume we are in a SettingsActivity. This is only safe because we currently use
// SettingsActivity as base for all preference fragments.
final SettingsActivity activity = (SettingsActivity) getActivity();
mMainSwitchBar = activity.getSwitchBar();
mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title));
mSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this);
mSwitchBarController.init(this);
getSettingsLifecycle().addObserver(mSwitchBarController);
mMainSwitchBar.show();
}
@Override
public void onSwitchBarChanged() {
updateVisibilityForAttachedPreferences();
}
private void updateVisibilityForAttachedPreferences() {
mAudioSharingDeviceVolumeGroupController.updateVisibility();
mCallsAndAlarmsPreferenceController.updateVisibility();
mAudioSharingNamePreferenceController.updateVisibility();
mAudioStreamsCategoryController.updateVisibility();
}
}

View File

@@ -1,86 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import java.util.ArrayList;
public class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = "AudioSharingDeviceAdapter";
private final ArrayList<AudioSharingDeviceItem> mDevices;
private final OnClickListener mOnClickListener;
private final String mPrefix;
public AudioSharingDeviceAdapter(
ArrayList<AudioSharingDeviceItem> devices, OnClickListener listener, String prefix) {
mDevices = devices;
mOnClickListener = listener;
mPrefix = prefix;
}
private class AudioSharingDeviceViewHolder extends RecyclerView.ViewHolder {
private final Button mButtonView;
AudioSharingDeviceViewHolder(View view) {
super(view);
mButtonView = view.findViewById(R.id.device_button);
}
public void bindView(int position) {
if (mButtonView != null) {
mButtonView.setText(mPrefix + mDevices.get(position).getName());
mButtonView.setOnClickListener(
v -> mOnClickListener.onClick(mDevices.get(position)));
} else {
Log.w(TAG, "bind view skipped due to button view is null");
}
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view =
LayoutInflater.from(parent.getContext())
.inflate(R.layout.audio_sharing_device_item, parent, false);
return new AudioSharingDeviceViewHolder(view);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((AudioSharingDeviceViewHolder) holder).bindView(position);
}
@Override
public int getItemCount() {
return mDevices.size();
}
public interface OnClickListener {
/** Called when an item has been clicked. */
void onClick(AudioSharingDeviceItem item);
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.os.Parcel;
import android.os.Parcelable;
public final class AudioSharingDeviceItem implements Parcelable {
private final String mName;
private final int mGroupId;
private final boolean mIsActive;
public AudioSharingDeviceItem(String name, int groupId, boolean isActive) {
mName = name;
mGroupId = groupId;
mIsActive = isActive;
}
public String getName() {
return mName;
}
public int getGroupId() {
return mGroupId;
}
public boolean isActive() {
return mIsActive;
}
public AudioSharingDeviceItem(Parcel in) {
mName = in.readString();
mGroupId = in.readInt();
mIsActive = in.readBoolean();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeInt(mGroupId);
dest.writeBoolean(mIsActive);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<AudioSharingDeviceItem> CREATOR =
new Creator<AudioSharingDeviceItem>() {
@Override
public AudioSharingDeviceItem createFromParcel(Parcel in) {
return new AudioSharingDeviceItem(in);
}
@Override
public AudioSharingDeviceItem[] newArray(int size) {
return new AudioSharingDeviceItem[size];
}
};
}

View File

@@ -1,604 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LeAudioProfile;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AudioSharingDevicePreferenceController extends BasePreferenceController
implements DefaultLifecycleObserver, DevicePreferenceCallback, BluetoothCallback {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "AudioSharingDevicePrefController";
private static final String KEY = "audio_sharing_device_list";
private static final String KEY_AUDIO_SHARING_SETTINGS =
"connected_device_audio_sharing_settings";
private final LocalBluetoothManager mLocalBtManager;
private final LocalBluetoothLeBroadcast mBroadcast;
private final LocalBluetoothLeBroadcastAssistant mAssistant;
private final Executor mExecutor;
private PreferenceGroup mPreferenceGroup;
private Preference mAudioSharingSettingsPreference;
private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
private DashboardFragment mFragment;
private List<BluetoothDevice> mTargetSinks = new ArrayList<>();
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@Override
public void onBroadcastStarted(int reason, int broadcastId) {
Log.d(
TAG,
"onBroadcastStarted(), reason = "
+ reason
+ ", broadcastId = "
+ broadcastId);
}
@Override
public void onBroadcastStartFailed(int reason) {
Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
// TODO: handle broadcast start fail
}
@Override
public void onBroadcastMetadataChanged(
int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
Log.d(
TAG,
"onBroadcastMetadataChanged(), broadcastId = "
+ broadcastId
+ ", metadata = "
+ metadata);
addSourceToTargetDevices(mTargetSinks);
mTargetSinks = new ArrayList<>();
}
@Override
public void onBroadcastStopped(int reason, int broadcastId) {
Log.d(
TAG,
"onBroadcastStopped(), reason = "
+ reason
+ ", broadcastId = "
+ broadcastId);
}
@Override
public void onBroadcastStopFailed(int reason) {
Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
// TODO: handle broadcast stop fail
}
@Override
public void onBroadcastUpdated(int reason, int broadcastId) {}
@Override
public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
@Override
public void onPlaybackStarted(int reason, int broadcastId) {}
@Override
public void onPlaybackStopped(int reason, int broadcastId) {}
};
private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
new BluetoothLeBroadcastAssistant.Callback() {
@Override
public void onSearchStarted(int reason) {}
@Override
public void onSearchStartFailed(int reason) {}
@Override
public void onSearchStopped(int reason) {}
@Override
public void onSearchStopFailed(int reason) {}
@Override
public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
@Override
public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(
TAG,
"onSourceAdded(), sink = "
+ sink
+ ", sourceId = "
+ sourceId
+ ", reason = "
+ reason);
mBluetoothDeviceUpdater.forceUpdate();
AudioSharingUtils.updateActiveDeviceIfNeeded(mLocalBtManager);
}
@Override
public void onSourceAddFailed(
@NonNull BluetoothDevice sink,
@NonNull BluetoothLeBroadcastMetadata source,
int reason) {
Log.d(
TAG,
"onSourceAddFailed(), sink = "
+ sink
+ ", source = "
+ source
+ ", reason = "
+ reason);
AudioSharingUtils.toastMessage(
mContext,
String.format(
Locale.US,
"Fail to add source to %s reason %d",
sink.getAddress(),
reason));
}
@Override
public void onSourceModified(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceModifyFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceRemoved(
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(
TAG,
"onSourceRemoved(), sink = "
+ sink
+ ", sourceId = "
+ sourceId
+ ", reason = "
+ reason);
mBluetoothDeviceUpdater.forceUpdate();
AudioSharingUtils.updateActiveDeviceIfNeeded(mLocalBtManager);
}
@Override
public void onSourceRemoveFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(
TAG,
"onSourceRemoveFailed(), sink = "
+ sink
+ ", sourceId = "
+ sourceId
+ ", reason = "
+ reason);
AudioSharingUtils.toastMessage(
mContext,
String.format(
Locale.US,
"Fail to remove source from %s reason %d",
sink.getAddress(),
reason));
}
@Override
public void onReceiveStateChanged(
BluetoothDevice sink,
int sourceId,
BluetoothLeBroadcastReceiveState state) {}
};
public AudioSharingDevicePreferenceController(Context context) {
super(context, KEY);
mLocalBtManager = Utils.getLocalBtManager(mContext);
mBroadcast = mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile();
mAssistant = mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
mExecutor = Executors.newSingleThreadExecutor();
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
if (mLocalBtManager == null) {
Log.d(TAG, "onStart() Bluetooth is not supported on this device");
return;
}
if (mBroadcast == null || mAssistant == null) {
Log.d(TAG, "onStart() Broadcast or assistant is not supported on this device");
return;
}
if (mBluetoothDeviceUpdater == null) {
Log.d(TAG, "onStart() Bluetooth device updater is not initialized");
return;
}
mLocalBtManager.getEventManager().registerCallback(this);
if (DEBUG) {
Log.d(TAG, "onStart() Register callbacks for broadcast and assistant.");
}
mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mBluetoothDeviceUpdater.registerCallback();
mBluetoothDeviceUpdater.refreshPreference();
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
if (mLocalBtManager == null) {
Log.d(TAG, "onStop() Bluetooth is not supported on this device");
return;
}
if (mBroadcast == null || mAssistant == null) {
Log.d(TAG, "onStop() Broadcast or assistant is not supported on this device");
return;
}
if (mBluetoothDeviceUpdater == null) {
Log.d(TAG, "onStop() Bluetooth device updater is not initialized");
return;
}
mLocalBtManager.getEventManager().unregisterCallback(this);
if (DEBUG) {
Log.d(TAG, "onStop() Unregister callbacks for broadcast and assistant.");
}
mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
mBluetoothDeviceUpdater.unregisterCallback();
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceGroup = screen.findPreference(KEY);
mAudioSharingSettingsPreference =
mPreferenceGroup.findPreference(KEY_AUDIO_SHARING_SETTINGS);
mPreferenceGroup.setVisible(false);
mAudioSharingSettingsPreference.setVisible(false);
if (isAvailable()) {
mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
mBluetoothDeviceUpdater.forceUpdate();
}
}
@Override
public int getAvailabilityStatus() {
return AudioSharingUtils.isFeatureEnabled() && mBluetoothDeviceUpdater != null
? AVAILABLE_UNSEARCHABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
public String getPreferenceKey() {
return KEY;
}
@Override
public void onDeviceAdded(Preference preference) {
if (mPreferenceGroup.getPreferenceCount() == 1) {
mPreferenceGroup.setVisible(true);
mAudioSharingSettingsPreference.setVisible(true);
}
mPreferenceGroup.addPreference(preference);
}
@Override
public void onDeviceRemoved(Preference preference) {
mPreferenceGroup.removePreference(preference);
if (mPreferenceGroup.getPreferenceCount() == 1) {
mPreferenceGroup.setVisible(false);
mAudioSharingSettingsPreference.setVisible(false);
}
}
@Override
public void onProfileConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice,
@ConnectionState int state,
int bluetoothProfile) {
if (state != BluetoothAdapter.STATE_CONNECTED || !cachedDevice.getDevice().isConnected()) {
Log.d(TAG, "Ignore onProfileConnectionStateChanged, not connected state");
return;
}
if (mFragment == null) {
Log.d(TAG, "Ignore onProfileConnectionStateChanged, no host fragment");
return;
}
if (mAssistant == null && mBroadcast == null) {
Log.d(
TAG,
"Ignore onProfileConnectionStateChanged, no broadcast or assistant supported");
return;
}
var unused =
ThreadUtils.postOnBackgroundThread(
() -> handleOnProfileStateChanged(cachedDevice, bluetoothProfile));
}
/**
* Initialize the controller.
*
* @param fragment The fragment to provide the context and metrics category for {@link
* AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
*/
public void init(DashboardFragment fragment) {
mFragment = fragment;
mBluetoothDeviceUpdater =
new AudioSharingBluetoothDeviceUpdater(
fragment.getContext(),
AudioSharingDevicePreferenceController.this,
fragment.getMetricsCategory());
}
private void handleOnProfileStateChanged(
@NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
boolean isLeAudioSupported = isLeAudioSupported(cachedDevice);
// For eligible (LE audio) remote device, we only check its connected LE audio profile.
if (isLeAudioSupported && bluetoothProfile != BluetoothProfile.LE_AUDIO) {
Log.d(
TAG,
"Ignore onProfileConnectionStateChanged, not the le profile for le audio"
+ " device");
return;
}
boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
// For ineligible (non LE audio) remote device, we only check its first connected profile.
if (!isLeAudioSupported && !isFirstConnectedProfile) {
Log.d(
TAG,
"Ignore onProfileConnectionStateChanged, not the first connected profile for"
+ " non le audio device");
return;
}
if (DEBUG) {
Log.d(
TAG,
"Start handling onProfileConnectionStateChanged for "
+ cachedDevice.getDevice().getAnonymizedAddress());
}
if (!isLeAudioSupported) {
// Handle connected ineligible (non LE audio) remote device
handleOnProfileStateChangedForNonLeAudioDevice(cachedDevice);
} else {
// Handle connected eligible (LE audio) remote device
handleOnProfileStateChangedForLeAudioDevice(cachedDevice);
}
}
private void handleOnProfileStateChangedForNonLeAudioDevice(
@NonNull CachedBluetoothDevice cachedDevice) {
if (isBroadcasting()) {
// Show stop audio sharing dialog when an ineligible (non LE audio) remote device
// connected during a sharing session.
ThreadUtils.postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingStopDialogFragment.show(
mFragment,
cachedDevice.getName(),
() -> mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId()));
});
} else {
// Do nothing for ineligible (non LE audio) remote device when no sharing session.
if (DEBUG) {
Log.d(
TAG,
"Ignore onProfileConnectionStateChanged for non LE audio without"
+ " sharing session");
}
}
}
private void handleOnProfileStateChangedForLeAudioDevice(
@NonNull CachedBluetoothDevice cachedDevice) {
Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
if (isBroadcasting()) {
if (groupedDevices.containsKey(cachedDevice.getGroupId())
&& groupedDevices.get(cachedDevice.getGroupId()).stream()
.anyMatch(
device ->
AudioSharingUtils.hasBroadcastSource(
device, mLocalBtManager))) {
Log.d(
TAG,
"Automatically add another device within the same group to the sharing: "
+ cachedDevice.getDevice().getAnonymizedAddress());
addSourceToTargetDevices(ImmutableList.of(cachedDevice.getDevice()));
return;
}
// Show audio sharing switch or join dialog according to device count in the sharing
// session.
ArrayList<AudioSharingDeviceItem> deviceItemsInSharingSession =
AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
// Show audio sharing switch dialog when the third eligible (LE audio) remote device
// connected during a sharing session.
if (deviceItemsInSharingSession.size() >= 2) {
ThreadUtils.postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingDisconnectDialogFragment.show(
mFragment,
deviceItemsInSharingSession,
cachedDevice.getName(),
(AudioSharingDeviceItem item) -> {
// Remove all sources from the device user clicked
if (groupedDevices.containsKey(item.getGroupId())) {
for (CachedBluetoothDevice device :
groupedDevices.get(item.getGroupId())) {
for (BluetoothLeBroadcastReceiveState source :
mAssistant.getAllSources(
device.getDevice())) {
mAssistant.removeSource(
device.getDevice(),
source.getSourceId());
}
}
}
// Add current broadcast to the latest connected device
mAssistant.addSource(
cachedDevice.getDevice(),
mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
/* isGroupOp= */ true);
});
});
} else {
// Show audio sharing join dialog when the first or second eligible (LE audio)
// remote device connected during a sharing session.
ThreadUtils.postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingJoinDialogFragment.show(
mFragment,
deviceItemsInSharingSession,
cachedDevice.getName(),
() -> {
// Add current broadcast to the latest connected device
mAssistant.addSource(
cachedDevice.getDevice(),
mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
/* isGroupOp= */ true);
});
});
}
} else {
ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
// Use random device in the group within the sharing session to represent the group.
CachedBluetoothDevice device = devices.get(0);
if (device.getGroupId() == cachedDevice.getGroupId()) {
continue;
}
deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
}
// Show audio sharing join dialog when the second eligible (LE audio) remote
// device connect and no sharing session.
if (deviceItems.size() == 1) {
ThreadUtils.postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingJoinDialogFragment.show(
mFragment,
deviceItems,
cachedDevice.getName(),
() -> {
mTargetSinks = new ArrayList<>();
for (List<CachedBluetoothDevice> devices :
groupedDevices.values()) {
for (CachedBluetoothDevice device : devices) {
mTargetSinks.add(device.getDevice());
}
}
mBroadcast.startBroadcast("test", null);
});
});
}
}
}
private boolean isLeAudioSupported(CachedBluetoothDevice cachedDevice) {
return cachedDevice.getProfiles().stream()
.anyMatch(
profile ->
profile instanceof LeAudioProfile
&& profile.isEnabled(cachedDevice.getDevice()));
}
private boolean isFirstConnectedProfile(
CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
return cachedDevice.getProfiles().stream()
.noneMatch(
profile ->
profile.getProfileId() != bluetoothProfile
&& profile.getConnectionStatus(cachedDevice.getDevice())
== BluetoothProfile.STATE_CONNECTED);
}
private boolean isBroadcasting() {
return mBroadcast != null && mBroadcast.isEnabled(null);
}
private void addSourceToTargetDevices(List<BluetoothDevice> sinks) {
if (sinks.isEmpty() || mBroadcast == null || mAssistant == null) {
Log.d(TAG, "Skip adding source to target.");
return;
}
BluetoothLeBroadcastMetadata broadcastMetadata =
mBroadcast.getLatestBluetoothLeBroadcastMetadata();
if (broadcastMetadata == null) {
Log.e(TAG, "Error: There is no broadcastMetadata.");
return;
}
for (BluetoothDevice sink : sinks) {
Log.d(
TAG,
"Add broadcast with broadcastId: "
+ broadcastMetadata.getBroadcastId()
+ "to the device: "
+ sink.getAnonymizedAddress());
mAssistant.addSource(sink, broadcastMetadata, /* isGroupOp= */ false);
}
}
private void closeOpeningDialogs() {
if (mFragment == null) return;
List<Fragment> fragments = mFragment.getChildFragmentManager().getFragments();
for (Fragment fragment : fragments) {
if (fragment instanceof DialogFragment) {
Log.d(TAG, "Remove staled opening dialog " + fragment.getTag());
((DialogFragment) fragment).dismiss();
}
}
}
}

View File

@@ -1,142 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.util.Log;
import android.widget.SeekBar;
import androidx.preference.Preference;
import com.android.settings.bluetooth.BluetoothDevicePreference;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
public class AudioSharingDeviceVolumeControlUpdater extends BluetoothDeviceUpdater
implements Preference.OnPreferenceClickListener {
private static final String TAG = "AudioSharingDeviceVolumeControlUpdater";
private static final String PREF_KEY = "audio_sharing_volume_control";
private final LocalBluetoothManager mLocalBtManager;
public AudioSharingDeviceVolumeControlUpdater(
Context context,
DevicePreferenceCallback devicePreferenceCallback,
int metricsCategory) {
super(context, devicePreferenceCallback, metricsCategory);
mLocalBtManager = Utils.getLocalBluetoothManager(context);
}
@Override
public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
boolean isFilterMatched = false;
if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) {
// If device is LE audio device and in a sharing session on current sharing device,
// it would show in volume control group.
if (cachedDevice.isConnectedLeAudioDevice()
&& AudioSharingUtils.isBroadcasting(mLocalBtManager)
&& AudioSharingUtils.hasBroadcastSource(cachedDevice, mLocalBtManager)) {
isFilterMatched = true;
}
}
Log.d(
TAG,
"isFilterMatched() device : "
+ cachedDevice.getName()
+ ", isFilterMatched : "
+ isFilterMatched);
return isFilterMatched;
}
@Override
public boolean onPreferenceClick(Preference preference) {
return true;
}
@Override
protected void addPreference(CachedBluetoothDevice cachedDevice) {
if (cachedDevice == null) return;
final BluetoothDevice device = cachedDevice.getDevice();
if (!mPreferenceMap.containsKey(device)) {
SeekBar.OnSeekBarChangeListener listener =
new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(
SeekBar seekBar, int progress, boolean fromUser) {}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (mLocalBtManager != null
&& mLocalBtManager.getProfileManager().getVolumeControlProfile()
!= null) {
mLocalBtManager
.getProfileManager()
.getVolumeControlProfile()
.setDeviceVolume(
cachedDevice.getDevice(),
seekBar.getProgress(),
/* isGroupOp= */ true);
}
}
};
AudioSharingDeviceVolumePreference vPreference =
new AudioSharingDeviceVolumePreference(mPrefContext, cachedDevice);
vPreference.initialize();
vPreference.setOnSeekBarChangeListener(listener);
vPreference.setKey(getPreferenceKey());
vPreference.setIcon(com.android.settingslib.R.drawable.ic_bt_untethered_earbuds);
vPreference.setTitle(cachedDevice.getName());
mPreferenceMap.put(device, vPreference);
mDevicePreferenceCallback.onDeviceAdded(vPreference);
}
}
@Override
protected String getPreferenceKey() {
return PREF_KEY;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
super.update(cachedBluetoothDevice);
Log.d(TAG, "Map : " + mPreferenceMap);
}
@Override
protected void addPreference(
CachedBluetoothDevice cachedDevice, @BluetoothDevicePreference.SortType int type) {}
@Override
protected void launchDeviceDetails(Preference preference) {}
@Override
public void refreshPreference() {}
}

View File

@@ -1,327 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.annotation.IntRange;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Context;
import android.media.AudioManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.VolumeControlProfile;
import com.android.settingslib.utils.ThreadUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePreferenceController
implements DevicePreferenceCallback {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "AudioSharingDeviceVolumeGroupController";
private static final String KEY = "audio_sharing_device_volume_group";
private final LocalBluetoothManager mLocalBtManager;
private final LocalBluetoothLeBroadcastAssistant mAssistant;
private final Executor mExecutor;
private VolumeControlProfile mVolumeControl;
private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
private FragmentManager mFragmentManager;
private PreferenceGroup mPreferenceGroup;
private Map<Preference, BluetoothVolumeControl.Callback> mCallbackMap =
new HashMap<Preference, BluetoothVolumeControl.Callback>();
private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
new BluetoothLeBroadcastAssistant.Callback() {
@Override
public void onSearchStarted(int reason) {}
@Override
public void onSearchStartFailed(int reason) {}
@Override
public void onSearchStopped(int reason) {}
@Override
public void onSearchStopFailed(int reason) {}
@Override
public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
@Override
public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(
TAG,
"onSourceAdded(), sink = "
+ sink
+ ", sourceId = "
+ sourceId
+ ", reason = "
+ reason);
mBluetoothDeviceUpdater.forceUpdate();
}
@Override
public void onSourceAddFailed(
@NonNull BluetoothDevice sink,
@NonNull BluetoothLeBroadcastMetadata source,
int reason) {
Log.d(
TAG,
"onSourceAddFailed(), sink = "
+ sink
+ ", source = "
+ source
+ ", reason = "
+ reason);
}
@Override
public void onSourceModified(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceModifyFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceRemoved(
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(
TAG,
"onSourceRemoved(), sink = "
+ sink
+ ", sourceId = "
+ sourceId
+ ", reason = "
+ reason);
mBluetoothDeviceUpdater.forceUpdate();
}
@Override
public void onSourceRemoveFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(
TAG,
"onSourceRemoveFailed(), sink = "
+ sink
+ ", sourceId = "
+ sourceId
+ ", reason = "
+ reason);
}
@Override
public void onReceiveStateChanged(
BluetoothDevice sink,
int sourceId,
BluetoothLeBroadcastReceiveState state) {}
};
public AudioSharingDeviceVolumeGroupController(Context context) {
super(context, KEY);
mLocalBtManager = Utils.getLocalBtManager(mContext);
mAssistant = mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
mExecutor = Executors.newSingleThreadExecutor();
if (mLocalBtManager != null) {
mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
}
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
super.onStart(owner);
if (mAssistant == null) {
Log.d(TAG, "onStart() Broadcast or assistant is not supported on this device");
return;
}
if (mBluetoothDeviceUpdater == null) {
Log.d(TAG, "onStart() Bluetooth device updater is not initialized");
return;
}
mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mBluetoothDeviceUpdater.registerCallback();
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
super.onStop(owner);
if (mAssistant == null) {
Log.d(TAG, "onStop() Broadcast or assistant is not supported on this device");
return;
}
if (mBluetoothDeviceUpdater == null) {
Log.d(TAG, "onStop() Bluetooth device updater is not initialized");
return;
}
mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
mBluetoothDeviceUpdater.unregisterCallback();
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
for (var entry : mCallbackMap.entrySet()) {
if (DEBUG) {
Log.d(TAG, "onDestroy: unregister callback for " + entry.getKey());
}
mVolumeControl.unregisterCallback(entry.getValue());
}
mCallbackMap.clear();
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceGroup = screen.findPreference(KEY);
mPreferenceGroup.setVisible(false);
if (isAvailable() && mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
mBluetoothDeviceUpdater.forceUpdate();
}
}
@Override
public String getPreferenceKey() {
return KEY;
}
@Override
public void onDeviceAdded(Preference preference) {
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(true);
}
mPreferenceGroup.addPreference(preference);
if (mVolumeControl != null && preference instanceof AudioSharingDeviceVolumePreference) {
BluetoothVolumeControl.Callback callback =
buildVcCallback((AudioSharingDeviceVolumePreference) preference);
mCallbackMap.put(preference, callback);
if (DEBUG) {
Log.d(TAG, "onDeviceAdded: register callback for " + preference);
}
mVolumeControl.registerCallback(mExecutor, callback);
}
}
@Override
public void onDeviceRemoved(Preference preference) {
mPreferenceGroup.removePreference(preference);
if (mPreferenceGroup.getPreferenceCount() == 0) {
mPreferenceGroup.setVisible(false);
}
if (mVolumeControl != null && mCallbackMap.containsKey(preference)) {
if (DEBUG) {
Log.d(TAG, "onDeviceRemoved: unregister callback for " + preference);
}
mVolumeControl.unregisterCallback(mCallbackMap.get(preference));
mCallbackMap.remove(preference);
}
}
@Override
public void updateVisibility() {
if (mPreferenceGroup != null) {
mPreferenceGroup.setVisible(false);
if (mPreferenceGroup.getPreferenceCount() > 0) {
super.updateVisibility();
}
}
}
/**
* Initialize the controller.
*
* @param fragment The fragment to provide the context and metrics category for {@link
* AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
*/
public void init(DashboardFragment fragment) {
mBluetoothDeviceUpdater =
new AudioSharingDeviceVolumeControlUpdater(
fragment.getContext(),
AudioSharingDeviceVolumeGroupController.this,
fragment.getMetricsCategory());
}
private BluetoothVolumeControl.Callback buildVcCallback(
AudioSharingDeviceVolumePreference preference) {
return new BluetoothVolumeControl.Callback() {
@Override
public void onVolumeOffsetChanged(BluetoothDevice device, int volumeOffset) {}
@Override
public void onDeviceVolumeChanged(
@android.annotation.NonNull BluetoothDevice device,
@IntRange(from = -255, to = 255) int volume) {
CachedBluetoothDevice cachedDevice =
mLocalBtManager.getCachedDeviceManager().findDevice(device);
if (cachedDevice == null) return;
if (preference.getCachedDevice() != null
&& preference.getCachedDevice().getGroupId() == cachedDevice.getGroupId()) {
// If the callback return invalid volume, try to get the volume from
// AudioManager.STREAM_MUSIC
int finalVolume = getAudioVolumeIfNeeded(volume);
Log.d(
TAG,
"onDeviceVolumeChanged: set volume to "
+ finalVolume
+ " for "
+ device.getAnonymizedAddress());
ThreadUtils.postOnMainThread(
() -> {
preference.setProgress(finalVolume);
});
}
}
};
}
private int getAudioVolumeIfNeeded(int volume) {
if (volume >= 0) return volume;
try {
AudioManager audioManager = mContext.getSystemService(AudioManager.class);
int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int min = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
return Math.round(
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) * 255f / (max - min));
} catch (RuntimeException e) {
Log.e(TAG, "Fail to fetch current music stream volume, error = " + e);
return volume;
}
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.content.Context;
import android.widget.SeekBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.widget.SeekBarPreference;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
public class AudioSharingDeviceVolumePreference extends SeekBarPreference {
public static final int MIN_VOLUME = 0;
public static final int MAX_VOLUME = 255;
protected SeekBar mSeekBar;
private final CachedBluetoothDevice mCachedDevice;
public AudioSharingDeviceVolumePreference(
Context context, @NonNull CachedBluetoothDevice device) {
super(context);
setLayoutResource(R.layout.preference_volume_slider);
mCachedDevice = device;
}
@Nullable
public CachedBluetoothDevice getCachedDevice() {
return mCachedDevice;
}
/**
* Initialize {@link AudioSharingDeviceVolumePreference}.
* Need to be called after creating the preference.
*/
public void initialize() {
setMax(MAX_VOLUME);
setMin(MIN_VOLUME);
}
}

View File

@@ -1,154 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
private static final String TAG = "AudioSharingDialog";
private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
// The host creates an instance of this dialog fragment must implement this interface to receive
// event callbacks.
public interface DialogEventListener {
/**
* Called when users click the device item for sharing in the dialog.
*
* @param item The device item clicked.
*/
void onItemClick(AudioSharingDeviceItem item);
}
private static DialogEventListener sListener;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_START_AUDIO_SHARING;
}
/**
* Display the {@link AudioSharingDialogFragment} dialog.
*
* @param host The Fragment this dialog will be hosted.
* @param deviceItems The connected device items eligible for audio sharing.
* @param listener The callback to handle the user action on this dialog.
*/
public static void show(
Fragment host,
ArrayList<AudioSharingDeviceItem> deviceItems,
DialogEventListener listener) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
sListener = listener;
if (manager.findFragmentByTag(TAG) == null) {
final Bundle bundle = new Bundle();
bundle.putParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
AudioSharingDialogFragment dialog = new AudioSharingDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, TAG);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle arguments = requireArguments();
ArrayList<AudioSharingDeviceItem> deviceItems =
arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS);
final AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity()).setCancelable(false);
LayoutInflater inflater = LayoutInflater.from(builder.getContext());
View customTitle = inflater.inflate(R.layout.dialog_custom_title_audio_sharing, null);
ImageView icon = customTitle.findViewById(R.id.title_icon);
icon.setImageResource(R.drawable.ic_bt_audio_sharing);
TextView title = customTitle.findViewById(R.id.title_text);
View rootView = inflater.inflate(R.layout.dialog_audio_sharing, /* parent= */ null);
TextView subTitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
TextView subTitle2 = rootView.findViewById(R.id.share_audio_subtitle2);
RecyclerView recyclerView = rootView.findViewById(R.id.btn_list);
Button shareBtn = rootView.findViewById(R.id.share_btn);
Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
if (deviceItems.isEmpty()) {
title.setText("Share your audio");
subTitle2.setText(
"To start sharing audio, "
+ "connect two pairs of headphones that support LE Audio");
ImageView image = rootView.findViewById(R.id.share_audio_guidance);
image.setVisibility(View.VISIBLE);
builder.setNegativeButton("Close", null);
} else if (deviceItems.size() == 1) {
title.setText("Share your audio");
subTitle1.setText(
deviceItems.stream()
.map(AudioSharingDeviceItem::getName)
.collect(Collectors.joining(" and ")));
subTitle2.setText(
"This device's music and videos will play on both pairs of headphones");
shareBtn.setText("Share audio");
shareBtn.setOnClickListener(
v -> {
sListener.onItemClick(Iterables.getOnlyElement(deviceItems));
dismiss();
});
cancelBtn.setOnClickListener(v -> dismiss());
subTitle1.setVisibility(View.VISIBLE);
shareBtn.setVisibility(View.VISIBLE);
cancelBtn.setVisibility(View.VISIBLE);
} else {
title.setText("Share audio with another device");
subTitle2.setText(
"This device's music and videos will play on the headphones you connect");
recyclerView.setAdapter(
new AudioSharingDeviceAdapter(
deviceItems,
(AudioSharingDeviceItem item) -> {
sListener.onItemClick(item);
dismiss();
},
"Connect "));
recyclerView.setLayoutManager(
new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
recyclerView.setVisibility(View.VISIBLE);
cancelBtn.setOnClickListener(v -> dismiss());
cancelBtn.setVisibility(View.VISIBLE);
}
AlertDialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}

View File

@@ -1,126 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import java.util.ArrayList;
public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFragment {
private static final String TAG = "AudioSharingDisconnectDialog";
private static final String BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS =
"bundle_key_device_to_disconnect_items";
private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
// The host creates an instance of this dialog fragment must implement this interface to receive
// event callbacks.
public interface DialogEventListener {
/**
* Called when users click the device item to disconnect from the audio sharing in the
* dialog.
*
* @param item The device item clicked.
*/
void onItemClick(AudioSharingDeviceItem item);
}
private static DialogEventListener sListener;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE;
}
/**
* Display the {@link AudioSharingDisconnectDialogFragment} dialog.
*
* @param host The Fragment this dialog will be hosted.
* @param deviceItems The existing connected device items in audio sharing session.
* @param newDeviceName The name of the latest connected device triggered this dialog.
* @param listener The callback to handle the user action on this dialog.
*/
public static void show(
Fragment host,
ArrayList<AudioSharingDeviceItem> deviceItems,
String newDeviceName,
DialogEventListener listener) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
sListener = listener;
final Bundle bundle = new Bundle();
bundle.putParcelableArrayList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDeviceName);
AudioSharingDisconnectDialogFragment dialog = new AudioSharingDisconnectDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle arguments = requireArguments();
ArrayList<AudioSharingDeviceItem> deviceItems =
arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS);
final AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity()).setCancelable(false);
LayoutInflater inflater = LayoutInflater.from(builder.getContext());
View customTitle = inflater.inflate(R.layout.dialog_custom_title_audio_sharing, null);
ImageView icon = customTitle.findViewById(R.id.title_icon);
icon.setImageResource(R.drawable.ic_bt_audio_sharing);
TextView title = customTitle.findViewById(R.id.title_text);
title.setText("Choose a device to disconnect");
View rootView =
inflater.inflate(R.layout.dialog_audio_sharing_disconnect, /* parent= */ null);
TextView subTitle = rootView.findViewById(R.id.share_audio_disconnect_description);
subTitle.setText("Only 2 devices can share audio at a time");
RecyclerView recyclerView = rootView.findViewById(R.id.device_btn_list);
recyclerView.setAdapter(
new AudioSharingDeviceAdapter(
deviceItems,
(AudioSharingDeviceItem item) -> {
sListener.onItemClick(item);
dismiss();
},
"Disconnect "));
recyclerView.setLayoutManager(
new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
cancelBtn.setOnClickListener(
v -> {
dismiss();
});
AlertDialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2024 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.connecteddevice.audiosharing;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
/** Feature provider for the audio sharing related features, */
public interface AudioSharingFeatureProvider {
/** Create audio sharing device preference controller. */
@Nullable
AbstractPreferenceController createAudioSharingDevicePreferenceController(
@NonNull Context context,
@Nullable DashboardFragment fragment,
@Nullable Lifecycle lifecycle);
/** Create available media device preference controller. */
AbstractPreferenceController createAvailableMediaDeviceGroupController(
@NonNull Context context,
@Nullable DashboardFragment fragment,
@Nullable Lifecycle lifecycle);
/**
* Check if the device match the audio sharing filter.
*
* <p>The filter is used to filter device in "Media devices" section.
*/
boolean isAudioSharingFilterMatched(
@NonNull CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager);
/** Handle preference onClick in "Media devices" section. */
void handleMediaDeviceOnClick(LocalBluetoothManager localBtManager);
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2024 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.connecteddevice.audiosharing;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.connecteddevice.AvailableMediaDeviceGroupController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
public class AudioSharingFeatureProviderImpl implements AudioSharingFeatureProvider {
@Nullable
@Override
public AbstractPreferenceController createAudioSharingDevicePreferenceController(
@NonNull Context context,
@Nullable DashboardFragment fragment,
@Nullable Lifecycle lifecycle) {
return null;
}
@Override
public AbstractPreferenceController createAvailableMediaDeviceGroupController(
@NonNull Context context,
@Nullable DashboardFragment fragment,
@Nullable Lifecycle lifecycle) {
return new AvailableMediaDeviceGroupController(context, fragment, lifecycle);
}
@Override
public boolean isAudioSharingFilterMatched(
@NonNull CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
return false;
}
@Override
public void handleMediaDeviceOnClick(LocalBluetoothManager localBtManager) {}
}

View File

@@ -1,126 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import java.util.ArrayList;
import java.util.Locale;
import java.util.stream.Collectors;
public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
private static final String TAG = "AudioSharingJoinDialog";
private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
// The host creates an instance of this dialog fragment must implement this interface to receive
// event callbacks.
public interface DialogEventListener {
/** Called when users click the share audio button in the dialog. */
void onShareClick();
}
private static DialogEventListener sListener;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_START_AUDIO_SHARING;
}
/**
* Display the {@link AudioSharingJoinDialogFragment} dialog.
*
* @param host The Fragment this dialog will be hosted.
* @param deviceItems The existing connected device items eligible for audio sharing.
* @param newDeviceName The name of the latest connected device triggered this dialog.
* @param listener The callback to handle the user action on this dialog.
*/
public static void show(
Fragment host,
ArrayList<AudioSharingDeviceItem> deviceItems,
String newDeviceName,
DialogEventListener listener) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
sListener = listener;
final Bundle bundle = new Bundle();
bundle.putParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDeviceName);
final AudioSharingJoinDialogFragment dialog = new AudioSharingJoinDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle arguments = requireArguments();
ArrayList<AudioSharingDeviceItem> deviceItems =
arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS);
String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
final AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity()).setCancelable(false);
LayoutInflater inflater = LayoutInflater.from(builder.getContext());
View customTitle =
inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* parent= */ null);
ImageView icon = customTitle.findViewById(R.id.title_icon);
icon.setImageResource(R.drawable.ic_bt_audio_sharing);
TextView title = customTitle.findViewById(R.id.title_text);
title.setText("Share your audio");
View rootView = inflater.inflate(R.layout.dialog_audio_sharing_join, /* parent= */ null);
TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
TextView subtitle2 = rootView.findViewById(R.id.share_audio_subtitle2);
if (deviceItems.isEmpty()) {
subtitle1.setText(newDeviceName);
} else {
subtitle1.setText(
String.format(
Locale.US,
"%s and %s",
deviceItems.stream()
.map(AudioSharingDeviceItem::getName)
.collect(Collectors.joining(", ")),
newDeviceName));
}
subtitle2.setText("This device's music and videos will play on both pairs of headphones");
Button shareBtn = rootView.findViewById(R.id.share_btn);
Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
shareBtn.setOnClickListener(
v -> {
sListener.onShareClick();
dismiss();
});
shareBtn.setText("Share audio");
cancelBtn.setOnClickListener(v -> dismiss());
Dialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}

View File

@@ -1,74 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageButton;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.widget.ValidatedEditTextPreference;
public class AudioSharingNamePreference extends ValidatedEditTextPreference {
private static final String TAG = "AudioSharingNamePreference";
public AudioSharingNamePreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
public AudioSharingNamePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
public AudioSharingNamePreference(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public AudioSharingNamePreference(Context context) {
super(context);
initialize();
}
private void initialize() {
setLayoutResource(
com.android.settingslib.widget.preference.twotarget.R.layout.preference_two_target);
setWidgetLayoutResource(R.layout.preference_widget_qrcode);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
shareButton.setOnClickListener(
unused ->
new SubSettingLauncher(getContext())
.setTitleText("Audio sharing QR code")
.setDestination(AudioStreamsQrCodeFragment.class.getName())
.setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
.launch());
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import com.android.settings.widget.ValidatedEditTextPreference;
public class AudioSharingNamePreferenceController extends AudioSharingBasePreferenceController
implements ValidatedEditTextPreference.Validator, Preference.OnPreferenceChangeListener {
private static final String TAG = "AudioSharingNamePreferenceController";
private static final String PREF_KEY = "audio_sharing_stream_name";
private AudioSharingNameTextValidator mAudioSharingNameTextValidator;
public AudioSharingNamePreferenceController(Context context) {
super(context, PREF_KEY);
mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
}
@Override
public String getPreferenceKey() {
return PREF_KEY;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// TODO: update broadcast when name is changed.
return true;
}
@Override
public boolean isTextValid(String value) {
return mAudioSharingNameTextValidator.isTextValid(value);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
super.onStart(owner);
// TODO
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
super.onStop(owner);
// TODO
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import com.android.settings.widget.ValidatedEditTextPreference;
public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
@Override
public boolean isTextValid(String value) {
// TODO: Add validate rule if applicable.
return true;
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.content.Context;
import com.android.settings.core.BasePreferenceController;
public class AudioSharingPreferenceController extends BasePreferenceController {
private static final String TAG = "AudioSharingPreferenceController";
private Context mContext;
public AudioSharingPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mContext = context;
}
@Override
public int getAvailabilityStatus() {
return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
private static final String TAG = "AudioSharingStopDialog";
private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
// The host creates an instance of this dialog fragment must implement this interface to receive
// event callbacks.
public interface DialogEventListener {
/** Called when users click the stop sharing button in the dialog. */
void onStopSharingClick();
}
private static DialogEventListener sListener;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_STOP_AUDIO_SHARING;
}
/**
* Display the {@link AudioSharingStopDialogFragment} dialog.
*
* @param host The Fragment this dialog will be hosted.
* @param newDeviceName The name of the latest connected device triggered this dialog.
* @param listener The callback to handle the user action on this dialog.
*/
public static void show(Fragment host, String newDeviceName, DialogEventListener listener) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
sListener = listener;
final Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDeviceName);
AudioSharingStopDialogFragment dialog = new AudioSharingStopDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle arguments = requireArguments();
String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
final AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity()).setCancelable(false);
LayoutInflater inflater = LayoutInflater.from(builder.getContext());
View customTitle =
inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* parent= */ null);
ImageView icon = customTitle.findViewById(R.id.title_icon);
icon.setImageResource(R.drawable.ic_warning_24dp);
TextView title = customTitle.findViewById(R.id.title_text);
title.setText("Stop sharing audio?");
builder.setMessage(
newDeviceName + " wants to connect, headphones in audio sharing will disconnect.");
builder.setPositiveButton(
"Stop sharing", (dialog, which) -> sListener.onStopSharingClick());
builder.setNegativeButton("Cancel", (dialog, which) -> dismiss());
AlertDialog dialog = builder.setCustomTitle(customTitle).create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}

View File

@@ -1,397 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.SettingsMainSwitchBar;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
public class AudioSharingSwitchBarController extends BasePreferenceController
implements DefaultLifecycleObserver, OnCheckedChangeListener {
private static final String TAG = "AudioSharingSwitchBarCtl";
private static final String PREF_KEY = "audio_sharing_main_switch";
interface OnSwitchBarChangedListener {
void onSwitchBarChanged();
}
private final SettingsMainSwitchBar mSwitchBar;
private final BluetoothAdapter mBluetoothAdapter;
private final LocalBluetoothManager mBtManager;
private final LocalBluetoothLeBroadcast mBroadcast;
private final LocalBluetoothLeBroadcastAssistant mAssistant;
private final Executor mExecutor;
private final OnSwitchBarChangedListener mListener;
private DashboardFragment mFragment;
private Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
private List<BluetoothDevice> mTargetActiveSinks = new ArrayList<>();
private ArrayList<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
@VisibleForTesting IntentFilter mIntentFilter;
@VisibleForTesting
BroadcastReceiver mReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) return;
int adapterState =
intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothDevice.ERROR);
mSwitchBar.setChecked(isBroadcasting());
mSwitchBar.setEnabled(adapterState == BluetoothAdapter.STATE_ON);
mListener.onSwitchBarChanged();
}
};
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@Override
public void onBroadcastStarted(int reason, int broadcastId) {
Log.d(
TAG,
"onBroadcastStarted(), reason = "
+ reason
+ ", broadcastId = "
+ broadcastId);
updateSwitch();
}
@Override
public void onBroadcastStartFailed(int reason) {
Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
// TODO: handle broadcast start fail
updateSwitch();
}
@Override
public void onBroadcastMetadataChanged(
int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
Log.d(
TAG,
"onBroadcastMetadataChanged(), broadcastId = "
+ broadcastId
+ ", metadata = "
+ metadata.getBroadcastName());
addSourceToTargetSinks(mTargetActiveSinks);
if (mFragment == null) {
Log.w(TAG, "Dialog fail to show due to null fragment.");
return;
}
ThreadUtils.postOnMainThread(
() -> {
AudioSharingDialogFragment.show(
mFragment,
mDeviceItemsForSharing,
item -> {
addSourceToTargetSinks(
mGroupedConnectedDevices
.getOrDefault(
item.getGroupId(),
ImmutableList.of())
.stream()
.map(CachedBluetoothDevice::getDevice)
.collect(Collectors.toList()));
});
});
}
@Override
public void onBroadcastStopped(int reason, int broadcastId) {
Log.d(
TAG,
"onBroadcastStopped(), reason = "
+ reason
+ ", broadcastId = "
+ broadcastId);
updateSwitch();
}
@Override
public void onBroadcastStopFailed(int reason) {
Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
// TODO: handle broadcast stop fail
updateSwitch();
}
@Override
public void onBroadcastUpdated(int reason, int broadcastId) {}
@Override
public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
@Override
public void onPlaybackStarted(int reason, int broadcastId) {}
@Override
public void onPlaybackStopped(int reason, int broadcastId) {}
};
private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
new BluetoothLeBroadcastAssistant.Callback() {
@Override
public void onSearchStarted(int reason) {}
@Override
public void onSearchStartFailed(int reason) {}
@Override
public void onSearchStopped(int reason) {}
@Override
public void onSearchStopFailed(int reason) {}
@Override
public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
@Override
public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(
TAG,
"onSourceAdded(), sink = "
+ sink
+ ", sourceId = "
+ sourceId
+ ", reason = "
+ reason);
AudioSharingUtils.updateActiveDeviceIfNeeded(mBtManager);
}
@Override
public void onSourceAddFailed(
@NonNull BluetoothDevice sink,
@NonNull BluetoothLeBroadcastMetadata source,
int reason) {
Log.d(
TAG,
"onSourceAddFailed(), sink = "
+ sink
+ ", source = "
+ source
+ ", reason = "
+ reason);
AudioSharingUtils.toastMessage(
mContext,
String.format(
Locale.US,
"Fail to add source to %s reason %d",
sink.getAddress(),
reason));
}
@Override
public void onSourceModified(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceModifyFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceRemoved(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceRemoveFailed(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onReceiveStateChanged(
BluetoothDevice sink,
int sourceId,
BluetoothLeBroadcastReceiveState state) {}
};
AudioSharingSwitchBarController(
Context context, SettingsMainSwitchBar switchBar, OnSwitchBarChangedListener listener) {
super(context, PREF_KEY);
mSwitchBar = switchBar;
mListener = listener;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
mBtManager = Utils.getLocalBtManager(context);
mBroadcast = mBtManager.getProfileManager().getLeAudioBroadcastProfile();
mAssistant = mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
mExecutor = Executors.newSingleThreadExecutor();
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
mSwitchBar.addOnSwitchChangeListener(this);
mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
if (mBroadcast != null) {
mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
}
if (mAssistant != null) {
mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
}
if (isAvailable()) {
mSwitchBar.setChecked(isBroadcasting());
mSwitchBar.setEnabled(mBluetoothAdapter != null && mBluetoothAdapter.isEnabled());
}
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
mSwitchBar.removeOnSwitchChangeListener(this);
mContext.unregisterReceiver(mReceiver);
if (mBroadcast != null) {
mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
}
if (mAssistant != null) {
mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// Filter out unnecessary callbacks when switch is disabled.
if (!buttonView.isEnabled()) return;
if (isChecked) {
startAudioSharing();
} else {
stopAudioSharing();
}
}
@Override
public int getAvailabilityStatus() {
return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
/**
* Initialize the controller.
*
* @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
*/
public void init(DashboardFragment fragment) {
this.mFragment = fragment;
}
private void startAudioSharing() {
mSwitchBar.setEnabled(false);
if (mBroadcast == null || isBroadcasting()) {
Log.d(TAG, "Already in broadcasting or broadcast not support, ignore!");
mSwitchBar.setEnabled(true);
return;
}
mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager);
ArrayList<AudioSharingDeviceItem> deviceItems =
AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ false);
// deviceItems is ordered. The active device is the first place if exits.
mDeviceItemsForSharing = new ArrayList<>(deviceItems);
if (!deviceItems.isEmpty() && deviceItems.get(0).isActive()) {
for (CachedBluetoothDevice device :
mGroupedConnectedDevices.getOrDefault(
deviceItems.get(0).getGroupId(), ImmutableList.of())) {
// If active device exists for audio sharing, share to it
// automatically once the broadcast is started.
mTargetActiveSinks.add(device.getDevice());
}
mDeviceItemsForSharing.remove(0);
}
// TODO: start broadcast with new API
mBroadcast.startBroadcast("test", null);
}
private void stopAudioSharing() {
mSwitchBar.setEnabled(false);
if (mBroadcast == null || !isBroadcasting()) {
Log.d(TAG, "Already not broadcasting or broadcast not support, ignore!");
mSwitchBar.setEnabled(true);
return;
}
mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId());
}
private void updateSwitch() {
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
boolean isBroadcasting = isBroadcasting();
ThreadUtils.postOnMainThread(
() -> {
if (mSwitchBar.isChecked() != isBroadcasting) {
mSwitchBar.setChecked(isBroadcasting);
}
mSwitchBar.setEnabled(true);
mListener.onSwitchBarChanged();
});
});
}
private boolean isBroadcasting() {
return mBroadcast != null && mBroadcast.isEnabled(null);
}
private void addSourceToTargetSinks(List<BluetoothDevice> sinks) {
if (sinks.isEmpty() || mBroadcast == null || mAssistant == null) {
Log.d(TAG, "Skip adding source to target.");
return;
}
BluetoothLeBroadcastMetadata broadcastMetadata =
mBroadcast.getLatestBluetoothLeBroadcastMetadata();
if (broadcastMetadata == null) {
Log.e(TAG, "Error: There is no broadcastMetadata.");
return;
}
for (BluetoothDevice sink : sinks) {
Log.d(
TAG,
"Add broadcast with broadcastId: "
+ broadcastMetadata.getBroadcastId()
+ "to the device: "
+ sink.getAnonymizedAddress());
mAssistant.addSource(sink, broadcastMetadata, /* isGroupOp= */ false);
}
}
}

View File

@@ -1,320 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.android.settings.flags.Flags;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
public class AudioSharingUtils {
private static final String TAG = "AudioSharingUtils";
private static final boolean DEBUG = BluetoothUtils.D;
/**
* Fetch {@link CachedBluetoothDevice}s connected to the broadcast assistant. The devices are
* grouped by CSIP group id.
*
* @param localBtManager The BT manager to provide BT functions.
* @return A map of connected devices grouped by CSIP group id.
*/
public static Map<Integer, List<CachedBluetoothDevice>> fetchConnectedDevicesByGroupId(
LocalBluetoothManager localBtManager) {
Map<Integer, List<CachedBluetoothDevice>> groupedDevices = new HashMap<>();
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) return groupedDevices;
// TODO: filter out devices with le audio disabled.
List<BluetoothDevice> connectedDevices = assistant.getConnectedDevices();
CachedBluetoothDeviceManager cacheManager = localBtManager.getCachedDeviceManager();
for (BluetoothDevice device : connectedDevices) {
CachedBluetoothDevice cachedDevice = cacheManager.findDevice(device);
if (cachedDevice == null) {
Log.d(TAG, "Skip device due to not being cached: " + device.getAnonymizedAddress());
continue;
}
int groupId = cachedDevice.getGroupId();
if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
Log.d(
TAG,
"Skip device due to no valid group id: " + device.getAnonymizedAddress());
continue;
}
if (!groupedDevices.containsKey(groupId)) {
groupedDevices.put(groupId, new ArrayList<>());
}
groupedDevices.get(groupId).add(cachedDevice);
}
if (DEBUG) {
Log.d(TAG, "fetchConnectedDevicesByGroupId: " + groupedDevices);
}
return groupedDevices;
}
/**
* Fetch a list of ordered connected lead {@link CachedBluetoothDevice}s eligible for audio
* sharing. The active device is placed in the first place if it exists. The devices can be
* filtered by whether it is already in the audio sharing session.
*
* @param localBtManager The BT manager to provide BT functions. *
* @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
* id.
* @param filterByInSharing Whether to filter the device by if is already in the sharing
* session.
* @return A list of ordered connected devices eligible for the audio sharing. The active device
* is placed in the first place if it exists.
*/
public static List<CachedBluetoothDevice> buildOrderedConnectedLeadDevices(
LocalBluetoothManager localBtManager,
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
boolean filterByInSharing) {
List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) return orderedDevices;
for (List<CachedBluetoothDevice> devices : groupedConnectedDevices.values()) {
CachedBluetoothDevice leadDevice = null;
for (CachedBluetoothDevice device : devices) {
if (!device.getMemberDevice().isEmpty()) {
leadDevice = device;
break;
}
}
if (leadDevice == null && !devices.isEmpty()) {
leadDevice = devices.get(0);
Log.d(
TAG,
"Empty member device, pick arbitrary device as the lead: "
+ leadDevice.getDevice().getAnonymizedAddress());
}
if (leadDevice == null) {
Log.d(TAG, "Skip due to no lead device");
continue;
}
if (filterByInSharing && !hasBroadcastSource(leadDevice, localBtManager)) {
Log.d(
TAG,
"Filtered the device due to not in sharing session: "
+ leadDevice.getDevice().getAnonymizedAddress());
continue;
}
orderedDevices.add(leadDevice);
}
orderedDevices.sort(
(CachedBluetoothDevice d1, CachedBluetoothDevice d2) -> {
// Active above not inactive
int comparison =
(isActiveLeAudioDevice(d2) ? 1 : 0)
- (isActiveLeAudioDevice(d1) ? 1 : 0);
if (comparison != 0) return comparison;
// Bonded above not bonded
comparison =
(d2.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0)
- (d1.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
if (comparison != 0) return comparison;
// Bond timestamp available above unavailable
comparison =
(d2.getBondTimestamp() != null ? 1 : 0)
- (d1.getBondTimestamp() != null ? 1 : 0);
if (comparison != 0) return comparison;
// Order by bond timestamp if it is available
// Otherwise order by device name
return d1.getBondTimestamp() != null
? d1.getBondTimestamp().compareTo(d2.getBondTimestamp())
: d1.getName().compareTo(d2.getName());
});
return orderedDevices;
}
/**
* Fetch a list of ordered connected lead {@link AudioSharingDeviceItem}s eligible for audio
* sharing. The active device is placed in the first place if it exists. The devices can be
* filtered by whether it is already in the audio sharing session.
*
* @param localBtManager The BT manager to provide BT functions. *
* @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
* id.
* @param filterByInSharing Whether to filter the device by if is already in the sharing
* session.
* @return A list of ordered connected devices eligible for the audio sharing. The active device
* is placed in the first place if it exists.
*/
public static ArrayList<AudioSharingDeviceItem> buildOrderedConnectedLeadAudioSharingDeviceItem(
LocalBluetoothManager localBtManager,
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
boolean filterByInSharing) {
return buildOrderedConnectedLeadDevices(
localBtManager, groupedConnectedDevices, filterByInSharing)
.stream()
.map(device -> buildAudioSharingDeviceItem(device))
.collect(Collectors.toCollection(ArrayList::new));
}
/** Build {@link AudioSharingDeviceItem} from {@link CachedBluetoothDevice}. */
public static AudioSharingDeviceItem buildAudioSharingDeviceItem(
CachedBluetoothDevice cachedDevice) {
return new AudioSharingDeviceItem(
cachedDevice.getName(),
cachedDevice.getGroupId(),
isActiveLeAudioDevice(cachedDevice));
}
/**
* Check if {@link CachedBluetoothDevice} is in an audio sharing session.
*
* @param cachedDevice The cached bluetooth device to check.
* @param localBtManager The BT manager to provide BT functions.
* @return Whether the device is in an audio sharing session.
*/
public static boolean hasBroadcastSource(
CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
return false;
}
List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()) return true;
// Return true if member device is in broadcast.
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice());
if (!list.isEmpty()) return true;
}
return false;
}
/**
* Check if {@link CachedBluetoothDevice} is an active le audio device.
*
* @param cachedDevice The cached bluetooth device to check.
* @return Whether the device is an active le audio device.
*/
public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
return BluetoothUtils.isActiveLeAudioDevice(cachedDevice);
}
/**
* Retrieves the one and only active Bluetooth LE Audio sink device, regardless if the device is
* currently in an audio sharing session.
*
* @param manager The LocalBluetoothManager instance used to fetch connected devices.
* @return An Optional containing the active LE Audio device, or an empty Optional if not found.
*/
public static Optional<CachedBluetoothDevice> getActiveSinkOnAssistant(
@Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
return Optional.empty();
}
var groupedDevices = fetchConnectedDevicesByGroupId(manager);
var leadDevices = buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
if (!leadDevices.isEmpty() && AudioSharingUtils.isActiveLeAudioDevice(leadDevices.get(0))) {
return Optional.of(leadDevices.get(0));
} else {
Log.w(TAG, "getActiveSinksOnAssistant(): No active lead device!");
}
return Optional.empty();
}
/** Toast message on main thread. */
public static void toastMessage(Context context, String message) {
ThreadUtils.postOnMainThread(
() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
}
/** Returns if the le audio sharing is enabled. */
public static boolean isFeatureEnabled() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
return Flags.enableLeAudioSharing()
&& adapter.isLeAudioBroadcastSourceSupported()
== BluetoothStatusCodes.FEATURE_SUPPORTED
&& adapter.isLeAudioBroadcastAssistantSupported()
== BluetoothStatusCodes.FEATURE_SUPPORTED;
}
/** Automatically update active device if needed. */
public static void updateActiveDeviceIfNeeded(LocalBluetoothManager localBtManager) {
if (localBtManager == null) return;
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices =
fetchConnectedDevicesByGroupId(localBtManager);
List<CachedBluetoothDevice> devicesInSharing =
buildOrderedConnectedLeadDevices(
localBtManager, groupedConnectedDevices, /* filterByInSharing= */ true);
if (devicesInSharing.isEmpty()) return;
List<BluetoothDevice> devices =
BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
CachedBluetoothDevice targetDevice = null;
int targetDeviceIdx = -1;
for (CachedBluetoothDevice device : devicesInSharing) {
if (devices.contains(device.getDevice())) {
int idx = devices.indexOf(device.getDevice());
if (idx > targetDeviceIdx) {
targetDeviceIdx = idx;
targetDevice = device;
}
}
}
if (targetDevice != null && !isActiveLeAudioDevice(targetDevice)) {
Log.d(
TAG,
"updateActiveDeviceIfNeeded, set active device: "
+ targetDevice.getDevice().getAnonymizedAddress());
targetDevice.setActive();
}
}
/** Returns if the broadcast is on-going. */
public static boolean isBroadcasting(LocalBluetoothManager manager) {
if (manager == null) return false;
LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile();
return broadcast != null && broadcast.isEnabled(null);
}
/** Stops the latest broadcast. */
public static void stopBroadcasting(LocalBluetoothManager manager) {
if (manager == null) return;
LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile();
broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import java.util.ArrayList;
/** Provides a dialog to choose the active device for calls and alarms. */
public class CallsAndAlarmsDialogFragment extends InstrumentedDialogFragment {
private static final String TAG = "CallsAndAlarmsDialog";
private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
// The host creates an instance of this dialog fragment must implement this interface to receive
// event callbacks.
public interface DialogEventListener {
/**
* Called when users click the device item to set active for calls and alarms in the dialog.
*
* @param item The device item clicked.
*/
void onItemClick(AudioSharingDeviceItem item);
}
private static DialogEventListener sListener;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_ACTIVE;
}
/**
* Display the {@link CallsAndAlarmsDialogFragment} dialog.
*
* @param host The Fragment this dialog will be hosted.
* @param deviceItems The connected device items in audio sharing session.
* @param listener The callback to handle the user action on this dialog.
*/
public static void show(
Fragment host,
ArrayList<AudioSharingDeviceItem> deviceItems,
DialogEventListener listener) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
sListener = listener;
if (manager.findFragmentByTag(TAG) == null) {
final Bundle bundle = new Bundle();
bundle.putParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
final CallsAndAlarmsDialogFragment dialog = new CallsAndAlarmsDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, TAG);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle arguments = requireArguments();
ArrayList<AudioSharingDeviceItem> deviceItems =
arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS);
int checkedItem = -1;
// deviceItems is ordered. The active device is put in the first place if it does exist
if (!deviceItems.isEmpty() && deviceItems.get(0).isActive()) checkedItem = 0;
String[] choices =
deviceItems.stream().map(AudioSharingDeviceItem::getName).toArray(String[]::new);
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity())
.setTitle(R.string.calls_and_alarms_device_title)
.setSingleChoiceItems(
choices,
checkedItem,
(dialog, which) -> {
sListener.onItemClick(deviceItems.get(which));
});
return builder.create();
}
}

View File

@@ -1,165 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing;
import android.annotation.Nullable;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.Utils;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** PreferenceController to control the dialog to choose the active device for calls and alarms */
public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferenceController
implements BluetoothCallback {
private static final String TAG = "CallsAndAlarmsPreferenceController";
private static final String PREF_KEY = "calls_and_alarms";
private final LocalBluetoothManager mLocalBtManager;
private DashboardFragment mFragment;
Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
private ArrayList<AudioSharingDeviceItem> mDeviceItemsInSharingSession = new ArrayList<>();
public CallsAndAlarmsPreferenceController(Context context) {
super(context, PREF_KEY);
mLocalBtManager = Utils.getLocalBtManager(mContext);
}
@Override
public String getPreferenceKey() {
return PREF_KEY;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference.setOnPreferenceClickListener(
preference -> {
if (mFragment == null) {
Log.w(TAG, "Dialog fail to show due to null host.");
return true;
}
updateDeviceItemsInSharingSession();
if (mDeviceItemsInSharingSession.size() >= 2) {
CallsAndAlarmsDialogFragment.show(
mFragment,
mDeviceItemsInSharingSession,
(AudioSharingDeviceItem item) -> {
for (CachedBluetoothDevice device :
mGroupedConnectedDevices.get(item.getGroupId())) {
device.setActive();
}
});
}
return true;
});
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
super.onStart(owner);
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().registerCallback(this);
}
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
super.onStop(owner);
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().unregisterCallback(this);
}
}
@Override
public void updateVisibility() {
if (mPreference == null) return;
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
boolean isVisible = isBroadcasting() && isBluetoothStateOn();
if (!isVisible) {
ThreadUtils.postOnMainThread(() -> mPreference.setVisible(false));
} else {
updateDeviceItemsInSharingSession();
// mDeviceItemsInSharingSession is ordered. The active device is the
// first
// place if exits.
if (!mDeviceItemsInSharingSession.isEmpty()
&& mDeviceItemsInSharingSession.get(0).isActive()) {
ThreadUtils.postOnMainThread(
() -> {
mPreference.setVisible(true);
mPreference.setSummary(
mDeviceItemsInSharingSession
.get(0)
.getName());
});
} else {
ThreadUtils.postOnMainThread(
() -> {
mPreference.setVisible(true);
mPreference.setSummary(
"No active device in sharing");
});
}
}
});
}
@Override
public void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
if (bluetoothProfile != BluetoothProfile.LE_AUDIO) {
Log.d(TAG, "Ignore onActiveDeviceChanged, not LE_AUDIO profile");
return;
}
mPreference.setSummary(activeDevice == null ? "" : activeDevice.getName());
}
/**
* Initialize the controller.
*
* @param fragment The fragment to host the {@link CallsAndAlarmsDialogFragment} dialog.
*/
public void init(DashboardFragment fragment) {
this.mFragment = fragment;
}
private void updateDeviceItemsInSharingSession() {
mGroupedConnectedDevices =
AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
mDeviceItemsInSharingSession =
AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
mLocalBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ true);
}
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.ActionButtonsPreference;
public class AudioStreamButtonController extends BasePreferenceController
implements DefaultLifecycleObserver {
private static final String KEY = "audio_stream_button";
private @Nullable ActionButtonsPreference mPreference;
private int mBroadcastId = -1;
public AudioStreamButtonController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public final void displayPreference(PreferenceScreen screen) {
mPreference = screen.findPreference(getPreferenceKey());
if (mPreference != null) {
mPreference.setButton1Enabled(true);
// TODO(chelseahao): update this based on stream connection state
mPreference
.setButton1Text(R.string.bluetooth_device_context_disconnect)
.setButton1Icon(R.drawable.ic_settings_close);
}
super.displayPreference(screen);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public String getPreferenceKey() {
return KEY;
}
/** Initialize with broadcast id */
void init(int broadcastId) {
mBroadcastId = broadcastId;
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import android.os.Bundle;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
public class AudioStreamDetailsFragment extends DashboardFragment {
static final String BROADCAST_NAME_ARG = "broadcast_name";
static final String BROADCAST_ID_ARG = "broadcast_id";
private static final String TAG = "AudioStreamDetailsFragment";
@Override
public void onAttach(Context context) {
super.onAttach(context);
Bundle arguments = getArguments();
if (arguments != null) {
use(AudioStreamHeaderController.class)
.init(
this,
arguments.getString(BROADCAST_NAME_ARG),
arguments.getInt(BROADCAST_ID_ARG));
use(AudioStreamButtonController.class).init(arguments.getInt(BROADCAST_ID_ARG));
}
}
@Override
public int getMetricsCategory() {
// TODO(chelseahao): update metrics id
return 0;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.audio_stream_details_fragment;
}
@Override
protected String getLogTag() {
return TAG;
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.widget.LayoutPreference;
import javax.annotation.Nullable;
public class AudioStreamHeaderController extends BasePreferenceController
implements DefaultLifecycleObserver {
private static final String KEY = "audio_stream_header";
private @Nullable EntityHeaderController mHeaderController;
private @Nullable DashboardFragment mFragment;
private String mBroadcastName = "";
private int mBroadcastId = -1;
public AudioStreamHeaderController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public final void displayPreference(PreferenceScreen screen) {
LayoutPreference headerPreference = screen.findPreference(KEY);
if (headerPreference != null && mFragment != null) {
mHeaderController =
EntityHeaderController.newInstance(
mFragment.getActivity(),
mFragment,
headerPreference.findViewById(R.id.entity_header));
if (mBroadcastName != null) {
mHeaderController.setLabel(mBroadcastName);
}
mHeaderController.setIcon(
screen.getContext().getDrawable(R.drawable.ic_bt_audio_sharing));
// TODO(chelseahao): update this based on stream connection state
mHeaderController.setSummary("Listening now");
mHeaderController.done(true);
screen.addPreference(headerPreference);
}
super.displayPreference(screen);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public String getPreferenceKey() {
return KEY;
}
/** Initialize with {@link AudioStreamDetailsFragment} and broadcast name and id */
void init(
AudioStreamDetailsFragment audioStreamDetailsFragment,
String broadcastName,
int broadcastId) {
mFragment = audioStreamDetailsFragment;
mBroadcastName = broadcastName;
mBroadcastId = broadcastId;
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
import com.google.common.base.Strings;
/**
* Custom preference class for managing audio stream preferences with an optional lock icon. Extends
* {@link TwoTargetPreference}.
*/
class AudioStreamPreference extends TwoTargetPreference {
private boolean mIsConnected = false;
/**
* Update preference UI based on connection status
*
* @param isConnected Is this streams connected
*/
void setIsConnected(
boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) {
if (mIsConnected == isConnected
&& getOnPreferenceClickListener() == onPreferenceClickListener) {
// Nothing to update.
return;
}
mIsConnected = isConnected;
setSummary(isConnected ? "Listening now" : "");
setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged();
}
AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setIcon(R.drawable.ic_bt_audio_sharing);
}
@Override
protected boolean shouldHideSecondTarget() {
return mIsConnected;
}
@Override
protected int getSecondTargetResId() {
return R.layout.preference_widget_lock;
}
static AudioStreamPreference fromMetadata(
Context context, BluetoothLeBroadcastMetadata source) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source));
return preference;
}
static AudioStreamPreference fromReceiveState(
Context context, BluetoothLeBroadcastReceiveState state) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(state));
return preference;
}
private static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
return source.getSubgroups().stream()
.map(s -> s.getContentMetadata().getProgramInfo())
.filter(i -> !Strings.isNullOrEmpty(i))
.findFirst()
.orElse("Broadcast Id: " + source.getBroadcastId());
}
private static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
return state.getSubgroupMetadata().stream()
.map(BluetoothLeAudioContentMetadata::getProgramInfo)
.filter(i -> !Strings.isNullOrEmpty(i))
.findFirst()
.orElse("Broadcast Id: " + state.getBroadcastId());
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
public class AudioStreamsActiveDeviceController extends BasePreferenceController
implements AudioStreamsActiveDeviceSummaryUpdater.OnSummaryChangeListener,
DefaultLifecycleObserver {
public static final String KEY = "audio_streams_active_device";
private final AudioStreamsActiveDeviceSummaryUpdater mSummaryHelper;
private Preference mPreference;
public AudioStreamsActiveDeviceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mSummaryHelper = new AudioStreamsActiveDeviceSummaryUpdater(mContext, this);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(KEY);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public void onSummaryChanged(String summary) {
mPreference.setSummary(summary);
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
mSummaryHelper.register(true);
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
mSummaryHelper.register(false);
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.annotation.Nullable;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
public class AudioStreamsActiveDeviceSummaryUpdater implements BluetoothCallback {
private static final String TAG = "AudioStreamsActiveDeviceSummaryUpdater";
private static final boolean DEBUG = BluetoothUtils.D;
private final LocalBluetoothManager mBluetoothManager;
private String mSummary;
private OnSummaryChangeListener mListener;
public AudioStreamsActiveDeviceSummaryUpdater(
Context context, OnSummaryChangeListener listener) {
mBluetoothManager = Utils.getLocalBluetoothManager(context);
mListener = listener;
}
@Override
public void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
if (DEBUG) {
Log.d(
TAG,
"onActiveDeviceChanged() with activeDevice : "
+ (activeDevice == null ? "null" : activeDevice.getAddress())
+ " on profile : "
+ bluetoothProfile);
}
if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
notifyChangeIfNeeded();
}
}
void register(boolean register) {
if (register) {
notifyChangeIfNeeded();
mBluetoothManager.getEventManager().registerCallback(this);
} else {
mBluetoothManager.getEventManager().unregisterCallback(this);
}
}
private void notifyChangeIfNeeded() {
ThreadUtils.postOnBackgroundThread(
() -> {
String summary = getSummary();
if (!TextUtils.equals(mSummary, summary)) {
mSummary = summary;
ThreadUtils.postOnMainThread(() -> mListener.onSummaryChanged(summary));
}
});
}
private String getSummary() {
var activeSink = AudioSharingUtils.getActiveSinkOnAssistant(mBluetoothManager);
if (activeSink.isEmpty()) {
return "No active LE Audio device";
}
return activeSink.get().getName();
}
/** Interface definition for a callback to be invoked when the summary has been changed. */
interface OnSummaryChangeListener {
/**
* Called when summary has changed.
*
* @param summary The new summary.
*/
void onSummaryChanged(String summary);
}
}

View File

@@ -1,173 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.util.Log;
import com.android.settingslib.bluetooth.BluetoothUtils;
import java.util.Locale;
public class AudioStreamsBroadcastAssistantCallback
implements BluetoothLeBroadcastAssistant.Callback {
private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
private static final boolean DEBUG = BluetoothUtils.D;
private final AudioStreamsProgressCategoryController mCategoryController;
public AudioStreamsBroadcastAssistantCallback(
AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
mCategoryController = audioStreamsProgressCategoryController;
}
@Override
public void onReceiveStateChanged(
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
if (DEBUG) {
Log.d(
TAG,
"onReceiveStateChanged() sink : "
+ sink.getAddress()
+ " sourceId: "
+ sourceId
+ " state: "
+ state);
}
mCategoryController.handleSourceConnected(state);
}
@Override
public void onSearchStartFailed(int reason) {
Log.w(TAG, "onSearchStartFailed() reason : " + reason);
mCategoryController.showToast(
String.format(Locale.US, "Failed to start scanning, reason %d", reason));
}
@Override
public void onSearchStarted(int reason) {
if (mCategoryController == null) {
Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
return;
}
if (DEBUG) {
Log.d(TAG, "onSearchStarted() reason : " + reason);
}
mCategoryController.setScanning(true);
}
@Override
public void onSearchStopFailed(int reason) {
Log.w(TAG, "onSearchStopFailed() reason : " + reason);
mCategoryController.showToast(
String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
}
@Override
public void onSearchStopped(int reason) {
if (mCategoryController == null) {
Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
return;
}
if (DEBUG) {
Log.d(TAG, "onSearchStopped() reason : " + reason);
}
mCategoryController.setScanning(false);
}
@Override
public void onSourceAddFailed(
BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
if (DEBUG) {
Log.d(
TAG,
"onSourceAddFailed() sink : "
+ sink.getAddress()
+ " source: "
+ source
+ " reason: "
+ reason);
}
mCategoryController.showToast(
String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
}
@Override
public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {
if (DEBUG) {
Log.d(
TAG,
"onSourceAdded() sink : "
+ sink.getAddress()
+ " sourceId: "
+ sourceId
+ " reason: "
+ reason);
}
}
@Override
public void onSourceFound(BluetoothLeBroadcastMetadata source) {
if (mCategoryController == null) {
Log.w(TAG, "onSourceFound() : mCategoryController is null!");
return;
}
if (DEBUG) {
Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
}
mCategoryController.handleSourceFound(source);
}
@Override
public void onSourceLost(int broadcastId) {
if (DEBUG) {
Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
}
mCategoryController.handleSourceLost(broadcastId);
}
@Override
public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}
@Override
public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason);
mCategoryController.showToast(
String.format(
Locale.US,
"Failed to remove source %d for sink %s",
sourceId,
sink.getAddress()));
}
@Override
public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
if (DEBUG) {
Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
}
mCategoryController.showToast(
String.format(
Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
}
}

View File

@@ -1,110 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.annotation.Nullable;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingBasePreferenceController;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.flags.Flags;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AudioStreamsCategoryController extends AudioSharingBasePreferenceController {
private static final String TAG = "AudioStreamsCategoryController";
private static final boolean DEBUG = BluetoothUtils.D;
private final LocalBluetoothManager mLocalBtManager;
private final Executor mExecutor;
private final BluetoothCallback mBluetoothCallback =
new BluetoothCallback() {
@Override
public void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
updateVisibility();
}
}
};
public AudioStreamsCategoryController(Context context, String key) {
super(context, key);
mLocalBtManager = Utils.getLocalBtManager(mContext);
mExecutor = Executors.newSingleThreadExecutor();
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
super.onStart(owner);
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
}
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
super.onStop(owner);
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
}
}
@Override
public int getAvailabilityStatus() {
return Flags.enableLeAudioQrCodePrivateBroadcastSharing()
? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
public void updateVisibility() {
if (mPreference == null) return;
mExecutor.execute(
() -> {
boolean hasActiveLe =
AudioSharingUtils.getActiveSinkOnAssistant(mLocalBtManager).isPresent();
boolean isBroadcasting = isBroadcasting();
boolean isBluetoothOn = isBluetoothStateOn();
if (DEBUG) {
Log.d(
TAG,
"updateVisibility() isBroadcasting : "
+ isBroadcasting
+ " hasActiveLe : "
+ hasActiveLe
+ " is BT on : "
+ isBluetoothOn);
}
ThreadUtils.postOnMainThread(
() ->
mPreference.setVisible(
isBluetoothOn && hasActiveLe && !isBroadcasting));
});
}
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
import android.app.Activity;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeFragment;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.android.settingslib.bluetooth.BluetoothUtils;
public class AudioStreamsDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioStreamsDashboardFrag";
private static final boolean DEBUG = BluetoothUtils.D;
private AudioStreamsScanQrCodeController mAudioStreamsScanQrCodeController;
public AudioStreamsDashboardFragment() {
super();
}
@Override
public int getMetricsCategory() {
// TODO: update category id.
return 0;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
public int getHelpResource() {
return R.string.help_url_audio_sharing;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.bluetooth_audio_streams;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mAudioStreamsScanQrCodeController = use(AudioStreamsScanQrCodeController.class);
mAudioStreamsScanQrCodeController.setFragment(this);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) {
Log.d(
TAG,
"onActivityResult() requestCode : "
+ requestCode
+ " resultCode : "
+ resultCode);
}
if (requestCode == REQUEST_SCAN_BT_BROADCAST_QR_CODE) {
if (resultCode == Activity.RESULT_OK) {
String broadcastMetadata =
data.getStringExtra(QrCodeScanModeFragment.KEY_BROADCAST_METADATA);
BluetoothLeBroadcastMetadata source =
BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
broadcastMetadata);
if (source == null) {
Log.w(TAG, "onActivityResult() source is null!");
return;
}
if (DEBUG) {
Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
}
if (mAudioStreamsScanQrCodeController == null) {
Log.w(TAG, "onActivityResult() AudioStreamsScanQrCodeController is null!");
return;
}
mAudioStreamsScanQrCodeController.addSource(source);
}
}
}
}

View File

@@ -1,165 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import static java.util.Collections.emptyList;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.util.Log;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.utils.ThreadUtils;
import java.util.List;
import java.util.stream.Stream;
import javax.annotation.Nullable;
/**
* A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices.
*/
class AudioStreamsHelper {
private static final String TAG = "AudioStreamsHelper";
private static final boolean DEBUG = BluetoothUtils.D;
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
mBluetoothManager = bluetoothManager;
mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
}
/**
* Adds the specified LE broadcast source to all active sinks.
*
* @param source The LE broadcast metadata representing the audio source.
*/
void addSource(BluetoothLeBroadcastMetadata source) {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
return;
}
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
for (var sink : getActiveSinksOnAssistant(mBluetoothManager)) {
if (DEBUG) {
Log.d(
TAG,
"addSource(): join broadcast broadcastId"
+ " : "
+ source.getBroadcastId()
+ " sink : "
+ sink.getAddress());
}
mLeBroadcastAssistant.addSource(sink, source, false);
}
});
}
/** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
void removeSource(int broadcastId) {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
return;
}
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
for (var sink : getActiveSinksOnAssistant(mBluetoothManager)) {
if (DEBUG) {
Log.d(
TAG,
"removeSource(): remove all sources with broadcast id :"
+ broadcastId
+ " from sink : "
+ sink.getAddress());
}
mLeBroadcastAssistant.getAllSources(sink).stream()
.filter(state -> state.getBroadcastId() == broadcastId)
.forEach(
state ->
mLeBroadcastAssistant.removeSource(
sink, state.getSourceId()));
}
});
}
/** Retrieves a list of all LE broadcast receive states from active sinks. */
List<BluetoothLeBroadcastReceiveState> getAllSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getActiveSinksOnAssistant(mBluetoothManager).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.toList();
}
@Nullable
LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
return mLeBroadcastAssistant;
}
static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
&& state.getBigEncryptionState()
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
}
private static List<BluetoothDevice> getActiveSinksOnAssistant(
@Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
return emptyList();
}
return AudioSharingUtils.getActiveSinkOnAssistant(manager)
.map(
cachedBluetoothDevice ->
Stream.concat(
Stream.of(cachedBluetoothDevice.getDevice()),
cachedBluetoothDevice.getMemberDevice().stream()
.map(CachedBluetoothDevice::getDevice))
.toList())
.orElse(emptyList());
}
private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
@Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
return null;
}
LocalBluetoothProfileManager profileManager = manager.getProfileManager();
if (profileManager == null) {
Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
return null;
}
return profileManager.getLeAudioBroadcastAssistantProfile();
}
}

View File

@@ -1,314 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import static java.util.Collections.emptyList;
import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.annotation.Nullable;
public class AudioStreamsProgressCategoryController extends BasePreferenceController
implements DefaultLifecycleObserver {
private static final String TAG = "AudioStreamsProgressCategoryController";
private static final boolean DEBUG = BluetoothUtils.D;
private final BluetoothCallback mBluetoothCallback =
new BluetoothCallback() {
@Override
public void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
mExecutor.execute(() -> init(activeDevice != null));
}
}
};
private final Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
private AudioStreamsProgressCategoryPreference mCategoryPreference;
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
super(context, preferenceKey);
mExecutor = Executors.newSingleThreadExecutor();
mBluetoothManager = Utils.getLocalBtManager(mContext);
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mCategoryPreference = screen.findPreference(getPreferenceKey());
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
if (mBluetoothManager != null) {
mBluetoothManager.getEventManager().registerCallback(mBluetoothCallback);
}
mExecutor.execute(
() -> {
boolean hasActive =
AudioSharingUtils.getActiveSinkOnAssistant(mBluetoothManager)
.isPresent();
init(hasActive);
});
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
if (mBluetoothManager != null) {
mBluetoothManager.getEventManager().unregisterCallback(mBluetoothCallback);
}
mExecutor.execute(this::stopScanning);
}
void setScanning(boolean isScanning) {
ThreadUtils.postOnMainThread(
() -> {
if (mCategoryPreference != null) mCategoryPreference.setProgress(isScanning);
});
}
void handleSourceFound(BluetoothLeBroadcastMetadata source) {
Preference.OnPreferenceClickListener addSourceOrShowDialog =
preference -> {
if (DEBUG) {
Log.d(
TAG,
"preferenceClicked(): attempt to join broadcast id : "
+ source.getBroadcastId());
}
if (source.isEncrypted()) {
ThreadUtils.postOnMainThread(
() -> launchPasswordDialog(source, preference));
} else {
mAudioStreamsHelper.addSource(source);
}
return true;
};
mBroadcastIdToPreferenceMap.computeIfAbsent(
source.getBroadcastId(),
k -> {
var preference = AudioStreamPreference.fromMetadata(mContext, source);
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(false, addSourceOrShowDialog);
if (mCategoryPreference != null) {
mCategoryPreference.addPreference(preference);
}
});
return preference;
});
}
void handleSourceLost(int broadcastId) {
var toRemove = mBroadcastIdToPreferenceMap.remove(broadcastId);
if (toRemove != null) {
ThreadUtils.postOnMainThread(
() -> {
if (mCategoryPreference != null) {
mCategoryPreference.removePreference(toRemove);
}
});
}
mAudioStreamsHelper.removeSource(broadcastId);
}
void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
if (!AudioStreamsHelper.isConnected(state)) {
return;
}
mBroadcastIdToPreferenceMap.compute(
state.getBroadcastId(),
(k, v) -> {
// True if this source has been added either by scanning, or it's currently
// connected to another active sink.
boolean existed = v != null;
AudioStreamPreference preference =
existed ? v : AudioStreamPreference.fromReceiveState(mContext, state);
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
true, p -> launchDetailFragment(state.getBroadcastId()));
if (mCategoryPreference != null && !existed) {
mCategoryPreference.addPreference(preference);
}
});
return preference;
});
}
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
private void init(boolean hasActive) {
mBroadcastIdToPreferenceMap.clear();
ThreadUtils.postOnMainThread(
() -> {
if (mCategoryPreference != null) {
mCategoryPreference.removeAll();
mCategoryPreference.setVisible(hasActive);
}
});
if (hasActive) {
startScanning();
} else {
stopScanning();
}
}
private void startScanning() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "startScanning(): LeBroadcastAssistant is null!");
return;
}
if (mLeBroadcastAssistant.isSearchInProgress()) {
showToast("Failed to start scanning, please try again.");
return;
}
if (DEBUG) {
Log.d(TAG, "startScanning()");
}
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mLeBroadcastAssistant.startSearchingForSources(emptyList());
// Display currently connected streams
var unused =
ThreadUtils.postOnBackgroundThread(
() ->
mAudioStreamsHelper
.getAllSources()
.forEach(this::handleSourceConnected));
}
private void stopScanning() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!");
return;
}
if (mLeBroadcastAssistant.isSearchInProgress()) {
if (DEBUG) {
Log.d(TAG, "stopScanning()");
}
mLeBroadcastAssistant.stopSearchingForSources();
}
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
private boolean launchDetailFragment(int broadcastId) {
if (!mBroadcastIdToPreferenceMap.containsKey(broadcastId)) {
Log.w(
TAG,
"launchDetailFragment(): broadcastId not exist in BroadcastIdToPreferenceMap!");
return false;
}
AudioStreamPreference preference = mBroadcastIdToPreferenceMap.get(broadcastId);
Bundle broadcast = new Bundle();
broadcast.putString(
AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) preference.getTitle());
broadcast.putInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG, broadcastId);
new SubSettingLauncher(mContext)
.setTitleText("Audio stream details")
.setDestination(AudioStreamDetailsFragment.class.getName())
// TODO(chelseahao): Add logging enum
.setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
.setArguments(broadcast)
.launch();
return true;
}
private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) {
View layout =
LayoutInflater.from(mContext)
.inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
((TextView) layout.requireViewById(R.id.broadcast_name_text))
.setText(preference.getTitle());
AlertDialog alertDialog =
new AlertDialog.Builder(mContext)
.setTitle(R.string.find_broadcast_password_dialog_title)
.setView(layout)
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(
R.string.bluetooth_connect_access_dialog_positive,
(dialog, which) -> {
var code =
((EditText)
layout.requireViewById(
R.id.broadcast_edit_text))
.getText()
.toString();
mAudioStreamsHelper.addSource(
new BluetoothLeBroadcastMetadata.Builder(source)
.setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8))
.build());
})
.create();
alertDialog.show();
}
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import android.util.AttributeSet;
import com.android.settings.ProgressCategory;
import com.android.settings.R;
public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
public AudioStreamsProgressCategoryPreference(Context context) {
super(context);
init();
}
public AudioStreamsProgressCategoryPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public AudioStreamsProgressCategoryPreference(
Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public AudioStreamsProgressCategoryPreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setEmptyTextRes(R.string.audio_streams_empty);
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.InstrumentedFragment;
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.qrcode.QrCodeGenerator;
import com.google.zxing.WriterException;
import java.util.Optional;
public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
private static final String TAG = "AudioStreamsQrCodeFragment";
@Override
public int getMetricsCategory() {
// TODO(chelseahao): update metrics id
return 0;
}
@Override
public final View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
getQrCodeBitmap()
.ifPresent(
bm ->
((ImageView) view.requireViewById(R.id.qrcode_view))
.setImageBitmap(bm));
return view;
}
private Optional<Bitmap> getQrCodeBitmap() {
String broadcastMetadata = getBroadcastMetadataQrCode();
if (broadcastMetadata.isEmpty()) {
Log.d(TAG, "onCreateView: broadcastMetadata is empty!");
return Optional.empty();
}
try {
int qrcodeSize = getContext().getResources().getDimensionPixelSize(R.dimen.qrcode_size);
Bitmap bitmap = QrCodeGenerator.encodeQrCode(broadcastMetadata, qrcodeSize);
return Optional.of(bitmap);
} catch (WriterException e) {
Log.d(
TAG,
"onCreateView: broadcastMetadata "
+ broadcastMetadata
+ " qrCode generation exception "
+ e);
}
return Optional.empty();
}
private String getBroadcastMetadataQrCode() {
LocalBluetoothLeBroadcast localBluetoothLeBroadcast =
Utils.getLocalBtManager(getActivity())
.getProfileManager()
.getLeAudioBroadcastProfile();
if (localBluetoothLeBroadcast == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!");
return "";
}
BluetoothLeBroadcastMetadata metadata =
localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
if (metadata == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!");
return "";
}
return BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
}
}

View File

@@ -1,139 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
public class AudioStreamsScanQrCodeController extends BasePreferenceController
implements DefaultLifecycleObserver {
static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0;
private static final String TAG = "AudioStreamsProgressCategoryController";
private static final boolean DEBUG = BluetoothUtils.D;
private static final String KEY = "audio_streams_scan_qr_code";
private final BluetoothCallback mBluetoothCallback =
new BluetoothCallback() {
@Override
public void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
updateVisibility();
}
}
};
private final LocalBluetoothManager mLocalBtManager;
private final AudioStreamsHelper mAudioStreamsHelper;
private AudioStreamsDashboardFragment mFragment;
private Preference mPreference;
public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
super(context, preferenceKey);
mLocalBtManager = Utils.getLocalBtManager(mContext);
mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
}
public void setFragment(AudioStreamsDashboardFragment fragment) {
mFragment = fragment;
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
}
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
}
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public String getPreferenceKey() {
return KEY;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
if (mPreference == null) {
Log.w(TAG, "displayPreference() mPreference is null!");
return;
}
mPreference.setOnPreferenceClickListener(
preference -> {
if (mFragment == null) {
Log.w(TAG, "displayPreference() mFragment is null!");
return false;
}
if (preference.getKey().equals(KEY)) {
Intent intent = new Intent(mContext, QrCodeScanModeActivity.class);
intent.setAction(
BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
mFragment.startActivityForResult(intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
if (DEBUG) {
Log.w(TAG, "displayPreference() sent intent : " + intent);
}
return true;
}
return false;
});
}
void addSource(BluetoothLeBroadcastMetadata source) {
mAudioStreamsHelper.addSource(source);
}
private void updateVisibility() {
ThreadUtils.postOnBackgroundThread(
() -> {
boolean hasActiveLe =
AudioSharingUtils.getActiveSinkOnAssistant(mLocalBtManager).isPresent();
ThreadUtils.postOnMainThread(() -> mPreference.setVisible(hasActiveLe));
});
}
}

View File

@@ -1,113 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams.qrcode;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.fragment.app.FragmentTransaction;
import com.android.settings.R;
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothUtils;
/**
* Finding a broadcast through QR code.
*
* <p>To use intent action {@link
* BluetoothBroadcastUtils#ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER}, specify the bluetooth device
* sink of the broadcast to be provisioned in {@link
* BluetoothBroadcastUtils#EXTRA_BLUETOOTH_DEVICE_SINK} and check the operation for all coordinated
* set members throughout one session or not by {@link
* BluetoothBroadcastUtils#EXTRA_BLUETOOTH_SINK_IS_GROUP}.
*/
public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "QrCodeScanModeActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void handleIntent(Intent intent) {
String action = intent != null ? intent.getAction() : null;
if (DEBUG) {
Log.d(TAG, "handleIntent(), action = " + action);
}
if (action == null) {
finish();
return;
}
switch (action) {
case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER:
showQrCodeScannerFragment(intent);
break;
default:
if (DEBUG) {
Log.e(TAG, "Launch with an invalid action");
}
finish();
}
}
protected void showQrCodeScannerFragment(Intent intent) {
if (intent == null) {
if (DEBUG) {
Log.d(TAG, "intent is null, can not get bluetooth information from intent.");
}
return;
}
if (DEBUG) {
Log.d(TAG, "showQrCodeScannerFragment");
}
if (DEBUG) {
Log.d(TAG, "get extra from intent");
}
QrCodeScanModeFragment fragment =
(QrCodeScanModeFragment)
mFragmentManager.findFragmentByTag(
BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
if (fragment == null) {
fragment = new QrCodeScanModeFragment();
} else {
if (fragment.isVisible()) {
return;
}
// When the fragment in back stack but not on top of the stack, we can simply pop
// stack because current fragment transactions are arranged in an order
mFragmentManager.popBackStackImmediate();
return;
}
final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.replace(
R.id.fragment_container,
fragment,
BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
fragmentTransaction.commit();
}
}

View File

@@ -1,64 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams.qrcode;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemProperties;
import androidx.fragment.app.FragmentManager;
import com.android.settings.R;
import com.android.settingslib.core.lifecycle.ObservableActivity;
import com.google.android.setupdesign.util.ThemeHelper;
import com.google.android.setupdesign.util.ThemeResolver;
public abstract class QrCodeScanModeBaseActivity extends ObservableActivity {
private static final String THEME_KEY = "setupwizard.theme";
private static final String THEME_DEFAULT_VALUE = "SudThemeGlifV3_DayNight";
protected FragmentManager mFragmentManager;
protected abstract void handleIntent(Intent intent);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
int defaultTheme =
ThemeHelper.isSetupWizardDayNightEnabled(this)
? com.google.android.setupdesign.R.style.SudThemeGlifV3_DayNight
: com.google.android.setupdesign.R.style.SudThemeGlifV3_Light;
ThemeResolver themeResolver =
new ThemeResolver.Builder(ThemeResolver.getDefault())
.setDefaultTheme(defaultTheme)
.setUseDayNight(true)
.build();
setTheme(
themeResolver.resolve(
SystemProperties.get(THEME_KEY, THEME_DEFAULT_VALUE),
/* suppressDayNight= */ !ThemeHelper.isSetupWizardDayNightEnabled(this)));
setContentView(R.layout.qrcode_scan_mode_activity);
mFragmentManager = getSupportFragmentManager();
if (savedInstanceState == null) {
handleIntent(getIntent());
}
}
}

View File

@@ -1,268 +0,0 @@
/*
* Copyright (C) 2023 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.connecteddevice.audiosharing.audiostreams.qrcode;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.graphics.Matrix;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import android.util.Size;
import android.view.LayoutInflater;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityEvent;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.android.settings.R;
import com.android.settings.core.InstrumentedFragment;
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.qrcode.QrCamera;
import java.time.Duration;
public class QrCodeScanModeFragment extends InstrumentedFragment
implements TextureView.SurfaceTextureListener, QrCamera.ScannerCallback {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "QrCodeScanModeFragment";
/** Message sent to hide error message */
private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1;
/** Message sent to show error message */
private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2;
/** Message sent to broadcast QR code */
private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3;
private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000;
private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000;
private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3);
public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
private int mCornerRadius;
private String mBroadcastMetadata;
private Context mContext;
private QrCamera mCamera;
private TextureView mTextureView;
private TextView mSummary;
private TextView mErrorMessage;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getContext();
}
@Override
public final View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(
R.layout.qrcode_scanner_fragment, container, /* attachToRoot */ false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mTextureView = view.findViewById(R.id.preview_view);
mCornerRadius =
mContext.getResources().getDimensionPixelSize(R.dimen.qrcode_preview_radius);
mTextureView.setSurfaceTextureListener(this);
mTextureView.setOutlineProvider(
new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(
0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
}
});
mTextureView.setClipToOutline(true);
mErrorMessage = view.findViewById(R.id.error_message);
}
private void initCamera(SurfaceTexture surface) {
// Check if the camera has already created.
if (mCamera == null) {
mCamera = new QrCamera(mContext, this);
mCamera.start(surface);
}
}
private void destroyCamera() {
if (mCamera != null) {
mCamera.stop();
mCamera = null;
}
}
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
initCamera(surface);
}
@Override
public void onSurfaceTextureSizeChanged(
@NonNull SurfaceTexture surface, int width, int height) {}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
destroyCamera();
return true;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {}
@Override
public void handleSuccessfulResult(String qrCode) {
if (DEBUG) {
Log.d(TAG, "handleSuccessfulResult(), get the qr code string.");
}
mBroadcastMetadata = qrCode;
handleBtLeAudioScanner();
}
@Override
public void handleCameraFailure() {
destroyCamera();
}
@Override
public Size getViewSize() {
return new Size(mTextureView.getWidth(), mTextureView.getHeight());
}
@Override
public Rect getFramePosition(Size previewSize, int cameraOrientation) {
return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight());
}
@Override
public void setTransform(Matrix transform) {
mTextureView.setTransform(transform);
}
@Override
public boolean isValid(String qrCode) {
if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) {
return true;
} else {
showErrorMessage(R.string.bt_le_audio_qr_code_is_not_valid_format);
return false;
}
}
protected boolean isDecodeTaskAlive() {
return mCamera != null && mCamera.isDecodeTaskAlive();
}
private final Handler mHandler =
new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_HIDE_ERROR_MESSAGE:
mErrorMessage.setVisibility(View.INVISIBLE);
break;
case MESSAGE_SHOW_ERROR_MESSAGE:
final String errorMessage = (String) msg.obj;
mErrorMessage.setVisibility(View.VISIBLE);
mErrorMessage.setText(errorMessage);
mErrorMessage.sendAccessibilityEvent(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
// Cancel any pending messages to hide error view and requeue the
// message so
// user has time to see error
removeMessages(MESSAGE_HIDE_ERROR_MESSAGE);
sendEmptyMessageDelayed(
MESSAGE_HIDE_ERROR_MESSAGE, SHOW_ERROR_MESSAGE_INTERVAL);
break;
case MESSAGE_SCAN_BROADCAST_SUCCESS:
Log.d(TAG, "scan success");
final Intent resultIntent = new Intent();
resultIntent.putExtra(KEY_BROADCAST_METADATA, mBroadcastMetadata);
getActivity().setResult(Activity.RESULT_OK, resultIntent);
notifyUserForQrCodeRecognition();
break;
default:
}
}
};
private void notifyUserForQrCodeRecognition() {
if (mCamera != null) {
mCamera.stop();
}
mErrorMessage.setVisibility(View.INVISIBLE);
triggerVibrationForQrCodeRecognition(getContext());
getActivity().finish();
}
private static void triggerVibrationForQrCodeRecognition(Context context) {
Vibrator vibrator = context.getSystemService(Vibrator.class);
if (vibrator == null) {
return;
}
vibrator.vibrate(
VibrationEffect.createOneShot(
VIBRATE_DURATION_QR_CODE_RECOGNITION.toMillis(),
VibrationEffect.DEFAULT_AMPLITUDE));
}
private void showErrorMessage(@StringRes int messageResId) {
final Message message =
mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, getString(messageResId));
message.sendToTarget();
}
private void handleBtLeAudioScanner() {
Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS);
mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
}
private void updateSummary() {
mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner));
}
@Override
public int getMetricsCategory() {
return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE;
}
}

View File

@@ -15,9 +15,10 @@
*/
package com.android.settings.connecteddevice.dock;
import android.annotation.NonNull;
import android.content.Context;
import androidx.annotation.NonNull;
/**
* Update the dock devices. It notifies the upper level whether to add/remove the preference
* through {@link DevicePreferenceCallback}

View File

@@ -16,9 +16,10 @@
package com.android.settings.connecteddevice.fastpair;
import android.annotation.Nullable;
import android.content.Context;
import androidx.annotation.Nullable;
/**
* Updates the Fast Pair devices. It notifies the upper level whether to add/remove the preference
* through {@link DevicePreferenceCallback}

View File

@@ -24,6 +24,7 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.hardware.input.InputSettings;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
@@ -73,6 +74,8 @@ public class StylusDevicesController extends AbstractPreferenceController implem
static final String KEY_IGNORE_BUTTON = "ignore_button";
@VisibleForTesting
static final String KEY_DEFAULT_NOTES = "default_notes";
@VisibleForTesting
static final String KEY_SHOW_STYLUS_POINTER_ICON = "show_stylus_pointer_icon";
private static final String TAG = "StylusDevicesController";
@@ -181,6 +184,26 @@ public class StylusDevicesController extends AbstractPreferenceController implem
return pref;
}
@Nullable
private SwitchPreferenceCompat createShowStylusPointerIconPreference(
SwitchPreferenceCompat preference) {
if (!mContext.getResources()
.getBoolean(com.android.internal.R.bool.config_enableStylusPointerIcon)) {
// If the config is not enabled, no need to show the preference to user
return null;
}
SwitchPreferenceCompat pref = preference == null ? new SwitchPreferenceCompat(mContext)
: preference;
pref.setKey(KEY_SHOW_STYLUS_POINTER_ICON);
pref.setTitle(mContext.getString(R.string.show_stylus_pointer_icon));
pref.setIcon(R.drawable.ic_stylus);
pref.setOnPreferenceClickListener(this);
pref.setChecked(Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.STYLUS_POINTER_ICON_ENABLED,
InputSettings.DEFAULT_STYLUS_POINTER_ICON_ENABLED) == 1);
return pref;
}
@Override
public boolean onPreferenceClick(Preference preference) {
String key = preference.getKey();
@@ -213,6 +236,11 @@ public class StylusDevicesController extends AbstractPreferenceController implem
Secure.STYLUS_BUTTONS_ENABLED,
((TwoStatePreference) preference).isChecked() ? 0 : 1);
break;
case KEY_SHOW_STYLUS_POINTER_ICON:
Settings.Secure.putInt(mContext.getContentResolver(),
Secure.STYLUS_POINTER_ICON_ENABLED,
((SwitchPreferenceCompat) preference).isChecked() ? 1 : 0);
break;
}
return true;
}
@@ -268,6 +296,13 @@ public class StylusDevicesController extends AbstractPreferenceController implem
if (buttonPref == null) {
mPreferencesContainer.addPreference(createButtonPressPreference());
}
SwitchPreferenceCompat currShowStylusPointerIconPref = mPreferencesContainer
.findPreference(KEY_SHOW_STYLUS_POINTER_ICON);
Preference showStylusPointerIconPref =
createShowStylusPointerIconPreference(currShowStylusPointerIconPref);
if (currShowStylusPointerIconPref == null && showStylusPointerIconPref != null) {
mPreferencesContainer.addPreference(showStylusPointerIconPref);
}
}
private boolean currentInputMethodSupportsHandwriting() {

View File

@@ -22,7 +22,6 @@ import static android.service.usb.UsbPortStatusProto.DATA_ROLE_HOST;
import static android.service.usb.UsbPortStatusProto.DATA_ROLE_NONE;
import static android.service.usb.UsbPortStatusProto.POWER_ROLE_SINK;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbManager;
@@ -32,6 +31,7 @@ import android.net.TetheringManager;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.List;

View File

@@ -23,6 +23,8 @@ import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.flags.Flags;
import com.android.settings.wifi.dpp.WifiDppUtils;
import com.android.settingslib.core.AbstractPreferenceController;
/**
@@ -61,4 +63,16 @@ public abstract class UsbDetailsController extends AbstractPreferenceController
*/
@UiThread
protected abstract void refresh(boolean connected, long functions, int powerRole, int dataRole);
/** Protects given action with an auth challenge. */
protected final void requireAuthAndExecute(Runnable action) {
if (Flags.enableAuthChallengeForUsbPreferences() && !mFragment.isUserAuthenticated()) {
WifiDppUtils.showLockScreen(mContext, () -> {
mFragment.setUserAuthenticated(true);
action.run();
});
} else {
action.run();
}
}
}

View File

@@ -98,17 +98,19 @@ public class UsbDetailsDataRoleController extends UsbDetailsController
@Override
public void onRadioButtonClicked(SelectorWithWidgetPreference preference) {
int role = UsbBackend.dataRoleFromString(preference.getKey());
if (role != mUsbBackend.getDataRole() && mNextRolePref == null
&& !Utils.isMonkeyRunning()) {
mUsbBackend.setDataRole(role);
mNextRolePref = preference;
preference.setSummary(R.string.usb_switching);
requireAuthAndExecute(() -> {
int role = UsbBackend.dataRoleFromString(preference.getKey());
if (role != mUsbBackend.getDataRole() && mNextRolePref == null
&& !Utils.isMonkeyRunning()) {
mUsbBackend.setDataRole(role);
mNextRolePref = preference;
preference.setSummary(R.string.usb_switching);
mHandler.postDelayed(mFailureCallback,
mUsbBackend.areAllRolesSupported() ? UsbBackend.PD_ROLE_SWAP_TIMEOUT_MS
: UsbBackend.NONPD_ROLE_SWAP_TIMEOUT_MS);
}
mHandler.postDelayed(mFailureCallback,
mUsbBackend.areAllRolesSupported() ? UsbBackend.PD_ROLE_SWAP_TIMEOUT_MS
: UsbBackend.NONPD_ROLE_SWAP_TIMEOUT_MS);
}
});
}
@Override

View File

@@ -45,6 +45,7 @@ public class UsbDetailsFragment extends DashboardFragment {
private List<UsbDetailsController> mControllers;
private UsbBackend mUsbBackend;
private boolean mUserAuthenticated = false;
@VisibleForTesting
UsbConnectionBroadcastReceiver mUsbReceiver;
@@ -56,6 +57,20 @@ public class UsbDetailsFragment extends DashboardFragment {
}
};
boolean isUserAuthenticated() {
return mUserAuthenticated;
}
void setUserAuthenticated(boolean userAuthenticated) {
mUserAuthenticated = userAuthenticated;
}
@Override
public void onStart() {
super.onStart();
mUserAuthenticated = false;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

View File

@@ -130,37 +130,39 @@ public class UsbDetailsFunctionsController extends UsbDetailsController
@Override
public void onRadioButtonClicked(SelectorWithWidgetPreference preference) {
final long function = UsbBackend.usbFunctionsFromString(preference.getKey());
final long previousFunction = mUsbBackend.getCurrentFunctions();
if (DEBUG) {
Log.d(TAG, "onRadioButtonClicked() function : " + function + ", toString() : "
+ UsbManager.usbFunctionsToString(function) + ", previousFunction : "
+ previousFunction + ", toString() : "
+ UsbManager.usbFunctionsToString(previousFunction));
}
if (function != previousFunction && !Utils.isMonkeyRunning()
&& !isClickEventIgnored(function, previousFunction)) {
mPreviousFunction = previousFunction;
//Update the UI in advance to make it looks smooth
final SelectorWithWidgetPreference prevPref =
(SelectorWithWidgetPreference) mProfilesContainer.findPreference(
UsbBackend.usbFunctionsToString(mPreviousFunction));
if (prevPref != null) {
prevPref.setChecked(false);
preference.setChecked(true);
requireAuthAndExecute(() -> {
final long function = UsbBackend.usbFunctionsFromString(preference.getKey());
final long previousFunction = mUsbBackend.getCurrentFunctions();
if (DEBUG) {
Log.d(TAG, "onRadioButtonClicked() function : " + function + ", toString() : "
+ UsbManager.usbFunctionsToString(function) + ", previousFunction : "
+ previousFunction + ", toString() : "
+ UsbManager.usbFunctionsToString(previousFunction));
}
if (function != previousFunction && !Utils.isMonkeyRunning()
&& !isClickEventIgnored(function, previousFunction)) {
mPreviousFunction = previousFunction;
if (function == UsbManager.FUNCTION_RNDIS || function == UsbManager.FUNCTION_NCM) {
// We need to have entitlement check for usb tethering, so use API in
// TetheringManager.
mTetheringManager.startTethering(
TetheringManager.TETHERING_USB, new HandlerExecutor(mHandler),
mOnStartTetheringCallback);
} else {
mUsbBackend.setCurrentFunctions(function);
//Update the UI in advance to make it looks smooth
final SelectorWithWidgetPreference prevPref =
(SelectorWithWidgetPreference) mProfilesContainer.findPreference(
UsbBackend.usbFunctionsToString(mPreviousFunction));
if (prevPref != null) {
prevPref.setChecked(false);
preference.setChecked(true);
}
if (function == UsbManager.FUNCTION_RNDIS || function == UsbManager.FUNCTION_NCM) {
// We need to have entitlement check for usb tethering, so use API in
// TetheringManager.
mTetheringManager.startTethering(
TetheringManager.TETHERING_USB, new HandlerExecutor(mHandler),
mOnStartTetheringCallback);
} else {
mUsbBackend.setCurrentFunctions(function);
}
}
}
});
}
private boolean isClickEventIgnored(long function, long previousFunction) {

View File

@@ -78,13 +78,15 @@ public class UsbDetailsTranscodeMtpController extends UsbDetailsController
@Override
public boolean onPreferenceClick(Preference preference) {
SystemProperties.set(TRANSCODE_MTP_SYS_PROP_KEY,
Boolean.toString(mSwitchPreference.isChecked()));
requireAuthAndExecute(() -> {
SystemProperties.set(TRANSCODE_MTP_SYS_PROP_KEY,
Boolean.toString(mSwitchPreference.isChecked()));
final long previousFunctions = mUsbBackend.getCurrentFunctions();
// Toggle the MTP connection to reload file sizes for files shared via MTP clients
mUsbBackend.setCurrentFunctions(previousFunctions & ~UsbManager.FUNCTION_MTP);
mUsbBackend.setCurrentFunctions(previousFunctions);
final long previousFunctions = mUsbBackend.getCurrentFunctions();
// Toggle the MTP connection to reload file sizes for files shared via MTP clients
mUsbBackend.setCurrentFunctions(previousFunctions & ~UsbManager.FUNCTION_MTP);
mUsbBackend.setCurrentFunctions(previousFunctions);
});
return true;
}