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

View File

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

View File

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

View File

@@ -22,8 +22,10 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingStart="16dp"
android:layout_marginEnd="16dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:orientation="vertical">
@@ -128,11 +130,5 @@
/>
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/listDivider" />
</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>
<!-- Category for more language settings. [CHAR LIMIT=NONE]-->
<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 -->
<!-- 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] -->
<string name="double_tap_power_camera_action_title">Camera</string>
<!-- 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] -->
<string name="double_tap_power_wallet_action_title">Wallet</string>
<!-- 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]-->
<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()) {
PreferenceScreenRegistry.INSTANCE.setPreferenceScreenMetadataFactories(
preferenceScreenFactories());
PreferenceScreenRegistry.INSTANCE.setPreferenceUiActionMetricsLogger(
new SettingsMetricsLogger(this));
PreferenceBindingFactory.setDefaultFactory(new SettingsPreferenceBindingFactory());
}

View File

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

View File

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

View File

@@ -16,9 +16,10 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.stream.Collectors.toList;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
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.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -61,6 +62,7 @@ public class AudioStreamHeaderController extends BasePreferenceController
private final Executor mExecutor;
private final AudioStreamsHelper mAudioStreamsHelper;
@Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final boolean mHysteresisModeFixAvailable;
@VisibleForTesting
final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
@@ -83,13 +85,13 @@ public class AudioStreamHeaderController extends BasePreferenceController
int sourceId,
BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state);
if (AudioStreamsHelper.isConnected(state)) {
var localSourceState = getLocalSourceState(state);
if (localSourceState == STREAMING) {
updateSummary();
mAudioStreamsHelper.startMediaService(
mContext, mBroadcastId, mBroadcastName);
} else if (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)
&& AudioStreamsHelper.hasSourcePresent(state)) {
// if source present but not connected, only update the summary
} else if (mHysteresisModeFixAvailable && localSourceState == PAUSED) {
// if source paused, only update the summary
updateSummary();
}
}
@@ -105,6 +107,8 @@ public class AudioStreamHeaderController extends BasePreferenceController
mExecutor = Executors.newSingleThreadExecutor();
mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
context);
}
@Override
@@ -151,38 +155,9 @@ public class AudioStreamHeaderController extends BasePreferenceController
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
var connectedSourceList =
mAudioStreamsHelper.getAllPresentSources().stream()
.filter(
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;
var sourceState = mAudioStreamsHelper.getConnectedBroadcastIdAndState(
mHysteresisModeFixAvailable).get(mBroadcastId);
var latestSummary = getLatestSummary(sourceState);
ThreadUtils.postOnMainThread(
() -> {
if (mHeaderController != null) {
@@ -212,4 +187,16 @@ public class AudioStreamHeaderController extends BasePreferenceController
mBroadcastName = broadcastName;
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;
import static java.util.Collections.emptyList;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -25,7 +23,6 @@ import android.app.Service;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothVolumeControl;
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.
private final AtomicInteger mLatestPositiveVolume = new AtomicInteger(25);
private final Object mLocalSessionLock = new Object();
private boolean mHysteresisModeFixAvailable;
private int mBroadcastId;
@Nullable private List<BluetoothDevice> mDevices;
@Nullable private LocalBluetoothManager mLocalBtManager;
@@ -139,6 +137,7 @@ public class AudioStreamMediaService extends Service {
Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
return;
}
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
mNotificationManager = getSystemService(NotificationManager.class);
if (mNotificationManager == null) {
@@ -309,13 +308,9 @@ public class AudioStreamMediaService extends Service {
}
private void handleRemoveSource() {
List<BluetoothLeBroadcastReceiveState> connected =
mAudioStreamsHelper == null
? emptyList()
: mAudioStreamsHelper.getAllConnectedSources();
if (connected.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.noneMatch(id -> id == mBroadcastId)) {
if (mAudioStreamsHelper != null
&& !mAudioStreamsHelper.getConnectedBroadcastIdAndState(
mHysteresisModeFixAvailable).containsKey(mBroadcastId)) {
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_TITLE;
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.emptyMap;
@@ -32,6 +38,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentActivity;
@@ -67,12 +74,6 @@ public class AudioStreamsHelper {
private final @Nullable LocalBluetoothManager mBluetoothManager;
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) {
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() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
return emptyList();
/**
* Gets a map of connected broadcast IDs to their corresponding local broadcast source states.
*
* <p>If multiple sources have the same broadcast ID, the state of the source that is
* {@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()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(AudioStreamsHelper::isConnected)
.toList();
.map(state -> new Pair<>(state.getBroadcastId(), getLocalSourceState(state)))
.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. */
@@ -163,47 +179,12 @@ public class AudioStreamsHelper {
.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. */
@Nullable
public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
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
* a connected LE device.
@@ -226,7 +207,7 @@ public class AudioStreamsHelper {
}
var deviceHasSource =
leadDevices.stream()
.filter(device -> hasConnectedBroadcastSource(device, manager))
.filter(device -> hasBroadcastSource(device, manager))
.findFirst();
if (deviceHasSource.isPresent()) {
Log.d(
@@ -258,38 +239,38 @@ public class AudioStreamsHelper {
return Optional.empty();
}
return leadDevices.stream()
.filter(device -> hasConnectedBroadcastSource(device, manager))
.filter(device -> hasBroadcastSource(device, manager))
.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 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) {
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;
}
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
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;
}
List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()
&& (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
localBtManager.getContext())
|| sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) {
boolean hysteresisModeFixAvailable = isAudioSharingHysteresisModeFixAvailable(
localBtManager.getContext());
if (hasReceiveState(sourceList, hysteresisModeFixAvailable)) {
Log.d(
TAG,
"Lead device has connected broadcast source, device = "
"Lead device has broadcast source, device = "
+ cachedDevice.getDevice().getAnonymizedAddress());
return true;
}
@@ -297,13 +278,10 @@ public class AudioStreamsHelper {
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice());
if (!list.isEmpty()
&& (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
localBtManager.getContext())
|| list.stream().anyMatch(AudioStreamsHelper::isConnected))) {
if (hasReceiveState(list, hysteresisModeFixAvailable)) {
Log.d(
TAG,
"Member device has connected broadcast source, device = "
"Member device has broadcast source, device = "
+ device.getDevice().getAnonymizedAddress());
return true;
}
@@ -311,6 +289,18 @@ public class AudioStreamsHelper {
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
* 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;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import com.android.settingslib.bluetooth.BluetoothUtils;
public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
private static final String TAG = "AudioStreamsProgressCategoryCallback";
private final Context mContext;
private final AudioStreamsProgressCategoryController mCategoryController;
public AudioStreamsProgressCategoryCallback(
Context context,
AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
mContext = context;
mCategoryController = audioStreamsProgressCategoryController;
}
@@ -40,15 +34,11 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA
public void onReceiveStateChanged(
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state);
if (AudioStreamsHelper.isConnected(state)) {
mCategoryController.handleSourceConnected(sink, state);
} else if (AudioStreamsHelper.isBadCode(state)) {
mCategoryController.handleSourceConnectBadCode(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);
var sourceState = getLocalSourceState(state);
switch (sourceState) {
case STREAMING -> mCategoryController.handleSourceStreaming(sink, state);
case DECRYPTION_FAILED -> mCategoryController.handleSourceConnectBadCode(state);
case PAUSED -> mCategoryController.handleSourcePaused(sink, state);
}
}

View File

@@ -16,6 +16,12 @@
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.stream.Collectors.toMap;
@@ -137,6 +143,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
private final boolean mHysteresisModeFixAvailable;
private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode;
private SourceOriginForLogging mSourceFromQrCodeOriginForLogging;
@Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference;
@@ -149,7 +156,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mMediaControlHelper = new MediaControlHelper(mContext, mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(context, this);
mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
mContext);
}
@Override
@@ -260,7 +269,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// change it's state.
existingPreference.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED
&& (!isAudioSharingHysteresisModeFixAvailable(mContext)
&& (!mHysteresisModeFixAvailable
|| fromState != AudioStreamState.SOURCE_PRESENT)) {
Log.w(
TAG,
@@ -336,8 +345,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (DEBUG) {
Log.d(TAG, "handleSourceLost()");
}
if (mAudioStreamsHelper.getAllConnectedSources().stream()
.anyMatch(connected -> connected.getBroadcastId() == broadcastId)) {
if (mAudioStreamsHelper.getConnectedBroadcastIdAndState(
mHysteresisModeFixAvailable).containsKey(broadcastId)) {
Log.d(
TAG,
"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
// state.
if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
|| (isAudioSharingHysteresisModeFixAvailable(mContext)
|| (mHysteresisModeFixAvailable
&& preference.getAudioStreamState()
== AudioStreamState.SOURCE_PRESENT))
&& mAudioStreamsHelper.getAllConnectedSources().stream()
.noneMatch(
connected ->
connected.getBroadcastId()
== preference.getAudioStreamBroadcastId())) {
&& !mAudioStreamsHelper.getConnectedBroadcastIdAndState(
mHysteresisModeFixAvailable).containsKey(
preference.getAudioStreamBroadcastId())) {
ThreadUtils.postOnMainThread(
() -> {
@@ -395,27 +402,27 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Expect one of the following:
// 1) No preference existed, create new preference with state SOURCE_ADDED
// 2) Any other state, move to SOURCE_ADDED
void handleSourceConnected(
void handleSourceStreaming(
BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) {
if (DEBUG) {
Log.d(TAG, "handleSourceConnected()");
Log.d(TAG, "handleSourceStreaming()");
}
if (!AudioStreamsHelper.isConnected(receiveState)) {
if (getLocalSourceState(receiveState) != STREAMING) {
return;
}
var broadcastIdConnected = receiveState.getBroadcastId();
var broadcastIdStreaming = receiveState.getBroadcastId();
Optional<BluetoothLeBroadcastMetadata> metadata =
getMetadataMatchingByBroadcastId(
device, receiveState.getSourceId(), broadcastIdConnected);
device, receiveState.getSourceId(), broadcastIdStreaming);
handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState);
mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected,
broadcastIdStreaming,
(k, existingPreference) -> {
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
// we retrieves the connected source during onStart() from
// AudioStreamsHelper#getAllConnectedSources() even before the source is
// we retrieves the streaming source during onStart() from
// AudioStreamsHelper#getAllStreamingSources() even before the source is
// founded by scanning.
return metadata.isPresent()
? addNewPreference(
@@ -440,7 +447,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (DEBUG) {
Log.d(TAG, "handleSourceConnectBadCode()");
}
if (!AudioStreamsHelper.isBadCode(receiveState)) {
if (getLocalSourceState(receiveState) != DECRYPTION_FAILED) {
return;
}
mBroadcastIdToPreferenceMap.computeIfPresent(
@@ -467,29 +474,28 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Find preference by receiveState and decide next state.
// Expect one preference existed, move to SOURCE_PRESENT
void handleSourcePresent(
void handleSourcePaused(
BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) {
if (DEBUG) {
Log.d(TAG, "handleSourcePresent()");
Log.d(TAG, "handleSourcePaused()");
}
if (!AudioStreamsHelper.hasSourcePresent(receiveState)) {
if (!mHysteresisModeFixAvailable || getLocalSourceState(receiveState) != PAUSED) {
return;
}
var broadcastIdConnected = receiveState.getBroadcastId();
var broadcastIdPaused = receiveState.getBroadcastId();
Optional<BluetoothLeBroadcastMetadata> metadata =
getMetadataMatchingByBroadcastId(
device, receiveState.getSourceId(), broadcastIdConnected);
device, receiveState.getSourceId(), broadcastIdPaused);
handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState);
mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected,
broadcastIdPaused,
(k, existingPreference) -> {
if (existingPreference == null) {
// No existing preference for this source even if it's already connected,
// add one and set initial state to SOURCE_PRESENT. This could happen
// because
// we retrieves the connected source during onStart() from
// AudioStreamsHelper#getAllPresentSources() even before the source is
// No existing preference for this source even if it's already existed but
// currently paused, add one and set initial state to SOURCE_PRESENT. This
// could happen because we retrieves the paused source during onStart() from
// AudioStreamsHelper#getAllPausedSources() even before the source is
// founded by scanning.
return metadata.isPresent()
? addNewPreference(
@@ -580,56 +586,43 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mExecutor.execute(
() -> {
// Handle QR code scan, display currently connected streams then start scanning
// sequentially
// Handle QR code scan, display currently streaming or paused streams then start
// scanning sequentially
handleSourceFromQrCodeIfExists();
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources =
mAudioStreamsHelper.getAllSourcesByDevice();
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> connectedSources =
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(
getStreamSourcesByDevice(sources).forEach(
(device, stateList) ->
stateList.forEach(
state -> handleSourceConnected(device, state)));
} else {
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>>
presentSources = getPresentSources(sources);
presentSources.forEach(
state -> handleSourceStreaming(device, state)));
if (mHysteresisModeFixAvailable) {
getPausedSourcesByDevice(sources).forEach(
(device, stateList) ->
stateList.forEach(
state -> handleSourcePresent(device, state)));
}
} else {
connectedSources.forEach(
(device, stateList) ->
stateList.forEach(
state -> handleSourceConnected(device, state)));
state -> handleSourcePaused(device, state)));
}
mLeBroadcastAssistant.startSearchingForSources(emptyList());
mMediaControlHelper.start();
});
}
private Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getConnectedSources(
private Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getStreamSourcesByDevice(
Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> sources) {
return sources.entrySet().stream()
.filter(
entry ->
entry.getValue().stream().anyMatch(AudioStreamsHelper::isConnected))
entry.getValue().stream().anyMatch(
state -> getLocalSourceState(state) == STREAMING))
.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) {
return sources.entrySet().stream()
.filter(
entry ->
entry.getValue().stream()
.anyMatch(AudioStreamsHelper::hasSourcePresent))
.anyMatch(state -> getLocalSourceState(state) == PAUSED))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@@ -742,8 +735,4 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
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.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.XmlRes;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;
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
public <T extends Preference> T findPreference(CharSequence key) {
if (key == null) {
@@ -147,17 +137,6 @@ public abstract class InstrumentedPreferenceFragment extends ObservablePreferenc
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 final InteractionJankMonitor mMonitor = InteractionJankMonitor.getInstance();
private final String mClassName;

View File

@@ -415,7 +415,6 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
removeControllersForHybridMode();
}
setPreferenceScreen(screen);
updateActivityTitleWithScreenTitle(screen);
} else {
addPreferencesFromResource(resId);
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_MANUAL
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.settings.PreferenceRestrictionMixin
import com.android.settings.R
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.KeyedObservableDelegate
import com.android.settingslib.datastore.SettingsStore
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.PreferenceMetadata
import com.android.settingslib.metadata.ProvidePreferenceScreen
@@ -42,10 +43,11 @@ import com.android.settingslib.preference.PreferenceScreenCreator
@ProvidePreferenceScreen(AutoBrightnessScreen.KEY)
class AutoBrightnessScreen :
PreferenceScreenCreator,
PreferenceScreenBinding,
PreferenceScreenBinding, // binding for screen page
PrimarySwitchPreferenceBinding, // binding for screen entry point widget
PreferenceAvailabilityProvider,
PreferenceRestrictionMixin,
BooleanPreference {
BooleanValuePreference {
override val key: String
get() = KEY
@@ -93,15 +95,10 @@ class AutoBrightnessScreen :
override val useAdminDisabledSummary: Boolean
get() = true
override fun createWidget(context: Context) = PrimarySwitchPreference(context)
override fun bind(preference: Preference, metadata: PreferenceMetadata) {
super.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)
}
override fun bind(preference: Preference, metadata: PreferenceMetadata) =
when (preference) {
is PreferenceScreen -> super<PreferenceScreenBinding>.bind(preference, metadata)
else -> super<PrimarySwitchPreferenceBinding>.bind(preference, metadata)
}
/**

View File

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

View File

@@ -16,29 +16,55 @@
package com.android.settings.inputmethod;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.input.InputSettings;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.SliderPreferenceController;
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) {
super(context, key);
mContentResolver = context.getContentResolver();
mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
updateAvailabilityStatus();
}
};
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
SeekBarPreference preference = screen.findPreference(getPreferenceKey());
preference.setMax(getMax());
preference.setMin(getMin());
preference.setProgress(getSliderPosition());
updateState(preference);
mPreference = screen.findPreference(getPreferenceKey());
mPreference.setMax(getMax());
mPreference.setMin(getMin());
mPreference.setProgress(getSliderPosition());
updateState(mPreference);
}
@Override
@@ -46,7 +72,7 @@ public class MouseScrollingSpeedPreferenceController extends SliderPreferenceCon
if (!InputSettings.isMouseScrollingAccelerationFeatureFlagEnabled()) {
return UNSUPPORTED_ON_DEVICE;
}
return AVAILABLE;
return shouldEnableSlideBar() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
}
@Override
@@ -73,4 +99,30 @@ public class MouseScrollingSpeedPreferenceController extends SliderPreferenceCon
public int getMax() {
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 PreferenceScreen mScreen;
private String mPreviousSelection;
private String mFinalSelectedLayout;
private String mLayout;
private String mFinalSelectedLayoutDescriptor;
private String mSelectedLayoutDescriptor;
private MetricsFeatureProvider mMetricsFeatureProvider;
private KeyboardLayoutSelectedCallback mKeyboardLayoutSelectedCallback;
@@ -83,8 +83,8 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
mInputMethodSubtype =
arguments.getParcelable(
InputPeripheralsSettingsUtils.EXTRA_INPUT_METHOD_SUBTYPE);
mLayout = getSelectedLayoutLabel();
mFinalSelectedLayout = mLayout;
mSelectedLayoutDescriptor = getSelectedLayoutDescriptor();
mFinalSelectedLayoutDescriptor = mSelectedLayoutDescriptor;
mKeyboardLayouts = mIm.getKeyboardLayoutListForInputDevice(
mInputDeviceIdentifier, mUserId, mInputMethodInfo, mInputMethodSubtype);
InputPeripheralsSettingsUtils.sortKeyboardLayoutsByLabel(mKeyboardLayouts);
@@ -106,8 +106,12 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
@Override
public void onStop() {
if (mLayout != null && !mLayout.equals(mFinalSelectedLayout)) {
String change = "From:" + mLayout + ", to:" + mFinalSelectedLayout;
if (mSelectedLayoutDescriptor != null
&& !mSelectedLayoutDescriptor.equals(mFinalSelectedLayoutDescriptor)) {
String change = "From:"
+ getLayoutLabel(mSelectedLayoutDescriptor)
+ ", to:"
+ getLayoutLabel(mFinalSelectedLayoutDescriptor);
mMetricsFeatureProvider.action(
mContext, SettingsEnums.ACTION_PK_LAYOUT_CHANGED, change);
}
@@ -152,7 +156,7 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
}
setLayout(pref);
mPreviousSelection = preference.getKey();
mFinalSelectedLayout = pref.getTitle().toString();
mFinalSelectedLayoutDescriptor = mPreviousSelection;
return true;
}
@@ -182,12 +186,12 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
pref = new TickButtonPreference(mScreen.getContext());
pref.setTitle(layout.getLabel());
if (mLayout.equals(layout.getLabel())) {
if (mSelectedLayoutDescriptor.equals(layout.getDescriptor())) {
if (mKeyboardLayoutSelectedCallback != null) {
mKeyboardLayoutSelectedCallback.onSelected(layout);
}
pref.setSelected(true);
mPreviousSelection = layout.getDescriptor();
mPreviousSelection = mSelectedLayoutDescriptor;
}
pref.setKey(layout.getDescriptor());
mScreen.addPreference(pref);
@@ -204,15 +208,19 @@ public class NewKeyboardLayoutPickerController extends BasePreferenceController
mPreferenceMap.get(preference).getDescriptor());
}
private String getSelectedLayoutLabel() {
String label = mContext.getString(R.string.keyboard_default_layout);
private String getSelectedLayoutDescriptor() {
KeyboardLayoutSelectionResult result = InputPeripheralsSettingsUtils.getKeyboardLayout(
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(
mIm, mUserId, mInputDeviceIdentifier, mInputMethodInfo, mInputMethodSubtype);
if (result.getLayoutDescriptor() != null) {
if (descriptor != null) {
for (KeyboardLayout keyboardLayout : keyboardLayouts) {
if (keyboardLayout.getDescriptor().equals(result.getLayoutDescriptor())) {
if (keyboardLayout.getDescriptor().equals(descriptor)) {
label = keyboardLayout.getLabel();
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;
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.SystemClock;
import android.util.Log;
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.stream.Collectors;
/**
* A locale utility class.
*/
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
* language list would not show two locales with the same language and region but different
@@ -50,4 +87,191 @@ public class LocaleUtils {
}
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.preference.Preference
import com.android.settings.AirplaneModeEnabler
import com.android.settings.PreferenceActionMetricsProvider
import com.android.settings.PreferenceRestrictionMixin
import com.android.settings.R
import com.android.settings.Utils
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.datastore.AbstractKeyedDataObservable
import com.android.settingslib.datastore.KeyValueStore
@@ -51,6 +51,7 @@ import com.android.settingslib.metadata.SwitchPreference
// LINT.IfChange
class AirplaneModePreference :
SwitchPreference(KEY, R.string.airplane_mode),
PreferenceActionMetricsProvider,
PreferenceAvailabilityProvider,
PreferenceLifecycleProvider,
PreferenceRestrictionMixin {
@@ -88,6 +89,9 @@ class AirplaneModePreference :
override val sensitivityLevel
get() = SensitivityLevel.HIGH_SENSITIVITY
override val preferenceActionMetrics: Int
get() = ACTION_AIRPLANE_TOGGLE
override fun storage(context: Context): KeyValueStore =
AirplaneModeStorage(context, SettingsGlobalStore.get(context))
@@ -109,16 +113,12 @@ class AirplaneModePreference :
(settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as 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)
val intent = Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)
intent.putExtra("state", value)
context.sendBroadcastAsUser(intent, UserHandle.ALL)
val metricsFeature = featureFactory.metricsFeatureProvider
metricsFeature.action(context, ACTION_AIRPLANE_TOGGLE, value)
}
}
override fun onFirstObserverAdded() {

View File

@@ -21,13 +21,13 @@ import android.app.settings.SettingsEnums;
import android.os.Bundle;
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
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new EnableZenModeDialog(getContext()).createDialog();
return new EnableDndDialogFactory(getContext()).createDialog();
}
@Override

View File

@@ -17,11 +17,14 @@
package com.android.settings.notification.history;
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 android.annotation.AttrRes;
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.app.ActionBar;
import android.app.ActivityManager;
import android.app.INotificationManager;
@@ -30,7 +33,6 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Outline;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -41,12 +43,10 @@ import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.Slog;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ImageView;
import android.widget.TextView;
@@ -95,22 +95,7 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
private PackageManager mPm;
private CountDownLatch mCountdownLatch;
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();
enum NotificationHistoryEvent implements UiEventLogger.UiEventEnum {
@@ -158,20 +143,28 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
private HistoryLoader.OnHistoryLoaderListener mOnHistoryLoaderListener = notifications -> {
findViewById(R.id.today_list).setVisibility(
notifications.isEmpty() ? View.GONE : View.VISIBLE);
notifications.isEmpty() ? GONE : VISIBLE);
mCountdownLatch.countDown();
View recyclerView = mTodayView.findViewById(R.id.apps);
recyclerView.setClipToOutline(true);
mTodayView.setOutlineProvider(mOutlineProvider);
mSnoozeView.setOutlineProvider(mOutlineProvider);
// for each package, new header and recycler view
for (int i = 0, notificationsSize = notifications.size(); i < notificationsSize; i++) {
NotificationHistoryPackage nhp = notifications.get(i);
View viewForPackage = LayoutInflater.from(this)
.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);
container.setVisibility(View.GONE);
container.setVisibility(GONE);
View header = viewForPackage.findViewById(R.id.app_header);
NotificationExpandButton expand = viewForPackage.findViewById(
com.android.internal.R.id.expand_button);
@@ -181,19 +174,19 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
expand.setDefaultPillColor(pillColor);
expand.setDefaultTextColor(textColor);
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_show));
int finalI = i;
header.setOnClickListener(v -> {
container.setVisibility(container.getVisibility() == View.VISIBLE
? View.GONE : View.VISIBLE);
expand.setExpanded(container.getVisibility() == View.VISIBLE);
header.setStateDescription(container.getVisibility() == View.VISIBLE
container.setVisibility(container.getVisibility() == VISIBLE
? GONE : VISIBLE);
expand.setExpanded(container.getVisibility() == VISIBLE);
header.setStateDescription(container.getVisibility() == VISIBLE
? getString(R.string.condition_expand_hide)
: getString(R.string.condition_expand_show));
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_CLOSE,
nhp.uid, nhp.pkgName, finalI);
@@ -217,7 +210,7 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
count.setText(StringUtil.getIcuPluralsString(this, newCount,
R.string.notification_history_count));
if (newCount == 0) {
viewForPackage.setVisibility(View.GONE);
viewForPackage.setVisibility(GONE);
}
}, mUiEventLogger));
((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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -240,8 +228,6 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
mTodayView = findViewById(R.id.apps);
mSnoozeView = findViewById(R.id.snoozed_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);
mHistoryOn = findViewById(R.id.history_on);
mHistoryEmpty = findViewById(R.id.history_on_empty);
@@ -289,11 +275,11 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
}
ThreadUtils.postOnMainThread(() -> {
if (mSwitchBar.isChecked()
&& findViewById(R.id.today_list).getVisibility() == View.GONE
&& mSnoozeView.getVisibility() == View.GONE
&& mDismissView.getVisibility() == View.GONE) {
mHistoryOn.setVisibility(View.GONE);
mHistoryEmpty.setVisibility(View.VISIBLE);
&& findViewById(R.id.today_list).getVisibility() == GONE
&& mSnoozeView.getVisibility() == GONE
&& mDismissView.getVisibility() == GONE) {
mHistoryOn.setVisibility(GONE);
mHistoryEmpty.setVisibility(VISIBLE);
}
});
});
@@ -320,6 +306,33 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
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) {
Resources.Theme theme = new ContextThemeWrapper(this,
android.R.style.Theme_DeviceDefault_DayNight).getTheme();
@@ -345,14 +358,14 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
private void toggleViews(boolean isChecked) {
if (isChecked) {
mHistoryOff.setVisibility(View.GONE);
mHistoryOn.setVisibility(View.VISIBLE);
mHistoryOff.setVisibility(GONE);
mHistoryOn.setVisibility(VISIBLE);
} else {
mHistoryOn.setVisibility(View.GONE);
mHistoryOff.setVisibility(View.VISIBLE);
mHistoryOn.setVisibility(GONE);
mHistoryOff.setVisibility(VISIBLE);
mTodayView.removeAllViews();
}
mHistoryEmpty.setVisibility(View.GONE);
mHistoryEmpty.setVisibility(GONE);
}
private final OnCheckedChangeListener mOnSwitchClickListener =
@@ -372,13 +385,13 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
Log.d(TAG, "onSwitchChange history to " + isChecked);
}
// Reset UI visibility to ensure it matches real state.
mHistoryOn.setVisibility(View.GONE);
mHistoryOn.setVisibility(GONE);
if (isChecked) {
mHistoryEmpty.setVisibility(View.VISIBLE);
mHistoryOff.setVisibility(View.GONE);
mHistoryEmpty.setVisibility(VISIBLE);
mHistoryOff.setVisibility(GONE);
} else {
mHistoryOff.setVisibility(View.VISIBLE);
mHistoryEmpty.setVisibility(View.GONE);
mHistoryOff.setVisibility(VISIBLE);
mHistoryEmpty.setVisibility(GONE);
}
mTodayView.removeAllViews();
};
@@ -410,7 +423,7 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
mSnoozedRv.setNestedScrollingEnabled(false);
if (snoozed == null || snoozed.length == 0) {
mSnoozeView.setVisibility(View.GONE);
mSnoozeView.setVisibility(GONE);
} else {
((NotificationSbnAdapter) mSnoozedRv.getAdapter()).onRebuildComplete(
new ArrayList<>(Arrays.asList(snoozed)));
@@ -426,9 +439,9 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
mDismissedRv.setNestedScrollingEnabled(false);
if (dismissed == null || dismissed.length == 0) {
mDismissView.setVisibility(View.GONE);
mDismissView.setVisibility(GONE);
} else {
mDismissView.setVisibility(View.VISIBLE);
mDismissView.setVisibility(VISIBLE);
((NotificationSbnAdapter) mDismissedRv.getAdapter()).onRebuildComplete(
new ArrayList<>(Arrays.asList(dismissed)));
}
@@ -446,10 +459,10 @@ public class NotificationHistoryActivity extends CollapsingToolbarBaseActivity {
int reason) {
if (reason == REASON_SNOOZED) {
((NotificationSbnAdapter) mSnoozedRv.getAdapter()).addSbn(sbn);
mSnoozeView.setVisibility(View.VISIBLE);
mSnoozeView.setVisibility(VISIBLE);
} else {
((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.Nullable;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -36,7 +35,6 @@ public class NotificationHistoryRecyclerView extends RecyclerView {
super(context, attrs, defStyle);
setLayoutManager(new LinearLayoutManager(getContext()));
addItemDecoration(new DividerItemDecoration(getContext(), LinearLayoutManager.VERTICAL));
ItemTouchHelper touchHelper = new ItemTouchHelper(
new DismissTouchHelper(0, ItemTouchHelper.START | ItemTouchHelper.END));
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_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.UserIdInt;
import android.app.ActivityManager;
@@ -111,13 +115,22 @@ public class NotificationSbnAdapter extends
int position) {
final StatusBarNotification sbn = mValues.get(position);
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.setIcon(loadIcon(sbn));
holder.setPackageLabel(loadPackageLabel(sbn.getPackageName()).toString());
holder.setTitle(getTitleString(sbn.getNotification()));
holder.setSummary(getTextString(mContext, sbn.getNotification()));
holder.setPostedTime(sbn.getPostTime());
holder.setDividerVisible(position < (mValues.size() -1));
int userId = normalizeUserId(sbn);
if (!mUserBadgeCache.containsKey(userId)) {
Drawable profile = mContext.getPackageManager().getUserBadgeForDensityNoBackground(

View File

@@ -49,7 +49,6 @@ public class NotificationSbnViewHolder extends RecyclerView.ViewHolder {
private final TextView mTitle;
private final TextView mSummary;
private final ImageView mProfileBadge;
private final View mDivider;
NotificationSbnViewHolder(View itemView) {
super(itemView);
@@ -59,7 +58,6 @@ public class NotificationSbnViewHolder extends RecyclerView.ViewHolder {
mTitle = itemView.findViewById(R.id.title);
mSummary = itemView.findViewById(R.id.text);
mProfileBadge = itemView.findViewById(R.id.profile_badge);
mDivider = itemView.findViewById(R.id.divider);
}
void setSummary(CharSequence summary) {
@@ -92,10 +90,6 @@ public class NotificationSbnViewHolder extends RecyclerView.ViewHolder {
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,
InstanceId instanceId,
boolean isSnoozed, UiEventLogger uiEventLogger) {

View File

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

View File

@@ -21,13 +21,13 @@ import android.app.settings.SettingsEnums;
import android.os.Bundle;
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 {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new ZenDurationDialog(getContext()).createDialog();
return new DndDurationDialogFactory(getContext()).createDialog();
}
@Override

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,17 +27,17 @@ import android.net.wifi.WifiManager
import android.os.UserManager
import android.text.BidiFormatter
import android.util.Log
import androidx.preference.Preference
import com.android.settings.PreferenceRestrictionMixin
import com.android.settings.R
import com.android.settings.Utils
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.utils.tetheringManager
import com.android.settings.wifi.utils.wifiApState
import com.android.settings.wifi.utils.wifiManager
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.datastore.AbstractKeyedDataObservable
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.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.PreferenceChangeReason
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.ReadWritePermit
import com.android.settingslib.metadata.SensitivityLevel
import com.android.settingslib.metadata.SwitchPreference
import com.android.settingslib.preference.PreferenceBinding
import com.android.settingslib.wifi.WifiUtils.Companion.getWifiTetherSummaryForConnectedDevices
import com.android.settings.datausage.DataSaverMainSwitchPreference.Companion.KEY as DATA_SAVER_KEY
// LINT.IfChange
class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStore) :
SwitchPreference(KEY, R.string.wifi_hotspot_checkbox_text),
PreferenceBinding,
PrimarySwitchPreferenceBinding,
PreferenceAvailabilityProvider,
PreferenceSummaryProvider,
PreferenceRestrictionMixin {
@@ -130,8 +127,6 @@ class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStor
override val sensitivityLevel
get() = SensitivityLevel.HIGH_SENSITIVITY
override fun createWidget(context: Context) = PrimarySwitchPreference(context)
override fun storage(context: Context): KeyValueStore = wifiHotspotStore
@Suppress("UNCHECKED_CAST")
@@ -201,16 +196,7 @@ class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStor
Log.e(TAG, "onTetheringFailed(),error=$error")
}
override fun onKeyChanged(key: String, reason: Int) =
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
}
override fun onKeyChanged(key: String, reason: Int) = notifyChange(KEY, reason)
}
companion object {

View File

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

View File

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

View File

@@ -16,6 +16,8 @@
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_ENABLE_LE_AUDIO_SHARING;
@@ -71,6 +73,7 @@ import org.robolectric.shadow.api.Shadow;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
@RunWith(RobolectricTestRunner.class)
@@ -164,9 +167,9 @@ public class AudioStreamButtonControllerTest {
}
@Test
public void testDisplayPreference_sourceConnected_setDisconnectButton() {
when(mAudioStreamsHelper.getAllConnectedSources())
.thenReturn(List.of(mBroadcastReceiveState));
public void testDisplayPreference_sourceStreaming_setDisconnectButton() {
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, STREAMING));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
mController.displayPreference(mScreen);
@@ -190,7 +193,8 @@ public class AudioStreamButtonControllerTest {
@Test
public void testDisplayPreference_sourceNotConnected_setConnectButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.setAudioStreamsRepositoryForTesting(mRepository);
var metadataToRejoin = mock(BluetoothLeBroadcastMetadata.class);
when(mRepository.getSavedMetadata(any(), anyInt())).thenReturn(metadataToRejoin);
@@ -216,7 +220,8 @@ public class AudioStreamButtonControllerTest {
@Test
public void testCallback_onSourceRemoved_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceRemoved(
@@ -230,9 +235,8 @@ public class AudioStreamButtonControllerTest {
@Test
public void testCallback_onSourceRemovedFailed_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources())
.thenReturn(List.of(mBroadcastReceiveState));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, STREAMING));
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceRemoveFailed(
@@ -250,9 +254,8 @@ public class AudioStreamButtonControllerTest {
@Test
public void testCallback_onReceiveStateChanged_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources())
.thenReturn(List.of(mBroadcastReceiveState));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, STREAMING));
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L);
@@ -273,7 +276,7 @@ public class AudioStreamButtonControllerTest {
}
@Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
public void testCallback_onReceiveStateChangedWithSourcePaused_updateButton() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
@@ -284,13 +287,16 @@ public class AudioStreamButtonControllerTest {
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
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.mBroadcastAssistantCallback.onReceiveStateChanged(
mock(BluetoothDevice.class), /* sourceId= */ 0, state);
verify(mFeatureFactory.metricsFeatureProvider, never())
verify(mFeatureFactory.metricsFeatureProvider)
.action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), anyInt());
// Called twice, once in displayPreference, the other one in callback
@@ -302,7 +308,8 @@ public class AudioStreamButtonControllerTest {
@Test
public void testCallback_onSourceAddFailed_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceAddFailed(
@@ -321,7 +328,8 @@ public class AudioStreamButtonControllerTest {
@Test
public void testCallback_onSourceLost_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen);
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_NOT_LISTENING_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_ENABLE_LE_AUDIO_SHARING;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -66,6 +69,7 @@ import org.robolectric.shadow.api.Shadow;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
@RunWith(RobolectricTestRunner.class)
@@ -160,10 +164,9 @@ public class AudioStreamHeaderControllerTest {
}
@Test
public void testDisplayPreference_sourceConnected_setSummary() {
when(mAudioStreamsHelper.getAllConnectedSources())
.thenReturn(List.of(mBroadcastReceiveState));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
public void testDisplayPreference_sourceStreaming_setSummary() {
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, STREAMING));
mController.displayPreference(mScreen);
@@ -176,8 +179,9 @@ public class AudioStreamHeaderControllerTest {
}
@Test
public void testDisplayPreference_sourceNotConnected_setSummary() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
public void testDisplayPreference_sourceNotStreaming_setSummary() {
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen);
@@ -189,18 +193,14 @@ public class AudioStreamHeaderControllerTest {
}
@Test
public void testDisplayPreference_sourcePresent_setSummary() {
public void testDisplayPreference_sourcePaused_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
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));
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, PAUSED));
// Create new controller to enable hysteresis mode
mController = new AudioStreamHeaderController(mContext, KEY);
mController.init(mFragment, BROADCAST_NAME, BROADCAST_ID);
mController.displayPreference(mScreen);
verify(mHeaderController).setLabel(BROADCAST_NAME);
@@ -212,10 +212,10 @@ public class AudioStreamHeaderControllerTest {
}
@Test
public void testDisplayPreference_sourceNotPresent_setSummary() {
public void testDisplayPreference_sourceNotPaused_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(Collections.emptyList());
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen);
@@ -228,7 +228,8 @@ public class AudioStreamHeaderControllerTest {
@Test
public void testCallback_onSourceRemoved_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceRemoved(
@@ -241,7 +242,8 @@ public class AudioStreamHeaderControllerTest {
@Test
public void testCallback_onSourceLost_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Collections.emptyMap());
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onSourceLost(/* broadcastId= */ 1);
@@ -253,8 +255,8 @@ public class AudioStreamHeaderControllerTest {
@Test
public void testCallback_onReceiveStateChanged_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources())
.thenReturn(List.of(mBroadcastReceiveState));
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean()))
.thenReturn(Map.of(BROADCAST_ID, STREAMING));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
List<Long> bisSyncState = new ArrayList<>();
@@ -272,17 +274,20 @@ public class AudioStreamHeaderControllerTest {
}
@Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
public void testCallback_onReceiveStateChangedWithSourcePaused_updateButton() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
when(mAudioStreamsHelper.getAllPresentSources())
.thenReturn(List.of(mBroadcastReceiveState));
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);
// Create new controller to enable hysteresis mode
mController = new AudioStreamHeaderController(mContext, KEY);
mController.init(mFragment, BROADCAST_NAME, BROADCAST_ID);
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onReceiveStateChanged(
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_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_ENABLE_LE_AUDIO_SHARING;
@@ -149,11 +151,14 @@ public class AudioStreamsHelperTest {
@Test
public void removeSource_noConnectedSource_doNothing() {
String address = "11:22:33:44:55:66";
List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice);
when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
when(source.getBroadcastId()).thenReturn(BROADCAST_ID_2);
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
@@ -214,15 +219,16 @@ public class AudioStreamsHelperTest {
}
@Test
public void getAllConnectedSources_noAssistant() {
public void getConnectedBroadcastIdAndState_noAssistant() {
when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(null);
mHelper = new AudioStreamsHelper(mLocalBluetoothManager);
assertThat(mHelper.getAllConnectedSources()).isEmpty();
assertThat(mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */
false)).isEmpty();
}
@Test
public void getAllConnectedSources_returnSource() {
public void getConnectedBroadcastIdAndState_returnStreamingSource() {
List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice);
when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
@@ -234,14 +240,15 @@ public class AudioStreamsHelperTest {
List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L);
when(source.getBisSyncState()).thenReturn(bisSyncState);
when(source.getBroadcastId()).thenReturn(BROADCAST_ID_1);
var list = mHelper.getAllConnectedSources();
assertThat(list).isNotEmpty();
assertThat(list.get(0)).isEqualTo(source);
var map = mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */ false);
assertThat(map).isNotEmpty();
assertThat(map.get(BROADCAST_ID_1)).isEqualTo(STREAMING);
}
@Test
public void getAllPresentSources_noSource() {
public void getConnectedBroadcastIdAndState_noSource() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
@@ -259,12 +266,12 @@ public class AudioStreamsHelperTest {
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
var list = mHelper.getAllPresentSources();
assertThat(list).isEmpty();
var map = mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */ true);
assertThat(map).isEmpty();
}
@Test
public void getAllPresentSources_returnSource() {
public void getConnectedBroadcastIdAndState_returnPausedSource() {
mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
@@ -282,10 +289,11 @@ public class AudioStreamsHelperTest {
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(source.getBisSyncState()).thenReturn(bisSyncState);
when(source.getBroadcastId()).thenReturn(BROADCAST_ID_1);
var list = mHelper.getAllPresentSources();
assertThat(list).isNotEmpty();
assertThat(list.get(0)).isEqualTo(source);
var map = mHelper.getConnectedBroadcastIdAndState(/* hysteresisModeFixAvailable= */ true);
assertThat(map).isNotEmpty();
assertThat(map.get(BROADCAST_ID_1)).isEqualTo(PAUSED);
}
@Test

View File

@@ -77,7 +77,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
BluetoothStatusCodes.FEATURE_SUPPORTED);
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
mCallback = new AudioStreamsProgressCategoryCallback(mContext, mController);
mCallback = new AudioStreamsProgressCategoryCallback(mController);
}
@Test
@@ -87,7 +87,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
when(mState.getBisSyncState()).thenReturn(bisSyncState);
mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
verify(mController).handleSourceConnected(any(), any());
verify(mController).handleSourceStreaming(any(), any());
}
@Test
@@ -102,7 +102,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
when(mSourceDevice.getAddress()).thenReturn(address);
mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
verify(mController).handleSourcePresent(any(), any());
verify(mController).handleSourcePaused(any(), any());
}
@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.UNSET_BROADCAST_ID;
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_ENABLE_LE_AUDIO_SHARING;
@@ -355,7 +356,7 @@ public class AudioStreamsProgressCategoryControllerTest {
}
@Test
public void testOnStart_handleSourceAlreadyConnected_useNameFromMetadata() {
public void testOnStart_handleSourceAlreadyStreaming_useNameFromMetadata() {
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
@@ -665,8 +666,8 @@ public class AudioStreamsProgressCategoryControllerTest {
shadowOf(Looper.getMainLooper()).idle();
// A new source found is lost, but the source is still connected
BluetoothLeBroadcastReceiveState connected = createConnectedMock(NEWLY_FOUND_BROADCAST_ID);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean())).thenReturn(
Map.of(NEWLY_FOUND_BROADCAST_ID, STREAMING));
mController.handleSourceLost(NEWLY_FOUND_BROADCAST_ID);
shadowOf(Looper.getMainLooper()).idle();
@@ -819,13 +820,15 @@ public class AudioStreamsProgressCategoryControllerTest {
}
@Test
public void testHandleSourcePresent_updateState() {
public void testHandleSourcePaused_updateState() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Create new controller to enable hysteresis mode
mController = spy(new TestController(mContext, KEY));
// Setup mPreference so it's not null
mController.displayPreference(mScreen);
@@ -844,7 +847,7 @@ public class AudioStreamsProgressCategoryControllerTest {
when(receiveState.getBisSyncState()).thenReturn(bisSyncState);
// The new found source is identified as failed to connect
mController.handleSourcePresent(mSourceDevice, receiveState);
mController.handleSourcePaused(mSourceDevice, receiveState);
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference =

View File

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

View File

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

View File

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