Merge 25Q1 (ab/12770256) to aosp-main-future
Bug: 385190204 Merged-In: Iaee6792d1a27be8fa4b443f783a47a3715b6d3a1 Change-Id: I0ac29cecfec526a38cf4a120b8ef704ee7bc01b3
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user