Snap for 12930767 from 878f860fd3 to 25Q2-release

Change-Id: I4f9ade7440b5adeb5d75895c8cc5874ffa301c24
This commit is contained in:
Android Build Coastguard Worker
2025-01-16 16:22:35 -08:00
48 changed files with 1065 additions and 512 deletions

View File

@@ -58,6 +58,13 @@ flag {
bug: "301198830" bug: "301198830"
} }
flag {
name: "enable_magnification_cursor_following_dialog"
namespace: "accessibility"
description: "Decides whether to show the magnification cursor following dialog in Settings app."
bug: "388335935"
}
flag { flag {
name: "enable_magnification_focus_following_dialog" name: "enable_magnification_focus_following_dialog"
namespace: "accessibility" namespace: "accessibility"

View File

@@ -0,0 +1,21 @@
<!--
Copyright (C) 2016 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"
android:tint="?android:attr/colorControlNormal">
<path android:fillColor="@android:color/white"
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,473 799.5,465.5Q799,458 799,453Q794,482 772,501Q750,520 720,520L640,520Q607,520 583.5,496.5Q560,473 560,440L560,400L400,400L400,320Q400,287 423.5,263.5Q447,240 480,240L520,240L520,240Q520,217 532.5,199.5Q545,182 563,171Q543,166 522.5,163Q502,160 480,160Q346,160 253,253Q160,346 160,480Q160,480 160,480Q160,480 160,480L360,480Q426,480 473,527Q520,574 520,640L520,680L400,680L400,790Q420,795 439.5,797.5Q459,800 480,800Z"/>
</vector>

View File

@@ -17,7 +17,6 @@
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scroll" android:id="@+id/scroll"
android:background="?android:attr/colorBackgroundFloating"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -132,7 +131,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:paddingTop="16dp"
android:paddingBottom="16dp">
<LinearLayout <LinearLayout
android:id="@+id/snoozed_list" android:id="@+id/snoozed_list"
@@ -145,12 +145,12 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:text="@string/notification_history_snooze" android:text="@string/notification_history_snooze"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:textAppearance="@style/TextAppearance.HomepageCardTitle" android:textAppearance="@style/TextAppearance.HomepageCardTitle"
android:paddingBottom="16dp" /> android:paddingBottom="16dp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/notification_list" android:id="@+id/notification_list"
android:background="@drawable/rounded_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="true" android:clipChildren="true"
@@ -170,10 +170,10 @@
android:text="@string/notification_history_dismiss" android:text="@string/notification_history_dismiss"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.HomepageCardTitle" android:textAppearance="@style/TextAppearance.HomepageCardTitle"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:paddingBottom="16dp" /> android:paddingBottom="16dp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/notification_list" android:id="@+id/notification_list"
android:background="@drawable/rounded_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="true" android:clipChildren="true"
@@ -191,13 +191,13 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.HomepageCardTitle" android:textAppearance="@style/TextAppearance.HomepageCardTitle"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:paddingBottom="16dp" /> android:paddingBottom="16dp" />
<LinearLayout <LinearLayout
android:id="@+id/apps" android:id="@+id/apps"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:background="@drawable/rounded_bg">
<!-- app based recycler views added here --> <!-- app based recycler views added here -->
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -25,6 +25,8 @@
android:id="@+id/app_header" android:id="@+id/app_header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="20dp" android:paddingTop="20dp"
android:paddingBottom="18dp" android:paddingBottom="18dp"
android:paddingStart="16dp" android:paddingStart="16dp"
@@ -84,15 +86,12 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/notification_list_wrapper" android:id="@+id/notification_list_wrapper"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd">
<com.android.settings.notification.history.NotificationHistoryRecyclerView <com.android.settings.notification.history.NotificationHistoryRecyclerView
android:id="@+id/notification_list" android:id="@+id/notification_list"

View File

@@ -17,17 +17,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="48dp" android:minHeight="48dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="16dp"
android:orientation="vertical" android:orientation="vertical"
android:foreground="?android:attr/selectableItemBackground"> android:foreground="?android:attr/selectableItemBackground">
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/listDivider" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@*android:dimen/status_bar_icon_size" android:minHeight="@*android:dimen/status_bar_icon_size"
android:layout_marginStart="54dp"
android:paddingTop="16dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:gravity="center_vertical"> android:gravity="center_vertical">
<TextView <TextView
@@ -36,7 +41,6 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="32dp"
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
@@ -77,11 +81,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="left|center_vertical" android:layout_gravity="left|center_vertical"
android:ellipsize="end" android:ellipsize="end"
android:layout_marginStart="32dp" android:layout_marginStart="54dp"
android:maxLines="7" android:maxLines="7"
android:paddingTop="4dp" android:paddingTop="4dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification"
android:textAlignment="viewStart" /> android:textAlignment="viewStart"
android:paddingBottom="16dp" />
</LinearLayout> </LinearLayout>

View File

@@ -22,8 +22,10 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingStart="16dp" android:paddingStart="16dp"
android:layout_marginEnd="16dp" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:orientation="vertical"> android:orientation="vertical">
@@ -128,11 +130,5 @@
/> />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/listDivider" />
</LinearLayout> </LinearLayout>

View File

@@ -527,6 +527,10 @@
<string name="top_intro_region_title">The region you choose affects how your phone displays time, dates, temperature, and more</string> <string name="top_intro_region_title">The region you choose affects how your phone displays time, dates, temperature, and more</string>
<!-- Category for more language settings. [CHAR LIMIT=NONE]--> <!-- Category for more language settings. [CHAR LIMIT=NONE]-->
<string name="more_language_settings_category">More language settings</string> <string name="more_language_settings_category">More language settings</string>
<!-- Title for asking to change system locale region or not. [CHAR LIMIT=50]-->
<string name="title_change_system_locale_region">Change region to %s ?</string>
<!-- Message for asking to change system locale region or not. [CHAR LIMIT=50]-->
<string name="body_change_system_locale_region">Your device will keep %s as a system language</string>
<!-- Regional Preferences begin --> <!-- Regional Preferences begin -->
<!-- The title of the menu entry of regional preferences. [CHAR LIMIT=50] --> <!-- The title of the menu entry of regional preferences. [CHAR LIMIT=50] -->
@@ -11326,11 +11330,11 @@
<!-- Double Tap Power Gesture camera launch action title [CHAR_LIMIT=60] --> <!-- Double Tap Power Gesture camera launch action title [CHAR_LIMIT=60] -->
<string name="double_tap_power_camera_action_title">Camera</string> <string name="double_tap_power_camera_action_title">Camera</string>
<!-- Setting summary to describe double tap power button will open camera. [CHAR LIMIT=NONE] --> <!-- Setting summary to describe double tap power button will open camera. [CHAR LIMIT=NONE] -->
<string name="double_tap_power_camera_action_summary">Access Camera</string> <string name="double_tap_power_camera_action_summary">Open Camera</string>
<!-- Double Tap Power Gesture wallet launch action title [CHAR_LIMIT=60] --> <!-- Double Tap Power Gesture wallet launch action title [CHAR_LIMIT=60] -->
<string name="double_tap_power_wallet_action_title">Wallet</string> <string name="double_tap_power_wallet_action_title">Wallet</string>
<!-- Setting summary to describe double tap power button will open wallet. [CHAR LIMIT=NONE] --> <!-- Setting summary to describe double tap power button will open wallet. [CHAR LIMIT=NONE] -->
<string name="double_tap_power_wallet_action_summary">Access Wallet</string> <string name="double_tap_power_wallet_action_summary">Open Wallet</string>
<!-- Title text for double twist for camera mode [CHAR LIMIT=60]--> <!-- Title text for double twist for camera mode [CHAR LIMIT=60]-->
<string name="double_twist_for_camera_mode_title">Flip camera for selfie</string> <string name="double_twist_for_camera_mode_title">Flip camera for selfie</string>

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2025 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
import android.content.Context
import com.android.settings.overlay.FeatureFactory
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider
import com.android.settingslib.metadata.PreferenceUiActionMetricsLogger
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.PreferenceScreenMetadata
/** Provides metrics for preference action. */
interface PreferenceActionMetricsProvider {
/** Metrics action id for the preference. */
val preferenceActionMetrics: Int
}
/** [PreferenceUiActionMetricsLogger] of Settings app. */
class SettingsMetricsLogger
@JvmOverloads
constructor(
private val context: Context,
private val metricsFeatureProvider: MetricsFeatureProvider =
FeatureFactory.featureFactory.metricsFeatureProvider,
) : PreferenceUiActionMetricsLogger {
override fun logPreferenceValueChange(
screen: PreferenceScreenMetadata,
preference: PreferenceMetadata,
value: Any?,
) {
if (preference !is PreferenceActionMetricsProvider) return
when (value) {
is Boolean ->
metricsFeatureProvider.action(context, preference.preferenceActionMetrics, value)
else -> {}
}
}
}

View File

@@ -77,6 +77,8 @@ public class SettingsApplication extends Application {
if (Flags.catalyst()) { if (Flags.catalyst()) {
PreferenceScreenRegistry.INSTANCE.setPreferenceScreenMetadataFactories( PreferenceScreenRegistry.INSTANCE.setPreferenceScreenMetadataFactories(
preferenceScreenFactories()); preferenceScreenFactories());
PreferenceScreenRegistry.INSTANCE.setPreferenceUiActionMetricsLogger(
new SettingsMetricsLogger(this));
PreferenceBindingFactory.setDefaultFactory(new SettingsPreferenceBindingFactory()); PreferenceBindingFactory.setDefaultFactory(new SettingsPreferenceBindingFactory());
} }

View File

@@ -82,7 +82,7 @@ public class BluetoothDetailsAudioSharingController extends BluetoothDetailsCont
mProfilesContainer.removeAll(); mProfilesContainer.removeAll();
mProfilesContainer.addPreference(createAudioSharingPreference()); mProfilesContainer.addPreference(createAudioSharingPreference());
if ((BluetoothUtils.isActiveLeAudioDevice(mCachedDevice) if ((BluetoothUtils.isActiveLeAudioDevice(mCachedDevice)
|| AudioStreamsHelper.hasConnectedBroadcastSource( || AudioStreamsHelper.hasBroadcastSource(
mCachedDevice, mLocalBluetoothManager)) mCachedDevice, mLocalBluetoothManager))
&& !BluetoothUtils.isBroadcasting(mLocalBluetoothManager)) { && !BluetoothUtils.isBroadcasting(mLocalBluetoothManager)) {
mProfilesContainer.addPreference(createFindAudioStreamPreference()); mProfilesContainer.addPreference(createFindAudioStreamPreference());

View File

@@ -16,6 +16,10 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -42,7 +46,6 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.ActionButtonsPreference; import com.android.settingslib.widget.ActionButtonsPreference;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -75,20 +78,19 @@ public class AudioStreamButtonController extends BasePreferenceController
int sourceId, int sourceId,
BluetoothLeBroadcastReceiveState state) { BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state); super.onReceiveStateChanged(sink, sourceId, state);
boolean shouldUpdateButton = var localSourceState = getLocalSourceState(state);
BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext) boolean shouldUpdateButton = mHysteresisModeFixAvailable
? AudioStreamsHelper.hasSourcePresent(state) ? (localSourceState == PAUSED || localSourceState == STREAMING)
: AudioStreamsHelper.isConnected(state); : localSourceState == STREAMING;
if (shouldUpdateButton) { if (shouldUpdateButton) {
updateButton(); updateButton();
if (AudioStreamsHelper.isConnected(state)) { // TODO(b/308368124): Verify if this log is too noisy.
mMetricsFeatureProvider.action( mMetricsFeatureProvider.action(
mContext, mContext,
SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED, SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED,
SOURCE_ORIGIN_REPOSITORY); SOURCE_ORIGIN_REPOSITORY);
} }
} }
}
@Override @Override
public void onSourceAddFailed( public void onSourceAddFailed(
@@ -113,6 +115,7 @@ public class AudioStreamButtonController extends BasePreferenceController
private final AudioStreamsHelper mAudioStreamsHelper; private final AudioStreamsHelper mAudioStreamsHelper;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final MetricsFeatureProvider mMetricsFeatureProvider; private final MetricsFeatureProvider mMetricsFeatureProvider;
private final boolean mHysteresisModeFixAvailable;
private @Nullable ActionButtonsPreference mPreference; private @Nullable ActionButtonsPreference mPreference;
private int mBroadcastId = -1; private int mBroadcastId = -1;
@@ -121,6 +124,8 @@ public class AudioStreamButtonController extends BasePreferenceController
mExecutor = Executors.newSingleThreadExecutor(); mExecutor = Executors.newSingleThreadExecutor();
mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context)); mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
context);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
} }
@@ -155,14 +160,8 @@ public class AudioStreamButtonController extends BasePreferenceController
return; return;
} }
List<BluetoothLeBroadcastReceiveState> sources = boolean isConnected = mAudioStreamsHelper.getConnectedBroadcastIdAndState(
BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext) mHysteresisModeFixAvailable).containsKey(mBroadcastId);
? mAudioStreamsHelper.getAllPresentSources()
: mAudioStreamsHelper.getAllConnectedSources();
boolean isConnected =
sources.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId);
View.OnClickListener onClickListener; View.OnClickListener onClickListener;

View File

