Merge 25Q1 (ab/12770256) to aosp-main-future

Bug: 385190204
Merged-In: Iaee6792d1a27be8fa4b443f783a47a3715b6d3a1
Change-Id: I0ac29cecfec526a38cf4a120b8ef704ee7bc01b3
This commit is contained in:
Xin Li
2025-02-26 11:59:26 -08:00
1291 changed files with 58828 additions and 22728 deletions

View File

@@ -132,7 +132,12 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
@Override
public void onSourceAdded(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(TAG, "onSourceAdded: update media device list.");
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.forceUpdate();
}
}
@Override
public void onSourceAddFailed(
@@ -165,21 +170,14 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
public void onReceiveStateChanged(
@NonNull BluetoothDevice sink,
int sourceId,
@NonNull BluetoothLeBroadcastReceiveState state) {
if (BluetoothUtils.isConnected(state)) {
Log.d(TAG, "onReceiveStateChanged: synced, update media device list.");
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.forceUpdate();
}
}
}
@NonNull BluetoothLeBroadcastReceiveState state) {}
};
public AvailableMediaDeviceGroupController(Context context) {
super(context, KEY);
mBtManager = Utils.getLocalBtManager(mContext);
mExecutor = Executors.newSingleThreadExecutor();
if (BluetoothUtils.isAudioSharingEnabled()) {
if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
mBroadcast =
mBtManager == null
? null
@@ -200,7 +198,7 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
Log.d(TAG, "onStart() Bluetooth is not supported on this device");
return;
}
if (BluetoothUtils.isAudioSharingEnabled()) {
if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
registerAudioSharingCallbacks();
}
mBtManager.getEventManager().registerCallback(this);
@@ -216,7 +214,7 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
Log.d(TAG, "onStop() Bluetooth is not supported on this device");
return;
}
if (BluetoothUtils.isAudioSharingEnabled()) {
if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
unregisterAudioSharingCallbacks();
}
if (mBluetoothDeviceUpdater != null) {
@@ -278,7 +276,7 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
public void onDeviceClick(Preference preference) {
final CachedBluetoothDevice cachedDevice =
((BluetoothDevicePreference) preference).getBluetoothDevice();
if (BluetoothUtils.isAudioSharingEnabled() && mDialogHandler != null) {
if (BluetoothUtils.isAudioSharingUIAvailable(mContext) && mDialogHandler != null) {
mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true);
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.action(mContext, SettingsEnums.ACTION_MEDIA_DEVICE_CLICK);
@@ -294,7 +292,7 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
fragment.getContext(),
AvailableMediaDeviceGroupController.this,
fragment.getMetricsCategory());
if (BluetoothUtils.isAudioSharingEnabled()) {
if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
}
}
@@ -341,7 +339,7 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
if (isAudioModeOngoingCall(mContext)) {
// in phone call
titleResId = R.string.connected_device_call_device_title;
} else if (BluetoothUtils.isAudioSharingEnabled()
} else if (BluetoothUtils.isAudioSharingUIAvailable(mContext)
&& BluetoothUtils.isBroadcasting(mBtManager)) {
// without phone call, in audio sharing
titleResId = R.string.audio_sharing_media_device_group_title;

View File

@@ -21,6 +21,8 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
@@ -120,4 +122,9 @@ public class BluetoothDashboardFragment extends DashboardFragment {
*/
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.bluetooth_screen);
@Override
public @Nullable String getPreferenceScreenBindingKey(@NonNull Context context) {
return BluetoothDashboardScreen.KEY;
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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
import android.content.Context
import com.android.settings.R
import com.android.settings.flags.Flags
import com.android.settingslib.metadata.ProvidePreferenceScreen
import com.android.settingslib.metadata.preferenceHierarchy
import com.android.settingslib.preference.PreferenceScreenCreator
@ProvidePreferenceScreen
class BluetoothDashboardScreen : PreferenceScreenCreator {
override val key: String
get() = KEY
override val title: Int
get() = R.string.bluetooth_settings_title
override val icon: Int
get() = R.drawable.ic_settings_bluetooth
override fun isFlagEnabled(context: Context) = Flags.catalystBluetoothSwitchbarScreen()
override fun hasCompleteHierarchy() = false
override fun fragmentClass() = BluetoothDashboardFragment::class.java
override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {}
companion object {
const val KEY = "bluetooth_switchbar_screen"
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.android.settings.R
import com.android.settings.widget.MainSwitchBarMetadata
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.NoOpKeyedObservable
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.metadata.PreferenceLifecycleProvider
import com.android.settingslib.metadata.ReadWritePermit
class BluetoothMainSwitchPreference(private val bluetoothAdapter: BluetoothAdapter?) :
MainSwitchBarMetadata, PreferenceLifecycleProvider {
private lateinit var broadcastReceiver: BroadcastReceiver
override val key
get() = "use_bluetooth"
override val title
get() = R.string.bluetooth_main_switch_title
override fun getReadPermit(context: Context, myUid: Int, callingUid: Int) =
ReadWritePermit.ALLOW
override fun getWritePermit(context: Context, value: Boolean?, myUid: Int, callingUid: Int) =
ReadWritePermit.ALLOW
override fun storage(context: Context) = BluetoothStateStore(bluetoothAdapter)
override fun onStart(context: PreferenceLifecycleContext) {
broadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(receiverContext: Context, intent: Intent) {
context.notifyPreferenceChange(key)
}
}
context.registerReceiver(
broadcastReceiver,
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED),
Context.RECEIVER_EXPORTED_UNAUDITED
)
}
override fun onStop(context: PreferenceLifecycleContext) {
if (::broadcastReceiver.isInitialized) {
context.unregisterReceiver(broadcastReceiver)
}
}
override fun isEnabled(context: Context): Boolean {
return bluetoothAdapter?.state.let {
it == BluetoothAdapter.STATE_ON || it == BluetoothAdapter.STATE_OFF
}
}
@Suppress("UNCHECKED_CAST")
class BluetoothStateStore(private val bluetoothAdapter: BluetoothAdapter?) :
NoOpKeyedObservable<String>(), KeyValueStore {
override fun contains(key: String) = true
override fun <T : Any> getValue(key: String, valueType: Class<T>): T? {
return (bluetoothAdapter?.state.let {
it == BluetoothAdapter.STATE_ON || it == BluetoothAdapter.STATE_TURNING_ON
}) as T
}
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
if (value is Boolean) {
if (value) {
bluetoothAdapter?.enable()
} else {
bluetoothAdapter?.disable()
}
}
}
}
}

View File

@@ -80,7 +80,7 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
+ ", action : "
+ action);
}
if (BluetoothUtils.isAudioSharingEnabled()) {
if (BluetoothUtils.isAudioSharingUIAvailable(context)) {
use(AudioSharingDevicePreferenceController.class).init(this);
}
use(AvailableMediaDeviceGroupController.class).init(this);

View File

@@ -54,10 +54,8 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final int MAX_DEVICE_NUM = 3;
private static final int DOCK_DEVICE_INDEX = 9;
private static final String KEY_SEE_ALL = "previously_connected_devices_see_all";
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;
@@ -118,6 +116,8 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
mContext.registerReceiver(mReceiver, mIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mBluetoothDeviceUpdater.refreshPreference();
Log.d(TAG, "Updating preference group by onStart on thread "
+ Thread.currentThread().getName());
updatePreferenceGroup();
}
@@ -146,55 +146,11 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
if (DEBUG) {
Log.d(TAG, "onDeviceAdded() " + preference.getTitle());
}
Log.d(TAG, "Updating preference group by onDeviceAdded on thread "
+ Thread.currentThread().getName());
updatePreferenceGroup();
}
private void addPreference(int index, Preference preference) {
if (preference instanceof BluetoothDevicePreference) {
if (index >= 0 && mDevicesList.size() >= index) {
mDevicesList.add(index, preference);
} else {
mDevicesList.add(preference);
}
} else {
mDockDevicesList.add(preference);
}
addPreference();
}
private void addPreference() {
mPreferenceGroup.removeAll();
mPreferenceGroup.addPreference(mSeeAllPreference);
final int size = getDeviceListSize();
for (int i = 0; i < size; i++) {
if (DEBUG) {
Log.d(TAG, "addPreference() add device : " + mDevicesList.get(i).getTitle());
}
mDevicesList.get(i).setOrder(i);
mPreferenceGroup.addPreference(mDevicesList.get(i));
}
if (mDockDevicesList.size() > 0) {
for (int i = 0; i < getDockDeviceListSize(MAX_DEVICE_NUM - size); i++) {
if (DEBUG) {
Log.d(TAG, "addPreference() add dock device : "
+ mDockDevicesList.get(i).getTitle());
}
mDockDevicesList.get(i).setOrder(DOCK_DEVICE_INDEX);
mPreferenceGroup.addPreference(mDockDevicesList.get(i));
}
}
}
private int getDeviceListSize() {
return mDevicesList.size() >= MAX_DEVICE_NUM
? MAX_DEVICE_NUM : mDevicesList.size();
}
private int getDockDeviceListSize(int availableSize) {
return mDockDevicesList.size() >= availableSize
? availableSize : mDockDevicesList.size();
}
@Override
public void onDeviceRemoved(Preference preference) {
if (preference instanceof BluetoothDevicePreference) {
@@ -207,37 +163,43 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc
if (DEBUG) {
Log.d(TAG, "onDeviceRemoved() " + preference.getTitle());
}
Log.d(TAG, "Updating preference group by onDeviceRemoved on thread "
+ Thread.currentThread().getName());
updatePreferenceGroup();
}
/** 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) {
mContext.getMainExecutor().execute(() -> {
mPreferenceGroup.removeAll();
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) {
Log.d(TAG, "Adding preference with order " + order + " when there are "
+ mPreferenceGroup.getPreferenceCount());
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;
}
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();
mPreferenceGroup.addPreference(mSeeAllPreference);
updatePreferenceVisibility();
});
}
@VisibleForTesting

View File

@@ -25,7 +25,7 @@ public class AudioSharingActivity extends SettingsActivity {
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (!BluetoothUtils.isAudioSharingEnabled()) {
if (!BluetoothUtils.isAudioSharingUIAvailable(this)) {
finish();
}
}

View File

@@ -55,7 +55,8 @@ public abstract class AudioSharingBasePreferenceController extends BasePreferenc
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
return (BluetoothUtils.isAudioSharingUIAvailable(mContext))
? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
@Override

View File

@@ -16,7 +16,6 @@
package com.android.settings.connecteddevice.audiosharing;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.Log;
@@ -30,6 +29,7 @@ import com.android.settings.connecteddevice.DevicePreferenceCallback;
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 AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater
implements Preference.OnPreferenceClickListener {
@@ -55,7 +55,7 @@ public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater
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 (BluetoothUtils.isAudioSharingEnabled()
if (BluetoothUtils.isAudioSharingUIAvailable(mContext)
&& cachedDevice.isConnectedLeAudioDevice()
&& BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, mLocalBtManager)) {
isFilterMatched = true;
@@ -73,7 +73,9 @@ public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater
@Override
public boolean onPreferenceClick(Preference preference) {
mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory);
mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUDIO_SHARING_DEVICE_CLICK);
var unused =
ThreadUtils.postOnBackgroundThread(
() -> mDevicePreferenceCallback.onDeviceClick(preference));
return true;
}

View File

@@ -65,27 +65,32 @@ public class AudioSharingCallAudioDialogFragment extends InstrumentedDialogFragm
* @param listener The callback to handle the user action on this dialog.
*/
public static void show(
@NonNull Fragment host,
@Nullable Fragment host,
@NonNull List<AudioSharingDeviceItem> deviceItems,
int checkedItemIndex,
@NonNull DialogEventListener listener) {
if (!BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
sListener = listener;
if (manager.findFragmentByTag(TAG) == null) {
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
bundle.putInt(BUNDLE_KEY_CHECKED_ITEM_INDEX, checkedItemIndex);
final AudioSharingCallAudioDialogFragment dialog =
new AudioSharingCallAudioDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, TAG);
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
}
sListener = listener;
if (manager.findFragmentByTag(TAG) == null) {
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
bundle.putInt(BUNDLE_KEY_CHECKED_ITEM_INDEX, checkedItemIndex);
final AudioSharingCallAudioDialogFragment dialog =
new AudioSharingCallAudioDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, TAG);
}
}
}

