From 56afae3c9345c8213527735b9471d7d90c6adbb7 Mon Sep 17 00:00:00 2001 From: Yining Liu Date: Wed, 11 Dec 2024 11:11:42 -0800 Subject: [PATCH 01/10] Update strings for the notifications on lock screen Update strings for the notifications on lock screen for localization. Bug: 367455695 Change-Id: Ibfba5728b583adf464e63edc0e0462b904eb2dab Flag: com.android.server.notification.notification_lock_screen_settings Test: Manual --- res/values/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 6fc9fb13af6..dc357695cf2 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8798,13 +8798,13 @@ Full list - + The current default placement is a full shelf and notification stack. Compact - + New notifications are collapsed into a shelf on your lockscreen. @@ -8869,16 +8869,16 @@ When your device is locked, how do you want profile notifications to show? - + Hide seen notifications - + Seen notifications are removed from the lock screen. - + Hide silent notifications - + Silent notifications and conversations are removed from the lock screen. From 18444f826f5583ececa3e35a04c825d2c18f5b70 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Wed, 11 Dec 2024 19:06:53 +0800 Subject: [PATCH 02/10] Use BluetoothLeBroadcastAssistant#getSourceMetadata to retrieve broadcast name. Test: atest Bug: 381944659 Flag: com.android.settingslib.flags.enable_le_audio_sharing Change-Id: I6e4c83a0858717727066de708fbde88e4b03ed8e --- .../audiostreams/AudioStreamsHelper.java | 14 ++ .../AudioStreamsProgressCategoryCallback.java | 5 +- ...udioStreamsProgressCategoryController.java | 148 +++++++++++------- ...ioStreamsProgressCategoryCallbackTest.java | 10 +- ...StreamsProgressCategoryControllerTest.java | 42 +++-- .../testshadows/ShadowAudioStreamsHelper.java | 7 + 6 files changed, 149 insertions(+), 77 deletions(-) diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java index 25a9135701b..c86222ca362 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java @@ -21,6 +21,8 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.stream.Collectors.toMap; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudioContentMetadata; @@ -48,7 +50,9 @@ import com.google.common.base.Strings; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -149,6 +153,16 @@ public class AudioStreamsHelper { .toList(); } + /** Retrieves a list of all LE broadcast receive states keyed by each active device. */ + public Map> getAllSourcesByDevice() { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "getAllSourcesByDevice(): LeBroadcastAssistant is null!"); + return emptyMap(); + } + return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream() + .collect(toMap(Function.identity(), mLeBroadcastAssistant::getAllSources)); + } + /** Retrieves a list of all LE broadcast receive states from sinks with source present. */ @VisibleForTesting public List getAllPresentSources() { diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java index f0034316372..87cea2c1e94 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java @@ -16,7 +16,6 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; - import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; @@ -43,13 +42,13 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA super.onReceiveStateChanged(sink, sourceId, state); if (AudioStreamsHelper.isConnected(state)) { - mCategoryController.handleSourceConnected(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(state); + mCategoryController.handleSourcePresent(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 f0a0c5b8f7f..6831c5a12e3 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -17,10 +17,12 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toMap; import android.app.AlertDialog; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; @@ -49,6 +51,8 @@ import com.android.settingslib.utils.ThreadUtils; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -391,34 +395,19 @@ 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(BluetoothLeBroadcastReceiveState receiveState) { + void handleSourceConnected( + BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) { if (DEBUG) { Log.d(TAG, "handleSourceConnected()"); } if (!AudioStreamsHelper.isConnected(receiveState)) { return; } - var broadcastIdConnected = receiveState.getBroadcastId(); - if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) { - // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the - // connected source receiveState. - if (DEBUG) { - Log.d( - TAG, - "handleSourceConnected() : processing mSourceFromQrCode with broadcastId" - + " unset"); - } - boolean updated = - maybeUpdateId( - AudioStreamsHelper.getBroadcastName(receiveState), - receiveState.getBroadcastId()); - if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) { - var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID); - mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference); - } - } - + Optional metadata = + getMetadataMatchingByBroadcastId( + device, receiveState.getSourceId(), broadcastIdConnected); + handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState); mBroadcastIdToPreferenceMap.compute( broadcastIdConnected, (k, existingPreference) -> { @@ -428,7 +417,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro // we retrieves the connected source during onStart() from // AudioStreamsHelper#getAllConnectedSources() even before the source is // founded by scanning. - return addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED); + return metadata.isPresent() + ? addNewPreference( + metadata.get(), + AudioStreamState.SOURCE_ADDED, + SourceOriginForLogging.UNKNOWN) + : addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED); } if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC && existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID @@ -473,7 +467,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro // Find preference by receiveState and decide next state. // Expect one preference existed, move to SOURCE_PRESENT - void handleSourcePresent(BluetoothLeBroadcastReceiveState receiveState) { + void handleSourcePresent( + BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) { if (DEBUG) { Log.d(TAG, "handleSourcePresent()"); } @@ -482,25 +477,10 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro } var broadcastIdConnected = receiveState.getBroadcastId(); - if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) { - // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the - // connected source receiveState. - if (DEBUG) { - Log.d( - TAG, - "handleSourcePresent() : processing mSourceFromQrCode with broadcastId" - + " unset"); - } - boolean updated = - maybeUpdateId( - AudioStreamsHelper.getBroadcastName(receiveState), - receiveState.getBroadcastId()); - if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) { - var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID); - mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference); - } - } - + Optional metadata = + getMetadataMatchingByBroadcastId( + device, receiveState.getSourceId(), broadcastIdConnected); + handleQrCodeWithUnsetBroadcastIdIfNeeded(metadata, receiveState); mBroadcastIdToPreferenceMap.compute( broadcastIdConnected, (k, existingPreference) -> { @@ -511,7 +491,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro // we retrieves the connected source during onStart() from // AudioStreamsHelper#getAllPresentSources() even before the source is // founded by scanning. - return addNewPreference(receiveState, AudioStreamState.SOURCE_PRESENT); + return metadata.isPresent() + ? addNewPreference( + metadata.get(), + AudioStreamState.SOURCE_PRESENT, + SourceOriginForLogging.UNKNOWN) + : addNewPreference(receiveState, AudioStreamState.SOURCE_PRESENT); } if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC && existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID @@ -598,28 +583,85 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro // Handle QR code scan, display currently connected 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. - List sources = - mAudioStreamsHelper.getAllConnectedSources(); - if (!sources.isEmpty()) { - sources.forEach(this::handleSourceConnected); + if (!connectedSources.isEmpty()) { + connectedSources.forEach( + (device, stateList) -> + stateList.forEach( + state -> handleSourceConnected(device, state))); } else { - mAudioStreamsHelper - .getAllPresentSources() - .forEach(this::handleSourcePresent); + Map> + presentSources = getPresentSources(sources); + presentSources.forEach( + (device, stateList) -> + stateList.forEach( + state -> handleSourcePresent(device, state))); } } else { - mAudioStreamsHelper - .getAllConnectedSources() - .forEach(this::handleSourceConnected); + connectedSources.forEach( + (device, stateList) -> + stateList.forEach( + state -> handleSourceConnected(device, state))); } mLeBroadcastAssistant.startSearchingForSources(emptyList()); mMediaControlHelper.start(); }); } + private Map> getConnectedSources( + Map> sources) { + return sources.entrySet().stream() + .filter( + entry -> + entry.getValue().stream().anyMatch(AudioStreamsHelper::isConnected)) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private Map> getPresentSources( + Map> sources) { + return sources.entrySet().stream() + .filter( + entry -> + entry.getValue().stream() + .anyMatch(AudioStreamsHelper::hasSourcePresent)) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private Optional getMetadataMatchingByBroadcastId( + BluetoothDevice device, int sourceId, int broadcastId) { + return Optional.ofNullable( + mLeBroadcastAssistant != null + ? mLeBroadcastAssistant.getSourceMetadata(device, sourceId) + : null) + .filter(m -> m.getBroadcastId() == broadcastId); + } + + private void handleQrCodeWithUnsetBroadcastIdIfNeeded( + Optional metadata, + BluetoothLeBroadcastReceiveState receiveState) { + if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) { + if (DEBUG) { + Log.d(TAG, "Processing mSourceFromQrCode with unset broadcastId"); + } + boolean updated = + maybeUpdateId( + metadata.isPresent() + ? AudioStreamsHelper.getBroadcastName(metadata.get()) + : AudioStreamsHelper.getBroadcastName(receiveState), + receiveState.getBroadcastId()); + if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) { + var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID); + mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference); + } + } + } + private void stopScanning() { if (mLeBroadcastAssistant == null) { Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!"); 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 4e962c7deb3..6aff8c38d7e 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 @@ -54,7 +54,7 @@ import java.util.List; @RunWith(RobolectricTestRunner.class) @Config( shadows = { - ShadowBluetoothAdapter.class, + ShadowBluetoothAdapter.class, }) public class AudioStreamsProgressCategoryCallbackTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @@ -70,8 +70,8 @@ public class AudioStreamsProgressCategoryCallbackTest { @Before public void setUp() { mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); - ShadowBluetoothAdapter shadowBluetoothAdapter = Shadow.extract( - BluetoothAdapter.getDefaultAdapter()); + ShadowBluetoothAdapter shadowBluetoothAdapter = + Shadow.extract(BluetoothAdapter.getDefaultAdapter()); shadowBluetoothAdapter.setEnabled(true); shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( BluetoothStatusCodes.FEATURE_SUPPORTED); @@ -87,7 +87,7 @@ public class AudioStreamsProgressCategoryCallbackTest { when(mState.getBisSyncState()).thenReturn(bisSyncState); mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState); - verify(mController).handleSourceConnected(any()); + verify(mController).handleSourceConnected(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()); + verify(mController).handleSourcePresent(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 78d4d6e1361..f042329200a 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 @@ -32,6 +32,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -41,7 +42,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; -import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -93,6 +94,7 @@ import org.robolectric.shadows.androidx.fragment.FragmentController; import java.util.ArrayList; import java.util.List; +import java.util.Map; @RunWith(RobolectricTestRunner.class) @Config( @@ -134,8 +136,8 @@ public class AudioStreamsProgressCategoryControllerTest { @Before public void setUp() { - ShadowBluetoothAdapter shadowBluetoothAdapter = Shadow.extract( - BluetoothAdapter.getDefaultAdapter()); + ShadowBluetoothAdapter shadowBluetoothAdapter = + Shadow.extract(BluetoothAdapter.getDefaultAdapter()); shadowBluetoothAdapter.setEnabled(true); shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( BluetoothStatusCodes.FEATURE_SUPPORTED); @@ -143,7 +145,7 @@ public class AudioStreamsProgressCategoryControllerTest { BluetoothStatusCodes.FEATURE_SUPPORTED); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant); - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList()); + when(mAudioStreamsHelper.getAllSourcesByDevice()).thenReturn(emptyMap()); mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; @@ -310,14 +312,12 @@ public class AudioStreamsProgressCategoryControllerTest { // Setup a device ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); - List connectedList = new ArrayList<>(); // Empty connected device list - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(connectedList); + when(mAudioStreamsHelper.getAllSourcesByDevice()).thenReturn(emptyMap()); mController.onStart(mLifecycleOwner); shadowOf(Looper.getMainLooper()).idle(); - verify(mAudioStreamsHelper).getAllPresentSources(); verify(mLeBroadcastAssistant).startSearchingForSources(any()); var dialog = ShadowAlertDialog.getLatestAlertDialog(); @@ -355,7 +355,7 @@ public class AudioStreamsProgressCategoryControllerTest { } @Test - public void testOnStart_handleSourceAlreadyConnected() { + public void testOnStart_handleSourceAlreadyConnected_useNameFromMetadata() { // Setup a device ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice); @@ -363,8 +363,14 @@ public class AudioStreamsProgressCategoryControllerTest { BluetoothLeBroadcastReceiveState connected = createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID); List list = new ArrayList<>(); + var data = mock(BluetoothLeAudioContentMetadata.class); + when(connected.getSubgroupMetadata()).thenReturn(ImmutableList.of(data)); + when(data.getProgramInfo()).thenReturn(BROADCAST_NAME_1); list.add(connected); - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(list); + when(mMetadata.getBroadcastId()).thenReturn(ALREADY_CONNECTED_BROADCAST_ID); + when(mMetadata.getBroadcastName()).thenReturn(BROADCAST_NAME_2); + when(mLeBroadcastAssistant.getSourceMetadata(any(), anyInt())).thenReturn(mMetadata); + when(mAudioStreamsHelper.getAllSourcesByDevice()).thenReturn(Map.of(mSourceDevice, list)); // Handle already connected source in onStart mController.displayPreference(mScreen); @@ -382,6 +388,7 @@ public class AudioStreamsProgressCategoryControllerTest { assertThat(preference.getValue()).isNotNull(); assertThat(preference.getValue().getAudioStreamBroadcastId()) .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID); + assertThat(preference.getValue().getTitle()).isEqualTo(BROADCAST_NAME_2); assertThat(state.getValue()).isEqualTo(SOURCE_ADDED); } @@ -409,7 +416,8 @@ public class AudioStreamsProgressCategoryControllerTest { var data = mock(BluetoothLeAudioContentMetadata.class); when(connected.getSubgroupMetadata()).thenReturn(ImmutableList.of(data)); when(data.getProgramInfo()).thenReturn(BROADCAST_NAME_1); - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected)); + when(mAudioStreamsHelper.getAllSourcesByDevice()) + .thenReturn(Map.of(mSourceDevice, ImmutableList.of(connected))); // Handle both source from qr code and already connected source in onStart mController.displayPreference(mScreen); @@ -578,8 +586,8 @@ public class AudioStreamsProgressCategoryControllerTest { // Setup source already connected BluetoothLeBroadcastReceiveState connected = createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID); - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected)); - + when(mAudioStreamsHelper.getAllSourcesByDevice()) + .thenReturn(Map.of(mSourceDevice, List.of(connected))); // Handle source already connected in onStart mController.displayPreference(mScreen); mController.onStart(mLifecycleOwner); @@ -687,7 +695,8 @@ public class AudioStreamsProgressCategoryControllerTest { // Setup already connected source BluetoothLeBroadcastReceiveState connected = createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID); - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected)); + when(mAudioStreamsHelper.getAllSourcesByDevice()) + .thenReturn(Map.of(mSourceDevice, List.of(connected))); // Handle connected source in onStart mController.displayPreference(mScreen); @@ -695,7 +704,7 @@ public class AudioStreamsProgressCategoryControllerTest { shadowOf(Looper.getMainLooper()).idle(); // The connect source is no longer connected - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList()); + when(mAudioStreamsHelper.getAllSourcesByDevice()).thenReturn(emptyMap()); mController.handleSourceRemoved(); shadowOf(Looper.getMainLooper()).idle(); @@ -728,7 +737,8 @@ public class AudioStreamsProgressCategoryControllerTest { // Setup a connected source BluetoothLeBroadcastReceiveState connected = createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID); - when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected)); + when(mAudioStreamsHelper.getAllSourcesByDevice()) + .thenReturn(Map.of(mSourceDevice, List.of(connected))); // Handle connected source in onStart mController.displayPreference(mScreen); @@ -834,7 +844,7 @@ public class AudioStreamsProgressCategoryControllerTest { when(receiveState.getBisSyncState()).thenReturn(bisSyncState); // The new found source is identified as failed to connect - mController.handleSourcePresent(receiveState); + mController.handleSourcePresent(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 c7d0c60efa8..e5e51fce717 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,7 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; @@ -31,6 +32,7 @@ import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; import java.util.List; +import java.util.Map; import java.util.Optional; @Implements(value = AudioStreamsHelper.class, callThroughByDefault = true) @@ -59,6 +61,11 @@ public class ShadowAudioStreamsHelper { return sMockHelper.getAllConnectedSources(); } + @Implementation + public Map> getAllSourcesByDevice() { + return sMockHelper.getAllSourcesByDevice(); + } + @Implementation public List getAllPresentSources() { return sMockHelper.getAllPresentSources(); From 17018dd7e11ea23354c24f9d7694ea3f547ae48a Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Thu, 12 Dec 2024 15:25:34 +0800 Subject: [PATCH 03/10] Avoid AudioManager#getMode in isFilterMatched AudioManager#getMode is a slow binder call which should not be called on UI thread. isFilterMatched will be frequently triggered on UI thread when updating the Connected devices page. Cache and update the audio mode when receive onModeChanged callback in this change. For long term, we should better separate the UI/background thread tasks in those classes. Also send request to Audio team to improve the API latency. Flag: EXEMPT small fix Bug: 380993178 Test: atest Change-Id: I054f3fa62f0fdf03b9a436a532ac1fb4738aaf58 --- .../AvailableMediaBluetoothDeviceUpdater.java | 11 ++-- .../ConnectedBluetoothDeviceUpdater.java | 11 ++-- ...ilableMediaBluetoothDeviceUpdaterTest.java | 59 ++++++++++--------- .../ConnectedBluetoothDeviceUpdaterTest.java | 54 +++++++++-------- 4 files changed, 76 insertions(+), 59 deletions(-) diff --git a/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java index 14f55b81264..bd160e17527 100644 --- a/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java @@ -39,6 +39,7 @@ public class AvailableMediaBluetoothDeviceUpdater extends BluetoothDeviceUpdater private final AudioManager mAudioManager; private final LocalBluetoothManager mLocalBtManager; + private int mAudioMode; public AvailableMediaBluetoothDeviceUpdater( Context context, @@ -47,21 +48,23 @@ public class AvailableMediaBluetoothDeviceUpdater extends BluetoothDeviceUpdater super(context, devicePreferenceCallback, metricsCategory); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mLocalBtManager = Utils.getLocalBtManager(context); + mAudioMode = mAudioManager.getMode(); } @Override public void onAudioModeChanged() { + // TODO: move to background thread + mAudioMode = mAudioManager.getMode(); forceUpdate(); } @Override public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) { - final int audioMode = mAudioManager.getMode(); final int currentAudioProfile; - if (audioMode == AudioManager.MODE_RINGTONE - || audioMode == AudioManager.MODE_IN_CALL - || audioMode == AudioManager.MODE_IN_COMMUNICATION) { + if (mAudioMode == AudioManager.MODE_RINGTONE + || mAudioMode == AudioManager.MODE_IN_CALL + || mAudioMode == AudioManager.MODE_IN_COMMUNICATION) { // in phone call currentAudioProfile = BluetoothProfile.HEADSET; } else { diff --git a/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java index 2107569d86b..7cc874caba9 100644 --- a/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java @@ -39,26 +39,29 @@ public class ConnectedBluetoothDeviceUpdater extends BluetoothDeviceUpdater { private static final String PREF_KEY_PREFIX = "connected_bt_"; private final AudioManager mAudioManager; + private int mAudioMode; public ConnectedBluetoothDeviceUpdater(Context context, DevicePreferenceCallback devicePreferenceCallback, int metricsCategory) { super(context, devicePreferenceCallback, metricsCategory); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mAudioMode = mAudioManager.getMode(); } @Override public void onAudioModeChanged() { + // TODO: move to background thread + mAudioMode = mAudioManager.getMode(); forceUpdate(); } @Override public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) { - final int audioMode = mAudioManager.getMode(); final int currentAudioProfile; - if (audioMode == AudioManager.MODE_RINGTONE - || audioMode == AudioManager.MODE_IN_CALL - || audioMode == AudioManager.MODE_IN_COMMUNICATION) { + if (mAudioMode == AudioManager.MODE_RINGTONE + || mAudioMode == AudioManager.MODE_IN_CALL + || mAudioMode == AudioManager.MODE_IN_COMMUNICATION) { // in phone call currentAudioProfile = BluetoothProfile.HEADSET; } else { diff --git a/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java index 9609af4a5e3..2251c3bff5a 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java @@ -124,24 +124,17 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pairs); when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(ImmutableSet.of()); - mBluetoothDeviceUpdater = - spy( - new AvailableMediaBluetoothDeviceUpdater( - mContext, mDevicePreferenceCallback, /* metricsCategory= */ 0)); - mBluetoothDeviceUpdater.setPrefContext(mContext); mPreference = new BluetoothDevicePreference( mContext, mCachedBluetoothDevice, false, BluetoothDevicePreference.SortType.TYPE_DEFAULT); - doNothing().when(mBluetoothDeviceUpdater).addPreference(any()); - doNothing().when(mBluetoothDeviceUpdater).removePreference(any()); } @Test public void onAudioModeChanged_hfpDeviceConnected_inCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -153,7 +146,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onAudioModeChanged_hfpDeviceConnected_notInCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -165,7 +158,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onAudioModeChanged_a2dpDeviceConnected_inCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -177,7 +170,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onAudioModeChanged_a2dpDeviceConnected_notInCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -189,7 +182,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_a2dpDeviceConnected_notInCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -202,7 +195,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_a2dpDeviceConnected_inCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -215,7 +208,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_hfpDeviceConnected_notInCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -228,7 +221,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_hfpDeviceConnected_inCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -241,7 +234,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_ashaHearingAidConnected_notInCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true); @@ -256,7 +249,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_ashaHearingAidConnected_inCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true); @@ -272,7 +265,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_leaConnected_notInCallSharingFlagOff_addPref() { mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -292,7 +285,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_leaConnected_notInCallNotInSharing_addPref() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -309,7 +302,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_leaConnected_inCallSharingFlagOff_addPref() { mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -326,7 +319,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_leaConnected_inCallNotInSharing_addPref() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -344,7 +337,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { public void onProfileConnectionStateChanged_leaConnected_notInCallInSharing_removePref() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -367,7 +360,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { onProfileConnectionStateChanged_leaConnected_noInCallInSharing_hysteresis_removePref() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -388,7 +381,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { public void onProfileConnectionStateChanged_leaConnected_inCallSharing_removePref() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -410,7 +403,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { public void onProfileConnectionStateChanged_leaConnected_inCallSharing_hysteresis_removePref() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -430,7 +423,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_deviceIsNotInList_notInCall_invokesRemovePreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -446,7 +439,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_deviceIsNotInList_inCall_invokesRemovePreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))) .thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -462,6 +455,7 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_deviceDisconnected_removePreference() { + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); mBluetoothDeviceUpdater.onProfileConnectionStateChanged( mCachedBluetoothDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP); @@ -470,8 +464,19 @@ public class AvailableMediaBluetoothDeviceUpdaterTest { @Test public void onClick_Preference_setActive() { + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); mBluetoothDeviceUpdater.onPreferenceClick(mPreference); verify(mDevicePreferenceCallback).onDeviceClick(mPreference); } + + private void setUpDeviceUpdaterWithAudioMode(int audioMode) { + mAudioManager.setMode(audioMode); + mBluetoothDeviceUpdater = + spy(new AvailableMediaBluetoothDeviceUpdater( + mContext, mDevicePreferenceCallback, /* metricsCategory= */ 0)); + mBluetoothDeviceUpdater.setPrefContext(mContext); + doNothing().when(mBluetoothDeviceUpdater).addPreference(any()); + doNothing().when(mBluetoothDeviceUpdater).removePreference(any()); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java index b2449dab39e..f68a8d4cf6a 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java @@ -112,16 +112,11 @@ public class ConnectedBluetoothDeviceUpdaterTest { when(mCachedBluetoothDevice.getAddress()).thenReturn(MAC_ADDRESS); when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pairs); mShadowCachedBluetoothDeviceManager.setCachedDevicesCopy(mCachedDevices); - mBluetoothDeviceUpdater = spy(new ConnectedBluetoothDeviceUpdater(mContext, - mDevicePreferenceCallback, /* metricsCategory= */ 0)); - mBluetoothDeviceUpdater.setPrefContext(mContext); - doNothing().when(mBluetoothDeviceUpdater).addPreference(any()); - doNothing().when(mBluetoothDeviceUpdater).removePreference(any()); } @Test public void onAudioModeChanged_hfpDeviceConnected_notInCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -133,7 +128,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onAudioModeChanged_hfpDeviceConnected_inCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -145,7 +140,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onAudioModeChanged_a2dpDeviceConnected_notInCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -157,7 +152,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onAudioModeChanged_a2dpDeviceConnected_inCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -169,7 +164,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_a2dpDeviceConnected_inCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -182,7 +177,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_deviceIsNotInList_inCall_invokesRemovePreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -196,7 +191,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_a2dpDeviceConnected_notInCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true); @@ -209,7 +204,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_hfpDeviceConnected_inCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -222,7 +217,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_hfpDeviceConnected_notInCall_addPreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -236,7 +231,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_ashaHearingAidConnected_inCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true); @@ -250,7 +245,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_ashaHearingAidConnected_notInCall_removePreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater. isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true); @@ -263,7 +258,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_leAudioDeviceConnected_inCall_removesPreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -277,7 +272,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_leAudioDeviceConnected_notInCall_removesPreference() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -290,7 +285,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_deviceIsNotInList_inCall_invokesRemovesPreference() { - mAudioManager.setMode(AudioManager.MODE_IN_CALL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_IN_CALL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -305,7 +300,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_deviceIsNotInList_notInCall_invokesRemovesPreference () { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); @@ -319,6 +314,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void onProfileConnectionStateChanged_deviceDisconnected_removePreference() { + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); mBluetoothDeviceUpdater.onProfileConnectionStateChanged(mCachedBluetoothDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP); @@ -327,6 +323,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test public void addPreference_addPreference_shouldHideSecondTarget() { + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); BluetoothDevicePreference btPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_DEFAULT); @@ -340,7 +337,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) public void update_notExclusiveManagedDevice_addDevice() { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -356,7 +353,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) public void update_exclusivelyManagedDevice_packageNotInstalled_addDevice() throws Exception { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -376,7 +373,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { throws Exception { ApplicationInfo appInfo = new ApplicationInfo(); appInfo.enabled = false; - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -393,7 +390,7 @@ public class ConnectedBluetoothDeviceUpdaterTest { @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) public void update_exclusivelyManagedDevice_packageInstalledAndEnabled_removePreference() throws Exception { - mAudioManager.setMode(AudioManager.MODE_NORMAL); + setUpDeviceUpdaterWithAudioMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); @@ -407,4 +404,13 @@ public class ConnectedBluetoothDeviceUpdaterTest { verify(mBluetoothDeviceUpdater).removePreference(mCachedBluetoothDevice); verify(mBluetoothDeviceUpdater, never()).addPreference(mCachedBluetoothDevice); } + + private void setUpDeviceUpdaterWithAudioMode(int audioMode) { + mAudioManager.setMode(audioMode); + mBluetoothDeviceUpdater = spy(new ConnectedBluetoothDeviceUpdater(mContext, + mDevicePreferenceCallback, /* metricsCategory= */ 0)); + mBluetoothDeviceUpdater.setPrefContext(mContext); + doNothing().when(mBluetoothDeviceUpdater).addPreference(any()); + doNothing().when(mBluetoothDeviceUpdater).removePreference(any()); + } } From 45eecec36f99917f17238590949ac510a045c580 Mon Sep 17 00:00:00 2001 From: shaoweishen Date: Thu, 12 Dec 2024 08:55:29 +0000 Subject: [PATCH 04/10] [Physical Keyboard Setting] Update text to match with Markup Bug: 377602364 Test: atest SettingsRoboTests Flag: com.android.settings.keyboard.keyboard_and_touchpad_a11y_new_page_enabled Change-Id: Ibb362d2f61ecdf209543de6b41bb114e71f18040 --- res/values/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 883147a1783..43a41ad3224 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4580,17 +4580,17 @@ Bounce keys - The keyboard ignores quickly repeated presses of the same key + Ignore quickly repeated presses of the same key - Bounce key threshold + Bounce key delay - Choose the duration of time your keyboard ignores repeated key presses + Choose how long the keyboard ignores repeated keystrokes - 0.2s + 0.2 seconds - 0.4s + 0.4 seconds - 0.6s + 0.6 seconds Custom @@ -4599,7 +4599,7 @@ Slow keys - Adjusts the time it takes for a key press to activate + Change how long you need to hold down a key before it\'s registered Sticky keys From fe361a526e2880f62750285058097f18e154d893 Mon Sep 17 00:00:00 2001 From: chenjean Date: Wed, 27 Nov 2024 23:28:26 +0800 Subject: [PATCH 05/10] feat(HCT): Perform custom migration logic for existing HCT users This logic is triggered by two scenarios: (A) During first bootup after OTA update to Android 16, if the user had HCT enabled. - Trigger: ACTION_PRE_BOOT_COMPLETED. - Migration: HCT is disabled and notification is shown. (B) Restore backup from Android 15 (or earlier), if the backup had HCT enabled and new device does not. - Trigger: SettingsProvider's restore process. - Migration: HCT is not restored and notification is shown. We store whether the user has seen this notification in a new secure setting ACCESSIBILITY_HCT_SHOW_PROMPT. This setting is also backed up. Bug: 369906140 Flag: com.android.graphics.hwui.flags.high_contrast_text_small_text_rect Test: atest SettingsRoboTests:com.android.settings.accessibility.HighContrastTextMigrationReceiverTest Test: flash an incremental update on a build with HCT enabled; observe HCT is disabled and a notification is sent. Test: flash an incremental update on a build with HCT disabled; observe no change to HCT and no notification. Change-Id: I4d294ffc0b2eabc59ee7988a579d678975a16380 --- AndroidManifest.xml | 10 + res/values/strings.xml | 4 + .../HighContrastTextMigrationReceiver.java | 158 ++++++++++++ .../HighTextContrastPreferenceController.java | 15 ++ ...HighContrastTextMigrationReceiverTest.java | 241 ++++++++++++++++++ 5 files changed, 428 insertions(+) create mode 100644 src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/HighContrastTextMigrationReceiverTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2295ee3dd1a..551a7dedb69 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5439,6 +5439,16 @@ + + + + + + + High contrast text Change text color to black or white. Maximizes contrast with the background. + + High contrast text has a new look and feel. + + Open Settings Maximize text contrast diff --git a/src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java b/src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java new file mode 100644 index 00000000000..ee3537bedd3 --- /dev/null +++ b/src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; +import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS; +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; + +import android.annotation.IntDef; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.graphics.hwui.flags.Flags; +import com.android.settings.R; + +import com.google.common.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Handling smooth migration to the new high contrast text appearance + */ +public class HighContrastTextMigrationReceiver extends BroadcastReceiver { + private static final String TAG = HighContrastTextMigrationReceiver.class.getSimpleName(); + @VisibleForTesting + static final String NOTIFICATION_CHANNEL = "high_contrast_text_notification_channel"; + @VisibleForTesting + static final String ACTION_RESTORED = + "com.android.settings.accessibility.ACTION_HIGH_CONTRAST_TEXT_RESTORED"; + @VisibleForTesting + static final int NOTIFICATION_ID = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PromptState.UNKNOWN, + PromptState.PROMPT_SHOWN, + PromptState.PROMPT_UNNECESSARY, + }) + public @interface PromptState { + int UNKNOWN = 0; + int PROMPT_SHOWN = 1; + int PROMPT_UNNECESSARY = 2; + } + + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + if (!Flags.highContrastTextSmallTextRect()) { + return; + } + + if (ACTION_RESTORED.equals(intent.getAction())) { + Log.i(TAG, "HCT attempted to be restored from backup; showing notification for userId: " + + context.getUserId()); + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, + PromptState.PROMPT_SHOWN); + showNotification(context); + } else if (Intent.ACTION_PRE_BOOT_COMPLETED.equals(intent.getAction())) { + final boolean hasSeenPromptIfNecessary = Settings.Secure.getInt( + context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, PromptState.UNKNOWN) + != PromptState.UNKNOWN; + if (hasSeenPromptIfNecessary) { + Log.i(TAG, "Has seen HCT prompt if necessary; skip HCT migration for userId: " + + context.getUserId()); + return; + } + + final boolean isHctEnabled = Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, OFF) == ON; + if (isHctEnabled) { + Log.i(TAG, "HCT enabled before OTA update; performing migration for userId: " + + context.getUserId()); + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, + OFF); + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, + PromptState.PROMPT_SHOWN); + showNotification(context); + } else { + Log.i(TAG, + "HCT was not enabled before OTA update; not performing migration for " + + "userId: " + context.getUserId()); + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, + PromptState.PROMPT_UNNECESSARY); + } + } + } + + private void showNotification(Context context) { + Notification.Builder notificationBuilder = new Notification.Builder(context, + NOTIFICATION_CHANNEL) + .setSmallIcon(R.drawable.ic_settings_24dp) + .setContentTitle(context.getString( + R.string.accessibility_toggle_high_text_contrast_preference_title)) + .setContentText(context.getString( + R.string.accessibility_notification_high_contrast_text_content)) + .setAutoCancel(true); + + Intent settingsIntent = new Intent(Settings.ACTION_TEXT_READING_SETTINGS); + settingsIntent.setPackage(context.getPackageName()); + if (settingsIntent.resolveActivity(context.getPackageManager()) != null) { + Bundle fragmentArgs = new Bundle(); + fragmentArgs.putString(EXTRA_FRAGMENT_ARG_KEY, + TextReadingPreferenceFragment.HIGH_TEXT_CONTRAST_KEY); + settingsIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs); + PendingIntent settingsPendingIntent = PendingIntent.getActivity(context, + /* requestCode = */ 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE); + + Notification.Action settingsAction = new Notification.Action.Builder( + /* icon= */ null, + context.getString( + R.string.accessibility_notification_high_contrast_text_action), + settingsPendingIntent + ).build(); + + notificationBuilder.addAction(settingsAction); + } + + NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + NotificationChannel notificationChannel = new NotificationChannel( + NOTIFICATION_CHANNEL, + context.getString( + R.string.accessibility_toggle_high_text_contrast_preference_title), + NotificationManager.IMPORTANCE_LOW); + notificationManager.createNotificationChannel(notificationChannel); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } +} diff --git a/src/com/android/settings/accessibility/HighTextContrastPreferenceController.java b/src/com/android/settings/accessibility/HighTextContrastPreferenceController.java index 7a3f4f6e912..c28af910beb 100644 --- a/src/com/android/settings/accessibility/HighTextContrastPreferenceController.java +++ b/src/com/android/settings/accessibility/HighTextContrastPreferenceController.java @@ -22,6 +22,7 @@ import android.provider.Settings; import androidx.preference.PreferenceScreen; import androidx.preference.TwoStatePreference; +import com.android.graphics.hwui.flags.Flags; import com.android.settings.R; import com.android.settings.accessibility.TextReadingPreferenceFragment.EntryPoint; import com.android.settings.core.TogglePreferenceController; @@ -60,6 +61,20 @@ public class HighTextContrastPreferenceController extends TogglePreferenceContro isChecked ? 1 : 0, AccessibilityStatsLogUtils.convertToEntryPoint(mEntryPoint)); + if (Flags.highContrastTextSmallTextRect()) { + // Set PROMPT_UNNECESSARY when the user modifies the HighContrastText setting + // This is needed for the following scenario: + // On Android 16, create secondary user, ACTION_PRE_BOOT_COMPLETED won't be sent to + // the secondary user. The user enables HCT. + // When updating OS to Android 17, ACTION_PRE_BOOT_COMPLETED will be sent to the + // secondary user when switch to the secondary user. + // If the prompt status is not updated in Android 16, we would automatically disable + // HCT and show the HCT prompt, which is an undesired behavior. + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, + HighContrastTextMigrationReceiver.PromptState.PROMPT_UNNECESSARY); + } + return Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, (isChecked ? 1 : 0)); } diff --git a/tests/robotests/src/com/android/settings/accessibility/HighContrastTextMigrationReceiverTest.java b/tests/robotests/src/com/android/settings/accessibility/HighContrastTextMigrationReceiverTest.java new file mode 100644 index 00000000000..0fedddc7416 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/HighContrastTextMigrationReceiverTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.accessibility; + +import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; +import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS; +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; +import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.ACTION_RESTORED; +import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.NOTIFICATION_CHANNEL; +import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.NOTIFICATION_ID; +import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.PromptState.PROMPT_SHOWN; +import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.PromptState.PROMPT_UNNECESSARY; +import static com.android.settings.accessibility.HighContrastTextMigrationReceiver.PromptState.UNKNOWN; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.os.Bundle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.graphics.hwui.flags.Flags; +import com.android.settings.R; +import com.android.settings.Utils; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowNotification; +import org.robolectric.shadows.ShadowNotificationManager; +import org.robolectric.shadows.ShadowPackageManager; + +/** Tests for {@link HighContrastTextMigrationReceiver}. */ +@RunWith(RobolectricTestRunner.class) +public class HighContrastTextMigrationReceiverTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private HighContrastTextMigrationReceiver mReceiver; + private ShadowNotificationManager mShadowNotificationManager; + + @Before + public void setUp() { + NotificationManager notificationManager = + mContext.getSystemService(NotificationManager.class); + mShadowNotificationManager = Shadows.shadowOf(notificationManager); + + // Setup Settings app as a system app + ShadowPackageManager shadowPm = Shadows.shadowOf(mContext.getPackageManager()); + ComponentName textReadingComponent = new ComponentName(Utils.SETTINGS_PACKAGE_NAME, + com.android.settings.Settings.TextReadingSettingsActivity.class.getName()); + ActivityInfo activityInfo = shadowPm.addActivityIfNotPresent(textReadingComponent); + activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + shadowPm.addOrUpdateActivity(activityInfo); + + mReceiver = new HighContrastTextMigrationReceiver(); + } + + @Test + @DisableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onReceive_flagOff_settingsNotSet() { + mReceiver.onReceive(mContext, new Intent(ACTION_RESTORED)); + + assertPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ OFF); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onRestored_hctStateOn_showPromptHctKeepsOn() { + setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ ON); + + mReceiver.onReceive(mContext, new Intent(ACTION_RESTORED)); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, ON); + verifyNotificationSent(); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onRestored_hctStateOff_showPromptHctKeepsOff() { + setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ OFF); + + mReceiver.onReceive(mContext, new Intent(ACTION_RESTORED)); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, OFF); + verifyNotificationSent(); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onPreBootCompleted_promptStateUnknownHctOn_showPromptAndAutoDisableHct() { + setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ ON); + + Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED); + mReceiver.onReceive(mContext, intent); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ OFF); + verifyNotificationSent(); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onPreBootCompleted_promptStateUnknownAndHctOff_promptIsUnnecessaryHctKeepsOff() { + setPromptStateAndHctState(/* promptState= */ UNKNOWN, /* hctState= */ OFF); + + Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED); + mReceiver.onReceive(mContext, intent); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ OFF); + verifyNotificationNotSent(); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onPreBootCompleted_promptStateShownAndHctOn_promptStateUnchangedHctKeepsOn() { + setPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ ON); + + Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED); + mReceiver.onReceive(mContext, intent); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ ON); + verifyNotificationNotSent(); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onPreBootCompleted_promptStateShownAndHctOff_promptStateUnchangedHctKeepsOff() { + setPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ OFF); + + Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED); + mReceiver.onReceive(mContext, intent); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_SHOWN, /* hctState= */ OFF); + verifyNotificationNotSent(); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onPreBootCompleted_promptStateUnnecessaryAndHctOn_promptStateUnchangedHctKeepsOn() { + setPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ ON); + + Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED); + mReceiver.onReceive(mContext, intent); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ ON); + verifyNotificationNotSent(); + } + + @Test + @EnableFlags(Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void onPreBootCompleted_promptStateUnnecessaryHctOff_promptStateUnchangedHctKeepsOff() { + setPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ OFF); + + Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED); + mReceiver.onReceive(mContext, intent); + + assertPromptStateAndHctState(/* promptState= */ PROMPT_UNNECESSARY, /* hctState= */ OFF); + verifyNotificationNotSent(); + } + + private void verifyNotificationNotSent() { + Notification notification = mShadowNotificationManager.getNotification(NOTIFICATION_ID); + assertThat(notification).isNull(); + } + + private void verifyNotificationSent() { + // Verify hct channel created + assertThat(mShadowNotificationManager.getNotificationChannels().stream().anyMatch( + channel -> channel.getId().equals(NOTIFICATION_CHANNEL))).isTrue(); + + // Verify hct notification is sent with correct content + Notification notification = mShadowNotificationManager.getNotification(NOTIFICATION_ID); + assertThat(notification).isNotNull(); + + ShadowNotification shadowNotification = Shadows.shadowOf(notification); + assertThat(shadowNotification.getContentTitle()).isEqualTo(mContext.getString( + R.string.accessibility_toggle_high_text_contrast_preference_title)); + assertThat(shadowNotification.getContentText()).isEqualTo( + mContext.getString(R.string.accessibility_notification_high_contrast_text_content)); + + assertThat(notification.actions.length).isEqualTo(1); + assertThat(notification.actions[0].title.toString()).isEqualTo( + mContext.getString(R.string.accessibility_notification_high_contrast_text_action)); + + PendingIntent pendingIntent = notification.actions[0].actionIntent; + Intent settingsIntent = Shadows.shadowOf(pendingIntent).getSavedIntent(); + Bundle fragmentArgs = settingsIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); + assertThat(fragmentArgs).isNotNull(); + assertThat(fragmentArgs.getString(EXTRA_FRAGMENT_ARG_KEY)) + .isEqualTo(TextReadingPreferenceFragment.HIGH_TEXT_CONTRAST_KEY); + } + + private void assertPromptStateAndHctState( + @HighContrastTextMigrationReceiver.PromptState int promptState, + @AccessibilityUtil.State int hctState) { + assertThat(Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, UNKNOWN)) + .isEqualTo(promptState); + assertThat(Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, OFF)) + .isEqualTo(hctState); + } + + private void setPromptStateAndHctState( + @HighContrastTextMigrationReceiver.PromptState int promptState, + @AccessibilityUtil.State int hctState) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS, promptState); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, hctState); + } +} From c3b8b7a21d4546a7231a3d65fe20ae5cfefc27fc Mon Sep 17 00:00:00 2001 From: Tom Hsu Date: Thu, 12 Dec 2024 22:33:46 -0800 Subject: [PATCH 06/10] Update the OWNERs Change-Id: Iee6e33e04d232b78bf2d2064687a2d434b4f78c0 Flag: EXEMPT only changing OWNERS Fix: b/383913940 Test: make pass --- src/com/android/settings/network/OWNERS | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/network/OWNERS b/src/com/android/settings/network/OWNERS index a63a825b0ea..ad3c9dada67 100644 --- a/src/com/android/settings/network/OWNERS +++ b/src/com/android/settings/network/OWNERS @@ -1,12 +1,8 @@ # Default reviewers for this and subdirectories. -allenwtsu@google.com -andychou@google.com -bonianchen@google.com -changbetty@google.com -leechou@google.com +chaohuiw@google.com +evanwu@google.com songferngwang@google.com tomhsu@google.com wengsu@google.com -zoeychen@google.com # Emergency approvers in case the above are not available From 06fe204a6985fd2a3741e859b5ea5f5ebfeb89c2 Mon Sep 17 00:00:00 2001 From: shaoweishen Date: Wed, 20 Nov 2024 05:16:48 +0000 Subject: [PATCH 07/10] [Physical Keybaord] Add keyboard touchpad/Mouse page - part2 screenshot: https://screenshot.googleplex.com/A4yihXmkTTo2nM3.png Add seperate controllers in page and guard with flag. Original controller will be disabled if flag is off. This is part of feature for keyboard setting update. document: go/new-a11y-touchpad-mouse-page Bug: 377602364 Test: atest SettingsRoboTests Flag: com.android.settings.keyboard.keyboard_and_touchpad_a11y_new_page_enabled Change-Id: I20bf7c65a7f9adc734c7382f23a92d1eb41822f1 --- res/drawable/ic_settings_mouse.xml | 14 ++++++++++++++ res/values/strings.xml | 2 ++ res/xml/system_dashboard_fragment.xml | 18 ++++++++++++++++++ .../TouchpadAndMouseSettingsController.java | 5 ++++- ...TouchpadAndMouseSettingsControllerTest.java | 7 +++++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 res/drawable/ic_settings_mouse.xml diff --git a/res/drawable/ic_settings_mouse.xml b/res/drawable/ic_settings_mouse.xml new file mode 100644 index 00000000000..4ce13d1898e --- /dev/null +++ b/res/drawable/ic_settings_mouse.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index f3c58f994b1..1eb8eaf3335 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4650,6 +4650,8 @@ Touchpad & mouse Mouse + + Pointer speed, swap buttons, button customisation Pointer speed, gestures diff --git a/res/xml/system_dashboard_fragment.xml b/res/xml/system_dashboard_fragment.xml index 83cdf64fc00..9c7f00121c9 100644 --- a/res/xml/system_dashboard_fragment.xml +++ b/res/xml/system_dashboard_fragment.xml @@ -46,6 +46,24 @@ android:fragment="com.android.settings.inputmethod.TouchpadAndMouseSettings" settings:controller="com.android.settings.inputmethod.TouchpadAndMouseSettingsController"/> + + + + Date: Fri, 13 Dec 2024 08:44:05 +0800 Subject: [PATCH 08/10] Skip authentication if device was unlocked recently - Sync the same behavior from SystemUI to Settings Bug: 365611488 Flag: EXEMPT bugfix Test: Manual testing atest -c WifiNetworkDetailsFragmentTest \ WifiDetailPreferenceController2Test \ WifiTetherSSIDPreferenceControllerTest \ com.android.settings.wifi.dpp.WifiDppUtilsTest atest -c com.android.settings.spa.wifi.dpp.WifiDppUtilsTest Change-Id: Ie3e8374b1fdbbc61e9e5bbf0f5162b18ba1452f3 --- .../network/NetworkProviderSettings.java | 2 +- .../AddDevicePreferenceController2.java | 3 +- .../WifiDetailPreferenceController2.java | 3 +- .../settings/wifi/dpp/WifiDppUtils.java | 121 ++++++++++++------ .../WifiTetherSSIDPreferenceController.java | 2 +- .../settings/spa/wifi/dpp/WifiDppUtilsTest.kt | 118 +++++++++++++++++ 6 files changed, 204 insertions(+), 45 deletions(-) create mode 100644 tests/spa_unit/src/com/android/settings/spa/wifi/dpp/WifiDppUtilsTest.kt diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java index c776987856e..1fc91014f10 100644 --- a/src/com/android/settings/network/NetworkProviderSettings.java +++ b/src/com/android/settings/network/NetworkProviderSettings.java @@ -706,7 +706,7 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment forget(mSelectedWifiEntry); return true; case MENU_ID_SHARE: - WifiDppUtils.showLockScreen(getContext(), + WifiDppUtils.showLockScreenForWifiSharing(getContext(), () -> launchWifiDppConfiguratorActivity(mSelectedWifiEntry)); return true; case MENU_ID_MODIFY: diff --git a/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java b/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java index 8f9741a6b34..4ffe279d6d4 100644 --- a/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java +++ b/src/com/android/settings/wifi/details2/AddDevicePreferenceController2.java @@ -57,7 +57,8 @@ public class AddDevicePreferenceController2 extends BasePreferenceController { @Override public boolean handlePreferenceTreeClick(Preference preference) { if (KEY_ADD_DEVICE.equals(preference.getKey())) { - WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorQrCodeScanner()); + WifiDppUtils.showLockScreenForWifiSharing(mContext, + () -> launchWifiDppConfiguratorQrCodeScanner()); return true; /* click is handled */ } diff --git a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java index a8d7f417a4a..ecddecfce74 100644 --- a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java +++ b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java @@ -980,7 +980,8 @@ public class WifiDetailPreferenceController2 extends AbstractPreferenceControlle * Share the wifi network with QR code. */ private void shareNetwork() { - WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorActivity()); + WifiDppUtils.showLockScreenForWifiSharing(mContext, + () -> launchWifiDppConfiguratorActivity()); } /** diff --git a/src/com/android/settings/wifi/dpp/WifiDppUtils.java b/src/com/android/settings/wifi/dpp/WifiDppUtils.java index 23a6a5423e4..24ab496cc3f 100644 --- a/src/com/android/settings/wifi/dpp/WifiDppUtils.java +++ b/src/com/android/settings/wifi/dpp/WifiDppUtils.java @@ -16,6 +16,8 @@ package com.android.settings.wifi.dpp; +import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; @@ -33,6 +35,9 @@ import android.os.Vibrator; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.Utils; @@ -58,6 +63,8 @@ import javax.crypto.SecretKey; * @see WifiQrCode */ public class WifiDppUtils { + private static final String TAG = "WifiDppUtils"; + /** * The fragment tag specified to FragmentManager for container activities to manage fragments. */ @@ -109,7 +116,15 @@ public class WifiDppUtils { private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3); - private static final String AES_CBC_PKCS7_PADDING = "AES/CBC/PKCS7Padding"; + /** + * Parameters to check whether the device has been locked recently + */ + @VisibleForTesting + public static final String AES_CBC_PKCS7_PADDING = "AES/CBC/PKCS7Padding"; + @VisibleForTesting + public static final String WIFI_SHARING_KEY_ALIAS = "wifi_sharing_auth_key"; + @VisibleForTesting + public static final int WIFI_SHARING_MAX_UNLOCK_SECONDS = 60; /** * Returns whether the device support WiFi DPP. @@ -426,51 +441,75 @@ public class WifiDppUtils { * Shows authentication screen to confirm credentials (pin, pattern or password) for the current * user of the device. * - * @param context The {@code Context} used to get {@code KeyguardManager} service + * @param context The {@code Context} used to get {@code KeyguardManager} service * @param successRunnable The {@code Runnable} which will be executed if the user does not setup * device security or if lock screen is unlocked */ - public static void showLockScreen(Context context, Runnable successRunnable) { - final KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService( - Context.KEYGUARD_SERVICE); - - if (keyguardManager.isKeyguardSecure()) { - final BiometricPrompt.AuthenticationCallback authenticationCallback = - new BiometricPrompt.AuthenticationCallback() { - @Override - public void onAuthenticationSucceeded( - BiometricPrompt.AuthenticationResult result) { - successRunnable.run(); - } - - @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { - //Do nothing - } - }; - - final int userId = UserHandle.myUserId(); - - final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context) - .setTitle(context.getText(R.string.wifi_dpp_lockscreen_title)); - - if (keyguardManager.isDeviceSecure()) { - builder.setDeviceCredentialAllowed(true); - builder.setTextForDeviceCredential( - null /* title */, - Utils.getConfirmCredentialStringForUser( - context, userId, Utils.getCredentialType(context, userId)), - null /* description */); - } - - final BiometricPrompt bp = builder.build(); - final Handler handler = new Handler(Looper.getMainLooper()); - bp.authenticate(new CancellationSignal(), - runnable -> handler.post(runnable), - authenticationCallback); - } else { + public static void showLockScreen(@NonNull Context context, @NonNull Runnable successRunnable) { + KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); + if (keyguardManager == null || !keyguardManager.isKeyguardSecure()) { successRunnable.run(); + return; } + showLockScreen(context, successRunnable, keyguardManager); + } + + /** + * Shows authentication screen to confirm credentials (pin, pattern or password) for the + * current user of the device. But if the device has been unlocked recently, the + * authentication screen will be skipped. + * + * @param context The {@code Context} used to get {@code KeyguardManager} service + * @param successRunnable The {@code Runnable} which will be executed if the user does not setup + * device security or if lock screen is unlocked + */ + public static void showLockScreenForWifiSharing(@NonNull Context context, + @NonNull Runnable successRunnable) { + KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); + if (keyguardManager == null || !keyguardManager.isKeyguardSecure()) { + successRunnable.run(); + return; + } + if (isUnlockedWithinSeconds(WIFI_SHARING_KEY_ALIAS, WIFI_SHARING_MAX_UNLOCK_SECONDS)) { + Log.d(TAG, "Bypassing the lock screen because the device was unlocked recently."); + successRunnable.run(); + return; + } + showLockScreen(context, successRunnable, keyguardManager); + } + + @SuppressLint("MissingPermission") + private static void showLockScreen(@NonNull Context context, @NonNull Runnable successRunnable, + @NonNull KeyguardManager keyguardManager) { + BiometricPrompt.AuthenticationCallback authenticationCallback = + new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded( + BiometricPrompt.AuthenticationResult result) { + successRunnable.run(); + } + + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + //Do nothing + } + }; + int userId = UserHandle.myUserId(); + BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context) + .setTitle(context.getText(R.string.wifi_dpp_lockscreen_title)); + if (keyguardManager.isDeviceSecure()) { + builder.setDeviceCredentialAllowed(true); + builder.setTextForDeviceCredential( + null /* title */, + Utils.getConfirmCredentialStringForUser( + context, userId, Utils.getCredentialType(context, userId)), + null /* description */); + } + BiometricPrompt bp = builder.build(); + Handler handler = new Handler(Looper.getMainLooper()); + bp.authenticate(new CancellationSignal(), + runnable -> handler.post(runnable), + authenticationCallback); } /** diff --git a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java index 1bcff1ea45e..d2d26ab84fa 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java @@ -123,7 +123,7 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference } private void shareHotspotNetwork(Intent intent) { - WifiDppUtils.showLockScreen(mContext, () -> { + WifiDppUtils.showLockScreenForWifiSharing(mContext, () -> { mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_HOTSPOT_QR_CODE, SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR, diff --git a/tests/spa_unit/src/com/android/settings/spa/wifi/dpp/WifiDppUtilsTest.kt b/tests/spa_unit/src/com/android/settings/spa/wifi/dpp/WifiDppUtilsTest.kt new file mode 100644 index 00000000000..31ee9e6a2c7 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/wifi/dpp/WifiDppUtilsTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.wifi.dpp + +import android.app.KeyguardManager +import android.content.Context +import android.hardware.biometrics.BiometricPrompt +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.settings.wifi.dpp.WifiDppUtils +import java.security.InvalidKeyException +import java.security.Key +import javax.crypto.Cipher +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoSession +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@RunWith(AndroidJUnit4::class) +class WifiDppUtilsTest { + private lateinit var mockSession: MockitoSession + + private val runnable = mock() + private val cipher = mock() + private var mockKeyguardManager = mock() + private var context: Context = + spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(KeyguardManager::class.java) } doReturn mockKeyguardManager + } + + @Before + fun setUp() { + mockSession = + ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(Cipher::class.java) + .mockStatic(BiometricPrompt::class.java) + .mockStatic(BiometricPrompt.Builder::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(context.applicationContext).thenReturn(context) + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + @Test + fun showLockScreen_notKeyguardSecure_runRunnable() { + mockKeyguardManager.stub { on { isKeyguardSecure } doReturn false } + + WifiDppUtils.showLockScreen(context, runnable) + + verify(runnable).run() + } + + @Test + fun showLockScreen_isKeyguardSecure_doNotRunRunnable() { + mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true } + + try { + WifiDppUtils.showLockScreen(context, runnable) + } catch (_: Exception) {} + + verify(runnable, never()).run() + } + + @Test + fun showLockScreenForWifiSharing_deviceUnlockedRecently_runRunnable() { + mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true } + whenever(Cipher.getInstance(WifiDppUtils.AES_CBC_PKCS7_PADDING)).thenReturn(cipher) + + WifiDppUtils.showLockScreenForWifiSharing(context, runnable) + + verify(runnable).run() + } + + @Test + fun showLockScreenForWifiSharing_deviceNotUnlockedRecently_doNotRunRunnable() { + mockKeyguardManager.stub { on { isKeyguardSecure } doReturn true } + whenever(Cipher.getInstance(WifiDppUtils.AES_CBC_PKCS7_PADDING)).thenReturn(cipher) + doThrow(InvalidKeyException()).whenever(cipher).init(anyInt(), any()) + + try { + WifiDppUtils.showLockScreenForWifiSharing(context, runnable) + } catch (_: Exception) {} + + verify(runnable, never()).run() + } +} From 9777bbe38f04f8cba4bab2a82f224162a5382b8f Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Tue, 10 Dec 2024 10:27:49 +0800 Subject: [PATCH 09/10] [Catalyst] Introduce a AccessibilitySeekBarPreference contains tool tip window Test: atest PreviewSizeSeekBarControllerTest TextReadingPreviewControllerTest Bug: 372776688 Flag: com.android.settings.flags.catalyst_text_reading_screen Change-Id: Ie93d2f26b1521e931ce648f0140894b153259f81 --- .../accessibility_text_reading_options.xml | 4 +- .../AccessibilitySeekBarPreference.kt | 66 +++++++++++++++++++ .../PreviewSizeSeekBarController.java | 63 +++++++----------- .../TextReadingPreviewController.java | 5 +- .../PreviewSizeSeekBarControllerTest.java | 13 ++-- .../TextReadingPreviewControllerTest.java | 9 ++- 6 files changed, 103 insertions(+), 57 deletions(-) create mode 100644 src/com/android/settings/accessibility/AccessibilitySeekBarPreference.kt diff --git a/res/xml/accessibility_text_reading_options.xml b/res/xml/accessibility_text_reading_options.xml index 795c4ffb9fc..8eed107d60a 100644 --- a/res/xml/accessibility_text_reading_options.xml +++ b/res/xml/accessibility_text_reading_options.xml @@ -25,7 +25,7 @@ android:key="preview" android:selectable="false"/> - - mSizeData; - private static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; private boolean mSeekByTouch; private Optional mInteractionListener = Optional.empty(); - private LabeledSeekBarPreference mSeekBarPreference; + private AccessibilitySeekBarPreference mSeekBarPreference; private int mLastProgress; - private boolean mNeedsQSTooltipReshow = false; - private AccessibilityQuickSettingsTooltipWindow mTooltipWindow; private final Handler mHandler; private String[] mStateLabels = null; @@ -101,30 +96,21 @@ abstract class PreviewSizeSeekBarController extends BasePreferenceController imp } @Override - public void onCreate(Bundle savedInstanceState) { - // Restore the tooltip. - if (savedInstanceState != null - && savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_RESHOW)) { - mNeedsQSTooltipReshow = savedInstanceState.getBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW); + public void onStart() { + if (mSeekBarPreference.getNeedsQSTooltipReshow()) { + mHandler.post(this::showQuickSettingsTooltipIfNeeded); } } + @Override + public void onStop() { + // all the messages/callbacks will be removed. + mHandler.removeCallbacksAndMessages(null); + } + @Override public void onDestroy() { - // remove runnables in the queue. - mHandler.removeCallbacksAndMessages(null); - final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); - if (isTooltipWindowShowing) { - mTooltipWindow.dismiss(); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); - if (mNeedsQSTooltipReshow || isTooltipWindowShowing) { - outState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); - } + mSeekBarPreference.dismissTooltip(); } void setInteractionListener(ProgressInteractionListener interactionListener) { @@ -148,9 +134,6 @@ abstract class PreviewSizeSeekBarController extends BasePreferenceController imp mSeekBarPreference.setProgress(initialIndex); mSeekBarPreference.setContinuousUpdates(true); mSeekBarPreference.setOnSeekBarChangeListener(mSeekBarChangeListener); - if (mNeedsQSTooltipReshow) { - mHandler.post(this::showQuickSettingsTooltipIfNeeded); - } setSeekbarStateDescription(mSeekBarPreference.getProgress()); } @@ -216,7 +199,8 @@ abstract class PreviewSizeSeekBarController extends BasePreferenceController imp return; } - if (!mNeedsQSTooltipReshow && AccessibilityQuickSettingUtils.hasValueInSharedPreferences( + if (!mSeekBarPreference.getNeedsQSTooltipReshow() + && AccessibilityQuickSettingUtils.hasValueInSharedPreferences( mContext, tileComponentName)) { // Returns if quick settings tooltip only show once. return; @@ -228,14 +212,15 @@ abstract class PreviewSizeSeekBarController extends BasePreferenceController imp // is not ready when we would like to show the tooltip. If the seekbar is not ready, // we give up showing the tooltip and also do not reshow it in the future. if (mSeekBarPreference.getSeekbar() != null) { - mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(mContext); - mTooltipWindow.setup(getTileTooltipContent(), + final AccessibilityQuickSettingsTooltipWindow tooltipWindow = + mSeekBarPreference.createTooltipWindow(); + tooltipWindow.setup(getTileTooltipContent(), R.drawable.accessibility_auto_added_qs_tooltip_illustration); - mTooltipWindow.showAtTopCenter(mSeekBarPreference.getSeekbar()); + tooltipWindow.showAtTopCenter(mSeekBarPreference.getSeekbar()); } AccessibilityQuickSettingUtils.optInValueToSharedPreferences(mContext, tileComponentName); - mNeedsQSTooltipReshow = false; + mSeekBarPreference.setNeedsQSTooltipReshow(false); } /** Returns the accessibility Quick Settings tile component name. */ diff --git a/src/com/android/settings/accessibility/TextReadingPreviewController.java b/src/com/android/settings/accessibility/TextReadingPreviewController.java index a983105cfad..99f1f3fa0c3 100644 --- a/src/com/android/settings/accessibility/TextReadingPreviewController.java +++ b/src/com/android/settings/accessibility/TextReadingPreviewController.java @@ -33,7 +33,6 @@ import com.android.settings.accessibility.TextReadingPreferenceFragment.EntryPoi import com.android.settings.core.BasePreferenceController; import com.android.settings.core.instrumentation.SettingsStatsLog; import com.android.settings.display.PreviewPagerAdapter; -import com.android.settings.widget.LabeledSeekBarPreference; import java.util.Objects; @@ -58,8 +57,8 @@ class TextReadingPreviewController extends BasePreferenceController implements private int mLastDisplayProgress; private long mLastCommitTime; private TextReadingPreviewPreference mPreviewPreference; - private LabeledSeekBarPreference mFontSizePreference; - private LabeledSeekBarPreference mDisplaySizePreference; + private AccessibilitySeekBarPreference mFontSizePreference; + private AccessibilitySeekBarPreference mDisplaySizePreference; @EntryPoint private int mEntryPoint; diff --git a/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java index 05273fc050d..ba9eaa5871e 100644 --- a/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.when; import android.app.Activity; import android.content.ComponentName; import android.content.Intent; -import android.os.Bundle; import android.view.LayoutInflater; import android.widget.PopupWindow; import android.widget.SeekBar; @@ -44,7 +43,6 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.testutils.shadow.ShadowFragment; -import com.android.settings.widget.LabeledSeekBarPreference; import com.android.settingslib.testutils.shadow.ShadowInteractionJankMonitor; import com.google.android.setupcompat.util.WizardManagerHelper; @@ -77,7 +75,7 @@ public class PreviewSizeSeekBarControllerTest { private Activity mContext; private PreviewSizeSeekBarController mSeekBarController; private FontSizeData mFontSizeData; - private LabeledSeekBarPreference mSeekBarPreference; + private AccessibilitySeekBarPreference mSeekBarPreference; private PreferenceScreen mPreferenceScreen; private TestFragment mFragment; @@ -109,7 +107,7 @@ public class PreviewSizeSeekBarControllerTest { mPreferenceScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null)); when(mPreferenceScreen.getPreferenceManager()).thenReturn(mPreferenceManager); doReturn(mPreferenceScreen).when(mFragment).getPreferenceScreen(); - mSeekBarPreference = spy(new LabeledSeekBarPreference(mContext, /* attrs= */ null)); + mSeekBarPreference = spy(new AccessibilitySeekBarPreference(mContext, /* attrs= */ null)); mSeekBarPreference.setKey(FONT_SIZE_KEY); LayoutInflater inflater = LayoutInflater.from(mContext); @@ -246,12 +244,11 @@ public class PreviewSizeSeekBarControllerTest { @Test @Config(shadows = ShadowFragment.class) - public void restoreValueFromSavedInstanceState_showTooltipView() { - final Bundle savedInstanceState = new Bundle(); - savedInstanceState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); - mSeekBarController.onCreate(savedInstanceState); + public void enabledNeedsQSTooltipReshow_showTooltipView() { + mSeekBarPreference.setNeedsQSTooltipReshow(true); mSeekBarController.displayPreference(mPreferenceScreen); + mSeekBarController.onStart(); ShadowLooper.idleMainLooper(); assertThat(getLatestPopupWindow().isShowing()).isTrue(); diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java index f768e42f2e6..375952f725c 100644 --- a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java @@ -28,7 +28,6 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.display.PreviewPagerAdapter; import com.android.settings.testutils.shadow.ShadowInteractionJankMonitor; -import com.android.settings.widget.LabeledSeekBarPreference; import org.junit.Before; import org.junit.Test; @@ -54,8 +53,8 @@ public class TextReadingPreviewControllerTest { private final Context mContext = ApplicationProvider.getApplicationContext(); private TextReadingPreviewController mPreviewController; private TextReadingPreviewPreference mPreviewPreference; - private LabeledSeekBarPreference mFontSizePreference; - private LabeledSeekBarPreference mDisplaySizePreference; + private AccessibilitySeekBarPreference mFontSizePreference; + private AccessibilitySeekBarPreference mDisplaySizePreference; @Mock private DisplaySizeData mDisplaySizeData; @@ -73,8 +72,8 @@ public class TextReadingPreviewControllerTest { mPreviewPreference = spy(new TextReadingPreviewPreference(mContext, /* attr= */ null)); mPreviewController = new TextReadingPreviewController(mContext, PREVIEW_KEY, fontSizeData, mDisplaySizeData); - mFontSizePreference = new LabeledSeekBarPreference(mContext, /* attr= */ null); - mDisplaySizePreference = new LabeledSeekBarPreference(mContext, /* attr= */ null); + mFontSizePreference = new AccessibilitySeekBarPreference(mContext, /* attr= */ null); + mDisplaySizePreference = new AccessibilitySeekBarPreference(mContext, /* attr= */ null); } @Test From 8927e4371d54361fec3c150c4d6548886588069c Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Wed, 11 Dec 2024 23:36:12 +0000 Subject: [PATCH 10/10] Support GetMetadata for Preference Service Bug: 379750656 Flag: com.android.settingslib.flags.settings_catalyst Test: unit test Change-Id: Ia9b438360b60ff509a259df0a079ec4d745fb595 --- .../settings/service/PreferenceService.kt | 38 ++++++++-- .../PreferenceServiceRequestTransformer.kt | 52 +++++++++++++ ...PreferenceServiceRequestTransformerTest.kt | 75 ++++++++++++++++++- 3 files changed, 156 insertions(+), 9 deletions(-) diff --git a/src/com/android/settings/service/PreferenceService.kt b/src/com/android/settings/service/PreferenceService.kt index 3a677629afd..d07e78d333e 100644 --- a/src/com/android/settings/service/PreferenceService.kt +++ b/src/com/android/settings/service/PreferenceService.kt @@ -16,6 +16,7 @@ package com.android.settings.service +import android.app.Application import android.os.Binder import android.os.OutcomeReceiver import android.os.Process @@ -26,38 +27,49 @@ import android.service.settings.preferences.MetadataResult import android.service.settings.preferences.SetValueRequest import android.service.settings.preferences.SetValueResult import android.service.settings.preferences.SettingsPreferenceService +import com.android.settingslib.graph.GetPreferenceGraphApiHandler +import com.android.settingslib.graph.GetPreferenceGraphRequest import com.android.settingslib.graph.PreferenceGetterApiHandler +import com.android.settingslib.graph.PreferenceGetterFlags import com.android.settingslib.graph.PreferenceSetterApiHandler import com.android.settingslib.ipc.ApiPermissionChecker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.lang.Exception class PreferenceService : SettingsPreferenceService() { - private val scope = CoroutineScope(Job() + Dispatchers.Main) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val getApiHandler = PreferenceGetterApiHandler(1, ApiPermissionChecker.alwaysAllow()) private val setApiHandler = PreferenceSetterApiHandler(2, ApiPermissionChecker.alwaysAllow()) + private val graphApi = GraphProvider(3) override fun onGetAllPreferenceMetadata( request: MetadataRequest, callback: OutcomeReceiver ) { - // TODO(379750656): Update graph API to be usable outside SettingsLib - callback.onError(UnsupportedOperationException("Not yet supported")) + scope.launch { + val graphProto = graphApi.invoke(application, Process.myUid(), Binder.getCallingUid(), + GetPreferenceGraphRequest( + includeValue = false, + flags = PreferenceGetterFlags.METADATA + )) + val result = transformCatalystGetMetadataResponse(this@PreferenceService, graphProto) + callback.onResult(result) + } } override fun onGetPreferenceValue( request: GetValueRequest, callback: OutcomeReceiver ) { - scope.launch(Dispatchers.IO) { + scope.launch { val apiRequest = transformFrameworkGetValueRequest(request) val response = getApiHandler.invoke(application, Process.myUid(), - Binder.getCallingPid(), apiRequest) + Binder.getCallingUid(), apiRequest) val result = transformCatalystGetValueResponse( this@PreferenceService, request, @@ -75,7 +87,7 @@ class PreferenceService : SettingsPreferenceService() { request: SetValueRequest, callback: OutcomeReceiver ) { - scope.launch(Dispatchers.IO) { + scope.launch { val apiRequest = transformFrameworkSetValueRequest(request) if (apiRequest == null) { callback.onResult( @@ -83,10 +95,20 @@ class PreferenceService : SettingsPreferenceService() { ) } else { val response = setApiHandler.invoke(application, Process.myUid(), - Binder.getCallingPid(), apiRequest) + Binder.getCallingUid(), apiRequest) callback.onResult(transformCatalystSetValueResponse(response)) } } } + + // Basic implementation - we already have permission to access Graph for Metadata via superclass + private class GraphProvider(override val id: Int) : GetPreferenceGraphApiHandler(emptySet()) { + override fun hasPermission( + application: Application, + myUid: Int, + callingUid: Int, + request: GetPreferenceGraphRequest + ) = true + } } diff --git a/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt b/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt index 7a4c7fc9dff..18307e0baf3 100644 --- a/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt +++ b/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt @@ -19,6 +19,7 @@ package com.android.settings.service import android.content.Context import android.service.settings.preferences.GetValueRequest import android.service.settings.preferences.GetValueResult +import android.service.settings.preferences.MetadataResult import android.service.settings.preferences.SetValueRequest import android.service.settings.preferences.SetValueResult import android.service.settings.preferences.SettingsPreferenceMetadata @@ -34,9 +35,55 @@ import com.android.settingslib.graph.preferenceValueProto import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.graph.proto.PreferenceValueProto import com.android.settingslib.graph.getText +import com.android.settingslib.graph.proto.PreferenceGraphProto +import com.android.settingslib.graph.proto.PreferenceOrGroupProto import com.android.settingslib.graph.toIntent import com.android.settingslib.metadata.SensitivityLevel +/** Transform Catalyst Graph result to Framework GET METADATA result */ +fun transformCatalystGetMetadataResponse( + context: Context, + graph: PreferenceGraphProto +): MetadataResult { + val preferences = mutableSetOf() + // recursive function to visit all nodes in preference group + fun traverseGroupOrPref( + screenKey: String, + groupOrPref: PreferenceOrGroupProto, + ) { + when (groupOrPref.kindCase) { + PreferenceOrGroupProto.KindCase.PREFERENCE -> + preferences.add( + PreferenceWithScreen(screenKey, groupOrPref.preference) + ) + PreferenceOrGroupProto.KindCase.GROUP -> { + for (child in groupOrPref.group.preferencesList) { + traverseGroupOrPref(screenKey, child) + } + } + else -> {} + } + } + // traverse all screens and all preferences on screen + for ((screenKey, screen) in graph.screensMap) { + for (groupOrPref in screen.root.preferencesList) { + traverseGroupOrPref(screenKey, groupOrPref) + } + } + + return if (preferences.isNotEmpty()) { + MetadataResult.Builder(MetadataResult.RESULT_OK) + .setMetadataList( + preferences.map { + it.preference.toMetadata(context, it.screenKey) + } + ) + .build() + } else { + MetadataResult.Builder(MetadataResult.RESULT_UNSUPPORTED).build() + } +} + /** Translate Framework GET VALUE request to Catalyst GET VALUE request */ fun transformFrameworkGetValueRequest( request: GetValueRequest, @@ -133,6 +180,11 @@ fun transformCatalystSetValueResponse(@PreferenceSetterResult response: Int): Se return SetValueResult.Builder(resultCode).build() } +private data class PreferenceWithScreen( + val screenKey: String, + val preference: PreferenceProto, +) + private fun PreferenceProto.toMetadata( context: Context, screenKey: String diff --git a/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt b/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt index f064b221282..7631a00e9d1 100644 --- a/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt +++ b/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt @@ -16,7 +16,6 @@ package com.android.settings.service -import android.content.ComponentName import android.content.Context import android.content.Intent import android.platform.test.annotations.RequiresFlagsEnabled @@ -24,6 +23,7 @@ import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.service.settings.preferences.GetValueRequest import android.service.settings.preferences.GetValueResult +import android.service.settings.preferences.MetadataResult import android.service.settings.preferences.SetValueRequest import android.service.settings.preferences.SetValueResult import android.service.settings.preferences.SettingsPreferenceMetadata @@ -37,9 +37,15 @@ import com.android.settingslib.graph.PreferenceGetterErrorCode import com.android.settingslib.graph.PreferenceGetterFlags import com.android.settingslib.graph.PreferenceGetterResponse import com.android.settingslib.graph.PreferenceSetterResult +import com.android.settingslib.graph.preferenceGroupProto +import com.android.settingslib.graph.preferenceOrGroupProto +import com.android.settingslib.graph.preferenceProto +import com.android.settingslib.graph.preferenceScreenProto +import com.android.settingslib.graph.proto.PreferenceGraphProto import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.graph.proto.PreferenceValueProto import com.android.settingslib.graph.proto.TextProto +import com.android.settingslib.graph.textProto import com.android.settingslib.graph.toProto import com.android.settingslib.metadata.SensitivityLevel import com.google.common.truth.Truth.assertThat @@ -54,6 +60,73 @@ class PreferenceServiceRequestTransformerTest { @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + @Test + fun transformCatalystGetMetadataResponse_emptyGraph_returnsFrameworkResponseWithError() { + val context: Context = ApplicationProvider.getApplicationContext() + val graphProto = PreferenceGraphProto.newBuilder().build() + val fResult = transformCatalystGetMetadataResponse(context, graphProto) + with(fResult) { + assertThat(resultCode).isEqualTo(MetadataResult.RESULT_UNSUPPORTED) + assertThat(metadataList).isEmpty() + } + } + + @Test + fun transformCatalystGetMetadataResponse_populatedGraph_returnsFrameworkResponseWithSuccess() { + val context: Context = ApplicationProvider.getApplicationContext() + val screen = preferenceScreenProto { + root = preferenceGroupProto { + addAllPreferences( + listOf( + preferenceOrGroupProto { + group = preferenceGroupProto { + addPreferences( + preferenceOrGroupProto { + preference = preferenceProto { + key = "key1" + title = textProto { string = "title1" } + enabled = true + } + } + ) + } + }, + preferenceOrGroupProto { + preference = preferenceProto { + key = "key2" + title = textProto { string = "title2" } + enabled = false + } + } + ) + ) + } + } + val graphProto = PreferenceGraphProto.newBuilder().putScreens("screen", screen).build() + + val fResult = transformCatalystGetMetadataResponse(context, graphProto) + with(fResult) { + assertThat(resultCode).isEqualTo(MetadataResult.RESULT_OK) + assertThat(metadataList.size).isEqualTo(2) + } + assertThat( + fResult.metadataList.any { + it.key == "key1" && + it.screenKey == "screen" && + it.title == "title1" && + it.isEnabled == true + } + ).isTrue() + assertThat( + fResult.metadataList.any { + it.key == "key2" && + it.screenKey == "screen" && + it.title == "title2" && + it.isEnabled == false + } + ).isTrue() + } + @Test fun transformFrameworkGetValueRequest_returnsValidCatalystRequest() { val fRequest = GetValueRequest.Builder("screen", "pref").build()