@@ -16,9 +16,10 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static java.util.stream.Collectors.toList; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -61,6 +62,7 @@ public class AudioStreamHeaderController extends BasePreferenceController
private final Executor mExecutor; private final Executor mExecutor;
private final AudioStreamsHelper mAudioStreamsHelper; private final AudioStreamsHelper mAudioStreamsHelper;
@Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final boolean mHysteresisModeFixAvailable;
@VisibleForTesting @VisibleForTesting
final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
@@ -83,13 +85,13 @@ public class AudioStreamHeaderController extends BasePreferenceController
int sourceId, int sourceId,
BluetoothLeBroadcastReceiveState state) { BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state); super.onReceiveStateChanged(sink, sourceId, state);
if (AudioStreamsHelper.isConnected(state)) { var localSourceState = getLocalSourceState(state);
if (localSourceState == STREAMING) {
updateSummary(); updateSummary();
mAudioStreamsHelper.startMediaService( mAudioStreamsHelper.startMediaService(
mContext, mBroadcastId, mBroadcastName); mContext, mBroadcastId, mBroadcastName);
} else if (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext) } else if (mHysteresisModeFixAvailable && localSourceState == PAUSED) {
&& AudioStreamsHelper.hasSourcePresent(state)) { // if source paused, only update the summary
// if source present but not connected, only update the summary
updateSummary(); updateSummary();
} }
} }
@@ -105,6 +107,8 @@ public class AudioStreamHeaderController extends BasePreferenceController
mExecutor = Executors.newSingleThreadExecutor(); mExecutor = Executors.newSingleThreadExecutor();
mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context)); mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
context);
} }
@Override @Override
@@ -151,38 +155,9 @@ public class AudioStreamHeaderController extends BasePreferenceController
var unused = var unused =
ThreadUtils.postOnBackgroundThread( ThreadUtils.postOnBackgroundThread(
() -> { () -> {
var connectedSourceList = var sourceState = mAudioStreamsHelper.getConnectedBroadcastIdAndState(
mAudioStreamsHelper.getAllPresentSources().stream() mHysteresisModeFixAvailable).get(mBroadcastId);
.filter( var latestSummary = getLatestSummary(sourceState);
state ->
(state.getBroadcastId()
== mBroadcastId))
.collect(toList());
var latestSummary =
audioSharingHysteresisModeFix()
? connectedSourceList.isEmpty()
? AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY
: (connectedSourceList.stream()
.anyMatch(
AudioStreamsHelper
::isConnected)
? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: mContext.getString(
AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY))
: mAudioStreamsHelper.getAllConnectedSources().stream()
.map(
BluetoothLeBroadcastReceiveState
::getBroadcastId)
.anyMatch(
connectedBroadcastId ->
connectedBroadcastId
== mBroadcastId)
? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
ThreadUtils.postOnMainThread( ThreadUtils.postOnMainThread(
() -> { () -> {
if (mHeaderController != null) { if (mHeaderController != null) {
@@ -212,4 +187,16 @@ public class AudioStreamHeaderController extends BasePreferenceController
mBroadcastName = broadcastName; mBroadcastName = broadcastName;
mBroadcastId = broadcastId; mBroadcastId = broadcastId;
} }
private String getLatestSummary(@Nullable LocalBluetoothLeBroadcastSourceState state) {
if (state == null) {
return AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
}
if (mHysteresisModeFixAvailable) {
return state == STREAMING
? mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY);
}
return mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY);
}
} }

View File

@@ -16,8 +16,6 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static java.util.Collections.emptyList;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -25,7 +23,6 @@ import android.app.Service;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothVolumeControl; import android.bluetooth.BluetoothVolumeControl;
import android.content.Intent; import android.content.Intent;
@@ -107,6 +104,7 @@ public class AudioStreamMediaService extends Service {
// override this value. Otherwise, we raise the volume to 25 when the play button is clicked. // 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 AtomicInteger mLatestPositiveVolume = new AtomicInteger(25);
private final Object mLocalSessionLock = new Object(); private final Object mLocalSessionLock = new Object();
private boolean mHysteresisModeFixAvailable;
private int mBroadcastId; private int mBroadcastId;
@Nullable private List<BluetoothDevice> mDevices; @Nullable private List<BluetoothDevice> mDevices;
@Nullable private LocalBluetoothManager mLocalBtManager; @Nullable private LocalBluetoothManager mLocalBtManager;
@@ -139,6 +137,7 @@ public class AudioStreamMediaService extends Service {
Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!"); Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
return; return;
} }
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
mNotificationManager = getSystemService(NotificationManager.class); mNotificationManager = getSystemService(NotificationManager.class);
if (mNotificationManager == null) { if (mNotificationManager == null) {
@@ -309,13 +308,9 @@ public class AudioStreamMediaService extends Service {
} }
private void handleRemoveSource() { private void handleRemoveSource() {
List<BluetoothLeBroadcastReceiveState> connected = if (mAudioStreamsHelper != null
mAudioStreamsHelper == null && !mAudioStreamsHelper.getConnectedBroadcastIdAndState(
? emptyList() mHysteresisModeFixAvailable).containsKey(mBroadcastId)) {
: mAudioStreamsHelper.getAllConnectedSources();
if (connected.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.noneMatch(id -> id == mBroadcastId)) {
stopSelf(); stopSelf();
} }
} }

View File

@@ -19,6 +19,12 @@ 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_ID;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE; 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.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
import static com.android.settingslib.bluetooth.BluetoothUtils.isAudioSharingHysteresisModeFixAvailable;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
@@ -32,6 +38,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
@@ -67,12 +74,6 @@ public class AudioStreamsHelper {
private final @Nullable LocalBluetoothManager mBluetoothManager; private final @Nullable LocalBluetoothManager mBluetoothManager;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
// Referring to Broadcast Audio Scan Service 1.0
// Table 3.9: Broadcast Receive State characteristic format
// 0x00000000: 0b0 = Not synchronized to BIS_index[x]
// 0xFFFFFFFF: Failed to sync to BIG
private static final long BIS_SYNC_NOT_SYNC_TO_BIS = 0x00000000L;
private static final long BIS_SYNC_FAILED_SYNC_TO_BIG = 0xFFFFFFFFL;
AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) { AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
mBluetoothManager = bluetoothManager; mBluetoothManager = bluetoothManager;
@@ -141,16 +142,31 @@ public class AudioStreamsHelper {
}); });
} }
/** Retrieves a list of all LE broadcast receive states from active sinks. */ /**
public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() { * Gets a map of connected broadcast IDs to their corresponding local broadcast source states.
if (mLeBroadcastAssistant == null) { *
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!"); * <p>If multiple sources have the same broadcast ID, the state of the source that is
return emptyList(); * {@code STREAMING} is preferred.
*/
public Map<Integer, LocalBluetoothLeBroadcastSourceState> getConnectedBroadcastIdAndState(
boolean hysteresisModeFixAvailable) {
if (mBluetoothManager == null || mLeBroadcastAssistant == null) {
Log.w(TAG,
"getConnectedBroadcastIdAndState(): BluetoothManager or LeBroadcastAssistant "
+ "is null!");
return emptyMap();
} }
return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream() return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream()) .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(AudioStreamsHelper::isConnected) .map(state -> new Pair<>(state.getBroadcastId(), getLocalSourceState(state)))
.toList(); .filter(pair -> pair.second == STREAMING
|| (hysteresisModeFixAvailable && pair.second == PAUSED))
.collect(toMap(
p -> p.first,
p -> p.second,
(existingState, newState) -> existingState == STREAMING ? existingState
: newState
));
} }
/** Retrieves a list of all LE broadcast receive states keyed by each active device. */ /** Retrieves a list of all LE broadcast receive states keyed by each active device. */
@@ -163,47 +179,12 @@ public class AudioStreamsHelper {
.collect(toMap(Function.identity(), mLeBroadcastAssistant::getAllSources)); .collect(toMap(Function.identity(), mLeBroadcastAssistant::getAllSources));
} }
/** Retrieves a list of all LE broadcast receive states from sinks with source present. */
@VisibleForTesting
public List<BluetoothLeBroadcastReceiveState> getAllPresentSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllPresentSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(AudioStreamsHelper::hasSourcePresent)
.toList();
}
/** Retrieves LocalBluetoothLeBroadcastAssistant. */ /** Retrieves LocalBluetoothLeBroadcastAssistant. */
@Nullable @Nullable
public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() { public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
return mLeBroadcastAssistant; return mLeBroadcastAssistant;
} }
/** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getBisSyncState().stream()
.anyMatch(
bitmap ->
(bitmap != BIS_SYNC_NOT_SYNC_TO_BIS
&& bitmap != BIS_SYNC_FAILED_SYNC_TO_BIG));
}
/** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean hasSourcePresent(BluetoothLeBroadcastReceiveState state) {
// Referring to Broadcast Audio Scan Service 1.0
// All zero address means no source on the sink device
return !state.getSourceDevice().getAddress().equals("00:00:00:00:00:00");
}
static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
&& state.getBigEncryptionState()
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE;
}
/** /**
* Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is * Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is
* a connected LE device. * a connected LE device.
@@ -226,7 +207,7 @@ public class AudioStreamsHelper {
} }
var deviceHasSource = var deviceHasSource =
leadDevices.stream() leadDevices.stream()
.filter(device -> hasConnectedBroadcastSource(device, manager)) .filter(device -> hasBroadcastSource(device, manager))
.findFirst(); .findFirst();
if (deviceHasSource.isPresent()) { if (deviceHasSource.isPresent()) {
Log.d( Log.d(
@@ -258,38 +239,38 @@ public class AudioStreamsHelper {
return Optional.empty(); return Optional.empty();
} }
return leadDevices.stream() return leadDevices.stream()
.filter(device -> hasConnectedBroadcastSource(device, manager)) .filter(device -> hasBroadcastSource(device, manager))
.findFirst(); .findFirst();
} }
/** /**
* Check if {@link CachedBluetoothDevice} has connected to a broadcast source. * Check if {@link CachedBluetoothDevice} has a broadcast source that is in STREAMING, PAUSED
* or DECRYPTION_FAILED state.
* *
* @param cachedDevice The cached bluetooth device to check. * @param cachedDevice The cached bluetooth device to check.
* @param localBtManager The BT manager to provide BT functions. * @param localBtManager The BT manager to provide BT functions.
* @return Whether the device has connected to a broadcast source. * @return Whether the device has a broadcast source.
*/ */
public static boolean hasConnectedBroadcastSource( public static boolean hasBroadcastSource(
CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) { CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
if (localBtManager == null) { if (localBtManager == null) {
Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null"); Log.d(TAG, "Skip check hasBroadcastSource due to bt manager is null");
return false; return false;
} }
LocalBluetoothLeBroadcastAssistant assistant = LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) { if (assistant == null) {
Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null"); Log.d(TAG, "Skip check hasBroadcastSource due to assistant profile is null");
return false; return false;
} }
List<BluetoothLeBroadcastReceiveState> sourceList = List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice()); assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty() boolean hysteresisModeFixAvailable = isAudioSharingHysteresisModeFixAvailable(
&& (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable( localBtManager.getContext());
localBtManager.getContext()) if (hasReceiveState(sourceList, hysteresisModeFixAvailable)) {
|| sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d( Log.d(
TAG, TAG,
"Lead device has connected broadcast source, device = " "Lead device has broadcast source, device = "
+ cachedDevice.getDevice().getAnonymizedAddress()); + cachedDevice.getDevice().getAnonymizedAddress());
return true; return true;
} }
@@ -297,13 +278,10 @@ public class AudioStreamsHelper {
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list = List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice()); assistant.getAllSources(device.getDevice());
if (!list.isEmpty() if (hasReceiveState(list, hysteresisModeFixAvailable)) {
&& (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
localBtManager.getContext())
|| list.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d( Log.d(
TAG, TAG,
"Member device has connected broadcast source, device = " "Member device has broadcast source, device = "
+ device.getDevice().getAnonymizedAddress()); + device.getDevice().getAnonymizedAddress());
return true; return true;
} }
@@ -311,6 +289,18 @@ public class AudioStreamsHelper {
return false; return false;
} }
private static boolean hasReceiveState(List<BluetoothLeBroadcastReceiveState> states,
boolean hysteresisModeFixAvailable) {
return states.stream().anyMatch(state -> {
var localSourceState = getLocalSourceState(state);
if (hysteresisModeFixAvailable) {
return localSourceState == STREAMING || localSourceState == DECRYPTION_FAILED
|| localSourceState == PAUSED;
}
return localSourceState == STREAMING || localSourceState == DECRYPTION_FAILED;
});
}
/** /**
* Retrieves a list of connected Bluetooth devices that belongs to one {@link * Retrieves a list of connected Bluetooth devices that belongs to one {@link
* CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE * CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE

View File

@@ -16,23 +16,17 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import com.android.settingslib.bluetooth.BluetoothUtils;
public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback { public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
private static final String TAG = "AudioStreamsProgressCategoryCallback";
private final Context mContext;
private final AudioStreamsProgressCategoryController mCategoryController; private final AudioStreamsProgressCategoryController mCategoryController;
public AudioStreamsProgressCategoryCallback( public AudioStreamsProgressCategoryCallback(
Context context,
AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) { AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
mContext = context;
mCategoryController = audioStreamsProgressCategoryController; mCategoryController = audioStreamsProgressCategoryController;
} }
@@ -40,15 +34,11 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA
public void onReceiveStateChanged( public void onReceiveStateChanged(
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state); super.onReceiveStateChanged(sink, sourceId, state);
var sourceState = getLocalSourceState(state);
if (AudioStreamsHelper.isConnected(state)) { switch (sourceState) {
mCategoryController.handleSourceConnected(sink, state); case STREAMING -> mCategoryController.handleSourceStreaming(sink, state);
} else if (AudioStreamsHelper.isBadCode(state)) { case DECRYPTION_FAILED -> mCategoryController.handleSourceConnectBadCode(state);
mCategoryController.handleSourceConnectBadCode(state); case PAUSED -> mCategoryController.handleSourcePaused(sink, state);
} else if (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)
&& AudioStreamsHelper.hasSourcePresent(state)) {
// Keep this check as the last, source might also present in above states
mCategoryController.handleSourcePresent(sink, state);
} }
} }

View File

@@ -16,6 +16,12 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.bluetooth.BluetoothUtils.isAudioSharingHysteresisModeFixAvailable;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toMap;
@@ -137,6 +143,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final @Nullable LocalBluetoothManager mBluetoothManager; private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap = private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private final boolean mHysteresisModeFixAvailable;
private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode; private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode;
private SourceOriginForLogging mSourceFromQrCodeOriginForLogging; private SourceOriginForLogging mSourceFromQrCodeOriginForLogging;
@Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference; @Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference;
@@ -149,7 +156,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager); mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mMediaControlHelper = new MediaControlHelper(mContext, mBluetoothManager); mMediaControlHelper = new MediaControlHelper(mContext, mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(context, this); mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
mContext);
} }
@Override @Override
@@ -260,7 +269,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// change it's state. // change it's state.
existingPreference.setAudioStreamMetadata(source); existingPreference.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED if (fromState != AudioStreamState.SOURCE_ADDED
&& (!isAudioSharingHysteresisModeFixAvailable(mContext) && (!mHysteresisModeFixAvailable
|| fromState != AudioStreamState.SOURCE_PRESENT)) { || fromState != AudioStreamState.SOURCE_PRESENT)) {
Log.w( Log.w(
TAG, TAG,
@@ -336,8 +345,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "handleSourceLost()"); Log.d(TAG, "handleSourceLost()");
} }
if (mAudioStreamsHelper.getAllConnectedSources().stream() if (mAudioStreamsHelper.getConnectedBroadcastIdAndState(
.anyMatch(connected -> connected.getBroadcastId() == broadcastId)) { mHysteresisModeFixAvailable).containsKey(broadcastId)) {
Log.d( Log.d(
TAG, TAG,
"handleSourceLost() : keep this preference as the source is still connected."); "handleSourceLost() : keep this preference as the source is still connected.");
@@ -366,14 +375,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// not, means the source is removed from the sink, we move back the preference to SYNCED // not, means the source is removed from the sink, we move back the preference to SYNCED
// state. // state.
if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
|| (isAudioSharingHysteresisModeFixAvailable(mContext) || (mHysteresisModeFixAvailable
&& preference.getAudioStreamState() && preference.getAudioStreamState()
== AudioStreamState.SOURCE_PRESENT)) == AudioStreamState.SOURCE_PRESENT))
&& mAudioStreamsHelper.getAllConnectedSources().stream() && !mAudioStreamsHelper.getConnectedBroadcastIdAndState(
.noneMatch( mHysteresisModeFixAvailable).containsKey(
connected -> preference.getAudioStreamBroadcastId())) {
connected.getBroadcastId()
== preference.getAudioStreamBroadcastId())) {
ThreadUtils.postOnMainThread( ThreadUtils.postOnMainThread(
() -> { () -> {
@@ -395,27 +402,27 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Expect one of the following: // Expect one of the following:
// 1) No preference existed, create new preference with state SOURCE_ADDED // 1) No preference existed, create new preference with state SOURCE_ADDED
// 2) Any other state, move to SOURCE_ADDED // 2) Any other state, move to SOURCE_ADDED
void handleSourceConnected( void handleSourceStreaming(
BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) { BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "handleSourceConnected()"); Log.d(TAG, "handleSourceStreaming()");
} }
if (!AudioStreamsHelper.isConnected(receiveState)) { if (getLocalSourceState(receiveState) != STREAMING) {
return; return;
} }
var broadcastIdConnected = receiveState.getBroadcastId(); var broadcastIdStreaming = receiveState.getBroadcastId();
Optional<BluetoothLeBroadcastMetadata> metadata = Optional<BluetoothLeBroadcastMetadata> metadata =
getMetadataMatchingByBroadcastId( getMetadataMatchingByBroadcastId(
device, receiveState.getSourceId(), broadcastIdConnected); device, receiveState.getSourceId(), broadcastIdStreaming);
handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState); handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState);
mBroadcastIdToPreferenceMap.compute( mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected, broadcastIdStreaming,
(k, existingPreference) -> { (k, existingPreference) -> {
if (existingPreference == null) { if (existingPreference == null) {
// No existing preference for this source even if it's already connected, // No existing preference for this source even if it's already streaming,
// add one and set initial state to SOURCE_ADDED. This could happen because // add one and set initial state to SOURCE_ADDED. This could happen because
// we retrieves the connected source during onStart() from // we retrieves the streaming source during onStart() from
// AudioStreamsHelper#getAllConnectedSources() even before the source is // AudioStreamsHelper#getAllStreamingSources() even before the source is
// founded by scanning. // founded by scanning.
return metadata.isPresent() return metadata.isPresent()
? addNewPreference( ? addNewPreference(
@@ -440,7 +447,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "handleSourceConnectBadCode()"); Log.d(TAG, "handleSourceConnectBadCode()");
} }
if (!AudioStreamsHelper.isBadCode(receiveState)) { if (getLocalSourceState(receiveState) != DECRYPTION_FAILED) {
return; return;
} }
mBroadcastIdToPreferenceMap.computeIfPresent( mBroadcastIdToPreferenceMap.computeIfPresent(
@@ -467,29 +474,28 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Find preference by receiveState and decide next state. // Find preference by receiveState and decide next state.
// Expect one preference existed, move to SOURCE_PRESENT // Expect one preference existed, move to SOURCE_PRESENT
void handleSourcePresent( void handleSourcePaused(
BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) { BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "handleSourcePresent()"); Log.d(TAG, "handleSourcePaused()");
} }
if (!AudioStreamsHelper.hasSourcePresent(receiveState)) { if (!mHysteresisModeFixAvailable || getLocalSourceState(receiveState) != PAUSED) {
return; return;
} }
var broadcastIdConnected = receiveState.getBroadcastId(); var broadcastIdPaused = receiveState.getBroadcastId();
Optional<BluetoothLeBroadcastMetadata> metadata = Optional<BluetoothLeBroadcastMetadata> metadata =
getMetadataMatchingByBroadcastId( getMetadataMatchingByBroadcastId(
device, receiveState.getSourceId(), broadcastIdConnected); device, receiveState.getSourceId(), broadcastIdPaused);
handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState); handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState);
mBroadcastIdToPreferenceMap.compute( mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected, broadcastIdPaused,
(k, existingPreference) -> { (k, existingPreference) -> {
if (existingPreference == null) { if (existingPreference == null) {
// No existing preference for this source even if it's already connected, // No existing preference for this source even if it's already existed but
// add one and set initial state to SOURCE_PRESENT. This could happen // currently paused, add one and set initial state to SOURCE_PRESENT. This
// because // could happen because we retrieves the paused source during onStart() from
// we retrieves the connected source during onStart() from // AudioStreamsHelper#getAllPausedSources() even before the source is
// AudioStreamsHelper#getAllPresentSources() even before the source is
// founded by scanning. // founded by scanning.
return metadata.isPresent() return metadata.isPresent()
? addNewPreference( ? addNewPreference(
@@ -580,56 +586,43 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mExecutor.execute( mExecutor.execute(
() -> { () -> {
// Handle QR code scan, display currently connected streams then start scanning // Handle QR code scan, display currently streaming or paused streams then start
// sequentially // scanning sequentially
handleSourceFromQrCodeIfExists(); handleSourceFromQrCodeIfExists();
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources = Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources =
mAudioStreamsHelper.getAllSourcesByDevice(); mAudioStreamsHelper.getAllSourcesByDevice();
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> connectedSources = getStreamSourcesByDevice(sources).forEach(
getConnectedSources(sources);
if (isAudioSharingHysteresisModeFixAvailable(mContext)) {
// With hysteresis mode, we prioritize showing connected sources first.
// If no connected sources are found, we then show present sources.
if (!connectedSources.isEmpty()) {
connectedSources.forEach(
(device, stateList) -> (device, stateList) ->
stateList.forEach( stateList.forEach(
state -> handleSourceConnected(device, state))); state -> handleSourceStreaming(device, state)));
} else { if (mHysteresisModeFixAvailable) {
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getPausedSourcesByDevice(sources).forEach(
presentSources = getPresentSources(sources);
presentSources.forEach(
(device, stateList) -> (device, stateList) ->
stateList.forEach( stateList.forEach(
state -> handleSourcePresent(device, state))); state -> handleSourcePaused(device, state)));
}
} else {
connectedSources.forEach(
(device, stateList) ->
stateList.forEach(
state -> handleSourceConnected(device, state)));
} }
mLeBroadcastAssistant.startSearchingForSources(emptyList()); mLeBroadcastAssistant.startSearchingForSources(emptyList());
mMediaControlHelper.start(); mMediaControlHelper.start();
}); });
} }
private Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getConnectedSources( private Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getStreamSourcesByDevice(
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources) { Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources) {
return sources.entrySet().stream() return sources.entrySet().stream()
.filter( .filter(
entry -> entry ->
entry.getValue().stream().anyMatch(AudioStreamsHelper::isConnected)) entry.getValue().stream().anyMatch(
state -> getLocalSourceState(state) == STREAMING))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
private Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getPresentSources( private Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getPausedSourcesByDevice(
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources) { Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources) {
return sources.entrySet().stream() return sources.entrySet().stream()
.filter( .filter(
entry -> entry ->
entry.getValue().stream() entry.getValue().stream()
.anyMatch(AudioStreamsHelper::hasSourcePresent)) .anyMatch(state -> getLocalSourceState(state) == PAUSED))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
@@ -742,8 +735,4 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
dialog.dismiss(); dialog.dismiss();
}); });
} }
private static boolean isAudioSharingHysteresisModeFixAvailable(Context context) {
return BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(context);
}
} }

View File

@@ -21,12 +21,8 @@ import static com.android.internal.jank.InteractionJankMonitor.Configuration;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.XmlRes;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference; import androidx.preference.TwoStatePreference;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -108,12 +104,6 @@ public abstract class InstrumentedPreferenceFragment extends ObservablePreferenc
} }
} }
@Override
public void addPreferencesFromResource(@XmlRes int preferencesResId) {
super.addPreferencesFromResource(preferencesResId);
updateActivityTitleWithScreenTitle(getPreferenceScreen());
}
@Override @Override
public <T extends Preference> T findPreference(CharSequence key) { public <T extends Preference> T findPreference(CharSequence key) {
if (key == null) { if (key == null) {
@@ -147,17 +137,6 @@ public abstract class InstrumentedPreferenceFragment extends ObservablePreferenc
mMetricsFeatureProvider.logClickedPreference(preference, getMetricsCategory()); mMetricsFeatureProvider.logClickedPreference(preference, getMetricsCategory());
} }
protected void updateActivityTitleWithScreenTitle(PreferenceScreen screen) {
if (screen != null) {
final CharSequence title = screen.getTitle();
if (!TextUtils.isEmpty(title)) {
getActivity().setTitle(title);
} else {
Log.w(TAG, "Screen title missing for fragment " + this.getClass().getName());
}
}
}
private static final class OnScrollListener extends RecyclerView.OnScrollListener { private static final class OnScrollListener extends RecyclerView.OnScrollListener {
private final InteractionJankMonitor mMonitor = InteractionJankMonitor.getInstance(); private final InteractionJankMonitor mMonitor = InteractionJankMonitor.getInstance();
private final String mClassName; private final String mClassName;

View File

@@ -415,7 +415,6 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
removeControllersForHybridMode(); removeControllersForHybridMode();
} }
setPreferenceScreen(screen); setPreferenceScreen(screen);
updateActivityTitleWithScreenTitle(screen);
} else { } else {
addPreferencesFromResource(resId); addPreferencesFromResource(resId);
screen = getPreferenceScreen(); screen = getPreferenceScreen();

View File

@@ -21,15 +21,16 @@ import android.provider.Settings
import android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC import android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC
import android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL import android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.settings.PreferenceRestrictionMixin import com.android.settings.PreferenceRestrictionMixin
import com.android.settings.R import com.android.settings.R
import com.android.settings.flags.Flags import com.android.settings.flags.Flags
import com.android.settingslib.PrimarySwitchPreference import com.android.settingslib.PrimarySwitchPreferenceBinding
import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.datastore.SettingsSystemStore import com.android.settingslib.datastore.SettingsSystemStore
import com.android.settingslib.metadata.BooleanPreference import com.android.settingslib.metadata.BooleanValuePreference
import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.ProvidePreferenceScreen import com.android.settingslib.metadata.ProvidePreferenceScreen
@@ -42,10 +43,11 @@ import com.android.settingslib.preference.PreferenceScreenCreator
@ProvidePreferenceScreen(AutoBrightnessScreen.KEY) @ProvidePreferenceScreen(AutoBrightnessScreen.KEY)
class AutoBrightnessScreen : class AutoBrightnessScreen :
PreferenceScreenCreator, PreferenceScreenCreator,
PreferenceScreenBinding, PreferenceScreenBinding, // binding for screen page
PrimarySwitchPreferenceBinding, // binding for screen entry point widget
PreferenceAvailabilityProvider, PreferenceAvailabilityProvider,
PreferenceRestrictionMixin, PreferenceRestrictionMixin,
BooleanPreference { BooleanValuePreference {
override val key: String override val key: String
get() = KEY get() = KEY
@@ -93,15 +95,10 @@ class AutoBrightnessScreen :
override val useAdminDisabledSummary: Boolean override val useAdminDisabledSummary: Boolean
get() = true get() = true
override fun createWidget(context: Context) = PrimarySwitchPreference(context) override fun bind(preference: Preference, metadata: PreferenceMetadata) =
when (preference) {
override fun bind(preference: Preference, metadata: PreferenceMetadata) { is PreferenceScreen -> super<PreferenceScreenBinding>.bind(preference, metadata)
super.bind(preference, metadata) else -> super<PrimarySwitchPreferenceBinding>.bind(preference, metadata)
(preference as PrimarySwitchPreference).apply {
isSwitchEnabled = isEnabled
// "true" is not the real default value (it is provided by AutoBrightnessDataStore)
isChecked = preferenceDataStore!!.getBoolean(key, true)
}
} }
/** /**

View File

@@ -20,12 +20,13 @@ import android.Manifest
import android.content.Context import android.content.Context
import android.os.PowerManager import android.os.PowerManager
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.settings.R import com.android.settings.R
import com.android.settings.flags.Flags import com.android.settings.flags.Flags
import com.android.settingslib.PrimarySwitchPreference import com.android.settingslib.PrimarySwitchPreferenceBinding
import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.Permissions import com.android.settingslib.datastore.Permissions
import com.android.settingslib.metadata.BooleanPreference import com.android.settingslib.metadata.BooleanValuePreference
import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.ProvidePreferenceScreen import com.android.settingslib.metadata.ProvidePreferenceScreen
@@ -39,8 +40,9 @@ import com.android.settingslib.preference.PreferenceScreenCreator
@ProvidePreferenceScreen(DarkModeScreen.KEY) @ProvidePreferenceScreen(DarkModeScreen.KEY)
class DarkModeScreen(context: Context) : class DarkModeScreen(context: Context) :
PreferenceScreenCreator, PreferenceScreenCreator,
PreferenceScreenBinding, PreferenceScreenBinding, // binding for screen page
BooleanPreference, PrimarySwitchPreferenceBinding, // binding for screen entry point widget
BooleanValuePreference,
PreferenceSummaryProvider { PreferenceSummaryProvider {
private val darkModeStorage = DarkModeStorage(context) private val darkModeStorage = DarkModeStorage(context)
@@ -82,14 +84,11 @@ class DarkModeScreen(context: Context) :
override fun storage(context: Context): KeyValueStore = darkModeStorage override fun storage(context: Context): KeyValueStore = darkModeStorage
override fun createWidget(context: Context) = PrimarySwitchPreference(context)
override fun bind(preference: Preference, metadata: PreferenceMetadata) { override fun bind(preference: Preference, metadata: PreferenceMetadata) {
super.bind(preference, metadata)
if (preference is DarkModePreference) preference.setCatalystEnabled(true) if (preference is DarkModePreference) preference.setCatalystEnabled(true)
(preference as? PrimarySwitchPreference)?.apply { when (preference) {
isSwitchEnabled = isEnabled is PreferenceScreen -> super<PreferenceScreenBinding>.bind(preference, metadata)
isChecked = darkModeStorage.getBoolean(KEY) == true else -> super<PrimarySwitchPreferenceBinding>.bind(preference, metadata)
} }
} }

View File

@@ -16,29 +16,55 @@
package com.android.settings.inputmethod; package com.android.settings.inputmethod;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.database.ContentObserver;
import android.hardware.input.InputSettings; import android.hardware.input.InputSettings;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.core.SliderPreferenceController; import com.android.settings.core.SliderPreferenceController;
import com.android.settings.widget.SeekBarPreference; import com.android.settings.widget.SeekBarPreference;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
public class MouseScrollingSpeedPreferenceController extends SliderPreferenceController {
public class MouseScrollingSpeedPreferenceController extends SliderPreferenceController implements
Preference.OnPreferenceChangeListener, LifecycleObserver, OnStop, OnStart {
private final ContentResolver mContentResolver;
private final ContentObserver mContentObserver;
@Nullable
private SeekBarPreference mPreference;
public MouseScrollingSpeedPreferenceController(@NonNull Context context, @NonNull String key) { public MouseScrollingSpeedPreferenceController(@NonNull Context context, @NonNull String key) {
super(context, key); super(context, key);
mContentResolver = context.getContentResolver();
mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
updateAvailabilityStatus();
}
};
} }
@Override @Override
public void displayPreference(@NonNull PreferenceScreen screen) { public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen); super.displayPreference(screen);
SeekBarPreference preference = screen.findPreference(getPreferenceKey()); mPreference = screen.findPreference(getPreferenceKey());
preference.setMax(getMax()); mPreference.setMax(getMax());
preference.setMin(getMin()); mPreference.setMin(getMin());
preference.setProgress(getSliderPosition()); mPreference.setProgress(getSliderPosition());
updateState(preference); updateState(mPreference);
} }
@Override @Override
@@ -46,7 +72,7 @@ public class MouseScrollingSpeedPreferenceController extends SliderPreferenceCon
if (!InputSettings.isMouseScrollingAccelerationFeatureFlagEnabled()) { if (!InputSettings.isMouseScrollingAccelerationFeatureFlagEnabled()) {
return UNSUPPORTED_ON_DEVICE; return UNSUPPORTED_ON_DEVICE;
} }
return AVAILABLE; return shouldEnableSlideBar() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
} }
@Override @Override
@@ -73,4 +99,30 @@ public class MouseScrollingSpeedPreferenceController extends SliderPreferenceCon
public int getMax() { public int getMax() {
return InputSettings.MAX_MOUSE_SCROLLING_SPEED; return InputSettings.MAX_MOUSE_SCROLLING_SPEED;
} }
/**
* Returns whether the mouse scrolling speed slide bar should allow users to customize or not.
*/
public boolean shouldEnableSlideBar() {
return !InputSettings.isMouseScrollingAccelerationEnabled(mContext);
}
@Override
public void onStart() {
mContentResolver.registerContentObserver(
Settings.System.getUriFor(
Settings.System.MOUSE_SCROLLING_ACCELERATION),
/* notifyForDescendants= */ false, mContentObserver);
}
@Override
public void onStop() {
mContentResolver.unregisterContentObserver(mContentObserver);
}
private void updateAvailabilityStatus() {
if (mPreference != null) {
mPreference.setEnabled(shouldEnableSlideBar());
}
}
} }

