diff --git a/aconfig/accessibility/accessibility_flags.aconfig b/aconfig/accessibility/accessibility_flags.aconfig index 268ae6af59d..f7ef9d5af46 100644 --- a/aconfig/accessibility/accessibility_flags.aconfig +++ b/aconfig/accessibility/accessibility_flags.aconfig @@ -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" diff --git a/res/drawable/ic_settings_globe.xml b/res/drawable/ic_settings_globe.xml new file mode 100644 index 00000000000..9834df6adba --- /dev/null +++ b/res/drawable/ic_settings_globe.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/res/layout/notification_history.xml b/res/layout/notification_history.xml index 29744cc7846..798ba4c3ac1 100644 --- a/res/layout/notification_history.xml +++ b/res/layout/notification_history.xml @@ -17,7 +17,6 @@ @@ -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"> + android:orientation="vertical"> diff --git a/res/layout/notification_history_app_layout.xml b/res/layout/notification_history_app_layout.xml index 143fff8f7e5..c3cece75fd1 100644 --- a/res/layout/notification_history_app_layout.xml +++ b/res/layout/notification_history_app_layout.xml @@ -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 @@ - - + android:layout_height="wrap_content" + android:layout_marginStart="?android:attr/listPreferredItemPaddingStart" + android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"> + + + android:textAlignment="viewStart" + android:paddingBottom="16dp" /> + diff --git a/res/layout/notification_sbn_log_row.xml b/res/layout/notification_sbn_log_row.xml index 23bc110da8b..cfd74d37c44 100644 --- a/res/layout/notification_sbn_log_row.xml +++ b/res/layout/notification_sbn_log_row.xml @@ -22,8 +22,10 @@ @@ -128,11 +130,5 @@ /> - - \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 9842843c9cd..9930c3685c0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -527,6 +527,10 @@ The region you choose affects how your phone displays time, dates, temperature, and more More language settings + + Change region to %s ? + + Your device will keep %s as a system language @@ -11326,11 +11330,11 @@ Camera - Access Camera + Open Camera Wallet - Access Wallet + Open Wallet Flip camera for selfie diff --git a/src/com/android/settings/Metrics.kt b/src/com/android/settings/Metrics.kt new file mode 100644 index 00000000000..0d5ea56aff3 --- /dev/null +++ b/src/com/android/settings/Metrics.kt @@ -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 -> {} + } + } +} diff --git a/src/com/android/settings/SettingsApplication.java b/src/com/android/settings/SettingsApplication.java index 442e3c2559a..9c5671f1027 100644 --- a/src/com/android/settings/SettingsApplication.java +++ b/src/com/android/settings/SettingsApplication.java @@ -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()); } diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java index bb4f2a78b47..1e13c2291dd 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java @@ -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()); diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java index f9cce4ce099..b9228a93eac 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java @@ -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,18 +78,17 @@ 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)) { - mMetricsFeatureProvider.action( - mContext, - SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED, - SOURCE_ORIGIN_REPOSITORY); - } + // TODO(b/308368124): Verify if this log is too noisy. + mMetricsFeatureProvider.action( + mContext, + SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED, + SOURCE_ORIGIN_REPOSITORY); } } @@ -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 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; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java index 88efff2b6ab..6037577162f 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java @@ -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); + } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java index 322fd3cad19..50231665865 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java @@ -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 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 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(); } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java index 4d6c4cab938..0890870442f 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java @@ -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 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. + * + *