View File

@@ -31,6 +31,7 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -109,7 +110,10 @@ public class AudioSharingCallAudioPreferenceController extends AudioSharingBaseP
@Override
public void onSourceAdded(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(TAG, "onSourceAdded: updateSummary");
updateSummary();
}
@Override
public void onSourceAddFailed(
@@ -137,12 +141,7 @@ public class AudioSharingCallAudioPreferenceController extends AudioSharingBaseP
public void onReceiveStateChanged(
@NonNull BluetoothDevice sink,
int sourceId,
@NonNull BluetoothLeBroadcastReceiveState state) {
if (BluetoothUtils.isConnected(state)) {
Log.d(TAG, "onReceiveStateChanged: synced, updateSummary");
updateSummary();
}
}
@NonNull BluetoothLeBroadcastReceiveState state) {}
};
public AudioSharingCallAudioPreferenceController(Context context) {
@@ -195,40 +194,33 @@ public class AudioSharingCallAudioPreferenceController extends AudioSharingBaseP
}
updateDeviceItemsInSharingSession();
if (!mDeviceItemsInSharingSession.isEmpty()) {
int checkedItemIndex = getActiveItemIndex(mDeviceItemsInSharingSession);
Pair<Integer, AudioSharingDeviceItem> pair = getActiveItemWithIndex();
AudioSharingCallAudioDialogFragment.show(
mFragment,
mDeviceItemsInSharingSession,
checkedItemIndex,
pair == null ? -1 : pair.first,
(AudioSharingDeviceItem item) -> {
int currentGroupId =
BluetoothUtils.getPrimaryGroupIdForBroadcast(
mContext.getContentResolver());
if (item.getGroupId() == currentGroupId) {
Log.d(
TAG,
"Skip set fallback active device: unchanged");
int clickedGroupId = item.getGroupId();
if (clickedGroupId == currentGroupId) {
Log.d(TAG, "Skip set call audio device: unchanged");
return;
}
List<BluetoothDevice> devices =
mGroupedConnectedDevices.getOrDefault(
item.getGroupId(), ImmutableList.of());
clickedGroupId, ImmutableList.of());
CachedBluetoothDevice lead =
AudioSharingUtils.getLeadDevice(
mCacheManager, devices);
if (lead != null) {
Log.d(
TAG,
"Set fallback active device: "
+ lead.getDevice()
.getAnonymizedAddress());
lead.setActive();
String addr = lead.getDevice().getAnonymizedAddress();
Log.d(TAG, "Set call audio device: " + addr);
AudioSharingUtils.setPrimary(mContext, lead);
logCallAudioDeviceChange(currentGroupId, lead);
} else {
Log.d(
TAG,
"Fail to set fallback active device: no"
+ " lead device");
Log.d(TAG, "Skip set call audio device: no lead");
}
});
}
@@ -263,6 +255,18 @@ public class AudioSharingCallAudioPreferenceController extends AudioSharingBaseP
}
}
@Override
public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice,
int bluetoothProfile) {
if (activeDevice != null && bluetoothProfile == BluetoothProfile.LE_AUDIO
&& BluetoothUtils.isBroadcasting(mBtManager)) {
Log.d(TAG, "onActiveDeviceChanged: update summary, device = "
+ activeDevice.getDevice().getAnonymizedAddress()
+ ", profile = " + bluetoothProfile);
updateSummary();
}
}
/**
* Initialize the controller.
*
@@ -348,30 +352,22 @@ public class AudioSharingCallAudioPreferenceController extends AudioSharingBaseP
*/
private void updateSummary() {
updateDeviceItemsInSharingSession();
int fallbackActiveGroupId =
BluetoothUtils.getPrimaryGroupIdForBroadcast(mContext.getContentResolver());
if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
for (AudioSharingDeviceItem item : mDeviceItemsInSharingSession) {
if (item.getGroupId() == fallbackActiveGroupId) {
Log.d(
TAG,
"updatePreference: set summary to fallback group "
+ fallbackActiveGroupId);
AudioSharingUtils.postOnMainThread(
mContext,
() -> {
if (mPreference != null) {
mPreference.setSummary(
mContext.getString(
R.string.audio_sharing_call_audio_description,
item.getName()));
}
});
return;
}
}
Pair<Integer, AudioSharingDeviceItem> pair = getActiveItemWithIndex();
if (pair != null) {
Log.d(TAG, "updateSummary, group = " + pair.second.getGroupId());
AudioSharingUtils.postOnMainThread(
mContext,
() -> {
if (mPreference != null) {
mPreference.setSummary(
mContext.getString(
R.string.audio_sharing_call_audio_description,
pair.second.getName()));
}
});
return;
}
Log.d(TAG, "updatePreference: set empty summary");
Log.d(TAG, "updateSummary: set empty");
AudioSharingUtils.postOnMainThread(
mContext,
() -> {
@@ -388,16 +384,26 @@ public class AudioSharingCallAudioPreferenceController extends AudioSharingBaseP
mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ true);
}
private int getActiveItemIndex(List<AudioSharingDeviceItem> deviceItems) {
int checkedItemIndex = -1;
@Nullable
private Pair<Integer, AudioSharingDeviceItem> getActiveItemWithIndex() {
List<AudioSharingDeviceItem> deviceItems = new ArrayList<>(mDeviceItemsInSharingSession);
int fallbackActiveGroupId =
BluetoothUtils.getPrimaryGroupIdForBroadcast(mContext.getContentResolver());
for (AudioSharingDeviceItem item : deviceItems) {
if (item.getGroupId() == fallbackActiveGroupId) {
return deviceItems.indexOf(item);
if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
for (AudioSharingDeviceItem item : deviceItems) {
if (item.getGroupId() == fallbackActiveGroupId) {
Log.d(TAG, "getActiveItemWithIndex, fallback group = " + item.getGroupId());
return new Pair<>(deviceItems.indexOf(item), item);
}
}
}
return checkedItemIndex;
for (AudioSharingDeviceItem item : deviceItems) {
if (item.isActive()) {
Log.d(TAG, "getActiveItemWithIndex, active LEA group = " + item.getGroupId());
return new Pair<>(deviceItems.indexOf(item), item);
}
}
return null;
}
@VisibleForTesting

View File

@@ -155,7 +155,8 @@ public class AudioSharingCompatibilityPreferenceController extends TogglePrefere
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override

View File

@@ -44,23 +44,28 @@ public class AudioSharingConfirmDialogFragment extends InstrumentedDialogFragmen
*
* @param host The Fragment this dialog will be hosted.
*/
public static void show(Fragment host) {
if (!BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
public static void show(@Nullable Fragment host) {
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
}
Log.d(TAG, "Show up the confirm dialog.");
AudioSharingConfirmDialogFragment dialogFrag = new AudioSharingConfirmDialogFragment();
dialogFrag.show(manager, TAG);
}
Log.d(TAG, "Show up the confirm dialog.");
AudioSharingConfirmDialogFragment dialogFrag = new AudioSharingConfirmDialogFragment();
dialogFrag.show(manager, TAG);
}
@Override
@@ -72,7 +77,7 @@ public class AudioSharingConfirmDialogFragment extends InstrumentedDialogFragmen
.setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
.setIsCustomBodyEnabled(true)
.setCustomMessage(R.string.audio_sharing_comfirm_dialog_content)
.setPositiveButton(com.android.settings.R.string.okay, (d, w) -> {})
.setPositiveButton(R.string.audio_sharing_close_button_label, (d, w) -> {})
.build();
dialog.setCanceledOnTouchOutside(true);
return dialog;

View File

@@ -45,6 +45,7 @@ public class AudioSharingDashboardFragment extends DashboardFragment
public static final int SHARE_THEN_PAIR_REQUEST_CODE = 1002;
SettingsMainSwitchBar mMainSwitchBar;
private Context mContext;
private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController;
private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
@@ -78,6 +79,7 @@ public class AudioSharingDashboardFragment extends DashboardFragment
@Override
public void onAttach(Context context) {
super.onAttach(context);
mContext = context;
mAudioSharingDeviceVolumeGroupController =
use(AudioSharingDeviceVolumeGroupController.class);
mAudioSharingDeviceVolumeGroupController.init(this);
@@ -107,23 +109,25 @@ public class AudioSharingDashboardFragment extends DashboardFragment
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (!BluetoothUtils.isAudioSharingEnabled()) return;
// In share then pair flow, after users be routed to pair new device page and successfully
// pair and connect an LEA headset, the pair fragment will be finished with RESULT_OK
// and EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, pass the BT device to switch bar controller,
// which is responsible for adding source to the device with loading indicator.
if (requestCode == SHARE_THEN_PAIR_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
BluetoothDevice btDevice =
data != null
? data.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE,
BluetoothDevice.class)
: null;
Log.d(TAG, "onActivityResult: RESULT_OK with device = " + btDevice);
if (btDevice != null) {
var unused = ThreadUtils.postOnBackgroundThread(
() -> mAudioSharingSwitchBarController.handleAutoAddSourceAfterPair(
btDevice));
if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
// In share then pair flow, after users be routed to pair new device page and
// successfully pair and connect an LEA headset, the pair fragment will be finished with
// RESULT_OK and EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, pass the BT device to switch bar
// controller, which is responsible for adding source to the device with loading
// indicator.
if (requestCode == SHARE_THEN_PAIR_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
BluetoothDevice btDevice =
data != null
? data.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE,
BluetoothDevice.class)
: null;
Log.d(TAG, "onActivityResult: RESULT_OK with device = " + btDevice);
if (btDevice != null) {
var unused = ThreadUtils.postOnBackgroundThread(
() -> mAudioSharingSwitchBarController.handleAutoAddSourceAfterPair(
btDevice));
}
}
}
}

View File

@@ -85,6 +85,10 @@ public class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView
mButtonView.setText(btnText);
mButtonView.setOnClickListener(
v -> mOnClickListener.onClick(mDevices.get(position)));
if (position == 0) {
mButtonView.setBackgroundResource(
com.android.settingslib.R.drawable.audio_sharing_rounded_bg_ripple_top);
}
} else {
Log.w(TAG, "bind view skipped due to button view is null");
}

View File

@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public final class AudioSharingDeviceItem implements Parcelable {
private final String mName;
private final int mGroupId;
@@ -72,4 +74,10 @@ public final class AudioSharingDeviceItem implements Parcelable {
return new AudioSharingDeviceItem[size];
}
};
@Override
@NonNull
public String toString() {
return "AudioSharingDeviceItem groupId = " + mGroupId + ", isActive = " + mIsActive;
}
}