View File

@@ -57,8 +57,8 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
private KeyboardLayout[] mKeyboardLayouts; private KeyboardLayout[] mKeyboardLayouts;
private PreferenceScreen mScreen; private PreferenceScreen mScreen;
private String mPreviousSelection; private String mPreviousSelection;
private String mFinalSelectedLayout; private String mFinalSelectedLayoutDescriptor;
private String mLayout; private String mSelectedLayoutDescriptor;
private MetricsFeatureProvider mMetricsFeatureProvider; private MetricsFeatureProvider mMetricsFeatureProvider;
private KeyboardLayoutSelectedCallback mKeyboardLayoutSelectedCallback; private KeyboardLayoutSelectedCallback mKeyboardLayoutSelectedCallback;
@@ -83,8 +83,8 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
mInputMethodSubtype = mInputMethodSubtype =
arguments.getParcelable( arguments.getParcelable(
InputPeripheralsSettingsUtils.EXTRA_INPUT_METHOD_SUBTYPE); InputPeripheralsSettingsUtils.EXTRA_INPUT_METHOD_SUBTYPE);
mLayout = getSelectedLayoutLabel(); mSelectedLayoutDescriptor = getSelectedLayoutDescriptor();
mFinalSelectedLayout = mLayout; mFinalSelectedLayoutDescriptor = mSelectedLayoutDescriptor;
mKeyboardLayouts = mIm.getKeyboardLayoutListForInputDevice( mKeyboardLayouts = mIm.getKeyboardLayoutListForInputDevice(
mInputDeviceIdentifier, mUserId, mInputMethodInfo, mInputMethodSubtype); mInputDeviceIdentifier, mUserId, mInputMethodInfo, mInputMethodSubtype);
InputPeripheralsSettingsUtils.sortKeyboardLayoutsByLabel(mKeyboardLayouts); InputPeripheralsSettingsUtils.sortKeyboardLayoutsByLabel(mKeyboardLayouts);
@@ -106,8 +106,12 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
@Override @Override
public void onStop() { public void onStop() {
if (mLayout != null && !mLayout.equals(mFinalSelectedLayout)) { if (mSelectedLayoutDescriptor != null
String change = "From:" + mLayout + ", to:" + mFinalSelectedLayout; && !mSelectedLayoutDescriptor.equals(mFinalSelectedLayoutDescriptor)) {
String change = "From:"
+ getLayoutLabel(mSelectedLayoutDescriptor)
+ ", to:"
+ getLayoutLabel(mFinalSelectedLayoutDescriptor);
mMetricsFeatureProvider.action( mMetricsFeatureProvider.action(
mContext, SettingsEnums.ACTION_PK_LAYOUT_CHANGED, change); mContext, SettingsEnums.ACTION_PK_LAYOUT_CHANGED, change);
} }
@@ -152,7 +156,7 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
} }
setLayout(pref); setLayout(pref);
mPreviousSelection = preference.getKey(); mPreviousSelection = preference.getKey();
mFinalSelectedLayout = pref.getTitle().toString(); mFinalSelectedLayoutDescriptor = mPreviousSelection;
return true; return true;
} }
@@ -182,12 +186,12 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
pref = new TickButtonPreference(mScreen.getContext()); pref = new TickButtonPreference(mScreen.getContext());
pref.setTitle(layout.getLabel()); pref.setTitle(layout.getLabel());
if (mLayout.equals(layout.getLabel())) { if (mSelectedLayoutDescriptor.equals(layout.getDescriptor())) {
if (mKeyboardLayoutSelectedCallback != null) { if (mKeyboardLayoutSelectedCallback != null) {
mKeyboardLayoutSelectedCallback.onSelected(layout); mKeyboardLayoutSelectedCallback.onSelected(layout);
} }
pref.setSelected(true); pref.setSelected(true);
mPreviousSelection = layout.getDescriptor(); mPreviousSelection = mSelectedLayoutDescriptor;
} }
pref.setKey(layout.getDescriptor()); pref.setKey(layout.getDescriptor());
mScreen.addPreference(pref); mScreen.addPreference(pref);
@@ -204,15 +208,19 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
mPreferenceMap.get(preference).getDescriptor()); mPreferenceMap.get(preference).getDescriptor());
} }
private String getSelectedLayoutLabel() { private String getSelectedLayoutDescriptor() {
String label = mContext.getString(R.string.keyboard_default_layout);
KeyboardLayoutSelectionResult result = InputPeripheralsSettingsUtils.getKeyboardLayout( KeyboardLayoutSelectionResult result = InputPeripheralsSettingsUtils.getKeyboardLayout(
mIm, mUserId, mInputDeviceIdentifier, mInputMethodInfo, mInputMethodSubtype); mIm, mUserId, mInputDeviceIdentifier, mInputMethodInfo, mInputMethodSubtype);
return result.getLayoutDescriptor();
}
private String getLayoutLabel(String descriptor) {
String label = mContext.getString(R.string.keyboard_default_layout);
KeyboardLayout[] keyboardLayouts = InputPeripheralsSettingsUtils.getKeyboardLayouts( KeyboardLayout[] keyboardLayouts = InputPeripheralsSettingsUtils.getKeyboardLayouts(
mIm, mUserId, mInputDeviceIdentifier, mInputMethodInfo, mInputMethodSubtype); mIm, mUserId, mInputDeviceIdentifier, mInputMethodInfo, mInputMethodSubtype);
if (result.getLayoutDescriptor() != null) { if (descriptor != null) {
for (KeyboardLayout keyboardLayout : keyboardLayouts) { for (KeyboardLayout keyboardLayout : keyboardLayouts) {
if (keyboardLayout.getDescriptor().equals(result.getLayoutDescriptor())) { if (keyboardLayout.getDescriptor().equals(descriptor)) {
label = keyboardLayout.getLabel(); label = keyboardLayout.getLabel();
break; break;
} }

View File

@@ -0,0 +1,203 @@
/**
* Copyright (C) 2025 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.localepicker;
import static com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_APPS_LOCALE;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.internal.app.AppLocaleCollector;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
import com.android.settings.applications.manageapplications.ManageApplicationsUtil;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/** A controller for handling suggested locale of app. */
public class AppLocaleSuggestedListPreferenceController extends
BasePreferenceController implements LocaleListSearchCallback {
private static final String TAG = "AppLocaleSuggestedListPreferenceController";
private static final String KEY_PREFERENCE_CATEGORY_APP_LANGUAGE_SUGGESTED =
"app_language_suggested_category";
private static final String KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST =
"app_locale_suggested_list";
private static final String KEY_PREFERENCE_CATEGORY_ADD_A_LANGUAGE_SUGGESTED =
"system_language_suggested_category";
@SuppressWarnings("NullAway")
private PreferenceCategory mPreferenceCategory;
private Set<LocaleStore.LocaleInfo> mLocaleList;
private List<LocaleStore.LocaleInfo> mLocaleOptions;
private Map<String, Preference> mSuggestedPreferences;
private boolean mIsCountryMode;
@Nullable private LocaleStore.LocaleInfo mParentLocale;
private AppLocaleCollector mAppLocaleCollector;
@SuppressWarnings("NullAway")
private String mPackageName;
private boolean mIsNumberingSystemMode;
@SuppressWarnings("NullAway")
public AppLocaleSuggestedListPreferenceController(@NonNull Context context,
@NonNull String preferenceKey) {
super(context, preferenceKey);
}
@SuppressWarnings("NullAway")
public AppLocaleSuggestedListPreferenceController(@NonNull Context context,
@NonNull String preferenceKey, @Nullable String packageName,
boolean isNumberingSystemMode, @NonNull LocaleStore.LocaleInfo parentLocale) {
super(context, preferenceKey);
mPackageName = packageName;
mIsNumberingSystemMode = isNumberingSystemMode;
mParentLocale = parentLocale;
mIsCountryMode = mParentLocale != null;
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceCategory = screen.findPreference(
(mIsNumberingSystemMode || mIsCountryMode)
? KEY_PREFERENCE_CATEGORY_ADD_A_LANGUAGE_SUGGESTED
: KEY_PREFERENCE_CATEGORY_APP_LANGUAGE_SUGGESTED);
mAppLocaleCollector = new AppLocaleCollector(mContext, mPackageName);
mSuggestedPreferences = new ArrayMap<>();
mLocaleOptions = new ArrayList<>();
updatePreferences();
}
private void updatePreferences() {
if (mPreferenceCategory == null) {
Log.d(TAG, "updatePreferences, mPreferenceCategory is null");
return;
}
List<LocaleStore.LocaleInfo> result = LocaleUtils.getSortedLocaleList(
getSuggestedLocaleList(), mIsCountryMode);
final Map<String, Preference> existingSuggestedPreferences = mSuggestedPreferences;
mSuggestedPreferences = new ArrayMap<>();
setupSuggestedPreference(result, existingSuggestedPreferences);
for (Preference pref : existingSuggestedPreferences.values()) {
mPreferenceCategory.removePreference(pref);
}
}
@Override
public void onSearchListChanged(@NonNull List<LocaleStore.LocaleInfo> newList,
@Nullable CharSequence prefix) {
if (mPreferenceCategory == null) {
Log.d(TAG, "onSearchListChanged, mPreferenceCategory is null");
return;
}
mPreferenceCategory.removeAll();
final Map<String, Preference> existingSuggestedPreferences = mSuggestedPreferences;
List<LocaleStore.LocaleInfo> sortedList = getSuggestedLocaleList();
newList = LocaleUtils.getSortedLocaleFromSearchList(newList, sortedList, mIsCountryMode);
setupSuggestedPreference(newList, existingSuggestedPreferences);
}
private void setupSuggestedPreference(List<LocaleStore.LocaleInfo> localeInfoList,
Map<String, Preference> existingSuggestedPreferences) {
for (LocaleStore.LocaleInfo locale : localeInfoList) {
if (mIsNumberingSystemMode || mIsCountryMode) {
Preference pref = existingSuggestedPreferences.remove(locale.getId());
if (pref == null) {
pref = new Preference(mContext);
setupPreference(pref, locale);
mPreferenceCategory.addPreference(pref);
}
} else {
SelectorWithWidgetPreference pref =
(SelectorWithWidgetPreference) existingSuggestedPreferences.remove(
locale.getId());
if (pref == null) {
pref = new SelectorWithWidgetPreference(mContext);
setupPreference(pref, locale);
mPreferenceCategory.addPreference(pref);
}
}
}
Log.d(TAG, "setupSuggestedPreference, mPreferenceCategory setVisible"
+ (mPreferenceCategory.getPreferenceCount() > 0));
mPreferenceCategory.setVisible(mPreferenceCategory.getPreferenceCount() > 0);
}
private void setupPreference(Preference pref, LocaleStore.LocaleInfo locale) {
String localeName = mIsCountryMode ? locale.getFullCountryNameNative()
: locale.getFullNameNative();
if (pref instanceof SelectorWithWidgetPreference) {
((SelectorWithWidgetPreference) pref).setChecked(locale.isAppCurrentLocale());
}
pref.setTitle(locale.isSystemLocale()
? mContext.getString(R.string.preference_of_system_locale_summary)
: localeName);
pref.setKey(locale.toString());
pref.setOnPreferenceClickListener(clickedPref -> {
LocaleUtils.onLocaleSelected(mContext, locale, mPackageName);
((Activity) mContext).finish();
return true;
});
mSuggestedPreferences.put(locale.getId(), pref);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
protected List<LocaleStore.LocaleInfo> getSuggestedLocaleList() {
setupLocaleList();
if (mLocaleList != null && !mLocaleList.isEmpty()) {
mLocaleOptions.addAll(
mLocaleList.stream().filter(localeInfo -> (localeInfo.isSuggested())).collect(
Collectors.toList()));
} else {
Log.d(TAG, "Can not get suggested locales because the locale list is null or empty.");
}
return mLocaleOptions;
}
private void setupLocaleList() {
mLocaleList = mAppLocaleCollector.getSupportedLocaleList(mParentLocale,
false, mIsCountryMode);
mLocaleOptions.clear();
}
@Override
public @NonNull String getPreferenceKey() {
return KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST;
}
}

View File

@@ -16,16 +16,53 @@
package com.android.settings.localepicker; package com.android.settings.localepicker;
import static com.android.settings.flags.Flags.localeNotificationEnabled;
import static com.android.settings.localepicker.LocaleListEditor.EXTRA_RESULT_LOCALE;
import static com.android.settings.localepicker.RegionAndNumberingSystemPickerFragment.EXTRA_IS_NUMBERING_SYSTEM;
import static com.android.settings.localepicker.RegionAndNumberingSystemPickerFragment.EXTRA_TARGET_LOCALE;
import android.app.Dialog;
import android.app.LocaleManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.LocaleList; import android.os.LocaleList;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import com.android.internal.app.LocaleHelper;
import com.android.internal.app.LocaleStore;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Collectors;
/** /**
* A locale utility class. * A locale utility class.
*/ */
public class LocaleUtils { public class LocaleUtils {
private static final String TAG = "LocaleUtils";
private static final String CHANNEL_ID_SUGGESTION = "suggestion";
private static final String CHANNEL_ID_SUGGESTION_TO_USER = "Locale suggestion";
private static final String EXTRA_APP_LOCALE = "app_locale";
private static final String EXTRA_NOTIFICATION_ID = "notification_id";
private static final int SIM_LOCALE = 1 << 0;
private static final int SYSTEM_LOCALE = 1 << 1;
private static final int APP_LOCALE = 1 << 2;
private static final int IME_LOCALE = 1 << 3;
/** /**
* Checks if the languageTag is in the system locale. Since in the current design, the system * Checks if the languageTag is in the system locale. Since in the current design, the system
* language list would not show two locales with the same language and region but different * language list would not show two locales with the same language and region but different
@@ -50,4 +87,191 @@ public class LocaleUtils {
} }
return false; return false;
} }
/**
* Logs the locale, sets the default locale for the app then broadcasts it.
*
* @param context Context
* @param localeInfo locale info
*/
public static void onLocaleSelected(@NonNull Context context,
@NonNull LocaleStore.LocaleInfo localeInfo,
@NonNull String packageName) {
if (localeInfo.getLocale() == null || localeInfo.isSystemLocale()) {
setAppDefaultLocale(context, "", packageName);
} else {
logLocaleSource(context, localeInfo);
setAppDefaultLocale(context, localeInfo.getLocale().toLanguageTag(),
packageName);
broadcastAppLocaleChange(context, localeInfo, packageName);
}
}
private static void logLocaleSource(Context context, LocaleStore.LocaleInfo localeInfo) {
if (!localeInfo.isSuggested() || localeInfo.isAppCurrentLocale()) {
return;
}
int localeSource = 0;
if (hasSuggestionType(localeInfo,
LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE)) {
localeSource |= SYSTEM_LOCALE;
}
if (hasSuggestionType(localeInfo,
LocaleStore.LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE)) {
localeSource |= APP_LOCALE;
}
if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE)) {
localeSource |= IME_LOCALE;
}
if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_SIM)) {
localeSource |= SIM_LOCALE;
}
MetricsFeatureProvider metricsFeatureProvider =
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
metricsFeatureProvider.action(context,
SettingsEnums.ACTION_CHANGE_APP_LANGUAGE_FROM_SUGGESTED, localeSource);
}
private static boolean hasSuggestionType(LocaleStore.LocaleInfo localeInfo,
int suggestionType) {
return localeInfo.isSuggestionOfType(suggestionType);
}
private static void setAppDefaultLocale(Context context, String languageTag,
String packageName) {
LocaleManager localeManager = context.getSystemService(LocaleManager.class);
if (localeManager == null) {
Log.w(TAG, "LocaleManager is null, cannot set default app locale");
return;
}
localeManager.setApplicationLocales(packageName,
LocaleList.forLanguageTags(languageTag));
}
private static void broadcastAppLocaleChange(Context context, LocaleStore.LocaleInfo localeInfo,
String packageName) {
if (!localeNotificationEnabled()) {
Log.w(TAG, "Locale notification is not enabled");
return;
}
if (localeInfo.isAppCurrentLocale()) {
return;
}
try {
NotificationController notificationController = NotificationController.getInstance(
context);
String localeTag = localeInfo.getLocale().toLanguageTag();
int uid = context.getPackageManager().getApplicationInfo(packageName,
PackageManager.GET_META_DATA).uid;
boolean launchNotification = notificationController.shouldTriggerNotification(
uid, localeTag);
if (launchNotification) {
triggerNotification(
context,
notificationController.getNotificationId(localeTag),
context.getString(R.string.title_system_locale_addition,
localeInfo.getFullNameNative()),
context.getString(R.string.desc_system_locale_addition),
localeTag);
MetricsFeatureProvider metricsFeatureProvider =
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
metricsFeatureProvider.action(context,
SettingsEnums.ACTION_NOTIFICATION_FOR_SYSTEM_LOCALE);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to find info for package: " + packageName);
}
}
private static void triggerNotification(
Context context,
int notificationId,
String title,
String description,
String localeTag) {
NotificationManager notificationManager = context.getSystemService(
NotificationManager.class);
final boolean channelExist =
notificationManager.getNotificationChannel(CHANNEL_ID_SUGGESTION) != null;
// Create an alert channel if it does not exist
if (!channelExist) {
NotificationChannel channel =
new NotificationChannel(
CHANNEL_ID_SUGGESTION,
CHANNEL_ID_SUGGESTION_TO_USER,
NotificationManager.IMPORTANCE_DEFAULT);
channel.setSound(/* sound */ null, /* audioAttributes */ null); // silent notification
notificationManager.createNotificationChannel(channel);
}
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, CHANNEL_ID_SUGGESTION)
.setSmallIcon(R.drawable.ic_settings_language)
.setAutoCancel(true)
.setContentTitle(title)
.setContentText(description)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(
createPendingIntent(context, localeTag, notificationId, false))
.setDeleteIntent(
createPendingIntent(context, localeTag, notificationId, true));
notificationManager.notify(notificationId, builder.build());
}
private static PendingIntent createPendingIntent(Context context, String locale,
int notificationId,
boolean isDeleteIntent) {
Intent intent = isDeleteIntent
? new Intent(context, NotificationCancelReceiver.class)
: new Intent(context, NotificationActionActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra(EXTRA_APP_LOCALE, locale)
.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
int flag = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT;
int elapsedTime = (int) SystemClock.elapsedRealtimeNanos();
return isDeleteIntent
? PendingIntent.getBroadcast(context, elapsedTime, intent, flag)
: PendingIntent.getActivity(context, elapsedTime, intent, flag);
}
/**
* Sort the locale's list.
*
* @param localeInfos list of locale Infos
* @param isCountryMode Whether the locale page is in country mode or not.
* @return localeInfos list of locale Infos
*/
public static @NonNull List<LocaleStore.LocaleInfo> getSortedLocaleList(
@NonNull List<LocaleStore.LocaleInfo> localeInfos, boolean isCountryMode) {
final Locale sortingLocale = Locale.getDefault();
final LocaleHelper.LocaleInfoComparator comp = new LocaleHelper.LocaleInfoComparator(
sortingLocale, isCountryMode);
Collections.sort(localeInfos, comp);
return localeInfos;
}
/**
* Sort the locale's list by keywords in search.
*
* @param searchList locale Infos in search bar
* @param localeList list of locale Infos
* @param isCountryMode Whether the locale page is in country mode or not.
* @return localeInfos list of locale Infos
*/
public static @NonNull List<LocaleStore.LocaleInfo> getSortedLocaleFromSearchList(
@NonNull List<LocaleStore.LocaleInfo> searchList,
@NonNull List<LocaleStore.LocaleInfo> localeList,
boolean isCountryMode) {
List<LocaleStore.LocaleInfo> searchItem = localeList.stream()
.filter(suggested -> searchList.stream()
.anyMatch(option -> option.getLocale() != null
&& option.getLocale().getLanguage().equals(
suggested.getLocale().getLanguage())))
.distinct()
.collect(Collectors.toList());
return getSortedLocaleList(searchItem, isCountryMode);
}
} }

View File

@@ -30,11 +30,11 @@ import android.telephony.TelephonyManager
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.preference.Preference import androidx.preference.Preference
import com.android.settings.AirplaneModeEnabler import com.android.settings.AirplaneModeEnabler
import com.android.settings.PreferenceActionMetricsProvider
import com.android.settings.PreferenceRestrictionMixin import com.android.settings.PreferenceRestrictionMixin
import com.android.settings.R import com.android.settings.R
import com.android.settings.Utils import com.android.settings.Utils
import com.android.settings.network.SatelliteRepository.Companion.isSatelliteOn import com.android.settings.network.SatelliteRepository.Companion.isSatelliteOn
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settingslib.RestrictedSwitchPreference import com.android.settingslib.RestrictedSwitchPreference
import com.android.settingslib.datastore.AbstractKeyedDataObservable import com.android.settingslib.datastore.AbstractKeyedDataObservable
import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyValueStore
@@ -51,6 +51,7 @@ import com.android.settingslib.metadata.SwitchPreference
// LINT.IfChange // LINT.IfChange
class AirplaneModePreference : class AirplaneModePreference :
SwitchPreference(KEY, R.string.airplane_mode), SwitchPreference(KEY, R.string.airplane_mode),
PreferenceActionMetricsProvider,
PreferenceAvailabilityProvider, PreferenceAvailabilityProvider,
PreferenceLifecycleProvider, PreferenceLifecycleProvider,
PreferenceRestrictionMixin { PreferenceRestrictionMixin {
@@ -88,6 +89,9 @@ class AirplaneModePreference :
override val sensitivityLevel override val sensitivityLevel
get() = SensitivityLevel.HIGH_SENSITIVITY get() = SensitivityLevel.HIGH_SENSITIVITY
override val preferenceActionMetrics: Int
get() = ACTION_AIRPLANE_TOGGLE
override fun storage(context: Context): KeyValueStore = override fun storage(context: Context): KeyValueStore =
AirplaneModeStorage(context, SettingsGlobalStore.get(context)) AirplaneModeStorage(context, SettingsGlobalStore.get(context))
@@ -109,16 +113,12 @@ class AirplaneModePreference :
(settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T (settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) { override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
if (value is Boolean) { if (value !is Boolean) return
settingsStore.setBoolean(key, value) settingsStore.setBoolean(key, value)
val intent = Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) val intent = Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)
intent.putExtra("state", value) intent.putExtra("state", value)
context.sendBroadcastAsUser(intent, UserHandle.ALL) context.sendBroadcastAsUser(intent, UserHandle.ALL)
val metricsFeature = featureFactory.metricsFeatureProvider
metricsFeature.action(context, ACTION_AIRPLANE_TOGGLE, value)
}
} }
override fun onFirstObserverAdded() { override fun onFirstObserverAdded() {

View File

@@ -21,13 +21,13 @@ import android.app.settings.SettingsEnums;
import android.os.Bundle; import android.os.Bundle;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.notification.modes.EnableZenModeDialog; import com.android.settingslib.notification.modes.EnableDndDialogFactory;
public class SettingsEnableZenModeDialog extends InstrumentedDialogFragment { public class EnableDndDialogFragment extends InstrumentedDialogFragment {
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
return new EnableZenModeDialog(getContext()).createDialog(); return new EnableDndDialogFactory(getContext()).createDialog();
} }
@Override @Override

View File

@@ -17,11 +17,14 @@
package com.android.settings.notification.history; package com.android.settings.notification.history;
import static android.provider.Settings.Secure.NOTIFICATION_HISTORY_ENABLED; import static android.provider.Settings.Secure.NOTIFICATION_HISTORY_ENABLED;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static androidx.core.view.accessibility.AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED; import static androidx.core.view.accessibility.AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
import android.annotation.AttrRes; import android.annotation.AttrRes;
import android.annotation.ColorInt; import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.app.ActionBar; import android.app.ActionBar;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.INotificationManager; import android.app.INotificationManager;
@@ -30,7 +33,6 @@ import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Resources; import android.content.res.Resources;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Outline;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.ServiceManager; import android.os.ServiceManager;
@@ -41,12 +43,10 @@ import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification; import android.service.notification.StatusBarNotification;
import android.util.Log; import android.util.Log;
import android.util.Slog; import android.util.Slog;
import android.util.TypedValue;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@@ -95,22 +95,7 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
private PackageManager mPm; private PackageManager mPm;
private CountDownLatch mCountdownLatch; private CountDownLatch mCountdownLatch;
private Future mCountdownFuture; private Future mCountdownFuture;
private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
final TypedArray ta = NotificationHistoryActivity.this.obtainStyledAttributes(
new int[]{android.R.attr.dialogCornerRadius});
final float dialogCornerRadius = ta.getDimension(0, 0);
ta.recycle();
TypedValue v = new TypedValue();
NotificationHistoryActivity.this.getTheme().resolveAttribute(
com.android.internal.R.attr.listDivider, v, true);
int bottomPadding = NotificationHistoryActivity.this.getDrawable(v.resourceId)
.getIntrinsicHeight();
outline.setRoundRect(0, 0, view.getWidth(), (view.getHeight() - bottomPadding),
dialogCornerRadius);
}
};
private UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); private UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
enum NotificationHistoryEvent implements UiEventLogger.UiEventEnum { enum NotificationHistoryEvent implements UiEventLogger.UiEventEnum {
@@ -158,20 +143,28 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
private HistoryLoader.OnHistoryLoaderListener mOnHistoryLoaderListener = notifications -> { private HistoryLoader.OnHistoryLoaderListener mOnHistoryLoaderListener = notifications -> {
findViewById(R.id.today_list).setVisibility( findViewById(R.id.today_list).setVisibility(
notifications.isEmpty() ? View.GONE : View.VISIBLE); notifications.isEmpty() ? GONE : VISIBLE);
mCountdownLatch.countDown(); mCountdownLatch.countDown();
View recyclerView = mTodayView.findViewById(R.id.apps); View recyclerView = mTodayView.findViewById(R.id.apps);
recyclerView.setClipToOutline(true); recyclerView.setClipToOutline(true);
mTodayView.setOutlineProvider(mOutlineProvider);
mSnoozeView.setOutlineProvider(mOutlineProvider);
// for each package, new header and recycler view // for each package, new header and recycler view
for (int i = 0, notificationsSize = notifications.size(); i < notificationsSize; i++) { for (int i = 0, notificationsSize = notifications.size(); i < notificationsSize; i++) {
NotificationHistoryPackage nhp = notifications.get(i); NotificationHistoryPackage nhp = notifications.get(i);
View viewForPackage = LayoutInflater.from(this) View viewForPackage = LayoutInflater.from(this)
.inflate(R.layout.notification_history_app_layout, null); .inflate(R.layout.notification_history_app_layout, null);
int cornerType = ROUND_CORNER_CENTER;
if (i == (notificationsSize - 1)) {
cornerType |= ROUND_CORNER_BOTTOM;
}
if (i == 0) {
cornerType |= ROUND_CORNER_TOP;
}
int backgroundRes = NotificationHistoryActivity.getRoundCornerDrawableRes(cornerType);
viewForPackage.setBackgroundResource(backgroundRes);
final View container = viewForPackage.findViewById(R.id.notification_list_wrapper); final View container = viewForPackage.findViewById(R.id.notification_list_wrapper);
container.setVisibility(View.GONE); container.setVisibility(GONE);
View header = viewForPackage.findViewById(R.id.app_header); View header = viewForPackage.findViewById(R.id.app_header);
NotificationExpandButton expand = viewForPackage.findViewById( NotificationExpandButton expand = viewForPackage.findViewById(
com.android.internal.R.id.expand_button); com.android.internal.R.id.expand_button);
@@ -181,19 +174,19 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
expand.setDefaultPillColor(pillColor); expand.setDefaultPillColor(pillColor);
expand.setDefaultTextColor(textColor); expand.setDefaultTextColor(textColor);
expand.setExpanded(false); expand.setExpanded(false);
header.setStateDescription(container.getVisibility() == View.VISIBLE header.setStateDescription(container.getVisibility() == VISIBLE
? getString(R.string.condition_expand_hide) ? getString(R.string.condition_expand_hide)
: getString(R.string.condition_expand_show)); : getString(R.string.condition_expand_show));
int finalI = i; int finalI = i;
header.setOnClickListener(v -> { header.setOnClickListener(v -> {
container.setVisibility(container.getVisibility() == View.VISIBLE container.setVisibility(container.getVisibility() == VISIBLE
? View.GONE : View.VISIBLE); ? GONE : VISIBLE);
expand.setExpanded(container.getVisibility() == View.VISIBLE); expand.setExpanded(container.getVisibility() == VISIBLE);
header.setStateDescription(container.getVisibility() == View.VISIBLE header.setStateDescription(container.getVisibility() == VISIBLE
? getString(R.string.condition_expand_hide) ? getString(R.string.condition_expand_hide)
: getString(R.string.condition_expand_show)); : getString(R.string.condition_expand_show));
header.sendAccessibilityEvent(TYPE_VIEW_ACCESSIBILITY_FOCUSED); header.sendAccessibilityEvent(TYPE_VIEW_ACCESSIBILITY_FOCUSED);
mUiEventLogger.logWithPosition((container.getVisibility() == View.VISIBLE) mUiEventLogger.logWithPosition((container.getVisibility() == VISIBLE)
? NotificationHistoryEvent.NOTIFICATION_HISTORY_PACKAGE_HISTORY_OPEN ? NotificationHistoryEvent.NOTIFICATION_HISTORY_PACKAGE_HISTORY_OPEN
: NotificationHistoryEvent.NOTIFICATION_HISTORY_PACKAGE_HISTORY_CLOSE, : NotificationHistoryEvent.NOTIFICATION_HISTORY_PACKAGE_HISTORY_CLOSE,
nhp.uid, nhp.pkgName, finalI); nhp.uid, nhp.pkgName, finalI);
@@ -217,7 +210,7 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
count.setText(StringUtil.getIcuPluralsString(this, newCount, count.setText(StringUtil.getIcuPluralsString(this, newCount,
R.string.notification_history_count)); R.string.notification_history_count));
if (newCount == 0) { if (newCount == 0) {
viewForPackage.setVisibility(View.GONE); viewForPackage.setVisibility(GONE);
} }
}, mUiEventLogger)); }, mUiEventLogger));
((NotificationHistoryAdapter) rv.getAdapter()).onRebuildComplete( ((NotificationHistoryAdapter) rv.getAdapter()).onRebuildComplete(
@@ -227,11 +220,6 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
} }
}; };
private void configureNotificationList(View recyclerView) {
recyclerView.setClipToOutline(true);
recyclerView.setOutlineProvider(mOutlineProvider);
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -240,8 +228,6 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
mTodayView = findViewById(R.id.apps); mTodayView = findViewById(R.id.apps);
mSnoozeView = findViewById(R.id.snoozed_list); mSnoozeView = findViewById(R.id.snoozed_list);
mDismissView = findViewById(R.id.recently_dismissed_list); mDismissView = findViewById(R.id.recently_dismissed_list);
configureNotificationList(mDismissView.findViewById(R.id.notification_list));
configureNotificationList(mSnoozeView.findViewById(R.id.notification_list));
mHistoryOff = findViewById(R.id.history_off); mHistoryOff = findViewById(R.id.history_off);
mHistoryOn = findViewById(R.id.history_on); mHistoryOn = findViewById(R.id.history_on);
mHistoryEmpty = findViewById(R.id.history_on_empty); mHistoryEmpty = findViewById(R.id.history_on_empty);
@@ -289,11 +275,11 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
} }
ThreadUtils.postOnMainThread(() -> { ThreadUtils.postOnMainThread(() -> {
if (mSwitchBar.isChecked() if (mSwitchBar.isChecked()
&& findViewById(R.id.today_list).getVisibility() == View.GONE && findViewById(R.id.today_list).getVisibility() == GONE
&& mSnoozeView.getVisibility() == View.GONE && mSnoozeView.getVisibility() == GONE
&& mDismissView.getVisibility() == View.GONE) { && mDismissView.getVisibility() == GONE) {
mHistoryOn.setVisibility(View.GONE); mHistoryOn.setVisibility(GONE);
mHistoryEmpty.setVisibility(View.VISIBLE); mHistoryEmpty.setVisibility(VISIBLE);
} }
}); });
}); });
@@ -320,6 +306,33 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
super.onDestroy(); super.onDestroy();
} }
public static final int ROUND_CORNER_CENTER = 1;
public static final int ROUND_CORNER_TOP = 1 << 1;
public static final int ROUND_CORNER_BOTTOM = 1 << 2;
public static @DrawableRes int getRoundCornerDrawableRes(int cornerType) {
if ((cornerType & ROUND_CORNER_CENTER) == 0) {
return 0;
}
if (((cornerType & ROUND_CORNER_TOP) != 0) && ((cornerType & ROUND_CORNER_BOTTOM) == 0)) {
// the first
return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background_top;
} else if (((cornerType & ROUND_CORNER_BOTTOM) != 0)
&& ((cornerType & ROUND_CORNER_TOP) == 0)) {
// the last
return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background_bottom;
} else if (((cornerType & ROUND_CORNER_TOP) != 0)
&& ((cornerType & ROUND_CORNER_BOTTOM) != 0)) {
// the only one preference
return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background;
} else {
// in the center
return com.android.settingslib.widget.theme.R.drawable.settingslib_round_background_center;
}
}
private @ColorInt int obtainThemeColor(@AttrRes int attrRes) { private @ColorInt int obtainThemeColor(@AttrRes int attrRes) {
Resources.Theme theme = new ContextThemeWrapper(this, Resources.Theme theme = new ContextThemeWrapper(this,
android.R.style.Theme_DeviceDefault_DayNight).getTheme(); android.R.style.Theme_DeviceDefault_DayNight).getTheme();
@@ -345,14 +358,14 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
private void toggleViews(boolean isChecked) { private void toggleViews(boolean isChecked) {
if (isChecked) { if (isChecked) {
mHistoryOff.setVisibility(View.GONE); mHistoryOff.setVisibility(GONE);
mHistoryOn.setVisibility(View.VISIBLE); mHistoryOn.setVisibility(VISIBLE);
} else { } else {
mHistoryOn.setVisibility(View.GONE); mHistoryOn.setVisibility(GONE);
mHistoryOff.setVisibility(View.VISIBLE); mHistoryOff.setVisibility(VISIBLE);
mTodayView.removeAllViews(); mTodayView.removeAllViews();
} }
mHistoryEmpty.setVisibility(View.GONE); mHistoryEmpty.setVisibility(GONE);
} }
private final OnCheckedChangeListener mOnSwitchClickListener = private final OnCheckedChangeListener mOnSwitchClickListener =
@@ -372,13 +385,13 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
Log.d(TAG, "onSwitchChange history to " + isChecked); Log.d(TAG, "onSwitchChange history to " + isChecked);
} }
// Reset UI visibility to ensure it matches real state. // Reset UI visibility to ensure it matches real state.
mHistoryOn.setVisibility(View.GONE); mHistoryOn.setVisibility(GONE);
if (isChecked) { if (isChecked) {
mHistoryEmpty.setVisibility(View.VISIBLE); mHistoryEmpty.setVisibility(VISIBLE);
mHistoryOff.setVisibility(View.GONE); mHistoryOff.setVisibility(GONE);
} else { } else {
mHistoryOff.setVisibility(View.VISIBLE); mHistoryOff.setVisibility(VISIBLE);
mHistoryEmpty.setVisibility(View.GONE); mHistoryEmpty.setVisibility(GONE);
} }
mTodayView.removeAllViews(); mTodayView.removeAllViews();
}; };
@@ -410,7 +423,7 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
mSnoozedRv.setNestedScrollingEnabled(false); mSnoozedRv.setNestedScrollingEnabled(false);
if (snoozed == null || snoozed.length == 0) { if (snoozed == null || snoozed.length == 0) {
mSnoozeView.setVisibility(View.GONE); mSnoozeView.setVisibility(GONE);
} else { } else {
((NotificationSbnAdapter) mSnoozedRv.getAdapter()).onRebuildComplete( ((NotificationSbnAdapter) mSnoozedRv.getAdapter()).onRebuildComplete(
new ArrayList<>(Arrays.asList(snoozed))); new ArrayList<>(Arrays.asList(snoozed)));
@@ -426,9 +439,9 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
mDismissedRv.setNestedScrollingEnabled(false); mDismissedRv.setNestedScrollingEnabled(false);
if (dismissed == null || dismissed.length == 0) { if (dismissed == null || dismissed.length == 0) {
mDismissView.setVisibility(View.GONE); mDismissView.setVisibility(GONE);
} else { } else {
mDismissView.setVisibility(View.VISIBLE); mDismissView.setVisibility(VISIBLE);
((NotificationSbnAdapter) mDismissedRv.getAdapter()).onRebuildComplete( ((NotificationSbnAdapter) mDismissedRv.getAdapter()).onRebuildComplete(
new ArrayList<>(Arrays.asList(dismissed))); new ArrayList<>(Arrays.asList(dismissed)));
} }
@@ -446,10 +459,10 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
int reason) { int reason) {
if (reason == REASON_SNOOZED) { if (reason == REASON_SNOOZED) {
((NotificationSbnAdapter) mSnoozedRv.getAdapter()).addSbn(sbn); ((NotificationSbnAdapter) mSnoozedRv.getAdapter()).addSbn(sbn);
mSnoozeView.setVisibility(View.VISIBLE); mSnoozeView.setVisibility(VISIBLE);
} else { } else {
((NotificationSbnAdapter) mDismissedRv.getAdapter()).addSbn(sbn); ((NotificationSbnAdapter) mDismissedRv.getAdapter()).addSbn(sbn);
mDismissView.setVisibility(View.VISIBLE); mDismissView.setVisibility(VISIBLE);
} }
} }
}; };