If multiple sources have the same broadcast ID, the state of the source that is + * {@code STREAMING} is preferred. + */ + public Map 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 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 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 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 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 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 diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java index 87cea2c1e94..f0d0bebcddb 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java @@ -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); } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java index 6831c5a12e3..24978c6a753 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -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 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,8 +269,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro // change it's state. existingPreference.setAudioStreamMetadata(source); if (fromState != AudioStreamState.SOURCE_ADDED - && (!isAudioSharingHysteresisModeFixAvailable(mContext) - || fromState != AudioStreamState.SOURCE_PRESENT)) { + && (!mHysteresisModeFixAvailable + || fromState != AudioStreamState.SOURCE_PRESENT)) { Log.w( TAG, "handleSourceFound(): unexpected state : " @@ -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 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 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> sources = mAudioStreamsHelper.getAllSourcesByDevice(); - Map> 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( - (device, stateList) -> - stateList.forEach( - state -> handleSourceConnected(device, state))); - } else { - Map> - presentSources = getPresentSources(sources); - presentSources.forEach( - (device, stateList) -> - stateList.forEach( - state -> handleSourcePresent(device, state))); - } - } else { - connectedSources.forEach( + getStreamSourcesByDevice(sources).forEach( + (device, stateList) -> + stateList.forEach( + state -> handleSourceStreaming(device, state))); + if (mHysteresisModeFixAvailable) { + getPausedSourcesByDevice(sources).forEach( (device, stateList) -> stateList.forEach( - state -> handleSourceConnected(device, state))); + state -> handleSourcePaused(device, state))); } mLeBroadcastAssistant.startSearchingForSources(emptyList()); mMediaControlHelper.start(); }); } - private Map> getConnectedSources( + private Map> getStreamSourcesByDevice( Map> 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> getPresentSources( + private Map> getPausedSourcesByDevice( Map> 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); - } } diff --git a/src/com/android/settings/core/InstrumentedPreferenceFragment.java b/src/com/android/settings/core/InstrumentedPreferenceFragment.java index ac87ea5abc6..2ec93b6e75e 100644 --- a/src/com/android/settings/core/InstrumentedPreferenceFragment.java +++ b/src/com/android/settings/core/InstrumentedPreferenceFragment.java @@ -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 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; diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index c79cc65bbfa..d163bda8d9f 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -415,7 +415,6 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment removeControllersForHybridMode(); } setPreferenceScreen(screen); - updateActivityTitleWithScreenTitle(screen); } else { addPreferencesFromResource(resId); screen = getPreferenceScreen(); diff --git a/src/com/android/settings/display/AutoBrightnessScreen.kt b/src/com/android/settings/display/AutoBrightnessScreen.kt index 48ef210e360..c42588853c6 100644 --- a/src/com/android/settings/display/AutoBrightnessScreen.kt +++ b/src/com/android/settings/display/AutoBrightnessScreen.kt @@ -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,16 +95,11 @@ 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.bind(preference, metadata) + else -> super.bind(preference, metadata) } - } /** * The datastore for brightness, which is persisted as integer but the external type is boolean. diff --git a/src/com/android/settings/display/darkmode/DarkModeScreen.kt b/src/com/android/settings/display/darkmode/DarkModeScreen.kt index f1a95f59874..527cd192b79 100644 --- a/src/com/android/settings/display/darkmode/DarkModeScreen.kt +++ b/src/com/android/settings/display/darkmode/DarkModeScreen.kt @@ -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.bind(preference, metadata) + else -> super.bind(preference, metadata) } } diff --git a/src/com/android/settings/inputmethod/MouseScrollingSpeedPreferenceController.java b/src/com/android/settings/inputmethod/MouseScrollingSpeedPreferenceController.java index 0a57085b7d2..a047e5699a7 100644 --- a/src/com/android/settings/inputmethod/MouseScrollingSpeedPreferenceController.java +++ b/src/com/android/settings/inputmethod/MouseScrollingSpeedPreferenceController.java @@ -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()); + } + } } diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java index e8a645f2b5e..d3b23a7128a 100644 --- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java +++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java @@ -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; } diff --git a/src/com/android/settings/localepicker/AppLocaleSuggestedListPreferenceController.java b/src/com/android/settings/localepicker/AppLocaleSuggestedListPreferenceController.java new file mode 100644 index 00000000000..3fe95508a54 --- /dev/null +++ b/src/com/android/settings/localepicker/AppLocaleSuggestedListPreferenceController.java @@ -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 mLocaleList; + private List mLocaleOptions; + private Map 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 result = LocaleUtils.getSortedLocaleList( + getSuggestedLocaleList(), mIsCountryMode); + final Map existingSuggestedPreferences = mSuggestedPreferences; + mSuggestedPreferences = new ArrayMap<>(); + setupSuggestedPreference(result, existingSuggestedPreferences); + for (Preference pref : existingSuggestedPreferences.values()) { + mPreferenceCategory.removePreference(pref); + } + } + + @Override + public void onSearchListChanged(@NonNull List newList, + @Nullable CharSequence prefix) { + if (mPreferenceCategory == null) { + Log.d(TAG, "onSearchListChanged, mPreferenceCategory is null"); + return; + } + + mPreferenceCategory.removeAll(); + final Map existingSuggestedPreferences = mSuggestedPreferences; + List sortedList = getSuggestedLocaleList(); + newList = LocaleUtils.getSortedLocaleFromSearchList(newList, sortedList, mIsCountryMode); + setupSuggestedPreference(newList, existingSuggestedPreferences); + } + + private void setupSuggestedPreference(List localeInfoList, + Map 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 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; + } +} diff --git a/src/com/android/settings/localepicker/LocaleUtils.java b/src/com/android/settings/localepicker/LocaleUtils.java index a84d0beb7a8..b6502180aa0 100644 --- a/src/com/android/settings/localepicker/LocaleUtils.java +++ b/src/com/android/settings/localepicker/LocaleUtils.java @@ -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 getSortedLocaleList( + @NonNull List 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 getSortedLocaleFromSearchList( + @NonNull List searchList, + @NonNull List localeList, + boolean isCountryMode) { + List 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); + } } diff --git a/src/com/android/settings/network/AirplaneModePreference.kt b/src/com/android/settings/network/AirplaneModePreference.kt index b870f30eaf1..3b2b58a5148 100644 --- a/src/com/android/settings/network/AirplaneModePreference.kt +++ b/src/com/android/settings/network/AirplaneModePreference.kt @@ -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 setValue(key: String, valueType: Class, value: T?) { - if (value is Boolean) { - settingsStore.setBoolean(key, value) + 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) - } + val intent = Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) + intent.putExtra("state", value) + context.sendBroadcastAsUser(intent, UserHandle.ALL) } override fun onFirstObserverAdded() { diff --git a/src/com/android/settings/notification/SettingsEnableZenModeDialog.java b/src/com/android/settings/notification/EnableDndDialogFragment.java similarity index 77% rename from src/com/android/settings/notification/SettingsEnableZenModeDialog.java rename to src/com/android/settings/notification/EnableDndDialogFragment.java index d851caf3bdc..d093ca43208 100644 --- a/src/com/android/settings/notification/SettingsEnableZenModeDialog.java +++ b/src/com/android/settings/notification/EnableDndDialogFragment.java @@ -21,17 +21,17 @@ 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 public int getMetricsCategory() { - return SettingsEnums.NOTIFICATION_ZEN_MODE_ENABLE_DIALOG; + return SettingsEnums.NOTIFICATION_ZEN_MODE_ENABLE_DIALOG; } } diff --git a/src/com/android/settings/notification/history/NotificationHistoryActivity.java b/src/com/android/settings/notification/history/NotificationHistoryActivity.java index 156df96e04e..709eb7fc9e0 100644 --- a/src/com/android/settings/notification/history/NotificationHistoryActivity.java +++ b/src/com/android/settings/notification/history/NotificationHistoryActivity.java @@ -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); } } }; diff --git a/src/com/android/settings/notification/history/NotificationHistoryRecyclerView.java b/src/com/android/settings/notification/history/NotificationHistoryRecyclerView.java index 5923a4e8dbf..ae7590c9d01 100644 --- a/src/com/android/settings/notification/history/NotificationHistoryRecyclerView.java +++ b/src/com/android/settings/notification/history/NotificationHistoryRecyclerView.java @@ -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); diff --git a/src/com/android/settings/notification/history/NotificationSbnAdapter.java b/src/com/android/settings/notification/history/NotificationSbnAdapter.java index 0301d7b7fd5..3262ec4aea9 100644 --- a/src/com/android/settings/notification/history/NotificationSbnAdapter.java +++ b/src/com/android/settings/notification/history/NotificationSbnAdapter.java @@ -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( diff --git a/src/com/android/settings/notification/history/NotificationSbnViewHolder.java b/src/com/android/settings/notification/history/NotificationSbnViewHolder.java index df8aec44c0d..36a8720a859 100644 --- a/src/com/android/settings/notification/history/NotificationSbnViewHolder.java +++ b/src/com/android/settings/notification/history/NotificationSbnViewHolder.java @@ -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) { diff --git a/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java index bb9d23cd558..72ff5249bb4 100644 --- a/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java @@ -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: diff --git a/src/com/android/settings/notification/zen/SettingsZenDurationDialog.java b/src/com/android/settings/notification/zen/SettingsZenDurationDialog.java index bb83a730837..ce7d7be9ea0 100644 --- a/src/com/android/settings/notification/zen/SettingsZenDurationDialog.java +++ b/src/com/android/settings/notification/zen/SettingsZenDurationDialog.java @@ -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 diff --git a/src/com/android/settings/notification/zen/ZenDurationDialogPreference.java b/src/com/android/settings/notification/zen/ZenDurationDialogPreference.java index f243a5170b6..a37ac5d21a0 100644 --- a/src/com/android/settings/notification/zen/ZenDurationDialogPreference.java +++ b/src/com/android/settings/notification/zen/ZenDurationDialogPreference.java @@ -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); } } diff --git a/src/com/android/settings/notification/zen/ZenModeButtonPreferenceController.java b/src/com/android/settings/notification/zen/ZenModeButtonPreferenceController.java index dc338bd5ac2..e427ca224a3 100644 --- a/src/com/android/settings/notification/zen/ZenModeButtonPreferenceController.java +++ b/src/com/android/settings/notification/zen/ZenModeButtonPreferenceController.java @@ -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); diff --git a/src/com/android/settings/password/ChooseLockPattern.java b/src/com/android/settings/password/ChooseLockPattern.java index 8c4c621242f..9680d5a2748 100644 --- a/src/com/android/settings/password/ChooseLockPattern.java +++ b/src/com/android/settings/password/ChooseLockPattern.java @@ -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()); } } } diff --git a/src/com/android/settings/widget/RadioButtonPickerFragment.java b/src/com/android/settings/widget/RadioButtonPickerFragment.java index 121458c714a..94b59121110 100644 --- a/src/com/android/settings/widget/RadioButtonPickerFragment.java +++ b/src/com/android/settings/widget/RadioButtonPickerFragment.java @@ -89,7 +89,6 @@ public abstract class RadioButtonPickerFragment extends SettingsPreferenceFragme if (isCatalystEnabled()) { PreferenceScreen preferenceScreen = createPreferenceScreen(); setPreferenceScreen(preferenceScreen); - updateActivityTitleWithScreenTitle(preferenceScreen); } else { super.onCreatePreferences(savedInstanceState, rootKey); } diff --git a/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt b/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt index 3877a026480..21b0e88d7ac 100644 --- a/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt +++ b/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt @@ -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 { diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp index 935c687ade3..464d9708c0a 100644 --- a/tests/robotests/Android.bp +++ b/tests/robotests/Android.bp @@ -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", ], diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java index 2ce68e486de..baf95fd4181 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java @@ -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 bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); when(mLocalManager .getProfileManager() .getLeAudioBroadcastAssistantProfile() diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java index 4c25c11db70..6f032097118 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java @@ -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 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 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); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java index 54b26ec228a..056514620d2 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java @@ -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 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 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); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java index abfc4b7cc80..ba37c83055a 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java @@ -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 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 devices = new ArrayList<>(); devices.add(mDevice); when(mAssistant.getAllConnectedDevices()).thenReturn(devices); @@ -234,14 +240,15 @@ public class AudioStreamsHelperTest { List 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 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 diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java index 6aff8c38d7e..199284657e3 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java @@ -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 diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java index f042329200a..8cccbd443f1 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java @@ -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 preference = diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java index e5e51fce717..1d3c7a0ddd6 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java @@ -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 getAllConnectedSources() { - return sMockHelper.getAllConnectedSources(); + public Map getConnectedBroadcastIdAndState( + boolean hysteresisModeFixAvailable) { + return sMockHelper.getConnectedBroadcastIdAndState(hysteresisModeFixAvailable); } @Implementation @@ -66,11 +69,6 @@ public class ShadowAudioStreamsHelper { return sMockHelper.getAllSourcesByDevice(); } - @Implementation - public List getAllPresentSources() { - return sMockHelper.getAllPresentSources(); - } - /** Gets {@link CachedBluetoothDevice} in sharing or le connected */ @Implementation public static Optional getCachedBluetoothDeviceInSharingOrLeConnected( diff --git a/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java index 838edc618a3..3af89f46481 100644 --- a/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/core/InstrumentedPreferenceFragmentTest.java @@ -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 diff --git a/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt b/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt index 75b843d8b46..5b39a269e97 100644 --- a/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt +++ b/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt @@ -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() private val mockPackageManager = mock() @@ -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 = diff --git a/tests/robotests/testutils/com/android/settings/testutils/MetricsRule.kt b/tests/robotests/testutils/com/android/settings/testutils/MetricsRule.kt new file mode 100644 index 00000000000..44fa4bf90c6 --- /dev/null +++ b/tests/robotests/testutils/com/android/settings/testutils/MetricsRule.kt @@ -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 + } +}