View File

@@ -16,6 +16,7 @@
package com.android.settings.connecteddevice.audiosharing;
import static com.android.settingslib.Utils.isAudioModeOngoingCall;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
import android.app.settings.SettingsEnums;
@@ -39,7 +40,9 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.bluetooth.BluetoothDevicePreference;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
@@ -91,6 +94,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
@Nullable private DashboardFragment mFragment;
@Nullable private AudioSharingDialogHandler mDialogHandler;
private AtomicBoolean mIntentHandled = new AtomicBoolean(false);
private AtomicBoolean mIsAudioModeOngoingCall = new AtomicBoolean(false);
@VisibleForTesting
BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
@@ -112,7 +116,18 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
@Override
public void onSourceAdded(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(TAG, "onSourceAdded: update sharing device list.");
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.forceUpdate();
}
if (mDeviceManager != null && mDialogHandler != null) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink);
if (cachedDevice != null) {
mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
}
}
}
@Override
public void onSourceAddFailed(
@@ -169,20 +184,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
public void onReceiveStateChanged(
@NonNull BluetoothDevice sink,
int sourceId,
@NonNull BluetoothLeBroadcastReceiveState state) {
if (BluetoothUtils.isConnected(state)) {
Log.d(TAG, "onSourceAdded: update sharing device list.");
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.forceUpdate();
}
if (mDeviceManager != null && mDialogHandler != null) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink);
if (cachedDevice != null) {
mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
}
}
}
}
@NonNull BluetoothLeBroadcastReceiveState state) {}
};
public AudioSharingDevicePreferenceController(Context context) {
@@ -201,51 +203,57 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
@Override
public void onStart(@NonNull LifecycleOwner owner) {
if (!isAvailable()) {
Log.d(TAG, "Skip onStart(), feature is not supported.");
return;
}
if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)
&& mProfileManager != null) {
Log.d(TAG, "Register profile service listener");
mProfileManager.addServiceListener(this);
}
if (mEventManager == null
|| mAssistant == null
|| mDialogHandler == null
|| mBluetoothDeviceUpdater == null) {
Log.d(TAG, "Skip onStart(), profile is not ready.");
return;
}
Log.d(TAG, "onStart() Register callbacks.");
mEventManager.registerCallback(this);
mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mDialogHandler.registerCallbacks(mExecutor);
mBluetoothDeviceUpdater.registerCallback();
mBluetoothDeviceUpdater.refreshPreference();
var unused = ThreadUtils.postOnBackgroundThread(() -> {
if (!isAvailable()) {
Log.d(TAG, "Skip onStart(), feature is not supported.");
return;
}
if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)
&& mProfileManager != null) {
Log.d(TAG, "Register profile service listener");
mProfileManager.addServiceListener(this);
}
if (mEventManager == null
|| mAssistant == null
|| mDialogHandler == null
|| mBluetoothDeviceUpdater == null) {
Log.d(TAG, "Skip onStart(), profile is not ready.");
return;
}
Log.d(TAG, "onStart() Register callbacks.");
mEventManager.registerCallback(this);
mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mDialogHandler.registerCallbacks(mExecutor);
mBluetoothDeviceUpdater.registerCallback();
mBluetoothDeviceUpdater.refreshPreference();
mIsAudioModeOngoingCall.set(isAudioModeOngoingCall(mContext));
updateTitle();
});
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
if (!isAvailable()) {
Log.d(TAG, "Skip onStop(), feature is not supported.");
return;
}
if (mProfileManager != null) {
mProfileManager.removeServiceListener(this);
}
if (mEventManager == null
|| mAssistant == null
|| mDialogHandler == null
|| mBluetoothDeviceUpdater == null) {
Log.d(TAG, "Skip onStop(), profile is not ready.");
return;
}
Log.d(TAG, "onStop() Unregister callbacks.");
mEventManager.unregisterCallback(this);
mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
mDialogHandler.unregisterCallbacks();
mBluetoothDeviceUpdater.unregisterCallback();
var unused = ThreadUtils.postOnBackgroundThread(() -> {
if (!isAvailable()) {
Log.d(TAG, "Skip onStop(), feature is not supported.");
return;
}
if (mProfileManager != null) {
mProfileManager.removeServiceListener(this);
}
if (mEventManager == null
|| mAssistant == null
|| mDialogHandler == null
|| mBluetoothDeviceUpdater == null) {
Log.d(TAG, "Skip onStop(), profile is not ready.");
return;
}
Log.d(TAG, "onStop() Unregister callbacks.");
mEventManager.unregisterCallback(this);
mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
mDialogHandler.unregisterCallbacks();
mBluetoothDeviceUpdater.unregisterCallback();
});
}
@Override
@@ -298,7 +306,8 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() && mBluetoothDeviceUpdater != null
return (BluetoothUtils.isAudioSharingUIAvailable(mContext)
&& mBluetoothDeviceUpdater != null)
? AVAILABLE_UNSEARCHABLE
: UNSUPPORTED_ON_DEVICE;
}
@@ -367,6 +376,25 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
handleOnProfileStateChanged(cachedDevice, bluetoothProfile);
}
@Override
public void onAudioModeChanged() {
mIsAudioModeOngoingCall.set(isAudioModeOngoingCall(mContext));
updateTitle();
}
@Override
public void onDeviceClick(@NonNull Preference preference) {
boolean isCallMode = mIsAudioModeOngoingCall.get();
if (isCallMode) {
Log.d(TAG, "onDeviceClick, set active in call mode");
CachedBluetoothDevice cachedDevice =
((BluetoothDevicePreference) preference).getBluetoothDevice();
AudioSharingUtils.setPrimary(mContext, cachedDevice);
}
mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUDIO_SHARING_DEVICE_CLICK,
isCallMode);
}
/**
* Initialize the controller.
*
@@ -499,4 +527,22 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true);
}
}
private void updateTitle() {
if (mPreferenceGroup == null) return;
int titleResId;
if (mIsAudioModeOngoingCall.get()) {
// in phone call
titleResId = R.string.connected_device_call_device_title;
} else {
// without phone call
titleResId = R.string.audio_sharing_device_group_title;
}
AudioSharingUtils.postOnMainThread(mContext,
() -> {
if (mPreferenceGroup != null) {
mPreferenceGroup.setTitle(titleResId);
}
});
}
}

View File

@@ -132,7 +132,12 @@ public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePre
@Override
public void onSourceAdded(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@NonNull BluetoothDevice sink, int sourceId, int reason) {
Log.d(TAG, "onSourceAdded: update volume list.");
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.forceUpdate();
}
}
@Override
public void onSourceAddFailed(
@@ -165,14 +170,7 @@ public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePre
public void onReceiveStateChanged(
@NonNull BluetoothDevice sink,
int sourceId,
@NonNull BluetoothLeBroadcastReceiveState state) {
if (BluetoothUtils.isConnected(state)) {
Log.d(TAG, "onReceiveStateChanged: synced, update volume list.");
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.forceUpdate();
}
}
}
@NonNull BluetoothLeBroadcastReceiveState state) {}
};
public AudioSharingDeviceVolumeGroupController(Context context) {

View File

@@ -31,6 +31,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.bluetooth.BluetoothPairingDetail;
@@ -83,32 +84,42 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
* @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@Nullable Fragment host,
@NonNull List<AudioSharingDeviceItem> deviceItems,
@NonNull DialogEventListener listener,
@NonNull Pair<Integer, Object>[] eventData) {
if (!BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
sHost = host;
sListener = listener;
sEventData = eventData;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return;
}
sHost = host;
sListener = listener;
sEventData = eventData;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
}
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
AudioSharingDialogFragment dialogFrag = new AudioSharingDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
AudioSharingDialogFragment dialogFrag = new AudioSharingDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
/** Return the tag of {@link AudioSharingDialogFragment} dialog. */

View File

@@ -192,7 +192,7 @@ public class AudioSharingDialogHandler {
// If this method is called with user triggered, e.g. manual click on the
// "Connected devices" page, we need call setActive for the device, since user
// intend to switch active device for the call.
cachedDevice.setActive();
AudioSharingUtils.setPrimary(mContext, cachedDevice);
}
return;
}

View File

@@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
@@ -79,67 +80,67 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
* @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@Nullable Fragment host,
@NonNull List<AudioSharingDeviceItem> deviceItems,
@NonNull CachedBluetoothDevice newDevice,
@NonNull DialogEventListener listener,
@NonNull Pair<Integer, Object>[] eventData) {
if (!BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
int newGroupId = BluetoothUtils.getGroupId(newDevice);
if (sNewDevice != null && newGroupId == BluetoothUtils.getGroupId(sNewDevice)) {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for the same device group %d, "
+ "update the content.",
newGroupId));
sListener = listener;
sNewDevice = newDevice;
sEventData = eventData;
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
} else {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for new device group %d, "
+ "dismiss current dialog.",
newGroupId));
dialog.dismiss();
var unused =
ThreadUtils.postOnBackgroundThread(
() ->
FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(
dialog.getContext(),
SettingsEnums
.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
SettingsEnums
.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
int newGroupId = BluetoothUtils.getGroupId(newDevice);
if (sNewDevice != null && newGroupId == BluetoothUtils.getGroupId(sNewDevice)) {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for the same device group %d, "
+ "update the content.",
newGroupId));
sListener = listener;
sNewDevice = newDevice;
sEventData = eventData;
return;
} else {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for new device group %d, "
+ "dismiss current dialog.",
newGroupId));
dialog.dismiss();
logDialogAutoDismiss(dialog);
}
}
sListener = listener;
sNewDevice = newDevice;
sEventData = eventData;
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
AudioSharingDisconnectDialogFragment dialogFrag =
new AudioSharingDisconnectDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
sListener = listener;
sNewDevice = newDevice;
sEventData = eventData;
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
AudioSharingDisconnectDialogFragment dialogFrag =
new AudioSharingDisconnectDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
/** Return the tag of {@link AudioSharingDisconnectDialogFragment} dialog. */
@@ -210,4 +211,17 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
AudioSharingDeviceAdapter.ActionType.REMOVE));
return builder.build();
}
private static void logDialogAutoDismiss(AlertDialog dialog) {
var unused =
ThreadUtils.postOnBackgroundThread(
() -> FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(
dialog.getContext(),
SettingsEnums
.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
SettingsEnums
.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
}
}

View File

@@ -25,7 +25,9 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.bluetooth.BluetoothUtils;
@@ -44,36 +46,44 @@ public class AudioSharingErrorDialogFragment extends InstrumentedDialogFragment
* @param host The Fragment this dialog will be hosted.
*/
public static void show(@Nullable Fragment host) {
if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
}
Log.d(TAG, "Show up the error dialog.");
AudioSharingErrorDialogFragment dialogFrag = new AudioSharingErrorDialogFragment();
dialogFrag.show(manager, TAG);
}
Log.d(TAG, "Show up the error dialog.");
AudioSharingErrorDialogFragment dialogFrag = new AudioSharingErrorDialogFragment();
dialogFrag.show(manager, TAG);
}
@Override
@NonNull
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
// TODO: put strings to res till they are finalized
AlertDialog dialog =
AudioSharingDialogFactory.newBuilder(getActivity())
.setTitle("Couldn't share audio")
.setTitleIcon(com.android.settings.R.drawable.ic_warning_24dp)
.setTitle(R.string.audio_sharing_retry_dialog_title)
.setTitleIcon(R.drawable.ic_warning_24dp)
.setIsCustomBodyEnabled(true)
.setCustomMessage("Something went wrong. Please try again.")
.setPositiveButton(com.android.settings.R.string.okay, (d, w) -> {
})
.setCustomMessage(R.string.audio_sharing_retry_dialog_content)
.setPositiveButton(R.string.okay, (d, w) -> {})
.build();
dialog.setCanceledOnTouchOutside(true);
return dialog;