View File

@@ -9,7 +9,6 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -36,7 +35,6 @@ public class NotificationHistoryRecyclerView extends RecyclerView {
super(context, attrs, defStyle); super(context, attrs, defStyle);
setLayoutManager(new LinearLayoutManager(getContext())); setLayoutManager(new LinearLayoutManager(getContext()));
addItemDecoration(new DividerItemDecoration(getContext(), LinearLayoutManager.VERTICAL));
ItemTouchHelper touchHelper = new ItemTouchHelper( ItemTouchHelper touchHelper = new ItemTouchHelper(
new DismissTouchHelper(0, ItemTouchHelper.START | ItemTouchHelper.END)); new DismissTouchHelper(0, ItemTouchHelper.START | ItemTouchHelper.END));
touchHelper.attachToRecyclerView(this); touchHelper.attachToRecyclerView(this);

View File

@@ -24,6 +24,10 @@ import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID; import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.EXTRA_CONVERSATION_ID; import static android.provider.Settings.EXTRA_CONVERSATION_ID;
import static com.android.settings.notification.history.NotificationHistoryActivity.ROUND_CORNER_BOTTOM;
import static com.android.settings.notification.history.NotificationHistoryActivity.ROUND_CORNER_CENTER;
import static com.android.settings.notification.history.NotificationHistoryActivity.ROUND_CORNER_TOP;
import android.annotation.ColorInt; import android.annotation.ColorInt;
import android.annotation.UserIdInt; import android.annotation.UserIdInt;
import android.app.ActivityManager; import android.app.ActivityManager;
@@ -111,13 +115,22 @@ public class NotificationSbnAdapter extends
int position) { int position) {
final StatusBarNotification sbn = mValues.get(position); final StatusBarNotification sbn = mValues.get(position);
if (sbn != null) { if (sbn != null) {
int cornerType = ROUND_CORNER_CENTER;
if (position == (getItemCount() - 1)) {
cornerType |= ROUND_CORNER_BOTTOM;
}
if (position == 0) {
cornerType |= ROUND_CORNER_TOP;
}
int backgroundRes = NotificationHistoryActivity.getRoundCornerDrawableRes(cornerType);
holder.itemView.setBackgroundResource(backgroundRes);
holder.setIconBackground(loadBackground(sbn)); holder.setIconBackground(loadBackground(sbn));
holder.setIcon(loadIcon(sbn)); holder.setIcon(loadIcon(sbn));
holder.setPackageLabel(loadPackageLabel(sbn.getPackageName()).toString()); holder.setPackageLabel(loadPackageLabel(sbn.getPackageName()).toString());
holder.setTitle(getTitleString(sbn.getNotification())); holder.setTitle(getTitleString(sbn.getNotification()));
holder.setSummary(getTextString(mContext, sbn.getNotification())); holder.setSummary(getTextString(mContext, sbn.getNotification()));
holder.setPostedTime(sbn.getPostTime()); holder.setPostedTime(sbn.getPostTime());
holder.setDividerVisible(position < (mValues.size() -1));
int userId = normalizeUserId(sbn); int userId = normalizeUserId(sbn);
if (!mUserBadgeCache.containsKey(userId)) { if (!mUserBadgeCache.containsKey(userId)) {
Drawable profile = mContext.getPackageManager().getUserBadgeForDensityNoBackground( Drawable profile = mContext.getPackageManager().getUserBadgeForDensityNoBackground(

View File

@@ -49,7 +49,6 @@ public class NotificationSbnViewHolder extends RecyclerView.ViewHolder {
private final TextView mTitle; private final TextView mTitle;
private final TextView mSummary; private final TextView mSummary;
private final ImageView mProfileBadge; private final ImageView mProfileBadge;
private final View mDivider;
NotificationSbnViewHolder(View itemView) { NotificationSbnViewHolder(View itemView) {
super(itemView); super(itemView);
@@ -59,7 +58,6 @@ public class NotificationSbnViewHolder extends RecyclerView.ViewHolder {
mTitle = itemView.findViewById(R.id.title); mTitle = itemView.findViewById(R.id.title);
mSummary = itemView.findViewById(R.id.text); mSummary = itemView.findViewById(R.id.text);
mProfileBadge = itemView.findViewById(R.id.profile_badge); mProfileBadge = itemView.findViewById(R.id.profile_badge);
mDivider = itemView.findViewById(R.id.divider);
} }
void setSummary(CharSequence summary) { void setSummary(CharSequence summary) {
@@ -92,10 +90,6 @@ public class NotificationSbnViewHolder extends RecyclerView.ViewHolder {
mProfileBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE); mProfileBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE);
} }
void setDividerVisible(boolean visible) {
mDivider.setVisibility(visible ? View.VISIBLE : View.GONE);
}
void addOnClick(int position, String pkg, int uid, int userId, PendingIntent pi, void addOnClick(int position, String pkg, int uid, int userId, PendingIntent pi,
InstanceId instanceId, InstanceId instanceId,
boolean isSnoozed, UiEventLogger uiEventLogger) { boolean isSnoozed, UiEventLogger uiEventLogger) {

View File

@@ -27,7 +27,7 @@ import androidx.fragment.app.Fragment;
import androidx.preference.Preference; import androidx.preference.Preference;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.notification.SettingsEnableZenModeDialog; import com.android.settings.notification.EnableDndDialogFragment;
import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenMode;
import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.notification.modes.ZenModesBackend;
import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.widget.LayoutPreference;
@@ -69,7 +69,7 @@ class ZenModeButtonPreferenceController extends AbstractZenModePreferenceControl
int zenDuration = mDurationHelper.getZenDuration(); int zenDuration = mDurationHelper.getZenDuration();
switch (zenDuration) { switch (zenDuration) {
case Settings.Secure.ZEN_DURATION_PROMPT: case Settings.Secure.ZEN_DURATION_PROMPT:
new SettingsEnableZenModeDialog().show( new EnableDndDialogFragment().show(
mParent.getParentFragmentManager(), TAG); mParent.getParentFragmentManager(), TAG);
break; break;
case Settings.Secure.ZEN_DURATION_FOREVER: case Settings.Secure.ZEN_DURATION_FOREVER:

View File

@@ -21,13 +21,13 @@ import android.app.settings.SettingsEnums;
import android.os.Bundle; import android.os.Bundle;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.notification.modes.ZenDurationDialog; import com.android.settingslib.notification.modes.DndDurationDialogFactory;
public class SettingsZenDurationDialog extends InstrumentedDialogFragment { public class SettingsZenDurationDialog extends InstrumentedDialogFragment {
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
return new ZenDurationDialog(getContext()).createDialog(); return new DndDurationDialogFactory(getContext()).createDialog();
} }
@Override @Override

View File

@@ -23,7 +23,7 @@ import android.util.AttributeSet;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import com.android.settingslib.CustomDialogPreferenceCompat; import com.android.settingslib.CustomDialogPreferenceCompat;
import com.android.settingslib.notification.modes.ZenDurationDialog; import com.android.settingslib.notification.modes.DndDurationDialogFactory;
public class ZenDurationDialogPreference extends CustomDialogPreferenceCompat { public class ZenDurationDialogPreference extends CustomDialogPreferenceCompat {
@@ -48,7 +48,7 @@ public class ZenDurationDialogPreference extends CustomDialogPreferenceCompat {
DialogInterface.OnClickListener listener) { DialogInterface.OnClickListener listener) {
super.onPrepareDialogBuilder(builder, listener); super.onPrepareDialogBuilder(builder, listener);
ZenDurationDialog zenDialog = new ZenDurationDialog(getContext()); DndDurationDialogFactory zenDialog = new DndDurationDialogFactory(getContext());
zenDialog.setupDialog(builder); zenDialog.setupDialog(builder);
} }
} }

View File

@@ -30,7 +30,7 @@ import androidx.preference.Preference;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.notification.SettingsEnableZenModeDialog; import com.android.settings.notification.EnableDndDialogFragment;
import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.widget.LayoutPreference;
@@ -118,7 +118,7 @@ public class ZenModeButtonPreferenceController extends AbstractZenModePreference
int zenDuration = getZenDuration(); int zenDuration = getZenDuration();
switch (zenDuration) { switch (zenDuration) {
case Settings.Secure.ZEN_DURATION_PROMPT: case Settings.Secure.ZEN_DURATION_PROMPT:
new SettingsEnableZenModeDialog().show(mFragment, TAG); new EnableDndDialogFragment().show(mFragment, TAG);
break; break;
case Settings.Secure.ZEN_DURATION_FOREVER: case Settings.Secure.ZEN_DURATION_FOREVER:
mBackend.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); mBackend.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);

View File

@@ -815,8 +815,6 @@ public class ChooseLockPattern extends SettingsActivity {
if (stage == Stage.NeedToConfirm) { if (stage == Stage.NeedToConfirm) {
// If the Stage is NeedToConfirm, move the a11y focus to the header. // If the Stage is NeedToConfirm, move the a11y focus to the header.
mHeaderText.requestAccessibilityFocus(); mHeaderText.requestAccessibilityFocus();
} else {
mHeaderText.announceForAccessibility(mHeaderText.getText());
} }
} }
} }

View File

@@ -89,7 +89,6 @@ public abstract class RadioButtonPickerFragment extends SettingsPreferenceFragme
if (isCatalystEnabled()) { if (isCatalystEnabled()) {
PreferenceScreen preferenceScreen = createPreferenceScreen(); PreferenceScreen preferenceScreen = createPreferenceScreen();
setPreferenceScreen(preferenceScreen); setPreferenceScreen(preferenceScreen);
updateActivityTitleWithScreenTitle(preferenceScreen);
} else { } else {
super.onCreatePreferences(savedInstanceState, rootKey); super.onCreatePreferences(savedInstanceState, rootKey);
} }

View File

@@ -27,17 +27,17 @@ import android.net.wifi.WifiManager
import android.os.UserManager import android.os.UserManager
import android.text.BidiFormatter import android.text.BidiFormatter
import android.util.Log import android.util.Log
import androidx.preference.Preference
import com.android.settings.PreferenceRestrictionMixin import com.android.settings.PreferenceRestrictionMixin
import com.android.settings.R import com.android.settings.R
import com.android.settings.Utils import com.android.settings.Utils
import com.android.settings.core.SubSettingLauncher import com.android.settings.core.SubSettingLauncher
import com.android.settings.datausage.DataSaverMainSwitchPreference.Companion.KEY as DATA_SAVER_KEY
import com.android.settings.wifi.WifiUtils.canShowWifiHotspot import com.android.settings.wifi.WifiUtils.canShowWifiHotspot
import com.android.settings.wifi.utils.tetheringManager import com.android.settings.wifi.utils.tetheringManager
import com.android.settings.wifi.utils.wifiApState import com.android.settings.wifi.utils.wifiApState
import com.android.settings.wifi.utils.wifiManager import com.android.settings.wifi.utils.wifiManager
import com.android.settings.wifi.utils.wifiSoftApSsid import com.android.settings.wifi.utils.wifiSoftApSsid
import com.android.settingslib.PrimarySwitchPreference import com.android.settingslib.PrimarySwitchPreferenceBinding
import com.android.settingslib.TetherUtil import com.android.settingslib.TetherUtil
import com.android.settingslib.datastore.AbstractKeyedDataObservable import com.android.settingslib.datastore.AbstractKeyedDataObservable
import com.android.settingslib.datastore.HandlerExecutor import com.android.settingslib.datastore.HandlerExecutor
@@ -46,19 +46,16 @@ import com.android.settingslib.datastore.KeyedObserver
import com.android.settingslib.datastore.Permissions import com.android.settingslib.datastore.Permissions
import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.PreferenceChangeReason import com.android.settingslib.metadata.PreferenceChangeReason
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.ReadWritePermit
import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.metadata.SensitivityLevel
import com.android.settingslib.metadata.SwitchPreference import com.android.settingslib.metadata.SwitchPreference
import com.android.settingslib.preference.PreferenceBinding
import com.android.settingslib.wifi.WifiUtils.Companion.getWifiTetherSummaryForConnectedDevices import com.android.settingslib.wifi.WifiUtils.Companion.getWifiTetherSummaryForConnectedDevices
import com.android.settings.datausage.DataSaverMainSwitchPreference.Companion.KEY as DATA_SAVER_KEY
// LINT.IfChange // LINT.IfChange
class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStore) : class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStore) :
SwitchPreference(KEY, R.string.wifi_hotspot_checkbox_text), SwitchPreference(KEY, R.string.wifi_hotspot_checkbox_text),
PreferenceBinding, PrimarySwitchPreferenceBinding,
PreferenceAvailabilityProvider, PreferenceAvailabilityProvider,
PreferenceSummaryProvider, PreferenceSummaryProvider,
PreferenceRestrictionMixin { PreferenceRestrictionMixin {
@@ -130,8 +127,6 @@ class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStor
override val sensitivityLevel override val sensitivityLevel
get() = SensitivityLevel.HIGH_SENSITIVITY get() = SensitivityLevel.HIGH_SENSITIVITY
override fun createWidget(context: Context) = PrimarySwitchPreference(context)
override fun storage(context: Context): KeyValueStore = wifiHotspotStore override fun storage(context: Context): KeyValueStore = wifiHotspotStore
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -201,16 +196,7 @@ class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStor
Log.e(TAG, "onTetheringFailed(),error=$error") Log.e(TAG, "onTetheringFailed(),error=$error")
} }
override fun onKeyChanged(key: String, reason: Int) = override fun onKeyChanged(key: String, reason: Int) = notifyChange(KEY, reason)
notifyChange(KEY, reason)
}
override fun bind(preference: Preference, metadata: PreferenceMetadata) {
super.bind(preference, metadata)
(preference as PrimarySwitchPreference).apply {
isChecked = preferenceDataStore!!.getBoolean(key, false)
isSwitchEnabled = isEnabled
}
} }
companion object { companion object {

View File

@@ -58,7 +58,6 @@ android_robolectric_test {
"Settings-robo-testutils", "Settings-robo-testutils",
"Settings-testutils2", "Settings-testutils2",
"SettingsLib-robo-testutils", "SettingsLib-robo-testutils",
"SettingsLibPreference-testutils",
"Settings_robolectric_meta_service_file", "Settings_robolectric_meta_service_file",
"aconfig_settings_flags_lib", "aconfig_settings_flags_lib",
"android.webkit.flags-aconfig-java", "android.webkit.flags-aconfig-java",
@@ -73,6 +72,7 @@ android_robolectric_test {
"kotlin-test", "kotlin-test",
"mockito-robolectric-prebuilt", // mockito deps order matters! "mockito-robolectric-prebuilt", // mockito deps order matters!
"mockito-kotlin2", "mockito-kotlin2",
"SettingsLibPreference-testutils", // order matters because it depends on mockito-kotlin2
"notification_flags_lib", "notification_flags_lib",
"platform-test-annotations", "platform-test-annotations",
"testables", "testables",
@@ -115,6 +115,7 @@ java_library {
libs: [ libs: [
"Robolectric_all-target", "Robolectric_all-target",
"Settings-core", "Settings-core",
"androidx.test.core",
"mockito-robolectric-prebuilt", "mockito-robolectric-prebuilt",
"truth", "truth",
], ],

View File

@@ -46,6 +46,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@@ -143,9 +144,12 @@ public class BluetoothDetailsAudioSharingControllerTest extends BluetoothDetails
@Test @Test
@EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
public void connected_hasConnectedBroadcastSource_showTwoPreference() { public void connected_hasBroadcastSource_showTwoPreference() {
when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true); when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true);
when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false); when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false);
List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L);
when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
when(mLocalManager when(mLocalManager
.getProfileManager() .getProfileManager()
.getLeAudioBroadcastAssistantProfile() .getLeAudioBroadcastAssistantProfile()

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;
@@ -71,6 +73,7 @@ import org.robolectric.shadow.api.Shadow;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@@ -164,9 +167,9 @@ public class AudioStreamButtonControllerTest {
} }
@Test @Test
public void testDisplayPreference_sourceConnected_setDisconnectButton() { public void testDisplayPreference_sourceStreaming_setDisconnectButton() {
when(mAudioStreamsHelper.getAllConnectedSources()) when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(List.of(mBroadcastReceiveState)); .thenReturn(Map.of(BROADCAST_ID, STREAMING));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
@@ -190,7 +193,8 @@ public class AudioStreamButtonControllerTest {
@Test @Test
public void testDisplayPreference_sourceNotConnected_setConnectButton() { public void testDisplayPreference_sourceNotConnected_setConnectButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.setAudioStreamsRepositoryForTesting(mRepository); mController.setAudioStreamsRepositoryForTesting(mRepository);
var metadataToRejoin = mock(BluetoothLeBroadcastMetadata.class); var metadataToRejoin = mock(BluetoothLeBroadcastMetadata.class);
when(mRepository.getSavedMetadata(any(), anyInt())).thenReturn(metadataToRejoin); when(mRepository.getSavedMetadata(any(), anyInt())).thenReturn(metadataToRejoin);
@@ -216,7 +220,8 @@ public class AudioStreamButtonControllerTest {
@Test @Test
public void testCallback_onSourceRemoved_updateButton() { public void testCallback_onSourceRemoved_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceRemoved( mController.mBroadcastAssistantCallback.onSourceRemoved(
@@ -230,9 +235,8 @@ public class AudioStreamButtonControllerTest {
@Test @Test
public void testCallback_onSourceRemovedFailed_updateButton() { public void testCallback_onSourceRemovedFailed_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()) when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(List.of(mBroadcastReceiveState)); .thenReturn(Map.of(BROADCAST_ID, STREAMING));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceRemoveFailed( mController.mBroadcastAssistantCallback.onSourceRemoveFailed(
@@ -250,9 +254,8 @@ public class AudioStreamButtonControllerTest {
@Test @Test
public void testCallback_onReceiveStateChanged_updateButton() { public void testCallback_onReceiveStateChanged_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()) when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(List.of(mBroadcastReceiveState)); .thenReturn(Map.of(BROADCAST_ID, STREAMING));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L); bisSyncState.add(1L);
@@ -273,7 +276,7 @@ public class AudioStreamButtonControllerTest {
} }
@Test @Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() { public void testCallback_onReceiveStateChangedWithSourcePaused_updateButton() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66"; String address = "11:22:33:44:55:66";
@@ -284,13 +287,16 @@ public class AudioStreamButtonControllerTest {
when(mSourceDevice.getAddress()).thenReturn(address); when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();
when(state.getBisSyncState()).thenReturn(bisSyncState); when(state.getBisSyncState()).thenReturn(bisSyncState);
when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(List.of(state)); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, PAUSED));
// Create new controller to enable hysteresis mode
mController = new AudioStreamButtonController(mContext, KEY);
mController.init(BROADCAST_ID);
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onReceiveStateChanged( mController.mBroadcastAssistantCallback.onReceiveStateChanged(
mock(BluetoothDevice.class), /* sourceId= */ 0, state); mock(BluetoothDevice.class), /* sourceId= */ 0, state);
verify(mFeatureFactory.metricsFeatureProvider, never()) verify(mFeatureFactory.metricsFeatureProvider)
.action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), anyInt()); .action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), anyInt());
// Called twice, once in displayPreference, the other one in callback // Called twice, once in displayPreference, the other one in callback
@@ -302,7 +308,8 @@ public class AudioStreamButtonControllerTest {
@Test @Test
public void testCallback_onSourceAddFailed_updateButton() { public void testCallback_onSourceAddFailed_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceAddFailed( mController.mBroadcastAssistantCallback.onSourceAddFailed(
@@ -321,7 +328,8 @@ public class AudioStreamButtonControllerTest {
@Test @Test
public void testCallback_onSourceLost_updateButton() { public void testCallback_onSourceLost_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 0); mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 0);

View File

@@ -19,10 +19,13 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@@ -66,6 +69,7 @@ import org.robolectric.shadow.api.Shadow;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@@ -160,10 +164,9 @@ public class AudioStreamHeaderControllerTest {
} }
@Test @Test
public void testDisplayPreference_sourceConnected_setSummary() { public void testDisplayPreference_sourceStreaming_setSummary() {
when(mAudioStreamsHelper.getAllConnectedSources()) when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(List.of(mBroadcastReceiveState)); .thenReturn(Map.of(BROADCAST_ID, STREAMING));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
@@ -176,8 +179,9 @@ public class AudioStreamHeaderControllerTest {
} }
@Test @Test
public void testDisplayPreference_sourceNotConnected_setSummary() { public void testDisplayPreference_sourceNotStreaming_setSummary() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
@@ -189,18 +193,14 @@ public class AudioStreamHeaderControllerTest {
} }
@Test @Test
public void testDisplayPreference_sourcePresent_setSummary() { public void testDisplayPreference_sourcePaused_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66"; when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, PAUSED));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
when(mBluetoothDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
when(mAudioStreamsHelper.getAllPresentSources())
.thenReturn(List.of(mBroadcastReceiveState));
// Create new controller to enable hysteresis mode
mController = new AudioStreamHeaderController(mContext, KEY);
mController.init(mFragment, BROADCAST_NAME, BROADCAST_ID);
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
verify(mHeaderController).setLabel(BROADCAST_NAME); verify(mHeaderController).setLabel(BROADCAST_NAME);
@@ -212,10 +212,10 @@ public class AudioStreamHeaderControllerTest {
} }
@Test @Test
public void testDisplayPreference_sourceNotPresent_setSummary() { public void testDisplayPreference_sourceNotPaused_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(Collections.emptyList()); .thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
@@ -228,7 +228,8 @@ public class AudioStreamHeaderControllerTest {
@Test @Test
public void testCallback_onSourceRemoved_updateButton() { public void testCallback_onSourceRemoved_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceRemoved( mController.mBroadcastAssistantCallback.onSourceRemoved(
@@ -241,7 +242,8 @@ public class AudioStreamHeaderControllerTest {
@Test @Test
public void testCallback_onSourceLost_updateButton() { public void testCallback_onSourceLost_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 1); mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 1);
@@ -253,8 +255,8 @@ public class AudioStreamHeaderControllerTest {
@Test @Test
public void testCallback_onReceiveStateChanged_updateButton() { public void testCallback_onReceiveStateChanged_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()) when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(List.of(mBroadcastReceiveState)); .thenReturn(Map.of(BROADCAST_ID, STREAMING));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();
@@ -272,17 +274,20 @@ public class AudioStreamHeaderControllerTest {
} }
@Test @Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() { public void testCallback_onReceiveStateChangedWithSourcePaused_updateButton() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66"; String address = "11:22:33:44:55:66";
when(mAudioStreamsHelper.getAllPresentSources()) when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(List.of(mBroadcastReceiveState)); .thenReturn(Map.of(BROADCAST_ID, PAUSED));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID); when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice); when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
when(mBluetoothDevice.getAddress()).thenReturn(address); when(mBluetoothDevice.getAddress()).thenReturn(address);
// Create new controller to enable hysteresis mode
mController = new AudioStreamHeaderController(mContext, KEY);
mController.init(mFragment, BROADCAST_NAME, BROADCAST_ID);
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onReceiveStateChanged( mController.mBroadcastAssistantCallback.onReceiveStateChanged(
mock(BluetoothDevice.class), /* sourceId= */ 0, mBroadcastReceiveState); mock(BluetoothDevice.class), /* sourceId= */ 0, mBroadcastReceiveState);

View File

@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;
@@ -149,11 +151,14 @@ public class AudioStreamsHelperTest {
@Test @Test
public void removeSource_noConnectedSource_doNothing() { public void removeSource_noConnectedSource_doNothing() {
String address = "11:22:33:44:55:66";
List<BluetoothDevice> devices = new ArrayList<>(); List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice); devices.add(mDevice);
when(mAssistant.getAllConnectedDevices()).thenReturn(devices); when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class); BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
when(source.getBroadcastId()).thenReturn(BROADCAST_ID_2); when(source.getBroadcastId()).thenReturn(BROADCAST_ID_2);
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice); when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
when(mCachedDevice.getDevice()).thenReturn(mDevice); when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID); when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
@@ -214,15 +219,16 @@ public class AudioStreamsHelperTest {
} }
@Test @Test
public void getAllConnectedSources_noAssistant() { public void getConnectedBroadcastIdAndState_noAssistant() {
when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(null); when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(null);
mHelper = new AudioStreamsHelper(mLocalBluetoothManager); mHelper = new AudioStreamsHelper(mLocalBluetoothManager);
assertThat(mHelper.getAllConnectedSources()).isEmpty(); assertThat(mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */
false)).isEmpty();
} }
@Test @Test
public void getAllConnectedSources_returnSource() { public void getConnectedBroadcastIdAndState_returnStreamingSource() {
List<BluetoothDevice> devices = new ArrayList<>(); List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice); devices.add(mDevice);
when(mAssistant.getAllConnectedDevices()).thenReturn(devices); when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
@@ -234,14 +240,15 @@ public class AudioStreamsHelperTest {
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L); bisSyncState.add(1L);
when(source.getBisSyncState()).thenReturn(bisSyncState); when(source.getBisSyncState()).thenReturn(bisSyncState);
when(source.getBroadcastId()).thenReturn(BROADCAST_ID_1);
var list = mHelper.getAllConnectedSources(); var map = mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */ false);
assertThat(list).isNotEmpty(); assertThat(map).isNotEmpty();
assertThat(list.get(0)).isEqualTo(source); assertThat(map.get(BROADCAST_ID_1)).isEqualTo(STREAMING);
} }
@Test @Test
public void getAllPresentSources_noSource() { public void getConnectedBroadcastIdAndState_noSource() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
@@ -259,12 +266,12 @@ public class AudioStreamsHelperTest {
when(source.getSourceDevice()).thenReturn(mSourceDevice); when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address); when(mSourceDevice.getAddress()).thenReturn(address);
var list = mHelper.getAllPresentSources(); var map = mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */ true);
assertThat(list).isEmpty(); assertThat(map).isEmpty();
} }
@Test @Test
public void getAllPresentSources_returnSource() { public void getConnectedBroadcastIdAndState_returnPausedSource() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66"; String address = "11:22:33:44:55:66";
@@ -282,10 +289,11 @@ public class AudioStreamsHelperTest {
when(mSourceDevice.getAddress()).thenReturn(address); when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();
when(source.getBisSyncState()).thenReturn(bisSyncState); when(source.getBisSyncState()).thenReturn(bisSyncState);
when(source.getBroadcastId()).thenReturn(BROADCAST_ID_1);
var list = mHelper.getAllPresentSources(); var map = mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */ true);
assertThat(list).isNotEmpty(); assertThat(map).isNotEmpty();
assertThat(list.get(0)).isEqualTo(source); assertThat(map.get(BROADCAST_ID_1)).isEqualTo(PAUSED);
} }
@Test @Test

View File

@@ -77,7 +77,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
BluetoothStatusCodes.FEATURE_SUPPORTED); BluetoothStatusCodes.FEATURE_SUPPORTED);
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED); BluetoothStatusCodes.FEATURE_SUPPORTED);
mCallback = new AudioStreamsProgressCategoryCallback(mContext, mController); mCallback = new AudioStreamsProgressCategoryCallback(mController);
} }
@Test @Test
@@ -87,7 +87,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
when(mState.getBisSyncState()).thenReturn(bisSyncState); when(mState.getBisSyncState()).thenReturn(bisSyncState);
mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState); mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
verify(mController).handleSourceConnected(any(), any()); verify(mController).handleSourceStreaming(any(), any());
} }
@Test @Test
@@ -102,7 +102,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
when(mSourceDevice.getAddress()).thenReturn(address); when(mSourceDevice.getAddress()).thenReturn(address);
mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState); mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
verify(mController).handleSourcePresent(any(), any()); verify(mController).handleSourcePaused(any(), any());
} }
@Test @Test

View File

@@ -25,6 +25,7 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;
import static com.android.settings.core.BasePreferenceController.AVAILABLE; import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX; import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;
@@ -355,7 +356,7 @@ public class AudioStreamsProgressCategoryControllerTest {
} }
@Test @Test
public void testOnStart_handleSourceAlreadyConnected_useNameFromMetadata() { public void testOnStart_handleSourceAlreadyStreaming_useNameFromMetadata() {
// Setup a device // Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
@@ -665,8 +666,8 @@ public class AudioStreamsProgressCategoryControllerTest {
shadowOf(Looper.getMainLooper()).idle(); shadowOf(Looper.getMainLooper()).idle();
// A new source found is lost, but the source is still connected // A new source found is lost, but the source is still connected
BluetoothLeBroadcastReceiveState connected = createConnectedMock(NEWLY_FOUND_BROADCAST_ID); when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean())).thenReturn(
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected)); Map.of(NEWLY_FOUND_BROADCAST_ID, STREAMING));
mController.handleSourceLost(NEWLY_FOUND_BROADCAST_ID); mController.handleSourceLost(NEWLY_FOUND_BROADCAST_ID);
shadowOf(Looper.getMainLooper()).idle(); shadowOf(Looper.getMainLooper()).idle();
@@ -819,13 +820,15 @@ public class AudioStreamsProgressCategoryControllerTest {
} }
@Test @Test
public void testHandleSourcePresent_updateState() { public void testHandleSourcePaused_updateState() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66"; String address = "11:22:33:44:55:66";
// Setup a device // Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Create new controller to enable hysteresis mode
mController = spy(new TestController(mContext, KEY));
// Setup mPreference so it's not null // Setup mPreference so it's not null
mController.displayPreference(mScreen); mController.displayPreference(mScreen);
@@ -844,7 +847,7 @@ public class AudioStreamsProgressCategoryControllerTest {
when(receiveState.getBisSyncState()).thenReturn(bisSyncState); when(receiveState.getBisSyncState()).thenReturn(bisSyncState);
// The new found source is identified as failed to connect // The new found source is identified as failed to connect
mController.handleSourcePresent(mSourceDevice, receiveState); mController.handleSourcePaused(mSourceDevice, receiveState);
shadowOf(Looper.getMainLooper()).idle(); shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference = ArgumentCaptor<AudioStreamPreference> preference =

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows; package com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -57,8 +59,9 @@ public class ShadowAudioStreamsHelper {
} }
@Implementation @Implementation
public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() { public Map<Integer, LocalBluetoothLeBroadcastSourceState> getConnectedBroadcastIdAndState(
return sMockHelper.getAllConnectedSources(); boolean hysteresisModeFixAvailable) {
return sMockHelper.getConnectedBroadcastIdAndState(hysteresisModeFixAvailable);
} }
@Implementation @Implementation
@@ -66,11 +69,6 @@ public class ShadowAudioStreamsHelper {
return sMockHelper.getAllSourcesByDevice(); return sMockHelper.getAllSourcesByDevice();
} }
@Implementation
public List<BluetoothLeBroadcastReceiveState> getAllPresentSources() {
return sMockHelper.getAllPresentSources();
}
/** Gets {@link CachedBluetoothDevice} in sharing or le connected */ /** Gets {@link CachedBluetoothDevice} in sharing or le connected */
@Implementation @Implementation
public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected( public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(

View File

@@ -16,7 +16,6 @@
package com.android.settings.core; package com.android.settings.core;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
@@ -84,7 +83,6 @@ public class InstrumentedPreferenceFragmentTest {
mFragment.onCreatePreferences(Bundle.EMPTY, null /* rootKey */); mFragment.onCreatePreferences(Bundle.EMPTY, null /* rootKey */);
verify(mFragment).addPreferencesFromResource(R.xml.screen_pinning_settings); verify(mFragment).addPreferencesFromResource(R.xml.screen_pinning_settings);
verify(mActivity, never()).setTitle(any());
} }
@Test @Test
@@ -97,7 +95,6 @@ public class InstrumentedPreferenceFragmentTest {
mFragment.onCreatePreferences(Bundle.EMPTY, null /* rootKey */); mFragment.onCreatePreferences(Bundle.EMPTY, null /* rootKey */);
verify(mFragment).addPreferencesFromResource(R.xml.screen_pinning_settings); verify(mFragment).addPreferencesFromResource(R.xml.screen_pinning_settings);
verify(mActivity).setTitle(title);
} }
public static class InstrumentedPreferenceFragmentTestable public static class InstrumentedPreferenceFragmentTestable

View File

@@ -27,10 +27,11 @@ import android.telephony.TelephonyManager
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.testutils.FakeFeatureFactory import com.android.settings.testutils.MetricsRule
import com.android.settingslib.datastore.SettingsGlobalStore import com.android.settingslib.datastore.SettingsGlobalStore
import com.android.settingslib.preference.createAndBindWidget import com.android.settingslib.preference.createAndBindWidget
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
@@ -41,6 +42,7 @@ import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AirplaneModePreferenceTest { class AirplaneModePreferenceTest {
@Rule(order = 0) @JvmField val metricsRule = MetricsRule()
private val mockResources = mock<Resources>() private val mockResources = mock<Resources>()
private val mockPackageManager = mock<PackageManager>() private val mockPackageManager = mock<PackageManager>()
@@ -106,24 +108,6 @@ class AirplaneModePreferenceTest {
assertThat(getValue).isFalse() assertThat(getValue).isFalse()
} }
@Test
fun setValue_valueTrue_metricsActionAirplaneToggleTrue() {
val metricsFeatureProvider = FakeFeatureFactory.setupForTest().metricsFeatureProvider
airplaneModePreference.storage(context).setBoolean(AirplaneModePreference.KEY, true)
verify(metricsFeatureProvider).action(context, ACTION_AIRPLANE_TOGGLE, true)
}
@Test
fun setValue_valueFalse_metricsActionAirplaneToggleFalse() {
val metricsFeatureProvider = FakeFeatureFactory.setupForTest().metricsFeatureProvider
airplaneModePreference.storage(context).setBoolean(AirplaneModePreference.KEY, false)
verify(metricsFeatureProvider).action(context, ACTION_AIRPLANE_TOGGLE, false)
}
@Test @Test
fun performClick_defaultOn_checkedIsFalse() { fun performClick_defaultOn_checkedIsFalse() {
SettingsGlobalStore.get(context).setInt(Settings.Global.AIRPLANE_MODE_ON, 1) SettingsGlobalStore.get(context).setInt(Settings.Global.AIRPLANE_MODE_ON, 1)
@@ -131,6 +115,7 @@ class AirplaneModePreferenceTest {
val preference = getSwitchPreference().apply { performClick() } val preference = getSwitchPreference().apply { performClick() }
assertThat(preference.isChecked).isFalse() assertThat(preference.isChecked).isFalse()
verify(metricsRule.metricsFeatureProvider).action(context, ACTION_AIRPLANE_TOGGLE, false)
} }
@Test @Test
@@ -140,6 +125,7 @@ class AirplaneModePreferenceTest {
val preference = getSwitchPreference().apply { performClick() } val preference = getSwitchPreference().apply { performClick() }
assertThat(preference.isChecked).isTrue() assertThat(preference.isChecked).isTrue()
verify(metricsRule.metricsFeatureProvider).action(context, ACTION_AIRPLANE_TOGGLE, true)
} }
private fun getSwitchPreference(): SwitchPreferenceCompat = private fun getSwitchPreference(): SwitchPreferenceCompat =

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2025 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.testutils
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.android.settings.SettingsMetricsLogger
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider
import com.android.settingslib.metadata.PreferenceScreenRegistry
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/** Test rule for metrics. */
class MetricsRule : TestWatcher() {
val metricsFeatureProvider: MetricsFeatureProvider =
FakeFeatureFactory.setupForTest().metricsFeatureProvider
override fun starting(description: Description) {
val context: Context = ApplicationProvider.getApplicationContext()
PreferenceScreenRegistry.preferenceUiActionMetricsLogger =
SettingsMetricsLogger(context, metricsFeatureProvider)
}
override fun finished(description: Description) {
PreferenceScreenRegistry.preferenceUiActionMetricsLogger = null
}
}