View File

@@ -26,7 +26,9 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.bluetooth.BluetoothUtils;
@@ -60,27 +62,37 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr
*/
public static void show(@Nullable Fragment host, @NonNull String deviceName,
@NonNull DialogEventListener listener) {
if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
sListener = listener;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return;
}
sListener = listener;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
return;
}
Log.d(TAG, "Show up the incompatible device dialog.");
final Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_DEVICE_NAME, deviceName);
AudioSharingIncompatibleDialogFragment dialogFrag =
new AudioSharingIncompatibleDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
Log.d(TAG, "Show up the incompatible device dialog.");
final Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_DEVICE_NAME, deviceName);
AudioSharingIncompatibleDialogFragment dialogFrag =
new AudioSharingIncompatibleDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
@Override
@@ -88,15 +100,14 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Bundle arguments = requireArguments();
String deviceName = arguments.getString(BUNDLE_KEY_DEVICE_NAME);
// TODO: move strings to res once they are finalized
AlertDialog dialog =
AudioSharingDialogFactory.newBuilder(getActivity())
.setTitle("Can't share audio with " + deviceName)
.setTitleIcon(com.android.settings.R.drawable.ic_warning_24dp)
.setTitle(getString(R.string.audio_sharing_incompatible_dialog_title,
deviceName))
.setTitleIcon(R.drawable.ic_warning_24dp)
.setIsCustomBodyEnabled(true)
.setCustomMessage(
"Audio sharing only works with headphones that support LE Audio.")
.setPositiveButton(com.android.settings.R.string.okay, (d, w) -> {})
.setCustomMessage(R.string.audio_sharing_incompatible_dialog_content)
.setPositiveButton(R.string.okay, (d, w) -> {})
.build();
dialog.setCanceledOnTouchOutside(true);
return dialog;

View File

@@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
@@ -76,34 +77,45 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
* @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@Nullable Fragment host,
@NonNull List<AudioSharingDeviceItem> deviceItems,
@NonNull CachedBluetoothDevice newDevice,
@NonNull DialogEventListener listener,
@NonNull Pair<Integer, Object>[] eventData) {
if (!BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
sListener = listener;
sNewDevice = newDevice;
sEventData = eventData;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, update the content.");
updateDialog(deviceItems, newDevice.getName(), dialog);
} else {
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
final AudioSharingJoinDialogFragment dialogFrag = new AudioSharingJoinDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return;
}
sListener = listener;
sNewDevice = newDevice;
sEventData = eventData;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, update the content.");
updateDialog(deviceItems, newDevice.getName(), dialog);
} else {
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
final AudioSharingJoinDialogFragment dialogFrag =
new AudioSharingJoinDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
}
}

View File

@@ -174,7 +174,8 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override

View File

@@ -41,6 +41,7 @@ public class AudioSharingPasswordPreference extends ValidatedEditTextPreference
@Nullable private EditText mEditText;
@Nullable private CheckBox mCheckBox;
@Nullable private View mDialogMessage;
@Nullable private View mEditTextFormatAlert;
private boolean mEditable = true;
interface OnDialogEventListener {
@@ -77,6 +78,7 @@ public class AudioSharingPasswordPreference extends ValidatedEditTextPreference
mEditText = view.findViewById(android.R.id.edit);
mCheckBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
mDialogMessage = view.findViewById(android.R.id.message);
mEditTextFormatAlert = view.findViewById(R.id.edit_alert_message);
if (mEditText == null || mCheckBox == null || mDialogMessage == null) {
Log.w(TAG, "onBindDialogView() : Invalid layout");
@@ -123,6 +125,14 @@ public class AudioSharingPasswordPreference extends ValidatedEditTextPreference
mDialogMessage.setVisibility(editable ? GONE : VISIBLE);
}
void showEditTextFormatAlert(boolean show) {
if (mEditTextFormatAlert == null) {
Log.w(TAG, "showEditTextFormatAlert() : Invalid layout");
return;
}
mEditTextFormatAlert.setVisibility(show ? VISIBLE : GONE);
}
void setChecked(boolean checked) {
if (mCheckBox == null) {
Log.w(TAG, "setChecked() : Invalid layout");

View File

@@ -113,7 +113,8 @@ public class AudioSharingPasswordPreferenceController extends BasePreferenceCont
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
@@ -136,7 +137,11 @@ public class AudioSharingPasswordPreferenceController extends BasePreferenceCont
@Override
public boolean isTextValid(String value) {
return mAudioSharingPasswordValidator.isTextValid(value);
boolean isValid = mAudioSharingPasswordValidator.isTextValid(value);
if (mPreference != null) {
mPreference.showEditTextFormatAlert(!isValid);
}
return isValid;
}
@Override

View File

@@ -57,7 +57,7 @@ public class AudioSharingPlaySoundPreferenceController
@Override
public int getAvailabilityStatus() {
return (mRingtone != null && BluetoothUtils.isAudioSharingEnabled())
return (mRingtone != null && BluetoothUtils.isAudioSharingUIAvailable(mContext))
? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}

View File

@@ -135,7 +135,8 @@ public class AudioSharingPreferenceController extends BasePreferenceController
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override

View File

@@ -31,6 +31,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
@@ -64,49 +65,66 @@ public class AudioSharingProgressDialogFragment extends InstrumentedDialogFragme
* @param message The content to be shown on the dialog.
*/
public static void show(@Nullable Fragment host, @NonNull String message) {
if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
if (!sMessage.equals(message)) {
Log.d(TAG, "Update dialog message.");
TextView messageView = dialog.findViewById(R.id.message);
if (messageView != null) {
messageView.setText(message);
}
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
}
Log.d(TAG, "Dialog is showing, return.");
return;
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
if (!sMessage.equals(message)) {
Log.d(TAG, "Update dialog message.");
TextView messageView = dialog.findViewById(R.id.message);
if (messageView != null) {
messageView.setText(message);
}
sMessage = message;
}
Log.d(TAG, "Dialog is showing, return.");
return;
}
sMessage = message;
Log.d(TAG, "Show up the progress dialog.");
Bundle args = new Bundle();
args.putString(BUNDLE_KEY_MESSAGE, message);
AudioSharingProgressDialogFragment dialogFrag =
new AudioSharingProgressDialogFragment();
dialogFrag.setArguments(args);
dialogFrag.show(manager, TAG);
}
sMessage = message;
Log.d(TAG, "Show up the progress dialog.");
Bundle args = new Bundle();
args.putString(BUNDLE_KEY_MESSAGE, message);
AudioSharingProgressDialogFragment dialogFrag = new AudioSharingProgressDialogFragment();
dialogFrag.setArguments(args);
dialogFrag.show(manager, TAG);
}
/** Dismiss the {@link AudioSharingProgressDialogFragment} dialog. */
public static void dismiss(@Nullable Fragment host) {
if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to dismiss dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to dismiss dialog, host is null");
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, dismiss.");
dialog.dismiss();
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to dismiss dialog: " + e.getMessage());
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, dismiss.");
dialog.dismiss();
}
}
}

View File

@@ -38,7 +38,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class AudioSharingReceiver extends BroadcastReceiver {
private static final String TAG = "AudioSharingNotification";
private static final String TAG = "AudioSharingReceiver";
private static final String ACTION_LE_AUDIO_SHARING_SETTINGS =
"com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS";
private static final String ACTION_LE_AUDIO_SHARING_STOP =
@@ -49,10 +49,6 @@ public class AudioSharingReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!BluetoothUtils.isAudioSharingEnabled()) {
Log.w(TAG, "Skip handling received intent, flag is off.");
return;
}
String action = intent.getAction();
if (action == null) {
Log.w(TAG, "Received unexpected intent with null action.");
@@ -66,13 +62,22 @@ public class AudioSharingReceiver extends BroadcastReceiver {
intent.getIntExtra(
LocalBluetoothLeBroadcast.EXTRA_LE_AUDIO_SHARING_STATE, -1);
if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_ON) {
if (!BluetoothUtils.isAudioSharingUIAvailable(context)) {
Log.w(TAG, "Skip showSharingNotification, feature disabled.");
return;
}
showSharingNotification(context);
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_SHOW_AUDIO_SHARING_NOTIFICATION);
} else if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_OFF) {
// TODO: check BluetoothUtils#isAudioSharingEnabled() till BluetoothAdapter#
// isLeAudioBroadcastSourceSupported() and BluetoothAdapter#
// isLeAudioBroadcastAssistantSupported() always return FEATURE_SUPPORTED
// or FEATURE_NOT_SUPPORTED when BT and BLE off
cancelSharingNotification(context);
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION);
context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION,
LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
} else {
Log.w(
TAG,
@@ -80,16 +85,24 @@ public class AudioSharingReceiver extends BroadcastReceiver {
}
break;
case ACTION_LE_AUDIO_SHARING_STOP:
LocalBluetoothManager manager = Utils.getLocalBtManager(context);
if (BluetoothUtils.isBroadcasting(manager)) {
AudioSharingUtils.stopBroadcasting(manager);
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_STOP_AUDIO_SHARING_FROM_NOTIFICATION);
} else {
cancelSharingNotification(context);
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION);
if (BluetoothUtils.isAudioSharingUIAvailable(context)) {
LocalBluetoothManager manager = Utils.getLocalBtManager(context);
if (BluetoothUtils.isBroadcasting(manager)) {
AudioSharingUtils.stopBroadcasting(manager);
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_STOP_AUDIO_SHARING_FROM_NOTIFICATION);
return;
}
}
Log.w(TAG, "cancelSharingNotification, feature disabled or not in broadcast.");
// TODO: check BluetoothUtils#isAudioSharingEnabled() till BluetoothAdapter#
// isLeAudioBroadcastSourceSupported() and BluetoothAdapter#
// isLeAudioBroadcastAssistantSupported() always return FEATURE_SUPPORTED
// or FEATURE_NOT_SUPPORTED when BT and BLE off
cancelSharingNotification(context);
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION,
ACTION_LE_AUDIO_SHARING_STOP);
break;
default:
Log.w(TAG, "Received unexpected intent " + intent.getAction());
@@ -129,15 +142,15 @@ public class AudioSharingReceiver extends BroadcastReceiver {
PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Action stopAction =
new NotificationCompat.Action.Builder(
0,
context.getString(R.string.audio_sharing_stop_button_label),
stopPendingIntent)
0,
context.getString(R.string.audio_sharing_stop_button_label),
stopPendingIntent)
.build();
NotificationCompat.Action settingsAction =
new NotificationCompat.Action.Builder(
0,
context.getString(R.string.audio_sharing_settings_button_label),
settingsPendingIntent)
0,
context.getString(R.string.audio_sharing_settings_button_label),
settingsPendingIntent)
.build();
final Bundle extras = new Bundle();
extras.putString(

View File

@@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
@@ -76,65 +77,66 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
* @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@Nullable Fragment host,
@NonNull List<AudioSharingDeviceItem> deviceItems,
@NonNull CachedBluetoothDevice newDevice,
@NonNull DialogEventListener listener,
@NonNull Pair<Integer, Object>[] eventData) {
if (!BluetoothUtils.isAudioSharingEnabled()) return;
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
if (host == null) {
Log.d(TAG, "Fail to show dialog, host is null");
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
int newGroupId = BluetoothUtils.getGroupId(newDevice);
if (sCachedDevice != null
&& newGroupId == BluetoothUtils.getGroupId(sCachedDevice)) {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for the same device group %d, return.",
newGroupId));
sListener = listener;
sCachedDevice = newDevice;
sEventData = eventData;
if (BluetoothUtils.isAudioSharingUIAvailable(host.getContext())) {
final FragmentManager manager;
try {
manager = host.getChildFragmentManager();
} catch (IllegalStateException e) {
Log.d(TAG, "Fail to show dialog: " + e.getMessage());
return;
} else {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for new device group %d, "
+ "dismiss current dialog.",
newGroupId));
dialog.dismiss();
var unused =
ThreadUtils.postOnBackgroundThread(
() ->
FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(
dialog.getContext(),
SettingsEnums
.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return;
}
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
int newGroupId = BluetoothUtils.getGroupId(newDevice);
if (sCachedDevice != null
&& newGroupId == BluetoothUtils.getGroupId(sCachedDevice)) {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for the same device group %d, return.",
newGroupId));
sListener = listener;
sCachedDevice = newDevice;
sEventData = eventData;
return;
} else {
Log.d(
TAG,
String.format(
Locale.US,
"Dialog is showing for new device group %d, "
+ "dismiss current dialog.",
newGroupId));
dialog.dismiss();
logDialogAutoDismiss(dialog);
}
}
sListener = listener;
sCachedDevice = newDevice;
sEventData = eventData;
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
AudioSharingStopDialogFragment dialogFrag = new AudioSharingStopDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
sListener = listener;
sCachedDevice = newDevice;
sEventData = eventData;
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
AudioSharingStopDialogFragment dialogFrag = new AudioSharingStopDialogFragment();
dialogFrag.setArguments(bundle);
dialogFrag.show(manager, TAG);
}
/** Return the tag of {@link AudioSharingStopDialogFragment} dialog. */
@@ -215,4 +217,16 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
AudioSharingDialogHelper.updateMessageStyle(dialog);
return dialog;
}
private static void logDialogAutoDismiss(AlertDialog dialog) {
var unused =
ThreadUtils.postOnBackgroundThread(
() -> FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(
dialog.getContext(),
SettingsEnums
.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
}
}

View File

@@ -177,6 +177,19 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
+ broadcastId
+ ", metadata = "
+ metadata.getBroadcastName());
if (mAssistant == null
|| mAssistant.getAllConnectedDevices().stream()
.anyMatch(
device -> BluetoothUtils
.hasActiveLocalBroadcastSourceForBtDevice(
device, mBtManager))) {
Log.d(
TAG,
"Skip handleOnBroadcastReady: null assistant or "
+ "sink has active local source.");
return;
}
handleOnBroadcastReady();
}
@Override
@@ -221,20 +234,6 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
+ reason
+ ", broadcastId = "
+ broadcastId);
if (mAssistant == null
|| mAssistant.getAllConnectedDevices().stream()
.anyMatch(
device -> BluetoothUtils
.hasActiveLocalBroadcastSourceForBtDevice(
device, mBtManager))) {
Log.d(
TAG,
"Skip handleOnBroadcastReady: null assistant or "
+ "sink has active local source.");
cleanUpStatesForStartSharing();
return;
}
handleOnBroadcastReady();
}
@Override
@@ -261,7 +260,30 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
@Override
public void onSourceAdded(
@NonNull BluetoothDevice sink, int sourceId, int reason) {}
@NonNull BluetoothDevice sink, int sourceId, int reason) {
if (mSinksInAdding.contains(sink)) {
mSinksInAdding.remove(sink);
}
dismissProgressDialogIfNeeded();
Log.d(TAG, "onSourceAdded(), sink = " + sink + ", remaining sinks = "
+ mSinksInAdding);
if (mSinksToWaitFor.contains(sink)) {
mSinksToWaitFor.remove(sink);
if (mSinksToWaitFor.isEmpty()) {
// To avoid users advance to share then pair flow before the
// primary/active sinks successfully join the audio sharing,
// popup dialog till adding source complete for mSinksToWaitFor.
Pair<Integer, Object>[] eventData =
AudioSharingUtils.buildAudioSharingDialogEventData(
SettingsEnums.AUDIO_SHARING_SETTINGS,
SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
/* userTriggered= */ false,
/* deviceCountInSharing= */ 1,
/* candidateDeviceCount= */ 0);
showAudioSharingDialog(eventData);
}
}
}
@Override
public void onSourceAddFailed(
@@ -307,34 +329,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
@NonNull BluetoothDevice sink,
int sourceId,
@NonNull BluetoothLeBroadcastReceiveState state) {
if (mStoppingSharing.get()) {
Log.d(TAG, "Skip onReceiveStateChanged, stopping broadcast");
return;
}
if (BluetoothUtils.isConnected(state)) {
if (mSinksInAdding.contains(sink)) {
mSinksInAdding.remove(sink);
}
dismissProgressDialogIfNeeded();
Log.d(TAG, "onReceiveStateChanged() connected, sink = " + sink
+ ", remaining sinks = " + mSinksInAdding);
if (mSinksToWaitFor.contains(sink)) {
mSinksToWaitFor.remove(sink);
if (mSinksToWaitFor.isEmpty()) {
// To avoid users advance to share then pair flow before the
// primary/active sinks successfully join the audio sharing,
// popup dialog till adding source complete for mSinksToWaitFor.
Pair<Integer, Object>[] eventData =
AudioSharingUtils.buildAudioSharingDialogEventData(
SettingsEnums.AUDIO_SHARING_SETTINGS,
SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
/* userTriggered= */ false,
/* deviceCountInSharing= */ 1,
/* candidateDeviceCount= */ 0);
showAudioSharingDialog(eventData);
}
}
}
Log.d(TAG,
"onReceiveStateChanged(), sink = " + sink + ", sourceId = " + sourceId
+ ", state = " + state);
}
};
@@ -426,9 +423,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
() -> {
mSwitchBar.setEnabled(true);
mSwitchBar.setChecked(false);
if (mFragment != null) {
AudioSharingConfirmDialogFragment.show(mFragment);
}
AudioSharingConfirmDialogFragment.show(mFragment);
});
return;
}
@@ -447,7 +442,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
@@ -571,10 +567,10 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
if (mBroadcast != null) {
mBroadcast.startPrivateBroadcast();
mSinksInAdding.clear();
// TODO: use string res once finalized.
AudioSharingUtils.postOnMainThread(mContext,
() -> AudioSharingProgressDialogFragment.show(mFragment,
"Starting audio stream..."));
mContext.getString(
R.string.audio_sharing_progress_dialog_start_stream_content)));
mMetricsFeatureProvider.action(
mContext,
SettingsEnums.ACTION_AUDIO_SHARING_MAIN_SWITCH_ON,
@@ -733,13 +729,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
};
AudioSharingUtils.postOnMainThread(
mContext,
() -> {
// Check nullability to pass NullAway check
if (mFragment != null) {
AudioSharingDialogFragment.show(
mFragment, mDeviceItemsForSharing, listener, eventData);
}
});
() -> AudioSharingDialogFragment.show(
mFragment, mDeviceItemsForSharing, listener, eventData));
}
private void showErrorDialog() {
@@ -767,7 +758,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
&& !(fragment instanceof AudioSharingErrorDialogFragment)
&& ((DialogFragment) fragment).getDialog() != null) {
Log.d(TAG, "Remove stale dialog = " + fragment.getTag());
((DialogFragment) fragment).dismiss();
((DialogFragment) fragment).dismissAllowingStateLoss();
}
}
}
@@ -830,8 +821,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
private void addSourceToTargetSinks(List<BluetoothDevice> targetActiveSinks,
@NonNull String sinkName) {
mSinksInAdding.addAll(targetActiveSinks);
// TODO: move to res once finalized
String progressMessage = "Sharing with " + sinkName + "...";
String progressMessage = mContext.getString(
R.string.audio_sharing_progress_dialog_add_source_content, sinkName);
showProgressDialog(progressMessage);
AudioSharingUtils.addSourceToTargetSinks(targetActiveSinks, mBtManager);
}

View File

@@ -21,6 +21,7 @@ import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtil
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID;
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID;
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID;
import static java.util.stream.Collectors.toList;
@@ -28,6 +29,7 @@ import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
import android.provider.Settings;
import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
@@ -219,8 +221,8 @@ public class AudioSharingUtils {
Log.d(TAG, "hasActiveConnectedLeadDevice return false due to null device manager.");
return false;
}
return deviceManager.getCachedDevicesCopy().stream().anyMatch(
BluetoothUtils::isActiveMediaDevice);
return deviceManager.getCachedDevicesCopy().stream()
.anyMatch(BluetoothUtils::isActiveMediaDevice);
}
/** Build {@link AudioSharingDeviceItem} from {@link CachedBluetoothDevice}. */
@@ -344,6 +346,28 @@ public class AudioSharingUtils {
return vc != null && vc.isProfileReady();
}
/** Set {@link CachedBluetoothDevice} as primary device for call audio */
public static void setPrimary(
@NonNull Context context, @Nullable CachedBluetoothDevice cachedDevice) {
if (cachedDevice == null) return;
cachedDevice.setActive();
if (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(context)) {
int groupId = BluetoothUtils.getGroupId(cachedDevice);
// TODO: use real key name in SettingsProvider
int userPreferredId =
Settings.Secure.getInt(
context.getContentResolver(),
BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID,
BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
if (groupId != userPreferredId) {
Settings.Secure.putInt(
context.getContentResolver(),
BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID,
groupId);
}
}
}
/**
* Build audio sharing dialog log event data
*

View File

@@ -91,7 +91,8 @@ public class StreamSettingsCategoryController extends BasePreferenceController
@Override
public int getAvailabilityStatus() {
return BluetoothUtils.isAudioSharingEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
return BluetoothUtils.isAudioSharingUIAvailable(mContext) ? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override

View File

@@ -16,8 +16,6 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -38,6 +36,7 @@ import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils;
@@ -77,7 +76,7 @@ public class AudioStreamButtonController extends BasePreferenceController
BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state);
boolean shouldUpdateButton =
audioSharingHysteresisModeFix()
BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)
? AudioStreamsHelper.hasSourcePresent(state)
: AudioStreamsHelper.isConnected(state);
if (shouldUpdateButton) {
@@ -157,7 +156,7 @@ public class AudioStreamButtonController extends BasePreferenceController
}
List<BluetoothLeBroadcastReceiveState> sources =
audioSharingHysteresisModeFix()
BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)
? mAudioStreamsHelper.getAllPresentSources()
: mAudioStreamsHelper.getAllConnectedSources();
boolean isConnected =

View File

@@ -214,15 +214,16 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
}
private int getDialogId(boolean hasMetadata, boolean hasConnectedDevice) {
if (!BluetoothUtils.isAudioSharingEnabled()) {
if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
if (!hasConnectedDevice) {
return SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_NO_LE_DEVICE;
}
return hasMetadata
? SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_LISTEN
: SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_DATA_ERROR;
} else {
return SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_FEATURE_UNSUPPORTED;
}
if (!hasConnectedDevice) {
return SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_NO_LE_DEVICE;
}
return hasMetadata
? SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_LISTEN
: SettingsEnums.DIALOG_AUDIO_STREAM_CONFIRM_DATA_ERROR;
}
@Nullable

View File

@@ -50,7 +50,7 @@ public class AudioStreamConfirmDialogActivity extends SettingsActivity
@Override
protected void createUiFromIntent(@Nullable Bundle savedState, Intent intent) {
if (BluetoothUtils.isAudioSharingEnabled()
if (BluetoothUtils.isAudioSharingUIAvailable(this)
&& !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
Log.d(TAG, "createUiFromIntent() : supported but not ready, skip createUiFromIntent");
mSavedState = savedState;
@@ -67,7 +67,7 @@ public class AudioStreamConfirmDialogActivity extends SettingsActivity
@Override
public void onStart() {
if (BluetoothUtils.isAudioSharingEnabled()
if (BluetoothUtils.isAudioSharingUIAvailable(this)
&& !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
Log.d(TAG, "onStart() : supported but not ready, listen to service ready");
if (mProfileManager != null) {
@@ -87,7 +87,7 @@ public class AudioStreamConfirmDialogActivity extends SettingsActivity
@Override
public void onServiceConnected() {
if (BluetoothUtils.isAudioSharingEnabled()
if (BluetoothUtils.isAudioSharingUIAvailable(this)
&& AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
if (mProfileManager != null) {
mProfileManager.removeServiceListener(this);

View File

@@ -37,6 +37,7 @@ import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.LayoutPreference;
@@ -86,7 +87,7 @@ public class AudioStreamHeaderController extends BasePreferenceController
updateSummary();
mAudioStreamsHelper.startMediaService(
mContext, mBroadcastId, mBroadcastName);
} else if (audioSharingHysteresisModeFix()
} else if (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)
&& AudioStreamsHelper.hasSourcePresent(state)) {
// if source present but not connected, only update the summary
updateSummary();
@@ -171,13 +172,13 @@ public class AudioStreamHeaderController extends BasePreferenceController
: mContext.getString(
AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY))
: mAudioStreamsHelper.getAllConnectedSources().stream()
.map(
BluetoothLeBroadcastReceiveState
::getBroadcastId)
.anyMatch(
connectedBroadcastId ->
connectedBroadcastId
== mBroadcastId)
.map(
BluetoothLeBroadcastReceiveState
::getBroadcastId)
.anyMatch(
connectedBroadcastId ->
connectedBroadcastId
== mBroadcastId)
? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;

View File

@@ -106,7 +106,7 @@ public class AudioStreamMediaService extends Service {
// If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will
// override this value. Otherwise, we raise the volume to 25 when the play button is clicked.
private final AtomicInteger mLatestPositiveVolume = new AtomicInteger(25);
private final AtomicBoolean mHasStopped = new AtomicBoolean(false);
private final Object mLocalSessionLock = new Object();
private int mBroadcastId;
@Nullable private List<BluetoothDevice> mDevices;
@Nullable private LocalBluetoothManager mLocalBtManager;
@@ -122,10 +122,10 @@ public class AudioStreamMediaService extends Service {
@Override
public void onCreate() {
if (!BluetoothUtils.isAudioSharingEnabled()) {
if (!BluetoothUtils.isAudioSharingUIAvailable(this)) {
return;
}
Log.d(TAG, "onCreate()");
super.onCreate();
mLocalBtManager = Utils.getLocalBtManager(this);
if (mLocalBtManager == null) {
@@ -146,47 +146,66 @@ public class AudioStreamMediaService extends Service {
return;
}
if (mNotificationManager.getNotificationChannel(CHANNEL_ID) == null) {
NotificationChannel notificationChannel =
new NotificationChannel(
CHANNEL_ID,
getString(com.android.settings.R.string.bluetooth),
NotificationManager.IMPORTANCE_HIGH);
mNotificationManager.createNotificationChannel(notificationChannel);
}
mExecutor.execute(
() -> {
if (mLocalBtManager == null
|| mLeBroadcastAssistant == null
|| mNotificationManager == null) {
return;
}
if (mNotificationManager.getNotificationChannel(CHANNEL_ID) == null) {
NotificationChannel notificationChannel =
new NotificationChannel(
CHANNEL_ID,
getString(com.android.settings.R.string.bluetooth),
NotificationManager.IMPORTANCE_HIGH);
mNotificationManager.createNotificationChannel(notificationChannel);
}
mBluetoothCallback = new BtCallback();
mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
mBluetoothCallback = new BtCallback();
mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
if (mVolumeControl != null) {
mVolumeControlCallback = new VolumeControlCallback();
mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
}
mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
if (mVolumeControl != null) {
mVolumeControlCallback = new VolumeControlCallback();
mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
}
mBroadcastAssistantCallback = new AssistantCallback();
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mBroadcastAssistantCallback = new AssistantCallback();
mLeBroadcastAssistant.registerServiceCallBack(
mExecutor, mBroadcastAssistantCallback);
});
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy()");
super.onDestroy();
if (!BluetoothUtils.isAudioSharingEnabled()) {
return;
}
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
}
if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) {
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
if (mVolumeControl != null && mVolumeControlCallback != null) {
mVolumeControl.unregisterCallback(mVolumeControlCallback);
}
if (mLocalSession != null) {
mLocalSession.release();
mLocalSession = null;
if (BluetoothUtils.isAudioSharingUIAvailable(this)) {
if (mDevices != null) {
mDevices.clear();
mDevices = null;
}
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
mLocalSession.release();
mLocalSession = null;
}
}
mExecutor.execute(
() -> {
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().unregisterCallback(
mBluetoothCallback);
}
if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) {
mLeBroadcastAssistant.unregisterServiceCallBack(
mBroadcastAssistantCallback);
}
if (mVolumeControl != null && mVolumeControlCallback != null) {
mVolumeControl.unregisterCallback(mVolumeControlCallback);
}
});
}
}
@@ -195,43 +214,45 @@ public class AudioStreamMediaService extends Service {
Log.d(TAG, "onStartCommand()");
if (intent == null) {
Log.w(TAG, "Intent is null. Service will not start.");
mHasStopped.set(true);
stopSelf();
return START_NOT_STICKY;
}
mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1);
if (mBroadcastId == -1) {
Log.w(TAG, "Invalid broadcast ID. Service will not start.");
mHasStopped.set(true);
stopSelf();
return START_NOT_STICKY;
}
var extra = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
if (extra == null || extra.isEmpty()) {
Log.w(TAG, "No device. Service will not start.");
mHasStopped.set(true);
stopSelf();
return START_NOT_STICKY;
}
mDevices = Collections.synchronizedList(extra);
createLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
startForeground(NOTIFICATION_ID, buildNotification());
// Reset in case the service is previously stopped but not yet destroyed.
mHasStopped.set(false);
MediaSession.Token token =
getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
startForeground(NOTIFICATION_ID, buildNotification(token));
return START_NOT_STICKY;
}
private void createLocalMediaSession(String title) {
mLocalSession = new MediaSession(this, TAG);
mLocalSession.setMetadata(
new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, title)
.putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
.build());
mLocalSession.setActive(true);
mLocalSession.setPlaybackState(getPlaybackState());
mMediaSessionCallback = new MediaSessionCallback();
mLocalSession.setCallback(mMediaSessionCallback);
private MediaSession.Token getOrCreateLocalMediaSession(String title) {
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
return mLocalSession.getSessionToken();
}
mLocalSession = new MediaSession(this, TAG);
mLocalSession.setMetadata(
new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, title)
.putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
.build());
mLocalSession.setActive(true);
mLocalSession.setPlaybackState(getPlaybackState());
mMediaSessionCallback = new MediaSessionCallback();
mLocalSession.setCallback(mMediaSessionCallback);
return mLocalSession.getSessionToken();
}
}
private PlaybackState getPlaybackState() {
@@ -252,12 +273,9 @@ public class AudioStreamMediaService extends Service {
return device != null ? device.getName() : DEFAULT_DEVICE_NAME;
}
private Notification buildNotification() {
private Notification buildNotification(MediaSession.Token token) {
String deviceName = getDeviceName();
Notification.MediaStyle mediaStyle =
new Notification.MediaStyle()
.setMediaSession(
mLocalSession != null ? mLocalSession.getSessionToken() : null);
Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(token);
if (deviceName != null && !deviceName.isEmpty()) {
mediaStyle.setRemotePlaybackInfo(
deviceName, com.android.settingslib.R.drawable.ic_bt_le_audio, null);
@@ -291,20 +309,15 @@ public class AudioStreamMediaService extends Service {
}
private void handleRemoveSource() {
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
List<BluetoothLeBroadcastReceiveState> connected =
mAudioStreamsHelper == null
? emptyList()
: mAudioStreamsHelper.getAllConnectedSources();
if (connected.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.noneMatch(id -> id == mBroadcastId)) {
mHasStopped.set(true);
stopSelf();
}
});
List<BluetoothLeBroadcastReceiveState> connected =
mAudioStreamsHelper == null
? emptyList()
: mAudioStreamsHelper.getAllConnectedSources();
if (connected.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.noneMatch(id -> id == mBroadcastId)) {
stopSelf();
}
}
}
@@ -326,7 +339,11 @@ public class AudioStreamMediaService extends Service {
mIsMuted.set(false);
mLatestPositiveVolume.set(volume);
}
updateNotification(getPlaybackState());
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
mLocalSession.setPlaybackState(getPlaybackState());
}
}
}
}
}
@@ -336,7 +353,6 @@ public class AudioStreamMediaService extends Service {
public void onBluetoothStateChanged(int bluetoothState) {
if (BluetoothAdapter.STATE_OFF == bluetoothState) {
Log.d(TAG, "onBluetoothStateChanged() : stopSelf");
mHasStopped.set(true);
stopSelf();
}
}
@@ -362,7 +378,6 @@ public class AudioStreamMediaService extends Service {
}
if (mDevices == null || mDevices.isEmpty()) {
Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf");
mHasStopped.set(true);
stopSelf();
}
}
@@ -371,7 +386,11 @@ public class AudioStreamMediaService extends Service {
private class MediaSessionCallback extends MediaSession.Callback {
public void onSeekTo(long pos) {
Log.d(TAG, "onSeekTo: " + pos);
updateNotification(getPlaybackState());
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
mLocalSession.setPlaybackState(getPlaybackState());
}
}
}
@Override
@@ -425,18 +444,4 @@ public class AudioStreamMediaService extends Service {
});
}
}
private void updateNotification(PlaybackState playbackState) {
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
if (mLocalSession != null) {
mLocalSession.setPlaybackState(playbackState);
if (mNotificationManager != null && !mHasStopped.get()) {
mNotificationManager.notify(
NOTIFICATION_ID, buildNotification());
}
}
});
}
}

View File

@@ -18,8 +18,6 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.os.Handler;
import android.os.Looper;
import android.text.SpannableString;
@@ -98,7 +96,8 @@ class AudioStreamStateHandler {
newState
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
|| (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
preference.getContext())
&& newState
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_PRESENT));

View File

@@ -31,7 +31,6 @@ 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.flags.Flags;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -80,13 +79,6 @@ public class AudioStreamsCategoryController extends AudioSharingBasePreferenceCo
}
}
@Override
public int getAvailabilityStatus() {
return Flags.enableLeAudioQrCodePrivateBroadcastSharing()
? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
public void updateVisibility() {
if (mPreference == null) return;

View File

@@ -19,7 +19,6 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList;
@@ -139,7 +138,6 @@ public class AudioStreamsHelper {
}
/** Retrieves a list of all LE broadcast receive states from active sinks. */
@VisibleForTesting
public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
@@ -165,7 +163,6 @@ public class AudioStreamsHelper {
}
/** Retrieves LocalBluetoothLeBroadcastAssistant. */
@VisibleForTesting
@Nullable
public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
return mLeBroadcastAssistant;
@@ -273,7 +270,8 @@ public class AudioStreamsHelper {
List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()
&& (audioSharingHysteresisModeFix()
&& (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
localBtManager.getContext())
|| sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d(
TAG,
@@ -286,7 +284,8 @@ public class AudioStreamsHelper {
List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice());
if (!list.isEmpty()
&& (audioSharingHysteresisModeFix()
&& (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
localBtManager.getContext())
|| list.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d(
TAG,

View File

@@ -16,19 +16,24 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import com.android.settingslib.bluetooth.BluetoothUtils;
public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
private static final String TAG = "AudioStreamsProgressCategoryCallback";
private final Context mContext;
private final AudioStreamsProgressCategoryController mCategoryController;
public AudioStreamsProgressCategoryCallback(
Context context,
AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
mContext = context;
mCategoryController = audioStreamsProgressCategoryController;
}
@@ -41,7 +46,8 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA
mCategoryController.handleSourceConnected(state);
} else if (AudioStreamsHelper.isBadCode(state)) {
mCategoryController.handleSourceConnectBadCode(state);
} else if (audioSharingHysteresisModeFix() && AudioStreamsHelper.hasSourcePresent(state)) {
} else if (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)
&& AudioStreamsHelper.hasSourcePresent(state)) {
// Keep this check as the last, source might also present in above states
mCategoryController.handleSourcePresent(state);
}

View File

@@ -16,8 +16,6 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList;
import android.app.AlertDialog;
@@ -101,7 +99,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
(p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
|| (isAudioSharingHysteresisModeFixAvailable(mContext)
&& p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState
@@ -147,7 +145,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mMediaControlHelper = new MediaControlHelper(mContext, mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(context, this);
}
@Override
@@ -258,7 +256,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// change it's state.
existingPreference.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED
&& (!audioSharingHysteresisModeFix()
&& (!isAudioSharingHysteresisModeFixAvailable(mContext)
|| fromState != AudioStreamState.SOURCE_PRESENT)) {
Log.w(
TAG,
@@ -364,7 +362,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// not, means the source is removed from the sink, we move back the preference to SYNCED
// state.
if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
|| (isAudioSharingHysteresisModeFixAvailable(mContext)
&& preference.getAudioStreamState()
== AudioStreamState.SOURCE_PRESENT))
&& mAudioStreamsHelper.getAllConnectedSources().stream()
@@ -600,7 +598,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Handle QR code scan, display currently connected streams then start scanning
// sequentially
handleSourceFromQrCodeIfExists();
if (audioSharingHysteresisModeFix()) {
if (isAudioSharingHysteresisModeFixAvailable(mContext)) {
// With hysteresis mode, we prioritize showing connected sources first.
// If no connected sources are found, we then show present sources.
List<BluetoothLeBroadcastReceiveState> sources =
@@ -702,4 +700,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
dialog.dismiss();
});
}
private static boolean isAudioSharingHysteresisModeFixAvailable(Context context) {
return BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(context);
}
}

View File

@@ -0,0 +1,149 @@
/*
* 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.display
import com.android.settings.R
import android.content.Context
import android.graphics.Point
import android.graphics.PointF
import android.graphics.RectF
import androidx.preference.Preference
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
/**
* Contains the parameters needed for transforming global display coordinates to and from topology
* pane coordinates. This is necessary for implementing an interactive display topology pane. The
* pane allows dragging and dropping display blocks into place to define the topology. Conversion to
* pane coordinates is necessary when rendering the original topology. Conversion in the other
* direction, to display coordinates, is necessary for resolve a drag position to display space.
*
* The topology pane coordinates are integral and represent the relative position from the upper-
* left corner of the pane. It uses a scale optimized for showing all displays with minimal or no
* scrolling. The display coordinates are floating point and the origin can be in any position. In
* practice the origin will be the upper-left coordinate of the primary display.
*/
class TopologyScale(paneWidth : Int, displaysPos : Collection<RectF>) {
/** Scale of block sizes to real-world display sizes. Should be less than 1. */
val blockRatio : Float
/** Height of topology pane needed to allow all display blocks to appear with some padding. */
val paneHeight : Int
/** Pane's X view coordinate that corresponds with topology's X=0 coordinate. */
val originPaneX : Int
/** Pane's Y view coordinate that corresponds with topology's Y=0 coordinate. */
val originPaneY : Int
init {
val displayBounds = RectF(
Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE)
var smallestDisplayDim = Float.MAX_VALUE
var biggestDisplayHeight = Float.MIN_VALUE
// displayBounds is the smallest rect encompassing all displays, in display space.
// smallestDisplayDim is the size of the smallest display edge, in display space.
for (pos in displaysPos) {
displayBounds.union(pos)
smallestDisplayDim = minOf(smallestDisplayDim, pos.height(), pos.width())
biggestDisplayHeight = max(biggestDisplayHeight, pos.height())
}
// Set height according to the width and the aspect ratio of the display bounds.
// 0.05 is a reasonable limit to the size of display blocks. It appears to match the
// ratio used in the ChromeOS topology editor. It prevents blocks from being too large,
// which would make dragging and dropping awkward.
val rawBlockRatio = min(0.05, paneWidth.toDouble() * 0.6 / displayBounds.width())
// If the `ratio` is set too low because one of the displays will have an edge less than
// 48dp long, increase it such that the smallest edge is that long. This may override the
// 0.05 limit since it is more important than it.
blockRatio = max(48.0 / smallestDisplayDim, rawBlockRatio).toFloat()
// Essentially, we just set the pane height based on the pre-determined pane width and the
// aspect ratio of the display bounds. But we may need to increase it slightly to achieve
// 20% padding above and below the display bounds - this is where the 0.6 comes from.
val rawPaneHeight = max(
paneWidth.toDouble() / displayBounds.width() * displayBounds.height(),
displayBounds.height() * blockRatio / 0.6)
// It is easy for the aspect ratio to result in an excessively tall pane, since the width is
// pre-determined and may be considerably wider than necessary. So we prevent the height
// from growing too large here, by limiting vertical padding to the size of the tallest
// display. This improves results for very tall display bounds.
paneHeight = min(
rawPaneHeight.toInt(),
(blockRatio * (displayBounds.height() + biggestDisplayHeight * 2f)).toInt())
// Set originPaneXY (the location of 0,0 in display space in the pane's coordinate system)
// such that the display bounds rect is centered in the pane.
// It is unlikely that either of these coordinates will be negative since blockRatio has
// been chosen to allow 20% padding around each side of the display blocks. However, the
// a11y requirement applied above (48.0 / smallestDisplayDim) may cause the blocks to not
// fit. This should be rare in practice, and can be worked around by moving the settings UI
// to a larger display.
val blockMostLeft = (paneWidth - displayBounds.width() * blockRatio) / 2
val blockMostTop = (paneHeight - displayBounds.height() * blockRatio) / 2
originPaneX = (blockMostLeft - displayBounds.left * blockRatio).toInt()
originPaneY = (blockMostTop - displayBounds.top * blockRatio).toInt()
}
/** Transforms coordinates in view pane space to display space. */
fun paneToDisplayCoor(panePos : Point) : PointF {
return PointF(
(panePos.x - originPaneX).toFloat() / blockRatio,
(panePos.y - originPaneY).toFloat() / blockRatio)
}
/** Transforms coordinates in display space to view pane space. */
fun displayToPaneCoor(displayPos : PointF) : Point {
return Point(
(displayPos.x * blockRatio).toInt() + originPaneX,
(displayPos.y * blockRatio).toInt() + originPaneY)
}
override fun toString() : String {
return String.format(
Locale.ROOT,
"{TopoScale blockRatio=%f originPaneXY=%d,%d paneHeight=%d}",
blockRatio, originPaneX, originPaneY, paneHeight)
}
}
const val PREFERENCE_KEY = "display_topology_preference"
/**
* DisplayTopologyPreference allows the user to change the display topology
* when there is one or more extended display attached.
*/
class DisplayTopologyPreference(context : Context) : Preference(context) {
init {
layoutResource = R.layout.display_topology_preference
// Prevent highlight when hovering with mouse.
isSelectable = false
key = PREFERENCE_KEY
}
}

View File

@@ -16,12 +16,12 @@
package com.android.settings.connecteddevice.display;
import static android.view.Display.INVALID_DISPLAY;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.forceShowDisplayList;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isUseDisplaySettingEnabled;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isResolutionSettingEnabled;
@@ -45,6 +45,7 @@ import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragmentBase;
import com.android.settings.accessibility.TextReadingPreferenceFragment;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
import com.android.settings.core.SubSettingLauncher;
@@ -63,6 +64,7 @@ import java.util.List;
public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmentBase {
static final int EXTERNAL_DISPLAY_SETTINGS_RESOURCE = R.xml.external_display_settings;
static final String DISPLAYS_LIST_PREFERENCE_KEY = "displays_list_preference";
static final String BUILTIN_DISPLAY_LIST_PREFERENCE_KEY = "builtin_display_list_preference";
static final String EXTERNAL_DISPLAY_USE_PREFERENCE_KEY = "external_display_use_preference";
static final String EXTERNAL_DISPLAY_ROTATION_KEY = "external_display_rotation";
static final String EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY = "external_display_resolution";
@@ -82,6 +84,8 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
R.string.external_display_rotation;
static final int EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE =
R.string.external_display_resolution_settings_title;
static final int BUILTIN_DISPLAY_SETTINGS_CATEGORY_RESOURCE =
R.string.builtin_display_settings_category;
@VisibleForTesting
static final String PREVIOUSLY_SHOWN_LIST_KEY = "mPreviouslyShownListOfDisplays";
private boolean mStarted;
@@ -96,8 +100,12 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
@Nullable
private FooterPreference mFooterPreference;
@Nullable
private Preference mDisplayTopologyPreference;
@Nullable
private PreferenceCategory mDisplaysPreference;
@Nullable
private PreferenceCategory mBuiltinDisplayPreference;
@Nullable
private Injector mInjector;
@Nullable
private String[] mRotationEntries;
@@ -197,7 +205,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
}
@VisibleForTesting
protected void launchDisplaySettings(final int displayId) {
protected void launchExternalDisplaySettings(final int displayId) {
final Bundle args = new Bundle();
var context = getPrefContext();
args.putInt(DISPLAY_ID_ARG, displayId);
@@ -207,6 +215,16 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
.setSourceMetricsCategory(getMetricsCategory()).launch();
}
@VisibleForTesting
protected void launchBuiltinDisplaySettings() {
final Bundle args = new Bundle();
var context = getPrefContext();
new SubSettingLauncher(context)
.setDestination(TextReadingPreferenceFragment.class.getName())
.setArguments(args)
.setSourceMetricsCategory(getMetricsCategory()).launch();
}
/**
* Returns the preference for the footer.
*/
@@ -278,6 +296,23 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
return mDisplaysPreference;
}
@NonNull
private PreferenceCategory getBuiltinDisplayListPreference(@NonNull Context context) {
if (mBuiltinDisplayPreference == null) {
mBuiltinDisplayPreference = new PreferenceCategory(context);
mBuiltinDisplayPreference.setPersistent(false);
}
return mBuiltinDisplayPreference;
}
@NonNull Preference getDisplayTopologyPreference(@NonNull Context context) {
if (mDisplayTopologyPreference == null) {
mDisplayTopologyPreference = new DisplayTopologyPreference(context);
mDisplayTopologyPreference.setPersistent(false);
}
return mDisplayTopologyPreference;
}
private void restoreState(@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
@@ -297,10 +332,13 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
private void updateScreenForDisplayId(final int displayId,
@NonNull final PreferenceScreen screen, @NonNull Context context) {
final var displaysToShow = getDisplaysToShow(displayId);
if (displaysToShow.isEmpty() && displayId == INVALID_DISPLAY) {
final boolean forceShowList = displayId == INVALID_DISPLAY
&& mInjector != null && forceShowDisplayList(mInjector.getFlags());
final var displaysToShow = externalDisplaysToShow(displayId);
if (!forceShowList && displaysToShow.isEmpty() && displayId == INVALID_DISPLAY) {
showTextWhenNoDisplaysToShow(screen, context);
} else if (displaysToShow.size() == 1
} else if (!forceShowList && displaysToShow.size() == 1
&& ((displayId == INVALID_DISPLAY && !mPreviouslyShownListOfDisplays)
|| displaysToShow.get(0).getDisplayId() == displayId)) {
showDisplaySettings(displaysToShow.get(0), screen, context);
@@ -359,6 +397,20 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
private void showDisplaysList(@NonNull List<Display> displaysToShow,
@NonNull PreferenceScreen screen, @NonNull Context context) {
if (mInjector != null && mInjector.getFlags().displayTopologyPaneInDisplayList()) {
screen.addPreference(getDisplayTopologyPreference(context));
// If topology is shown, we also show a preference for the built-in display for
// consistency with the topology.
var builtinCategory = getBuiltinDisplayListPreference(context);
builtinCategory.setKey(BUILTIN_DISPLAY_LIST_PREFERENCE_KEY);
builtinCategory.setTitle(BUILTIN_DISPLAY_SETTINGS_CATEGORY_RESOURCE);
builtinCategory.removeAll();
screen.addPreference(builtinCategory);
builtinCategory.addPreference(new BuiltinDisplaySizeAndTextPreference(context));
}
var pref = getDisplaysListPreference(context);
pref.setKey(DISPLAYS_LIST_PREFERENCE_KEY);
pref.removeAll();
@@ -370,7 +422,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
}
}
private List<Display> getDisplaysToShow(int displayIdToShow) {
private List<Display> externalDisplaysToShow(int displayIdToShow) {
if (mInjector == null) {
return List.of();
}
@@ -511,6 +563,24 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
mInjector.getHandler().removeCallbacks(mUpdateRunnable);
}
private class BuiltinDisplaySizeAndTextPreference extends Preference
implements Preference.OnPreferenceClickListener {
BuiltinDisplaySizeAndTextPreference(@NonNull final Context context) {
super(context);
setPersistent(false);
setKey("builtin_display_size_and_text");
setTitle(R.string.accessibility_text_reading_options_title);
setOnPreferenceClickListener(this);
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
launchBuiltinDisplaySettings();
return true;
}
}
@VisibleForTesting
class DisplayPreference extends TwoTargetPreference
implements Preference.OnPreferenceClickListener {
@@ -519,6 +589,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
DisplayPreference(@NonNull final Context context, @NonNull final Display display) {
super(context);
mDisplayId = display.getDisplayId();
setPersistent(false);
setKey("display_id_" + mDisplayId);
setTitle(display.getName());
@@ -529,7 +600,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
launchDisplaySettings(mDisplayId);
launchExternalDisplaySettings(mDisplayId);
writePreferenceClickMetric(preference);
return true;
}

View File

@@ -19,8 +19,8 @@ import static android.content.Context.DISPLAY_SERVICE;
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
import static android.hardware.display.DisplayManager.PRIVATE_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
import static android.view.Display.INVALID_DISPLAY;
import static com.android.server.display.feature.flags.Flags.enableModeLimitForExternalDisplay;
@@ -159,8 +159,8 @@ public class ExternalDisplaySettingsConfiguration {
return;
}
dm.registerDisplayListener(listener, mHandler, EVENT_FLAG_DISPLAY_ADDED
| EVENT_FLAG_DISPLAY_CHANGED | EVENT_FLAG_DISPLAY_REMOVED
| EVENT_FLAG_DISPLAY_CONNECTION_CHANGED);
| EVENT_FLAG_DISPLAY_CHANGED | EVENT_FLAG_DISPLAY_REMOVED,
PRIVATE_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED);
}
/**
@@ -319,7 +319,16 @@ public class ExternalDisplaySettingsConfiguration {
*/
public static boolean isExternalDisplaySettingsPageEnabled(@NonNull FeatureFlags flags) {
return flags.rotationConnectedDisplaySetting()
|| flags.resolutionAndEnableConnectedDisplaySetting();
|| flags.resolutionAndEnableConnectedDisplaySetting()
|| flags.displayTopologyPaneInDisplayList();
}
/**
* If true, indicates the display list activity should be shown even if there is only one
* display.
*/
public static boolean forceShowDisplayList(@NonNull FeatureFlags flags) {
return flags.displayTopologyPaneInDisplayList();
}
static boolean isDisplayAllowed(@NonNull Display display,

View File

@@ -16,6 +16,7 @@
package com.android.settings.connecteddevice.display;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.forceShowDisplayList;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
import android.content.Context;
@@ -142,6 +143,10 @@ public class ExternalDisplayUpdater {
}
}
if (forceShowDisplayList(mInjector.getFlags())) {
return context.getString(R.string.external_display_off);
}
for (var display : mInjector.getAllDisplays()) {
if (display != null && isDisplayAllowed(display, mInjector)) {
return context.getString(R.string.external_display_off);

View File

@@ -0,0 +1,111 @@
/*
* 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.display
import android.graphics.RectF
import kotlin.math.hypot
// Unfortunately, in the world of IEEE 32-bit floats, A + X - X is not always == A
// For example: A = 1075.4271f
// C = 1249.2203f
// For example: - A - 173.79326f = - C
// However: - C + A = - 173.79321f
// So we need to keep track of how the movingDisplay block is attaching to otherDisplays throughout
// the calculations below. We cannot use the rect.left with its width as a proxy for rect.right. We
// have to save the "inner" or attached side and use the width or height to calculate the "external"
// side.
/** A potential X position for the display to clamp at. */
private class XCoor(
val left : Float, val right : Float,
/**
* If present, the position of the display being attached to. If absent, indicates the X
* position is derived from the exact drag position.
*/
val attaching : RectF?,
)
/** A potential Y position for the display to clamp at. */
private class YCoor(
val top : Float, val bottom : Float,
/**
* If present, the position of the display being attached to. If absent, indicates the Y
* position is derived from the exact drag position.
*/
val attaching : RectF?,
)
/**
* Finds the optimal clamp position assuming the user has dragged the block to `movingDisplay`.
*
* @param otherDisplays positions of the stationary displays (every one not being dragged)
* @param movingDisplay the position the user is current holding the block during a drag
*
* @return the clamp position as a RectF, whose dimensions will match that of `movingDisplay`
*/
fun clampPosition(otherDisplays : Iterable<RectF>, movingDisplay : RectF) : RectF {
val xCoors = otherDisplays.flatMap {
listOf(
// Attaching to left edge of `it`
XCoor(it.left - movingDisplay.width(), it.left, it),
// Attaching to right edge of `it`
XCoor(it.right, it.right + movingDisplay.width(), it),
)
}.plusElement(XCoor(movingDisplay.left, movingDisplay.right, null))
val yCoors = otherDisplays.flatMap {
listOf(
// Attaching to the top edge of `it`
YCoor(it.top - movingDisplay.height(), it.top, it),
// Attaching to the bottom edge of `it`
YCoor(it.bottom, it.bottom + movingDisplay.height(), it),
)
}.plusElement(YCoor(movingDisplay.top, movingDisplay.bottom, null))
class Cand(val x : XCoor, val y : YCoor)
val candidateGrid = xCoors.flatMap { x -> yCoors.map { y -> Cand(x, y) }}
val hasAttachInRange = candidateGrid.filter {
if (it.x.attaching != null) {
// Attaching to a vertical (left or right) edge. The y range of dragging and
// stationary blocks must overlap.
it.y.top <= it.x.attaching.bottom && it.y.bottom >= it.x.attaching.top
} else if (it.y.attaching != null) {
// Attaching to a horizontal (top or bottom) edge. The x range of dragging and
// stationary blocks must overlap.
it.x.left <= it.y.attaching.right && it.x.right >= it.y.attaching.left
} else {
// Not attaching to another display's edge at all, so not a valid clamp position.
false
}
}
// Clamp positions closest to the user's drag position are best. Sort by increasing distance
// from it, so the best will be first.
val prioritized = hasAttachInRange.sortedBy {
hypot(it.x.left - movingDisplay.left, it.y.top - movingDisplay.top)
}
val notIntersectingAny = prioritized.asSequence()
.map { RectF(it.x.left, it.y.top, it.x.right, it.y.bottom) }
.filter { p -> otherDisplays.all { !RectF.intersects(p, it) } }
// Note we return a copy of `movingDisplay` if there is no valid clamp position, which will only
// happen if `otherDisplays` is empty or has no valid rectangles. It may not be wise to rely on
// this behavior.
return notIntersectingAny.firstOrNull() ?: RectF(movingDisplay)
}