From 6bafe1b1c03dc5fcea9ca260e9a162bdd2ec1102 Mon Sep 17 00:00:00 2001 From: hoffc Date: Tue, 11 Mar 2025 12:28:02 +0800 Subject: [PATCH 01/22] Fix force close in WiFi API test code Enable WiFi network with empty net id, force close will occur. Add number format exception catch when parsing net id. Bug: 402260924 Change-Id: I540fdeddc7072e7a9414ac7f9d7aca348d06cd30 --- src/com/android/settings/wifi/WifiAPITest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/wifi/WifiAPITest.java b/src/com/android/settings/wifi/WifiAPITest.java index 89538751685..8567f4173d3 100644 --- a/src/com/android/settings/wifi/WifiAPITest.java +++ b/src/com/android/settings/wifi/WifiAPITest.java @@ -162,12 +162,18 @@ public class WifiAPITest extends SettingsPreferenceFragment implements final EditText input = new EditText(getPrefContext()); alert.setView(input); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { + public void onClick(DialogInterface dialog, int whichButton) { Editable value = input.getText(); - netid = Integer.parseInt(value.toString()); - mWifiManager.enableNetwork(netid, false); + try { + netid = Integer.parseInt(value.toString()); + } catch (NumberFormatException e) { + // Invalid netid + e.printStackTrace(); + return; } - }); + mWifiManager.enableNetwork(netid, false); + } + }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // Canceled. From d4774b37aae74e00f45ba37b34f2e95c5930a794 Mon Sep 17 00:00:00 2001 From: yqian Date: Mon, 17 Feb 2025 16:14:02 +0800 Subject: [PATCH 02/22] Update the audio sharing QR code logic to enable OEMs providing customized QR code image Test: atest AudioSharingFeatureProviderImplTest Bug: b/397137711 Flag: EXEMPT no-op Change-Id: I124d6ded50faf9fa0ca06bd5b34d72e9399f3e59 --- .../AudioSharingDialogFragment.java | 27 ++++-- .../AudioSharingFeatureProvider.java | 43 +++++++++ .../AudioSharingFeatureProviderImpl.java | 39 ++++++++ .../AudioStreamsQrCodeFragment.java | 85 ++++++++++------- .../settings/overlay/FeatureFactory.kt | 6 ++ .../settings/overlay/FeatureFactoryImpl.kt | 6 ++ .../AudioSharingFeatureProviderImplTest.java | 93 +++++++++++++++++++ .../testutils/FakeFeatureFactory.java | 10 +- .../settings/testutils/FakeFeatureFactory.kt | 3 + .../testutils/FakeFeatureFactory.java | 10 +- 10 files changed, 279 insertions(+), 43 deletions(-) create mode 100644 src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProvider.java create mode 100644 src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImpl.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImplTest.java diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java index 14a559014cf..6f844813a76 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java @@ -40,7 +40,9 @@ import com.android.settings.R; import com.android.settings.bluetooth.BluetoothPairingDetail; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment; import com.android.settings.core.SubSettingLauncher; +import com.android.settings.overlay.FeatureFactory; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt; import com.android.settingslib.bluetooth.BluetoothUtils; import com.google.common.collect.Iterables; @@ -75,6 +77,9 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { private static Pair[] sEventData = new Pair[0]; @Nullable private static Fragment sHost; + AudioSharingFeatureProvider audioSharingFeatureProvider = + FeatureFactory.getFeatureFactory().getAudioSharingFeatureProvider(); + @Override public int getMetricsCategory() { return SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE; @@ -158,6 +163,9 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { Log.d(TAG, "Create dialog error: null deviceItems"); return builder.build(); } + BluetoothLeBroadcastMetadata metadata = arguments.getParcelable( + BUNDLE_KEY_BROADCAST_METADATA, BluetoothLeBroadcastMetadata.class); + Drawable qrCodeDrawable = null; if (deviceItems.isEmpty()) { builder.setTitle(R.string.audio_sharing_share_dialog_title) .setCustomPositiveButton( @@ -181,9 +189,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { } launcher.launch(); }); - BluetoothLeBroadcastMetadata metadata = arguments.getParcelable( - BUNDLE_KEY_BROADCAST_METADATA, BluetoothLeBroadcastMetadata.class); - Drawable qrCodeDrawable = metadata == null ? null : getQrCodeDrawable(metadata, + qrCodeDrawable = metadata == null ? null : getQrCodeDrawable(metadata, getContext()).orElse(null); if (qrCodeDrawable != null) { String broadcastName = @@ -195,8 +201,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { new String(metadata.getBroadcastCode(), StandardCharsets.UTF_8)) : getString(R.string.audio_sharing_dialog_qr_code_content_no_password, broadcastName); - builder.setCustomImage(qrCodeDrawable) - .setCustomMessage(message) + builder.setCustomMessage(message) .setCustomMessage2(R.string.audio_sharing_dialog_pair_new_device_content) .setCustomNegativeButton(R.string.audio_streams_dialog_close, v -> onCancelClick()); @@ -251,7 +256,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { .setCustomNegativeButton( com.android.settings.R.string.cancel, v -> onCancelClick()); } - return builder.build(); + Dialog dialog = builder.build(); + dialog.show(); + if (deviceItems.isEmpty() && qrCodeDrawable != null) { + audioSharingFeatureProvider.setQrCode( + this, + dialog.getWindow().getDecorView(), + R.id.description_image, + qrCodeDrawable, + BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata)); + } + return dialog; } private void onCancelClick() { diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProvider.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProvider.java new file mode 100644 index 00000000000..40183eb1d87 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.audiosharing; + +import android.annotation.IdRes; +import androidx.annotation.NonNull; +import android.graphics.drawable.Drawable; +import android.view.View; + +import androidx.fragment.app.Fragment; + +/** Feature provider for the audio sharing features. */ +public interface AudioSharingFeatureProvider { + /** + * Sets the QR code for audio sharing dialogs + * + * @param fragment the fragment to be updated + * @param qrcodeContainer the view to be updated + * @param qrCodeImageViewId the view ID to search for + * @param drawable the drawable asset of the QR code + * @param qrCode the value of the qrCode + */ + public void setQrCode( + @NonNull Fragment fragment, + @NonNull View qrcodeContainer, + @IdRes int qrCodeImageViewId, + @NonNull Drawable drawable, + @NonNull String qrCode); +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImpl.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImpl.java new file mode 100644 index 00000000000..bb885a8f1a8 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.audiosharing; + +import android.annotation.IdRes; +import androidx.annotation.NonNull; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.ImageView; + +import androidx.fragment.app.Fragment; + +/** Default implementation for {@link AudioSharingFeatureProvider} */ +public class AudioSharingFeatureProviderImpl implements AudioSharingFeatureProvider { + public void setQrCode( + @NonNull Fragment fragment, + @NonNull View qrcodeContainer, + @IdRes int qrCodeImageViewId, + @NonNull Drawable drawable, + @NonNull String qrCode) { + ImageView imageView = ((ImageView) qrcodeContainer.requireViewById(qrCodeImageViewId)); + imageView.setImageDrawable(drawable); + imageView.setVisibility(View.VISIBLE); + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java index daa7a2e1c23..9968c87edff 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java @@ -37,11 +37,13 @@ import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import com.android.settings.R; import com.android.settings.bluetooth.Utils; +import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider; import com.android.settings.core.InstrumentedFragment; import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.qrcode.QrCodeGenerator; import com.android.settingslib.utils.ThreadUtils; +import com.android.settings.overlay.FeatureFactory; import com.google.zxing.WriterException; @@ -52,6 +54,9 @@ import java.util.Optional; public class AudioStreamsQrCodeFragment extends InstrumentedFragment { private static final String TAG = "AudioStreamsQrCodeFragment"; + AudioSharingFeatureProvider audioSharingFeatureProvider = + FeatureFactory.getFeatureFactory().getAudioSharingFeatureProvider(); + @Override public int getMetricsCategory() { return SettingsEnums.AUDIO_STREAM_QR_CODE; @@ -68,42 +73,52 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment { super.onViewCreated(view, savedInstanceState); // Collapse or expand the app bar based on orientation for better display the qr code image. AudioStreamsHelper.configureAppBarByOrientation(getActivity()); - var unused = ThreadUtils.postOnBackgroundThread( - () -> { - BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata(); - if (broadcastMetadata == null) { - return; - } - Drawable drawable = getQrCodeDrawable(broadcastMetadata, getActivity()).orElse( - null); - if (drawable == null) { - return; - } + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata(); + if (broadcastMetadata == null) { + return; + } + Drawable drawable = + getQrCodeDrawable(broadcastMetadata, getActivity()) + .orElse(null); + if (drawable == null) { + return; + } - ThreadUtils.postOnMainThread( - () -> { - ((ImageView) view.requireViewById(R.id.qrcode_view)) - .setImageDrawable(drawable); - if (broadcastMetadata.getBroadcastCode() != null) { - String password = - new String( - broadcastMetadata.getBroadcastCode(), - StandardCharsets.UTF_8); - String passwordText = - getString( - R.string.audio_streams_qr_code_page_password, - password); - ((TextView) view.requireViewById(R.id.password)) - .setText(passwordText); - } - TextView summaryView = view.requireViewById(android.R.id.summary); - String summary = - getString( - R.string.audio_streams_qr_code_page_description, - broadcastMetadata.getBroadcastName()); - summaryView.setText(summary); - }); - }); + ThreadUtils.postOnMainThread( + () -> { + audioSharingFeatureProvider.setQrCode( + this, + view, + R.id.qrcode_view, + drawable, + BluetoothLeBroadcastMetadataExt.INSTANCE + .toQrCodeString(broadcastMetadata)); + if (broadcastMetadata.getBroadcastCode() != null) { + String password = + new String( + broadcastMetadata.getBroadcastCode(), + StandardCharsets.UTF_8); + String passwordText = + getString( + R.string + .audio_streams_qr_code_page_password, + password); + ((TextView) view.requireViewById(R.id.password)) + .setText(passwordText); + } + TextView summaryView = + view.requireViewById(android.R.id.summary); + String summary = + getString( + R.string + .audio_streams_qr_code_page_description, + broadcastMetadata.getBroadcastName()); + summaryView.setText(summary); + }); + }); } /** Gets an optional drawable from metadata. */ diff --git a/src/com/android/settings/overlay/FeatureFactory.kt b/src/com/android/settings/overlay/FeatureFactory.kt index 7e04f0d4373..6c081c70fc5 100644 --- a/src/com/android/settings/overlay/FeatureFactory.kt +++ b/src/com/android/settings/overlay/FeatureFactory.kt @@ -25,6 +25,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider import com.android.settings.biometrics.face.FaceFeatureProvider import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider import com.android.settings.bluetooth.BluetoothFeatureProvider +import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider import com.android.settings.connecteddevice.stylus.StylusFeatureProvider import com.android.settings.dashboard.DashboardFeatureProvider @@ -179,6 +180,11 @@ abstract class FeatureFactory { */ abstract val fastPairFeatureProvider: FastPairFeatureProvider + /** + * Gets implementation for audio sharing related feature. + */ + abstract val audioSharingFeatureProvider: AudioSharingFeatureProvider + /** * Gets implementation for Private Space account login feature. */ diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.kt b/src/com/android/settings/overlay/FeatureFactoryImpl.kt index 4949c3f7f0c..8442d0e90a4 100644 --- a/src/com/android/settings/overlay/FeatureFactoryImpl.kt +++ b/src/com/android/settings/overlay/FeatureFactoryImpl.kt @@ -37,6 +37,8 @@ import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider import com.android.settings.biometrics.fingerprint.FingerprintFeatureProviderImpl import com.android.settings.bluetooth.BluetoothFeatureProvider import com.android.settings.bluetooth.BluetoothFeatureProviderImpl +import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider +import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProviderImpl import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider import com.android.settings.connecteddevice.fastpair.FastPairFeatureProviderImpl @@ -194,6 +196,10 @@ open class FeatureFactoryImpl : FeatureFactory() { FastPairFeatureProviderImpl() } + override val audioSharingFeatureProvider: AudioSharingFeatureProvider by lazy { + AudioSharingFeatureProviderImpl() + } + override val privateSpaceLoginFeatureProvider: PrivateSpaceLoginFeatureProvider by lazy { PrivateSpaceLoginFeatureProviderImpl() } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImplTest.java new file mode 100644 index 00000000000..3ffd7310822 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImplTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.audiosharing; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.Assert.assertEquals; + +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; + +import com.android.settings.R; +import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment; + +import androidx.fragment.app.Fragment; +import androidx.test.core.app.ApplicationProvider; + +@RunWith(RobolectricTestRunner.class) +public class AudioSharingFeatureProviderImplTest { + + private AudioSharingFeatureProvider mFeatureProvider; + @Mock private Fragment mFragment; + @Mock private View mockView; + private Context mContext; + @Mock private Drawable mDrawable; + + @Before + public void setup() { + mContext = ApplicationProvider.getApplicationContext(); + mFeatureProvider = new AudioSharingFeatureProviderImpl(); + } + + @Test + public void setQrCode_correctDialogLayout() { + mFragment = new AudioSharingDialogFragment(); + View view = + LayoutInflater.from(mContext) + .inflate(R.layout.dialog_custom_body_audio_sharing, null); + mFeatureProvider.setQrCode(mFragment, view, R.id.description_image, mDrawable, ""); + ImageView imageView = view.findViewById(R.id.description_image); + + assertThat(imageView.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(imageView.getDrawable()).isEqualTo(mDrawable); + } + + @Test + public void setQrCode_correctLayout() { + mFragment = new AudioStreamsQrCodeFragment(); + View view = + LayoutInflater.from(mContext) + .inflate(R.layout.bluetooth_audio_streams_qr_code, null); + mFeatureProvider.setQrCode(mFragment, view, R.id.qrcode_view, mDrawable, ""); + ImageView imageView = view.findViewById(R.id.qrcode_view); + + assertThat(imageView.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(imageView.getDrawable()).isEqualTo(mDrawable); + } + + @Test(expected = IllegalArgumentException.class) + public void setQrCode_nonExistedViewId() { + mFragment = new AudioStreamsQrCodeFragment(); + View view = + LayoutInflater.from(mContext) + .inflate(R.layout.bluetooth_audio_streams_qr_code, null); + mFeatureProvider.setQrCode(mFragment, view, R.id.description_image, mDrawable, ""); + } +} diff --git a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java index e002de1e391..3a5682daea5 100644 --- a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java @@ -28,6 +28,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider; import com.android.settings.biometrics.face.FaceFeatureProvider; import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProvider; +import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider; import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider; import com.android.settings.connecteddevice.stylus.StylusFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProvider; @@ -105,6 +106,7 @@ public class FakeFeatureFactory extends FeatureFactory { public DisplayFeatureProvider mDisplayFeatureProvider; public SyncAcrossDevicesFeatureProvider mSyncAcrossDevicesFeatureProvider; public AccessibilityFeedbackFeatureProvider mAccessibilityFeedbackFeatureProvider; + public AudioSharingFeatureProvider mAudioSharingFeatureProvider; /** * Call this in {@code @Before} method of the test class to use fake factory. @@ -155,6 +157,7 @@ public class FakeFeatureFactory extends FeatureFactory { mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class); mDisplayFeatureProvider = mock(DisplayFeatureProvider.class); mSyncAcrossDevicesFeatureProvider = mock(SyncAcrossDevicesFeatureProvider.class); + mAudioSharingFeatureProvider = mock(AudioSharingFeatureProvider.class); } @Override @@ -347,4 +350,9 @@ public class FakeFeatureFactory extends FeatureFactory { public AccessibilityFeedbackFeatureProvider getAccessibilityFeedbackFeatureProvider() { return mAccessibilityFeedbackFeatureProvider; } -} + + @Override + public AudioSharingFeatureProvider getAudioSharingFeatureProvider() { + return mAudioSharingFeatureProvider; + } +} \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt index 7b1bdc0ed9c..2a2ede1f4b1 100644 --- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt +++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt @@ -26,6 +26,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider import com.android.settings.biometrics.face.FaceFeatureProvider import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider import com.android.settings.bluetooth.BluetoothFeatureProvider +import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider import com.android.settings.connecteddevice.stylus.StylusFeatureProvider import com.android.settings.dashboard.DashboardFeatureProvider @@ -148,4 +149,6 @@ class FakeFeatureFactory : FeatureFactory() { get() = TODO("Not yet implemented") override val syncAcrossDevicesFeatureProvider: SyncAcrossDevicesFeatureProvider get() = TODO("Not yet implemented") + override val audioSharingFeatureProvider: AudioSharingFeatureProvider + get() = TODO("Not yet implemented") } diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java index eda0aeb934c..6f3f8df4ecd 100644 --- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java @@ -28,6 +28,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider; import com.android.settings.biometrics.face.FaceFeatureProvider; import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProvider; +import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider; import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider; import com.android.settings.connecteddevice.stylus.StylusFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProvider; @@ -104,6 +105,7 @@ public class FakeFeatureFactory extends FeatureFactory { public DisplayFeatureProvider mDisplayFeatureProvider; public SyncAcrossDevicesFeatureProvider mSyncAcrossDevicesFeatureProvider; public AccessibilityFeedbackFeatureProvider mAccessibilityFeedbackFeatureProvider; + public AudioSharingFeatureProvider mAudioSharingFeatureProvider; /** Call this in {@code @Before} method of the test class to use fake factory. */ public static FakeFeatureFactory setupForTest() { @@ -156,6 +158,7 @@ public class FakeFeatureFactory extends FeatureFactory { mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class); mDisplayFeatureProvider = mock(DisplayFeatureProvider.class); mSyncAcrossDevicesFeatureProvider = mock(SyncAcrossDevicesFeatureProvider.class); + mAudioSharingFeatureProvider = mock(AudioSharingFeatureProvider.class); } @Override @@ -348,4 +351,9 @@ public class FakeFeatureFactory extends FeatureFactory { public AccessibilityFeedbackFeatureProvider getAccessibilityFeedbackFeatureProvider() { return mAccessibilityFeedbackFeatureProvider; } -} + + @Override + public AudioSharingFeatureProvider getAudioSharingFeatureProvider() { + return mAudioSharingFeatureProvider; + } +} \ No newline at end of file From 519f1752f0b8d795328bf14103fb71cc0ddb1a48 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Fri, 14 Mar 2025 14:55:33 +0800 Subject: [PATCH 03/22] Show bond loss UI in device details Bug: 380801155 Test: atest BluetoothDetailsFragmentTest Flag: EXEMPT minor change Change-Id: I458778e1a3adde4ec1ddd8b84b8dc7f1d91621f5 --- .../bluetooth_details_banner_background.xml | 32 +++++ res/layout/bluetooth_details_banner.xml | 50 ++++++++ res/xml/bluetooth_device_details_fragment.xml | 7 ++ .../BluetoothDetailsBannerController.kt | 54 ++++++++ .../BluetoothDetailsConfigurableFragment.kt | 95 ++++++++++++++ .../BluetoothDetailsHeaderController.java | 1 - .../BluetoothDeviceDetailsFragment.java | 29 ++++- .../BluetoothDetailsBannerControllerTest.kt | 54 ++++++++ .../bluetooth/BluetoothDetailsFragmentTest.kt | 116 ++++++++++++++++++ 9 files changed, 432 insertions(+), 6 deletions(-) create mode 100644 res/drawable/bluetooth_details_banner_background.xml create mode 100644 res/layout/bluetooth_details_banner.xml create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsBannerController.kt create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsConfigurableFragment.kt create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsBannerControllerTest.kt create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsFragmentTest.kt diff --git a/res/drawable/bluetooth_details_banner_background.xml b/res/drawable/bluetooth_details_banner_background.xml new file mode 100644 index 00000000000..4a4e8f79ae7 --- /dev/null +++ b/res/drawable/bluetooth_details_banner_background.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/bluetooth_details_banner.xml b/res/layout/bluetooth_details_banner.xml new file mode 100644 index 00000000000..4572dd311a8 --- /dev/null +++ b/res/layout/bluetooth_details_banner.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index 0c8662664f4..66e27f7f166 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -19,6 +19,13 @@ xmlns:settings="http://schemas.android.com/apk/res-auto" android:title="@string/device_details_title"> + + (R.id.bluetooth_details_banner_message).text = + context.getString(R.string.device_details_key_missing_title, cachedDevice.name) + } + + override fun isAvailable(): Boolean = + BluetoothUtils.getKeyMissingCount(cachedDevice.device)?.let { it > 0 } ?: false + + private companion object { + const val KEY_BLUETOOTH_DETAILS_BANNER: String = "bluetooth_details_banner" + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsConfigurableFragment.kt b/src/com/android/settings/bluetooth/BluetoothDetailsConfigurableFragment.kt new file mode 100644 index 00000000000..c3b7fd2d501 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsConfigurableFragment.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth + +import android.os.Bundle +import android.os.UserManager +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceGroup +import com.android.settings.dashboard.RestrictedDashboardFragment + +/** Base class for bluetooth settings which makes the preference visibility/order configurable. */ +abstract class BluetoothDetailsConfigurableFragment : + RestrictedDashboardFragment(UserManager.DISALLOW_CONFIG_BLUETOOTH) { + private var displayOrder: List? = null + + fun setPreferenceDisplayOrder(prefKeyOrder: List?) { + if (displayOrder == prefKeyOrder) { + return + } + displayOrder = prefKeyOrder + updatePreferenceOrder() + } + + private val invisiblePrefCategory: PreferenceGroup by lazy { + preferenceScreen.findPreference(INVISIBLE_CATEGORY) + ?: run { + PreferenceCategory(requireContext()) + .apply { + key = INVISIBLE_CATEGORY + isVisible = false + isOrderingAsAdded = true + } + .also { preferenceScreen.addPreference(it) } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + updatePreferenceOrder() + } + + private fun updatePreferenceOrder() { + val order = displayOrder?: return + if (preferenceScreen == null) { + return + } + preferenceScreen.isOrderingAsAdded = true + val allPrefs = + (invisiblePrefCategory.getAndRemoveAll() + preferenceScreen.getAndRemoveAll()).filter { + it != invisiblePrefCategory + } + allPrefs.forEach { it.order = Preference.DEFAULT_ORDER } + val visiblePrefs = + allPrefs.filter { order.contains(it.key) }.sortedBy { order.indexOf(it.key) } + val invisiblePrefs = allPrefs.filter { !order.contains(it.key) } + preferenceScreen.addPreferences(visiblePrefs) + preferenceScreen.addPreference(invisiblePrefCategory) + invisiblePrefCategory.addPreferences(invisiblePrefs) + } + + private fun PreferenceGroup.getAndRemoveAll(): List { + val prefs = mutableListOf() + for (i in 0..) { + for (pref in prefs) { + addPreference(pref) + } + } + + private companion object { + const val INVISIBLE_CATEGORY = "invisible_profile_category" + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java index 3fbd445c8fc..0727025e669 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java @@ -62,7 +62,6 @@ public class BluetoothDetailsHeaderController extends BluetoothDetailsController final LayoutPreference headerPreference = screen.findPreference(KEY_DEVICE_HEADER); mHeaderController = EntityHeaderController.newInstance(mFragment.getActivity(), mFragment, headerPreference.findViewById(R.id.entity_header)); - screen.addPreference(headerPreference); } protected void setHeaderProperties() { diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 352242a8943..66c39d63108 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -17,7 +17,6 @@ package com.android.settings.bluetooth; import static android.bluetooth.BluetoothDevice.BOND_NONE; -import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; import android.app.Activity; import android.app.settings.SettingsEnums; @@ -49,7 +48,6 @@ import com.android.settings.R; import com.android.settings.bluetooth.ui.model.FragmentTypeModel; import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; import com.android.settings.connecteddevice.stylus.StylusDevicesController; -import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settings.flags.Flags; import com.android.settings.inputmethod.KeyboardSettingsPreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -66,7 +64,7 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment { +public class BluetoothDeviceDetailsFragment extends BluetoothDetailsConfigurableFragment { public static final String KEY_DEVICE_ADDRESS = "device_address"; private static final String TAG = "BTDeviceDetailsFrg"; private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; @@ -102,6 +100,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment BluetoothAdapter mBluetoothAdapter; @VisibleForTesting DeviceDetailsFragmentFormatter mFormatter; + boolean mIsKeyMissingDevice = false; @Nullable InputDevice mInputDevice; @@ -144,7 +143,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment }; public BluetoothDeviceDetailsFragment() { - super(DISALLOW_CONFIG_BLUETOOTH); + super(); } @VisibleForTesting @@ -212,6 +211,9 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment finish(); return; } + Integer keyMissingCount = BluetoothUtils.getKeyMissingCount(mCachedDevice.getDevice()); + mIsKeyMissingDevice = keyMissingCount != null && keyMissingCount > 0; + setPreferenceDisplayOrder(generateDisplayedPreferenceKeys(mIsKeyMissingDevice)); getController( AdvancedBluetoothDetailsHeaderController.class, controller -> controller.init(mCachedDevice, this)); @@ -342,7 +344,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - if (Flags.enableBluetoothDeviceDetailsPolish()) { + if (!mIsKeyMissingDevice && Flags.enableBluetoothDeviceDetailsPolish()) { if (mFormatter == null) { List controllers = getPreferenceControllers().stream() .flatMap(List::stream) @@ -412,12 +414,29 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment return super.onOptionsItemSelected(menuItem); } + @Nullable + private List generateDisplayedPreferenceKeys(boolean bondingLoss) { + if (bondingLoss) { + return List.of( + use(BluetoothDetailsBannerController.class).getPreferenceKey(), + use(AdvancedBluetoothDetailsHeaderController.class).getPreferenceKey(), + use(BluetoothDetailsHeaderController.class).getPreferenceKey(), + use(LeAudioBluetoothDetailsHeaderController.class).getPreferenceKey(), + use(BluetoothDetailsButtonsController.class).getPreferenceKey(), + use(BluetoothDetailsMacAddressController.class).getPreferenceKey()); + } + return null; + } + @Override protected List createPreferenceControllers(Context context) { ArrayList controllers = new ArrayList<>(); if (mCachedDevice != null) { Lifecycle lifecycle = getSettingsLifecycle(); + controllers.add( + new BluetoothDetailsBannerController( + context, this, mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsHeaderController(context, this, mCachedDevice, lifecycle)); controllers.add( diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsBannerControllerTest.kt b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsBannerControllerTest.kt new file mode 100644 index 00000000000..e5c1298bfd1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsBannerControllerTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.bluetooth + +import android.bluetooth.BluetoothDevice +import com.android.settings.R +import com.android.settings.testutils.FakeFeatureFactory +import com.android.settingslib.widget.LayoutPreference +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.whenever + +class BluetoothDetailsBannerControllerTest : BluetoothDetailsControllerTestBase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + + private lateinit var controller: BluetoothDetailsBannerController + private lateinit var preference: LayoutPreference + + override fun setUp() { + super.setUp() + FakeFeatureFactory.setupForTest() + controller = + BluetoothDetailsBannerController(mContext, mFragment, mCachedDevice, mLifecycle) + preference = LayoutPreference(mContext, R.layout.bluetooth_details_banner) + preference.key = controller.getPreferenceKey() + mScreen.addPreference(preference) + } + + @Test + fun iaAvailable_notKeyMissing_false() { + setupDevice(makeDefaultDeviceConfig()) + + assertThat(controller.isAvailable).isFalse() + } + + // TODO(b/379729762): add more tests after BluetoothDevice.getKeyMissingCount is available. +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsFragmentTest.kt b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsFragmentTest.kt new file mode 100644 index 00000000000..b517fae9bca --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsFragmentTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.bluetooth + +import android.content.Context +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.testing.EmptyFragmentActivity +import androidx.preference.Preference +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class BluetoothDetailsFragmentTest { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + + private lateinit var activity: FragmentActivity + private lateinit var fragment: TestConfigurableFragment + private lateinit var context: Context + + @Before + fun setUp() { + context = spy(ApplicationProvider.getApplicationContext()) + } + + @Test + fun setPreferenceDisplayOrder_null_unchanged() = buildFragment { + fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key1" }) + fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key2" }) + + fragment.setPreferenceDisplayOrder(null) + + assertThat(this.displayedKeys).containsExactly("key1", "key2") + } + + @Test + fun setPreferenceDisplayOrder_hideItem() = buildFragment { + fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key1" }) + fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key2" }) + + fragment.setPreferenceDisplayOrder(mutableListOf("key2")) + + assertThat(this.displayedKeys).containsExactly("key2") + } + + @Test + fun setPreferenceDisplayOrder_hideAndReShownItem() = buildFragment { + fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key1" }) + fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key2" }) + + fragment.setPreferenceDisplayOrder(mutableListOf("key2")) + fragment.setPreferenceDisplayOrder(mutableListOf("key2", "key1")) + + assertThat(this.displayedKeys).containsExactly("key2", "key1") + } + + private fun buildFragment(r: (() -> Unit)) { + ActivityScenario.launch(EmptyFragmentActivity::class.java).use { activityScenario -> + activityScenario.onActivity { activity: EmptyFragmentActivity -> + this@BluetoothDetailsFragmentTest.activity = activity + fragment = TestConfigurableFragment() + activity.supportFragmentManager.beginTransaction().add(fragment, null).commitNow() + fragment.setPreferenceScreen( + fragment.preferenceManager.createPreferenceScreen(context) + ) + r.invoke() + } + } + } + + private val displayedKeys: List + get() { + val keys: MutableList = mutableListOf() + for (i in 0.. Date: Tue, 18 Mar 2025 06:27:35 +0000 Subject: [PATCH 04/22] [Catalyst] Add multiple flags for 25Q3 Test: compile Bug: 352179685 Flag: com.android.settings.flags.catalyst_display_settings_screen_25q3 Flag: com.android.settings.flags.catalyst_night_display Flag: com.android.settings.flags.catalyst_color_mode Flag: com.android.settings.flags.catalyst_gesture_system_navigation_input_summary Flag: com.android.settings.flags.catalyst_screen_resolution Change-Id: I5166b70da88cffa45981204fbc8a2f7b4d414020 --- aconfig/catalyst/display.aconfig | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/aconfig/catalyst/display.aconfig b/aconfig/catalyst/display.aconfig index f321708a7bf..e3bc82cda14 100644 --- a/aconfig/catalyst/display.aconfig +++ b/aconfig/catalyst/display.aconfig @@ -8,6 +8,13 @@ flag { bug: "323791114" } +flag { + name: "catalyst_display_settings_screen_25q3" + namespace: "android_settings" + description: "Flag for Display in 25Q3" + bug: "352179685" +} + flag { name: "catalyst_screen_timeout" namespace: "android_settings" @@ -35,3 +42,31 @@ flag { description: "Flag for Adaptive brightness" bug: "323791114" } + +flag { + name: "catalyst_night_display" + namespace: "android_settings" + description: "Flag for Night Light in 25Q3" + bug: "352179685" +} + +flag { + name: "catalyst_color_mode" + namespace: "android_settings" + description: "Flag for Colors in 25Q3" + bug: "352179685" +} + +flag { + name: "catalyst_gesture_system_navigation_input_summary" + namespace: "android_settings" + description: "Flag for Navigation mode in 25Q3" + bug: "352179685" +} + +flag { + name: "catalyst_screen_resolution" + namespace: "android_settings" + description: "Flag for Screen resolution in 25Q3" + bug: "352179685" +} From e8bd9e66b8cd9bbc9ada77944fefd17569a18064 Mon Sep 17 00:00:00 2001 From: Allen Su Date: Tue, 18 Mar 2025 10:37:37 +0000 Subject: [PATCH 05/22] Improve dialog popup 1. Disable dialog when the sys language doesn't change 2. Associate region with system locale Bug: 402600866 Test: atest LocaleListEditTest Flag: EXEMPT bugfix Change-Id: I2c7ddbf89b6caa02a35e247ebe97086f5880adee --- .../android/settings/localepicker/LocaleListEditor.java | 6 +++++- .../RegionPickerBaseListPreferenceController.java | 9 ++++----- .../regionalpreferences/RegionPickerFragment.java | 3 +-- .../regionalpreferences/RegionPreferenceController.java | 5 +++-- .../settings/localepicker/LocaleListEditorTest.java | 2 ++ 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java index 2f8d2032483..87deea167d0 100644 --- a/src/com/android/settings/localepicker/LocaleListEditor.java +++ b/src/com/android/settings/localepicker/LocaleListEditor.java @@ -529,7 +529,11 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View @Nullable Locale defaultLocaleBeforeRemoval) { Locale currentSystemLocale = LocalePicker.getLocales().get(0); if (!localeInfo.getLocale().equals(currentSystemLocale)) { - displayDialogFragment(localeInfo, true); + if (Locale.getDefault().equals(localeInfo.getLocale())) { + mAdapter.doTheUpdate(); + } else { + displayDialogFragment(localeInfo, true); + } } else { if (!localeInfo.isTranslated()) { if (defaultLocaleBeforeRemoval == null) { diff --git a/src/com/android/settings/regionalpreferences/RegionPickerBaseListPreferenceController.java b/src/com/android/settings/regionalpreferences/RegionPickerBaseListPreferenceController.java index fc12d1baa3f..5768421b11f 100644 --- a/src/com/android/settings/regionalpreferences/RegionPickerBaseListPreferenceController.java +++ b/src/com/android/settings/regionalpreferences/RegionPickerBaseListPreferenceController.java @@ -18,7 +18,6 @@ package com.android.settings.regionalpreferences; import android.content.Context; import android.os.Bundle; -import android.os.LocaleList; import android.util.Log; import androidx.annotation.NonNull; @@ -92,7 +91,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe ? getSuggestedLocaleList() : getSupportedLocaleList(); if (getPreferenceCategoryKey().contains(KEY_SUGGESTED)) { - Locale systemLocale = LocaleList.getDefault().get(0); + Locale systemLocale = Locale.getDefault(); LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo(systemLocale); result.add(localeInfo); } @@ -106,7 +105,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe mPreferenceCategory.addPreference(pref); pref.setTitle(locale.getFullCountryNameNative()); pref.setKey(locale.toString()); - if (locale.getLocale().equals(LocaleList.getDefault().get(0))) { + if (locale.getLocale().equals(Locale.getDefault())) { pref.setChecked(true); } else { pref.setChecked(false); @@ -154,7 +153,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe private List getSortedLocaleList( List localeInfos) { - final Locale sortingLocale = LocaleList.getDefault().get(0); + final Locale sortingLocale = Locale.getDefault(); final LocaleHelper.LocaleInfoComparator comp = new LocaleHelper.LocaleInfoComparator(sortingLocale, true); Collections.sort(localeInfos, comp); @@ -162,7 +161,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe } private void switchRegion(LocaleStore.LocaleInfo localeInfo) { - if (localeInfo.getLocale().equals(LocaleList.getDefault().get(0))) { + if (localeInfo.getLocale().equals(Locale.getDefault())) { return; } diff --git a/src/com/android/settings/regionalpreferences/RegionPickerFragment.java b/src/com/android/settings/regionalpreferences/RegionPickerFragment.java index c064565fce8..cf4d9b99ba0 100644 --- a/src/com/android/settings/regionalpreferences/RegionPickerFragment.java +++ b/src/com/android/settings/regionalpreferences/RegionPickerFragment.java @@ -19,7 +19,6 @@ package com.android.settings.regionalpreferences; import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; -import android.os.LocaleList; import android.provider.Settings; import androidx.annotation.NonNull; @@ -84,7 +83,7 @@ public class RegionPickerFragment extends DashboardFragment{ private List buildPreferenceControllers( @NonNull Context context) { - Locale parentLocale = LocaleStore.getLocaleInfo(LocaleList.getDefault().get(0)).getParent(); + Locale parentLocale = LocaleStore.getLocaleInfo(Locale.getDefault()).getParent(); LocaleStore.LocaleInfo parentLocaleInfo = LocaleStore.getLocaleInfo(parentLocale); SystemRegionSuggestedListPreferenceController mSuggestedListPreferenceController = new SystemRegionSuggestedListPreferenceController( diff --git a/src/com/android/settings/regionalpreferences/RegionPreferenceController.java b/src/com/android/settings/regionalpreferences/RegionPreferenceController.java index 345bd755d8d..a9ce1205b08 100644 --- a/src/com/android/settings/regionalpreferences/RegionPreferenceController.java +++ b/src/com/android/settings/regionalpreferences/RegionPreferenceController.java @@ -17,7 +17,6 @@ package com.android.settings.regionalpreferences; import android.content.Context; -import android.os.LocaleList; import androidx.annotation.NonNull; import androidx.preference.Preference; @@ -27,6 +26,8 @@ import com.android.internal.app.LocaleStore; import com.android.settings.core.BasePreferenceController; import com.android.settings.flags.Flags; +import java.util.Locale; + /** A controller for the entry of region picker page */ public class RegionPreferenceController extends BasePreferenceController { @@ -39,7 +40,7 @@ public class RegionPreferenceController extends BasePreferenceController { super.displayPreference(screen); Preference preference = screen.findPreference(getPreferenceKey()); LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo( - LocaleList.getDefault().get(0)); + Locale.getDefault()); preference.setSummary(localeInfo.getFullCountryNameNative()); } diff --git a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java index 22d39e37c12..5f5b8f6bd6d 100644 --- a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java +++ b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java @@ -275,6 +275,7 @@ public class LocaleListEditorTest { public void showConfirmDialog_systemLocaleSelected_shouldShowLocaleChangeDialog() throws Exception { //pre-condition + Locale.setDefault(Locale.forLanguageTag("zh-TW")); setUpLocaleConditions(true); final Configuration config = new Configuration(); config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US"))); @@ -379,6 +380,7 @@ public class LocaleListEditorTest { @Test public void onTouch_dragDifferentLocaleToTop_showConfirmDialog() throws Exception { MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0.0f, 0.0f, 0); + Locale.setDefault(Locale.forLanguageTag("zh-TW")); setUpLocaleConditions(true); final Configuration config = new Configuration(); config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US"))); From 2252b2b4184673fe19f065243da2b4d80a2a03ce Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Tue, 18 Mar 2025 20:58:30 +0800 Subject: [PATCH 06/22] Update string id to use generic 'Done' button Bug: 404145007 Test: check UI of 'Default microphone' dialog in device details page Flag: EXEMPT bugfix Change-Id: I03fae534bb19ab90359e7db1283ac4c139175d4b --- .../settings/bluetooth/HearingDeviceInputRoutingPreference.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java b/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java index 2d09f6103f1..e1e129402a3 100644 --- a/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java +++ b/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java @@ -80,7 +80,7 @@ public class HearingDeviceInputRoutingPreference extends CustomDialogPreferenceC setDialogTitle(R.string.bluetooth_hearing_device_input_routing_dialog_title); setDialogLayoutResource(R.layout.hearing_device_input_routing_dialog); setNegativeButtonText(R.string.cancel); - setPositiveButtonText(R.string.done_button); + setPositiveButtonText(R.string.done); } /** From 12602faf7139eebfa9766e8859f1c986ecf7cbaf Mon Sep 17 00:00:00 2001 From: Fan Wu Date: Wed, 19 Mar 2025 03:45:03 +0000 Subject: [PATCH 07/22] [Catalyst] Add flag for Tether screen 25q3 release Bug: 352179685 Test: NA Flag: com.android.settings.flags.catalyst_tether_settings_25q3 Change-Id: Ia65214550e4ae40b59a545f6a3bacaa22bc844b0 --- aconfig/catalyst/network_and_internet.aconfig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aconfig/catalyst/network_and_internet.aconfig b/aconfig/catalyst/network_and_internet.aconfig index e39b182bdee..7fd35b31ff3 100644 --- a/aconfig/catalyst/network_and_internet.aconfig +++ b/aconfig/catalyst/network_and_internet.aconfig @@ -29,6 +29,13 @@ flag { bug: "323791114" } +flag { + name: "catalyst_tether_settings_25q3" + namespace: "android_settings" + description: "Flag for widgets inside Hotspot & tethering for 25q3 release" + bug: "352179685" +} + flag { name: "catalyst_adaptive_connectivity" namespace: "android_settings" From cf2bcd7f1a8858963576ed7970de2d87e4f9b06d Mon Sep 17 00:00:00 2001 From: Wes Okuhara Date: Wed, 26 Feb 2025 15:09:10 -0800 Subject: [PATCH 08/22] Settings: Start search result deep link in same task This fixes a bug where the deep link initiated from a search result would open the results page in a separate window. The root cause was the inclusion of the FLAG_ACTIVITY_NEW_TASK intent flag. Bug: 381127948 Test: atest SettingsSpaUnitTests:com.android.settings.activityembedding Test: atest CtsSettingsTestCases:SettingsMultiPaneDeepLinkTest Test: Manually verify UI Flag: com.android.settings.flags.settings_search_result_deep_link_in_same_task Change-Id: I91f9e5096ad02bd87e43bae03a2b3e69256be651 --- .../android/settings/SettingsActivity.java | 6 ++ .../EmbeddedDeepLinkUtils.kt | 24 ++++++ .../homepage/SettingsHomepageActivity.java | 5 +- .../search/SearchResultTrampoline.java | 7 +- .../EmbeddedDeepLinkUtilsTest.kt | 78 +++++++++++++++++++ 5 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index 6097b0682da..83c229e3365 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -162,6 +162,12 @@ public class SettingsActivity extends SettingsBaseActivity public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab"; + /** + * Whether the settings homepage activity is initiated from a search result deeplink. + */ + public static final String EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH = + ":settings:is_deeplink_home_started_from_search"; + public static final String META_DATA_KEY_FRAGMENT_CLASS = "com.android.settings.FRAGMENT_CLASS"; diff --git a/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtils.kt b/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtils.kt index 9bbb723af60..0878b8932c9 100644 --- a/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtils.kt +++ b/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtils.kt @@ -24,7 +24,9 @@ import android.content.pm.UserInfo import android.provider.Settings import android.util.Log import com.android.settings.SettingsActivity +import com.android.settings.SettingsActivity.EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH import com.android.settings.Utils +import com.android.settings.flags.Flags import com.android.settings.homepage.DeepLinkHomepageActivityInternal import com.android.settings.homepage.SettingsHomepageActivity import com.android.settings.password.PasswordUtils @@ -94,6 +96,28 @@ object EmbeddedDeepLinkUtils { } } + /** + * Returns the deep link trampoline intent for settings search results for large screen devices. + */ + @JvmStatic + fun getTrampolineIntentForSearchResult( + context: Context, + intent: Intent, + highlightMenuKey: String? + ): Intent { + return getTrampolineIntent(intent, highlightMenuKey).apply { + if (Flags.settingsSearchResultDeepLinkInSameTask()) { + // Ensure the deep link intent does not include FLAG_ACTIVITY_NEW_TASK which + // causes the search result deep link to open in a separate window. + removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + putExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, true) + } else { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + } + + setClass(context, DeepLinkHomepageActivityInternal::class.java) + } + } /** * Returns whether the user is a sub profile. diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 08acbc74c7e..cf9dc6d2106 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -20,6 +20,7 @@ import static android.provider.Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY; import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI; +import static com.android.settings.SettingsActivity.EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH; import static com.android.settings.SettingsActivity.EXTRA_USER_HANDLE; import android.animation.LayoutTransition; @@ -232,7 +233,9 @@ public class SettingsHomepageActivity extends FragmentActivity implements } } - if (!isTaskRoot) { + final boolean isDeepLinkStartedFromSearch = getIntent().getBooleanExtra( + EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, false /* defaultValue */); + if (!isTaskRoot && !isDeepLinkStartedFromSearch) { if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { Log.i(TAG, "Activity has been started, finishing"); } else { diff --git a/src/com/android/settings/search/SearchResultTrampoline.java b/src/com/android/settings/search/SearchResultTrampoline.java index 04d9db56eb7..c40402e4795 100644 --- a/src/com/android/settings/search/SearchResultTrampoline.java +++ b/src/com/android/settings/search/SearchResultTrampoline.java @@ -19,6 +19,7 @@ package com.android.settings.search; import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS; import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB; import static com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntent; +import static com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntentForSearchResult; import android.app.Activity; import android.content.Intent; @@ -35,7 +36,6 @@ import com.android.settings.SubSettings; import com.android.settings.activityembedding.ActivityEmbeddingRulesController; import com.android.settings.activityembedding.ActivityEmbeddingUtils; import com.android.settings.core.FeatureFlags; -import com.android.settings.homepage.DeepLinkHomepageActivityInternal; import com.android.settings.homepage.SettingsHomepageActivity; import com.android.settings.overlay.FeatureFactory; @@ -107,10 +107,7 @@ public class SearchResultTrampoline extends Activity { startActivity(intent); } else if (isSettingsIntelligence(callerPackage)) { if (FeatureFlagUtils.isEnabled(this, FeatureFlags.SETTINGS_SEARCH_ALWAYS_EXPAND)) { - startActivity(getTrampolineIntent(intent, highlightMenuKey) - .setClass(this, DeepLinkHomepageActivityInternal.class) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)); + startActivity(getTrampolineIntentForSearchResult(this, intent, highlightMenuKey)); } else { // Register SplitPairRule for SubSettings, set clearTop false to prevent unexpected // back navigation behavior. diff --git a/tests/spa_unit/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtilsTest.kt b/tests/spa_unit/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtilsTest.kt index 9a638b27ce5..1509f2f29c3 100644 --- a/tests/spa_unit/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtilsTest.kt +++ b/tests/spa_unit/src/com/android/settings/activityembedding/EmbeddedDeepLinkUtilsTest.kt @@ -19,16 +19,26 @@ package com.android.settings.activityembedding import android.content.Context import android.content.Intent import android.net.Uri +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 androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.SettingsActivity.EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH import com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntent +import com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntentForSearchResult +import com.android.settings.flags.Flags +import com.android.settings.homepage.DeepLinkHomepageActivityInternal import com.google.common.truth.Truth.assertThat +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EmbeddedDeepLinkUtilsTest { + @get:Rule + val setFlagsRule = SetFlagsRule() private val context: Context = ApplicationProvider.getApplicationContext() @@ -58,4 +68,72 @@ class EmbeddedDeepLinkUtilsTest { val parsedIntent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME) assertThat(parsedIntent.action).isEqualTo(intent.action) } + + @Test + fun getTrampolineIntent_shouldNotHaveNewTaskFlag() { + val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE") + + val resultIntent = getTrampolineIntent(intent, "menu_key") + + val hasNewTaskFlag = (resultIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0 + assertThat(hasNewTaskFlag).isFalse() + } + + @Test + fun getTrampolineIntentForSearchResult_shouldHaveDeepLinkHomepageClass() { + val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE") + + val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key") + + val className = resultIntent.getComponent()!!.className + assertThat(className).isEqualTo(DeepLinkHomepageActivityInternal::class.java.name) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK) + fun getTrampolineIntentForSearchResult_shouldHaveNewTaskFlag() { + val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE") + + val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key") + + val hasNewTaskFlag = (resultIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0 + assertThat(hasNewTaskFlag).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK) + fun getTrampolineIntentForSearchResult_shouldNotHaveNewTaskFlag() { + val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE") + + val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key") + + val hasNewTaskFlag = (resultIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0 + assertThat(hasNewTaskFlag).isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK) + fun getTrampolineIntentForSearchResult_shouldNotHaveExtraStartedFromSearch() { + val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE") + + val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key") + + assertThat(resultIntent.hasExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH)).isFalse() + assertThat( + resultIntent.getBooleanExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, false) + ).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK) + fun getTrampolineIntentForSearchResult_shouldHaveExtraStartedFromSearch() { + val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE") + + val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key") + + assertThat(resultIntent.hasExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH)).isTrue() + assertThat( + resultIntent.getBooleanExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, false) + ).isTrue() + } } From 617cc51bdbde9939fdb4983e6164547c73729049 Mon Sep 17 00:00:00 2001 From: David Jacobo Date: Tue, 18 Mar 2025 16:41:09 -0700 Subject: [PATCH 09/22] settings: Set explicit intent for legal info This CL sets the full component for the Settings intent that opens "Legal information->Third-party...". Bug: 300111126 Test: make -j64 SettingsRoboTests Test: create ika environment, manually open legal info. Flag: EXEMPT bug fix Change-Id: I3169620c0f2ff78a39b456597384a9738bc38a9f --- src/com/android/settings/SettingsLicenseActivity.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/com/android/settings/SettingsLicenseActivity.java b/src/com/android/settings/SettingsLicenseActivity.java index 598c97394c1..a0c7b63ae54 100644 --- a/src/com/android/settings/SettingsLicenseActivity.java +++ b/src/com/android/settings/SettingsLicenseActivity.java @@ -17,6 +17,7 @@ package com.android.settings; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; @@ -103,6 +104,9 @@ public class SettingsLicenseActivity extends FragmentActivity implements | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); } intent.addCategory(Intent.CATEGORY_DEFAULT); + ComponentName componentName = new ComponentName( + "com.android.htmlviewer", "com.android.htmlviewer.HTMLViewerActivity"); + intent.setComponent(componentName); intent.setPackage("com.android.htmlviewer"); try { From ab325352976381149bd3de0bc7d0a53e23870a79 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 19 Mar 2025 12:45:45 +0800 Subject: [PATCH 10/22] Update talkback when device is disconnected Test: local tested Flag: EXEMPT minor fix Bug: 404422637 Change-Id: If69b728d07f13d557ba2318af9eaf794311cc683 --- .../bluetooth/AdvancedBluetoothDetailsHeaderController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java index d0962be5d8b..4a7de237521 100644 --- a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java +++ b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java @@ -476,6 +476,7 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont Supplier preloadedLowBatteryLevel, Supplier preloadedIsUntethered, Supplier preloadedNativeBatteryLevel) { + linearLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); final String iconUri = preloadedIconUri.get(); final ImageView imageView = linearLayout.findViewById(R.id.header_icon); @@ -685,6 +686,9 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont private void updateDisconnectLayout() { mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE); mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE); + mLayoutPreference + .findViewById(R.id.layout_middle) + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); // Hide title, battery icon and battery summary final LinearLayout linearLayout = mLayoutPreference.findViewById(R.id.layout_middle); From a4d68b6ac2f0875ae970401cccb72db7aa73d4c4 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 19 Mar 2025 13:06:22 +0800 Subject: [PATCH 11/22] Update tint of device details icon Test: local tested Flag: EXEMPT minor fix Bug: 343317785 Change-Id: I33bbc1aa5ea920fc9799ab489d59609aecf4365a --- .../ui/view/DeviceDetailsFragmentFormatter.kt | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index 0658b1da40d..39bc853303e 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -265,12 +265,10 @@ class DeviceDetailsFragmentFormatterImpl( summary = model.summary icon = getDrawable(model.icon) onPreferenceClickListener = - object : Preference.OnPreferenceClickListener { - override fun onPreferenceClick(p: Preference): Boolean { - logItemClick(prefKey, EVENT_CLICK_PRIMARY) - model.action?.let { triggerAction(it) } - return true - } + Preference.OnPreferenceClickListener { + logItemClick(prefKey, EVENT_CLICK_PRIMARY) + model.action?.let { triggerAction(it) } + true } } } @@ -314,12 +312,10 @@ class DeviceDetailsFragmentFormatterImpl( isEnabled = !model.disabled isSwitchEnabled = !model.disabled onPreferenceClickListener = - object : Preference.OnPreferenceClickListener { - override fun onPreferenceClick(p: Preference): Boolean { - logItemClick(prefKey, EVENT_CLICK_PRIMARY) - triggerAction(model.action) - return true - } + Preference.OnPreferenceClickListener { + logItemClick(prefKey, EVENT_CLICK_PRIMARY) + triggerAction(model.action) + true } onPreferenceChangeListener = object : Preference.OnPreferenceChangeListener { @@ -391,6 +387,12 @@ class DeviceDetailsFragmentFormatterImpl( deviceSettingIcon.bitmap.toDrawable(context.resources) is DeviceSettingIcon.ResourceIcon -> context.getDrawable(deviceSettingIcon.resId) null -> null + }?.apply { + setTint( + context.getColor( + com.android.settingslib.widget.theme.R.color.settingslib_materialColorOnSurfaceVariant + ) + ) } @Composable From 9896b590d3b531a95a1b3f28f8b3fe5055fe7b9d Mon Sep 17 00:00:00 2001 From: tom hsu Date: Wed, 19 Mar 2025 03:11:04 +0000 Subject: [PATCH 12/22] [Satellite] Change footer format in satellite setting - Use controller base design instead of current Flag: EXEMPT bug fix Bug: b/401648126 Fix: b/402037025 Test: Manual test Test: atest pass Change-Id: Icb3fd6a1c5340e554593fc57cb238bf98d3ed0e3 --- res/values/strings.xml | 24 ++- res/xml/satellite_setting.xml | 3 +- .../telephony/satellite/SatelliteSetting.java | 43 +---- .../SatelliteSettingFooterController.java | 124 ++++++++++++++ ...eSettingsPreferenceCategoryController.java | 2 +- .../SatelliteSettingFooterControllerTest.java | 162 ++++++++++++++++++ 6 files changed, 308 insertions(+), 50 deletions(-) create mode 100644 src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterController.java create mode 100644 tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 6d865c21096..5fcb29d2e1b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12725,12 +12725,24 @@ Data usage charges may apply. You can text anyone, including emergency services. Your phone will reconnect to a mobile network when available. After your phone is connected, you can text anyone, including emergency services. - - A satellite connection may be slower and is available only in some areas. Weather and certain structures may affect the connection. Calling by satellite isn\u2019t available. Emergency calls may still connect.\n\nIt may take some time for account changes to show in Settings. Contact %1$s for details. - - A satellite connection may be slower and is available only in some areas. Weather and certain structures may affect the connection. Calling by satellite isn\u2019t available. Emergency calls may still connect. Texting with emergency services may not be available in all areas.\n\nIt may take some time for account changes to show in Settings. Contact %1$s for details. - - More about satellite connectivity + + Keep in mind + + Satellite connectivity may take longer and is available only in some areas. + + Weather and certain structures may affect your satellite connection. + + Calling by satellite isn\u2019t available. + + Emergency calls may still connect. + + Mobile or Wi\u2011Fi network required to view external links. + + Texting with emergency services may not be available in all areas. + + It may take some time for account changes to show in Settings. Contact %1$s for details. + + More about satellite connectivity Can’t turn on %1$s diff --git a/res/xml/satellite_setting.xml b/res/xml/satellite_setting.xml index 06509700cf7..308d696e5a4 100644 --- a/res/xml/satellite_setting.xml +++ b/res/xml/satellite_setting.xml @@ -86,6 +86,7 @@ android:key="satellite_setting_extra_info_footer_pref" android:layout="@layout/satellite_setting_more_information_layout" android:selectable="false" - settings:searchable="false"/> + settings:searchable="false" + settings:controller="com.android.settings.network.telephony.satellite.SatelliteSettingFooterController"/> diff --git a/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java b/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java index 5af26584613..e0f1d62a22e 100644 --- a/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java +++ b/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java @@ -27,13 +27,11 @@ import static android.telephony.CarrierConfigManager.KEY_SATELLITE_INFORMATION_R import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; import android.os.Bundle; import android.os.PersistableBundle; import android.os.UserManager; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; import android.telephony.satellite.SatelliteManager; import android.util.Log; import android.view.View; @@ -45,8 +43,6 @@ import androidx.preference.PreferenceCategory; import com.android.settings.R; import com.android.settings.dashboard.RestrictedDashboardFragment; -import com.android.settingslib.HelpUtils; -import com.android.settingslib.widget.FooterPreference; import java.util.Set; @@ -54,7 +50,6 @@ import java.util.Set; public class SatelliteSetting extends RestrictedDashboardFragment { private static final String TAG = "SatelliteSetting"; private static final String PREF_KEY_CATEGORY_HOW_IT_WORKS = "key_category_how_it_works"; - private static final String KEY_FOOTER_PREFERENCE = "satellite_setting_extra_info_footer_pref"; private static final String KEY_SATELLITE_CONNECTION_GUIDE = "key_satellite_connection_guide"; private static final String KEY_SUPPORTED_SERVICE = "key_supported_service"; @@ -67,7 +62,6 @@ public class SatelliteSetting extends RestrictedDashboardFragment { private SatelliteManager mSatelliteManager; private PersistableBundle mConfigBundle; private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; - private String mSimOperatorName = ""; private boolean mIsServiceDataType = false; private boolean mIsSmsAvailableForManualType = false; @@ -95,6 +89,7 @@ public class SatelliteSetting extends RestrictedDashboardFragment { use(SatelliteSettingAboutContentController.class).init(mSubId); use(SatelliteSettingAccountInfoController.class).init(mSubId, mConfigBundle, mIsSmsAvailableForManualType, mIsServiceDataType); + use(SatelliteSettingFooterController.class).init(mSubId, mConfigBundle); } @Override @@ -111,10 +106,7 @@ public class SatelliteSetting extends RestrictedDashboardFragment { Log.d(TAG, "SatelliteSettings: KEY_SATELLITE_ATTACH_SUPPORTED_BOOL is false, " + "do nothing."); finish(); - return; } - - mSimOperatorName = getSystemService(TelephonyManager.class).getSimOperatorName(mSubId); } @Override @@ -122,7 +114,6 @@ public class SatelliteSetting extends RestrictedDashboardFragment { super.onViewCreated(view, savedInstanceState); boolean isSatelliteEligible = isSatelliteEligible(); updateHowItWorksContent(isSatelliteEligible); - updateFooterContent(); } @Override @@ -154,34 +145,6 @@ public class SatelliteSetting extends RestrictedDashboardFragment { supportedService.setSummary(R.string.summary_supported_service_for_manual_type); } - private void updateFooterContent() { - // More about satellite messaging - FooterPreference footerPreference = findPreference(KEY_FOOTER_PREFERENCE); - if (footerPreference != null) { - int summary = mConfigBundle.getBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL) - ? R.string.satellite_setting_summary_more_information - : R.string.satellite_setting_summary_more_information_no_emergency_messaging; - footerPreference.setSummary(getResources().getString(summary, mSimOperatorName)); - - final String[] link = new String[1]; - link[0] = readSatelliteMoreInfoString(); - if (link[0] != null && !link[0].isEmpty()) { - footerPreference.setLearnMoreAction(view -> { - if (!link[0].isEmpty()) { - Intent helpIntent = HelpUtils.getHelpIntent(mActivity, link[0], - this.getClass().getName()); - if (helpIntent != null) { - mActivity.startActivityForResult(helpIntent, /*requestCode=*/ 0); - } - } - }); - - footerPreference.setLearnMoreText( - getString(R.string.more_about_satellite_messaging)); - } - } - } - private boolean isSatelliteEligible() { if (isCarrierRoamingNtnConnectedTypeManual()) { return mIsSmsAvailableForManualType; @@ -218,10 +181,6 @@ public class SatelliteSetting extends RestrictedDashboardFragment { return bundle; } - private String readSatelliteMoreInfoString() { - return mConfigBundle.getString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING, ""); - } - private boolean isCarrierRoamingNtnConnectedTypeManual() { return CARRIER_ROAMING_NTN_CONNECT_MANUAL == mConfigBundle.getInt( KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT, CARRIER_ROAMING_NTN_CONNECT_AUTOMATIC); diff --git a/src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterController.java b/src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterController.java new file mode 100644 index 00000000000..a43ed87a63d --- /dev/null +++ b/src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterController.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony.satellite; + +import static android.telephony.CarrierConfigManager.KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL; +import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL; +import static android.telephony.CarrierConfigManager.KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING; + +import android.content.Context; +import android.content.Intent; +import android.os.PersistableBundle; +import android.telephony.TelephonyManager; +import android.text.Html; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.network.telephony.TelephonyBasePreferenceController; +import com.android.settingslib.HelpUtils; +import com.android.settingslib.widget.FooterPreference; + +/** A controller for showing the dynamic disclaimer of Satellite service. */ +public class SatelliteSettingFooterController extends TelephonyBasePreferenceController { + private static final String TAG = "SatelliteSettingFooterController"; + @VisibleForTesting + static final String KEY_FOOTER_PREFERENCE = "satellite_setting_extra_info_footer_pref"; + + private PersistableBundle mConfigBundle = new PersistableBundle(); + private String mSimOperatorName; + + public SatelliteSettingFooterController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + void init(int subId, PersistableBundle configBundle) { + mSubId = subId; + mConfigBundle = configBundle; + mSimOperatorName = mContext.getSystemService(TelephonyManager.class).getSimOperatorName( + subId); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + updateFooterContent(screen); + } + + @Override + public int getAvailabilityStatus(int subId) { + return AVAILABLE_UNSEARCHABLE; + } + + private void updateFooterContent(PreferenceScreen screen) { + // More about satellite messaging + FooterPreference footerPreference = screen.findPreference(KEY_FOOTER_PREFERENCE); + if (footerPreference == null) { + return; + } + footerPreference.setSummary( + Html.fromHtml(getFooterContent(), Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM)); + final String link = readSatelliteMoreInfoString(); + if (link.isEmpty()) { + return; + } + footerPreference.setLearnMoreAction(view -> { + Intent helpIntent = HelpUtils.getHelpIntent(mContext, link, this.getClass().getName()); + if (helpIntent != null) { + mContext.startActivityForResult(mContext.getPackageName(), + helpIntent, /*requestCode=*/ 0, null); + } + }); + footerPreference.setLearnMoreText( + mContext.getString(R.string.more_about_satellite_connectivity)); + } + + private String getFooterContent() { + String result = ""; + result = mContext.getString(R.string.satellite_footer_content_section_0) + "\n\n"; + result += getHtmlStringCombination(R.string.satellite_footer_content_section_1); + result += getHtmlStringCombination(R.string.satellite_footer_content_section_2); + result += getHtmlStringCombination(R.string.satellite_footer_content_section_3); + result += getHtmlStringCombination(R.string.satellite_footer_content_section_4); + result += getHtmlStringCombination(R.string.satellite_footer_content_section_5); + if (!mConfigBundle.getBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL)) { + result += getHtmlStringCombination(R.string.satellite_footer_content_section_6); + } + if (mConfigBundle.getBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL)) { + result += getHtmlStringCombination(R.string.satellite_footer_content_section_7, + mSimOperatorName); + } + return result; + } + + private String getHtmlStringCombination(int resId) { + String prefix = "
  •  "; + String subfix = "
  • "; + return prefix + mContext.getString(resId) + subfix; + } + + private String getHtmlStringCombination(int resId, Object... value) { + String prefix = "
  •  "; + String subfix = "
  • "; + return prefix + mContext.getString(resId, value) + subfix; + } + + private String readSatelliteMoreInfoString() { + return mConfigBundle.getString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING); + } +} diff --git a/src/com/android/settings/network/telephony/satellite/SatelliteSettingsPreferenceCategoryController.java b/src/com/android/settings/network/telephony/satellite/SatelliteSettingsPreferenceCategoryController.java index d0cb1bcc5ca..6bf635e18f8 100644 --- a/src/com/android/settings/network/telephony/satellite/SatelliteSettingsPreferenceCategoryController.java +++ b/src/com/android/settings/network/telephony/satellite/SatelliteSettingsPreferenceCategoryController.java @@ -159,7 +159,7 @@ public class SatelliteSettingsPreferenceCategoryController @Override public void onResult(Boolean result) { mIsSatelliteSupported.set(result); - Log.d(TAG, "Satellite requestIsSupported : " + result); + Log.d(TAG, "Satellite requestIsSupported onResult : " + result); SatelliteSettingsPreferenceCategoryController.this.displayPreference(); } }); diff --git a/tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterControllerTest.java new file mode 100644 index 00000000000..a5873474765 --- /dev/null +++ b/tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteSettingFooterControllerTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony.satellite; + +import static android.telephony.CarrierConfigManager.KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL; +import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL; +import static android.telephony.CarrierConfigManager.KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING; + +import static com.android.settings.network.telephony.satellite.SatelliteSettingFooterController.KEY_FOOTER_PREFERENCE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Looper; +import android.os.PersistableBundle; +import android.telephony.TelephonyManager; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.testutils.ResourcesUtils; +import com.android.settingslib.widget.FooterPreference; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class SatelliteSettingFooterControllerTest { + private static final int TEST_SUB_ID = 5; + private static final String TEST_OPERATOR_NAME = "test_operator_name"; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + private TelephonyManager mTelephonyManager; + + private Context mContext; + private SatelliteSettingFooterController mController; + private final PersistableBundle mPersistableBundle = new PersistableBundle(); + + @Before + public void setUp() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + mContext = spy(ApplicationProvider.getApplicationContext()); + mController = new SatelliteSettingFooterController(mContext, + KEY_FOOTER_PREFERENCE); + when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); + when(mTelephonyManager.getSimOperatorName(TEST_SUB_ID)).thenReturn(TEST_OPERATOR_NAME); + mPersistableBundle.putString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING, ""); + } + + @Test + public void displayPreferenceScreen_updateContent_hasBasicContent() { + PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + FooterPreference preference = new FooterPreference(mContext); + preference.setKey(KEY_FOOTER_PREFERENCE); + screen.addPreference(preference); + mController.init(TEST_SUB_ID, mPersistableBundle); + + mController.displayPreference(screen); + String summary = preference.getSummary().toString(); + + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_0"))).isTrue(); + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_1"))).isTrue(); + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_2"))).isTrue(); + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_3"))).isTrue(); + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_4"))).isTrue(); + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_5"))).isTrue(); + } + + @Test + public void displayPreferenceScreen_noEmergencyMsgSupport_hasEmergencyContent() { + mPersistableBundle.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, false); + PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + FooterPreference preference = new FooterPreference(mContext); + preference.setKey(KEY_FOOTER_PREFERENCE); + screen.addPreference(preference); + mController.init(TEST_SUB_ID, mPersistableBundle); + + mController.displayPreference(screen); + String summary = preference.getSummary().toString(); + + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_6"))).isTrue(); + } + + @Test + public void displayPreferenceScreen_emergencyMsgSupport_noEmergencyContent() { + mPersistableBundle.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, true); + PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + FooterPreference preference = new FooterPreference(mContext); + preference.setKey(KEY_FOOTER_PREFERENCE); + screen.addPreference(preference); + mController.init(TEST_SUB_ID, mPersistableBundle); + + mController.displayPreference(screen); + String summary = preference.getSummary().toString(); + + assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext, + "satellite_footer_content_section_6"))).isFalse(); + } + + @Test + public void displayPreferenceScreen_entitlementSupport_hasEntitlementContent() { + mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true); + PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + FooterPreference preference = new FooterPreference(mContext); + preference.setKey(KEY_FOOTER_PREFERENCE); + screen.addPreference(preference); + mController.init(TEST_SUB_ID, mPersistableBundle); + + mController.displayPreference(screen); + String summary = preference.getSummary().toString(); + + assertThat(summary.contains(TEST_OPERATOR_NAME)).isTrue(); + } + + @Test + public void displayPreferenceScreen_entitlementNotSupport_noEntitlementContent() { + mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false); + PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + FooterPreference preference = new FooterPreference(mContext); + preference.setKey(KEY_FOOTER_PREFERENCE); + screen.addPreference(preference); + mController.init(TEST_SUB_ID, mPersistableBundle); + + mController.displayPreference(screen); + String summary = preference.getSummary().toString(); + + assertThat(summary.contains(TEST_OPERATOR_NAME)).isFalse(); + } +} From d6f7270748564effbd892b6d958da9cbe1137dfb Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 19 Mar 2025 14:01:50 +0800 Subject: [PATCH 13/22] Add toast for bond loss Test: local tested, haven't find good way to add unit test for reflection. Flag: EXEMPT minor fix Bug: 380801155 Change-Id: Ia0d05ef933b0ae24077f31e4ff46c9948b99628a --- res/values/strings.xml | 2 ++ .../BluetoothKeyMissingDialogFragment.java | 2 +- .../BluetoothKeyMissingReceiver.java | 27 ++++++++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 6d865c21096..266decdaab6 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2099,6 +2099,8 @@ Device settings Close + + %1$s failed to connect Device details diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java index 671e282c338..6b25bed88f4 100644 --- a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java @@ -66,7 +66,7 @@ public class BluetoothKeyMissingDialogFragment extends InstrumentedDialogFragmen View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_key_missing, null); TextView keyMissingTitle = view.findViewById(R.id.bluetooth_key_missing_title); keyMissingTitle.setText( - getString(R.string.bluetooth_key_missing_title, mBluetoothDevice.getName())); + getString(R.string.bluetooth_key_missing_title, mBluetoothDevice.getAlias())); builder.setView(view); builder.setPositiveButton(getString(R.string.bluetooth_key_missing_device_settings), this); builder.setNegativeButton(getString(R.string.bluetooth_key_missing_close), this); diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java index cfe9c056d39..2ae9fd9f846 100644 --- a/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java +++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java @@ -28,6 +28,7 @@ import android.os.PowerManager; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; +import android.widget.Toast; import androidx.core.app.NotificationCompat; @@ -68,15 +69,23 @@ public final class BluetoothKeyMissingReceiver extends BroadcastReceiver { return; } Integer keyMissingCount = BluetoothUtils.getKeyMissingCount(device); - if (keyMissingCount != null && keyMissingCount != 1) { - Log.d(TAG, "Key missing count is " + keyMissingCount + ", skip."); - return; - } + boolean keyMissingFirstTime = keyMissingCount == null || keyMissingCount == 1; if (shouldShowDialog(context, device, powerManager)) { - Intent pairingIntent = getKeyMissingDialogIntent(context, device); - Log.d(TAG, "Show key missing dialog:" + device); - context.startActivityAsUser(pairingIntent, UserHandle.CURRENT); - } else { + if (keyMissingFirstTime) { + Intent pairingIntent = getKeyMissingDialogIntent(context, device); + Log.d(TAG, "Show key missing dialog:" + device); + context.startActivityAsUser(pairingIntent, UserHandle.CURRENT); + } else { + Log.d(TAG, "Show key missing toast:" + device); + Toast.makeText( + context, + context.getString( + R.string.bluetooth_key_missing_toast, + device.getAlias()), + Toast.LENGTH_SHORT) + .show(); + } + } else if (keyMissingFirstTime) { Log.d(TAG, "Show key missing notification: " + device); showNotification(context, device); } @@ -123,7 +132,7 @@ public final class BluetoothKeyMissingReceiver extends BroadcastReceiver { .setLocalOnly(true); builder.setContentTitle( context.getString( - R.string.bluetooth_key_missing_title, bluetoothDevice.getName())) + R.string.bluetooth_key_missing_title, bluetoothDevice.getAlias())) .setContentText(context.getString(R.string.bluetooth_key_missing_message)) .setContentIntent(pairIntent) .setAutoCancel(true) From 590a3649023dadebb2ab7b5cf3b84ee7244b647f Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Wed, 19 Mar 2025 17:08:39 +0800 Subject: [PATCH 14/22] [Catalyst] Add flags for 25Q3 sound screens Test: compile Bug: 352179685 Flag: com.android.settings.flags.catalyst_sound_screen_25q3 Flag: com.android.settings.flags.catalyst_spatial_audio Change-Id: I3295469a7c8d0eaf0eb5f73ba77931383b4b7cbb --- aconfig/catalyst/sound_screen.aconfig | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/aconfig/catalyst/sound_screen.aconfig b/aconfig/catalyst/sound_screen.aconfig index 4ba339625c5..9faeffb68ce 100644 --- a/aconfig/catalyst/sound_screen.aconfig +++ b/aconfig/catalyst/sound_screen.aconfig @@ -4,27 +4,41 @@ container: "system_ext" flag { name: "catalyst_sound_screen" namespace: "android_settings" - description: "Flag for sound and vibration page" + description: "Flag for Sound & vibration" bug: "323791114" } +flag { + name: "catalyst_sound_screen_25q3" + namespace: "android_settings" + description: "Flag for Sound & vibration in 25Q3" + bug: "352179685" +} + flag { name: "catalyst_media_controls" namespace: "android_settings" - description: "Flag for media page" + description: "Flag for Media" bug: "337243570" } flag { name: "catalyst_vibration_intensity_screen" namespace: "android_settings" - description: "Flag for vibration and haptics page" + description: "Flag for Vibration & haptics" bug: "323791114" } flag { name: "catalyst_vibration_screen" namespace: "android_settings" - description: "Flag for vibration and haptics full page migration" + description: "Flag for Vibration & haptics full page migration" bug: "323791114" -} \ No newline at end of file +} + +flag { + name: "catalyst_spatial_audio" + namespace: "android_settings" + description: "Flag for Spatial audio in 25Q3" + bug: "352179685" +} From 4bebfc4c5909761c85ce7ba91d931e70fd4b4915 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Wed, 19 Mar 2025 09:11:52 +0000 Subject: [PATCH 15/22] [Catalyst] Add flag for Auto-rotate screen Test: compile Bug: 352179685 Flag: com.android.settings.flags.catalyst_auto_rotate Change-Id: I2423d9a2a62a9796a147ae74e26955d0269621d5 --- aconfig/catalyst/display.aconfig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aconfig/catalyst/display.aconfig b/aconfig/catalyst/display.aconfig index e3bc82cda14..b7219a75b07 100644 --- a/aconfig/catalyst/display.aconfig +++ b/aconfig/catalyst/display.aconfig @@ -70,3 +70,10 @@ flag { description: "Flag for Screen resolution in 25Q3" bug: "352179685" } + +flag { + name: "catalyst_auto_rotate" + namespace: "android_settings" + description: "Flag for Auto-rotate screen in 25Q3" + bug: "352179685" +} From dc788ed9d1479064509a1cb36a08b0fcea7e1cbe Mon Sep 17 00:00:00 2001 From: tom hsu Date: Wed, 19 Mar 2025 08:53:27 +0000 Subject: [PATCH 16/22] [Satellite] Use satellite data mode for UX visibility - Use SatelliteManager#getSatelliteDataSupportMode() to check data restricted or not. Flag: EXEMPT bug fix Bug: b/401043401 Test: atest pass Test: Manual test Change-Id: I6f3e25610717eca0f6e871a9e1158422659058ea --- .../settings/network/SatelliteRepository.kt | 3 - .../SatelliteAppListCategoryController.java | 84 ++++++++--- .../telephony/satellite/SatelliteSetting.java | 53 ++++--- ...atelliteAppListCategoryControllerTest.java | 134 +++++++++++++++--- 4 files changed, 215 insertions(+), 59 deletions(-) diff --git a/src/com/android/settings/network/SatelliteRepository.kt b/src/com/android/settings/network/SatelliteRepository.kt index 994f8ec96d4..543961646de 100644 --- a/src/com/android/settings/network/SatelliteRepository.kt +++ b/src/com/android/settings/network/SatelliteRepository.kt @@ -202,9 +202,6 @@ open class SatelliteRepository( * e.g. "com.android.settings" */ open fun getSatelliteDataOptimizedApps(): List { - if (!Flags.satellite25q4Apis()) { - return emptyList() - } val satelliteManager: SatelliteManager? = context.getSystemService(SatelliteManager::class.java) if (satelliteManager == null) { diff --git a/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryController.java b/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryController.java index a29a388579a..d182229dd01 100644 --- a/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryController.java +++ b/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryController.java @@ -16,10 +16,17 @@ package com.android.settings.network.telephony.satellite; +import static android.telephony.CarrierConfigManager.CARRIER_ROAMING_NTN_CONNECT_MANUAL; +import static android.telephony.CarrierConfigManager.KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT; +import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL; + import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.os.PersistableBundle; +import android.telephony.satellite.SatelliteManager; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -27,20 +34,23 @@ import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import com.android.internal.telephony.flags.Flags; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.network.SatelliteRepository; +import com.android.settings.network.telephony.TelephonyBasePreferenceController; import com.android.settingslib.Utils; import java.util.List; +import java.util.Set; /** A controller to show some of apps info which supported on Satellite service. */ -public class SatelliteAppListCategoryController extends BasePreferenceController { +public class SatelliteAppListCategoryController extends TelephonyBasePreferenceController { private static final String TAG = "SatelliteAppListCategoryController"; @VisibleForTesting static final int MAXIMUM_OF_PREFERENCE_AMOUNT = 3; private List mPackageNameList; + private boolean mIsSmsAvailable; + private boolean mIsDataAvailable; + private boolean mIsSatelliteEligible; + private PersistableBundle mConfigBundle = new PersistableBundle(); public SatelliteAppListCategoryController( @NonNull Context context, @@ -49,14 +59,14 @@ public class SatelliteAppListCategoryController extends BasePreferenceController } /** Initialize the necessary applications' data*/ - public void init() { - SatelliteRepository satelliteRepository = new SatelliteRepository(mContext); - init(satelliteRepository); - } - - @VisibleForTesting - void init(@NonNull SatelliteRepository satelliteRepository) { - mPackageNameList = satelliteRepository.getSatelliteDataOptimizedApps(); + public void init(int subId, @NonNull PersistableBundle configBundle, boolean isSmsAvailable, + boolean isDataAvailable) { + mSubId = subId; + mConfigBundle = configBundle; + mIsSmsAvailable = isSmsAvailable; + mIsDataAvailable = isDataAvailable; + mPackageNameList = getSatelliteDataOptimizedApps(); + mIsSatelliteEligible = isSatelliteEligible(); } @Override @@ -78,13 +88,53 @@ public class SatelliteAppListCategoryController extends BasePreferenceController } @Override - public int getAvailabilityStatus() { - if (!Flags.satellite25q4Apis()) { + public int getAvailabilityStatus(int subId) { + // Only when carrier support entitlement check, it shall check account eligible or not. + if (mConfigBundle.getBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL) + && !mIsSatelliteEligible) { return CONDITIONALLY_UNAVAILABLE; } - return mPackageNameList.isEmpty() - ? CONDITIONALLY_UNAVAILABLE - : AVAILABLE; + Log.d(TAG, "Supported apps have " + mPackageNameList.size()); + + return mIsDataAvailable && !mPackageNameList.isEmpty() + ? AVAILABLE_UNSEARCHABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @VisibleForTesting + protected List getSatelliteDataOptimizedApps() { + SatelliteManager satelliteManager = mContext.getSystemService(SatelliteManager.class); + if (satelliteManager == null) { + return List.of(); + } + try { + return satelliteManager.getSatelliteDataOptimizedApps(); + } catch (IllegalStateException e) { + Log.d(TAG, "getSatelliteDataOptimizedApps failed due to " + e); + } + return List.of(); + } + + @VisibleForTesting + protected boolean isSatelliteEligible() { + if (mConfigBundle.getInt(KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT) + == CARRIER_ROAMING_NTN_CONNECT_MANUAL) { + return mIsSmsAvailable; + } + SatelliteManager satelliteManager = mContext.getSystemService(SatelliteManager.class); + if (satelliteManager == null) { + Log.d(TAG, "SatelliteManager is null."); + return false; + } + try { + Set restrictionReason = + satelliteManager.getAttachRestrictionReasonsForCarrier(mSubId); + return !restrictionReason.contains( + SatelliteManager.SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT); + } catch (SecurityException | IllegalStateException | IllegalArgumentException ex) { + Log.d(TAG, "Error to getAttachRestrictionReasonsForCarrier : " + ex); + return false; + } } static ApplicationInfo getApplicationInfo(Context context, String packageName) { diff --git a/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java b/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java index e0f1d62a22e..8b7fdc68a4c 100644 --- a/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java +++ b/src/com/android/settings/network/telephony/satellite/SatelliteSetting.java @@ -23,6 +23,7 @@ import static android.telephony.CarrierConfigManager.KEY_EMERGENCY_MESSAGING_SUP import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ATTACH_SUPPORTED_BOOL; import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL; import static android.telephony.CarrierConfigManager.KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING; +import static android.telephony.CarrierConfigManager.SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED; import android.app.Activity; import android.app.settings.SettingsEnums; @@ -78,35 +79,30 @@ public class SatelliteSetting extends RestrictedDashboardFragment { public void onAttach(Context context) { super.onAttach(context); mActivity = getActivity(); - mSubId = mActivity.getIntent().getIntExtra(SUB_ID, - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - mConfigBundle = fetchCarrierConfigData(mSubId); - mIsServiceDataType = getIntent().getBooleanExtra(EXTRA_IS_SERVICE_DATA_TYPE, false); - mIsSmsAvailableForManualType = getIntent().getBooleanExtra( - EXTRA_IS_SMS_AVAILABLE_FOR_MANUAL_TYPE, false); - - use(SatelliteAppListCategoryController.class).init(); - use(SatelliteSettingAboutContentController.class).init(mSubId); - use(SatelliteSettingAccountInfoController.class).init(mSubId, mConfigBundle, - mIsSmsAvailableForManualType, mIsServiceDataType); - use(SatelliteSettingFooterController.class).init(mSubId, mConfigBundle); - } - - @Override - public void onCreate(@NonNull Bundle savedInstanceState) { - super.onCreate(savedInstanceState); mSatelliteManager = mActivity.getSystemService(SatelliteManager.class); if (mSatelliteManager == null) { Log.d(TAG, "SatelliteManager is null, do nothing."); finish(); return; } - - if (!isSatelliteAttachSupported(mSubId)) { + mSubId = mActivity.getIntent().getIntExtra(SUB_ID, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + mConfigBundle = fetchCarrierConfigData(mSubId); + if (!isSatelliteAttachSupported()) { Log.d(TAG, "SatelliteSettings: KEY_SATELLITE_ATTACH_SUPPORTED_BOOL is false, " + "do nothing."); finish(); } + mIsServiceDataType = getIntent().getBooleanExtra(EXTRA_IS_SERVICE_DATA_TYPE, false); + mIsSmsAvailableForManualType = getIntent().getBooleanExtra( + EXTRA_IS_SMS_AVAILABLE_FOR_MANUAL_TYPE, false); + boolean isDataAvailableAndNotRestricted = isDataAvailableAndNotRestricted(); + use(SatelliteAppListCategoryController.class).init(mSubId, mConfigBundle, + mIsSmsAvailableForManualType, isDataAvailableAndNotRestricted); + use(SatelliteSettingAboutContentController.class).init(mSubId); + use(SatelliteSettingAccountInfoController.class).init(mSubId, mConfigBundle, + mIsSmsAvailableForManualType, isDataAvailableAndNotRestricted); + use(SatelliteSettingFooterController.class).init(mSubId, mConfigBundle); } @Override @@ -152,6 +148,7 @@ public class SatelliteSetting extends RestrictedDashboardFragment { try { Set restrictionReason = mSatelliteManager.getAttachRestrictionReasonsForCarrier(mSubId); + Log.d(TAG, "Restriction reason : " + restrictionReason); return !restrictionReason.contains( SatelliteManager.SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT); } catch (SecurityException | IllegalStateException | IllegalArgumentException ex) { @@ -186,10 +183,26 @@ public class SatelliteSetting extends RestrictedDashboardFragment { KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT, CARRIER_ROAMING_NTN_CONNECT_AUTOMATIC); } - private boolean isSatelliteAttachSupported(int subId) { + private boolean isSatelliteAttachSupported() { return mConfigBundle.getBoolean(KEY_SATELLITE_ATTACH_SUPPORTED_BOOL, false); } + private boolean isDataAvailableAndNotRestricted() { + return getIntent().getBooleanExtra(EXTRA_IS_SERVICE_DATA_TYPE, false) + && !isDataRestricted(); + } + + private boolean isDataRestricted() { + int dataMode = SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED; + try { + dataMode = mSatelliteManager.getSatelliteDataSupportMode(mSubId); + Log.d(TAG, "Data mode : " + dataMode); + } catch (IllegalStateException e) { + Log.d(TAG, "Failed to get data mode : " + e); + } + return dataMode <= SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED; + } + private static void loge(String message) { Log.e(TAG, message); } diff --git a/tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryControllerTest.java index 56d4b9fe183..8f2fb7225a1 100644 --- a/tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryControllerTest.java +++ b/tests/unit/src/com/android/settings/network/telephony/satellite/SatelliteAppListCategoryControllerTest.java @@ -16,7 +16,11 @@ package com.android.settings.network.telephony.satellite; -import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static android.telephony.CarrierConfigManager.CARRIER_ROAMING_NTN_CONNECT_MANUAL; +import static android.telephony.CarrierConfigManager.KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT; +import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.android.settings.network.telephony.satellite.SatelliteAppListCategoryController.MAXIMUM_OF_PREFERENCE_AMOUNT; @@ -31,6 +35,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Looper; +import android.os.PersistableBundle; import android.platform.test.annotations.EnableFlags; import androidx.preference.PreferenceCategory; @@ -39,7 +44,6 @@ import androidx.preference.PreferenceScreen; import androidx.test.core.app.ApplicationProvider; import com.android.internal.telephony.flags.Flags; -import com.android.settings.network.SatelliteRepository; import org.junit.Before; import org.junit.Rule; @@ -48,25 +52,23 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.util.Collections; import java.util.List; public class SatelliteAppListCategoryControllerTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + private static final int TEST_SUB_ID = 0; private static final List PACKAGE_NAMES = List.of("com.android.settings", "com.android.apps.messaging", "com.android.dialer", "com.android.systemui"); private static final String KEY = "SatelliteAppListCategoryControllerTest"; @Mock private PackageManager mPackageManager; - @Mock - private SatelliteRepository mRepository; private Context mContext; private SatelliteAppListCategoryController mController; - + private PersistableBundle mPersistableBundle = new PersistableBundle(); @Before public void setUp() { @@ -75,16 +77,28 @@ public class SatelliteAppListCategoryControllerTest { } mContext = spy(ApplicationProvider.getApplicationContext()); when(mContext.getPackageManager()).thenReturn(mPackageManager); + mPersistableBundle.putInt(KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT, + CARRIER_ROAMING_NTN_CONNECT_MANUAL); + mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false); } @Test @EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS) public void displayPreference_has4SatSupportedApps_showMaxPreference() throws Exception { - when(mRepository.getSatelliteDataOptimizedApps()).thenReturn(PACKAGE_NAMES); when(mPackageManager.getApplicationInfoAsUser(any(), anyInt(), anyInt())).thenReturn( new ApplicationInfo()); - mController = new SatelliteAppListCategoryController(mContext, KEY); - mController.init(mRepository); + mController = new SatelliteAppListCategoryController(mContext, KEY) { + @Override + protected boolean isSatelliteEligible() { + return true; + } + + @Override + protected List getSatelliteDataOptimizedApps() { + return PACKAGE_NAMES; + } + }; + mController.init(TEST_SUB_ID, mPersistableBundle, true, true); PreferenceManager preferenceManager = new PreferenceManager(mContext); PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext); PreferenceCategory category = new PreferenceCategory(mContext); @@ -100,25 +114,107 @@ public class SatelliteAppListCategoryControllerTest { @Test @EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS) public void getAvailabilityStatus_hasSatSupportedApps_returnAvailable() { - when(mRepository.getSatelliteDataOptimizedApps()).thenReturn(PACKAGE_NAMES); - mController = new SatelliteAppListCategoryController(mContext, KEY); - mController.init(mRepository); + mController = new SatelliteAppListCategoryController(mContext, KEY) { + @Override + protected boolean isSatelliteEligible() { + return true; + } - int result = mController.getAvailabilityStatus(); + @Override + protected List getSatelliteDataOptimizedApps() { + return PACKAGE_NAMES; + } + }; + mController.init(TEST_SUB_ID, mPersistableBundle, true, true); - assertThat(result).isEqualTo(AVAILABLE); + int result = mController.getAvailabilityStatus(TEST_SUB_ID); + + assertThat(result).isEqualTo(AVAILABLE_UNSEARCHABLE); } @Test @EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS) public void getAvailabilityStatus_noSatSupportedApps_returnUnavailable() { - List packageNames = Collections.emptyList(); - when(mRepository.getSatelliteDataOptimizedApps()).thenReturn(packageNames); - mController = new SatelliteAppListCategoryController(mContext, KEY); - mController.init(mRepository); + mController = new SatelliteAppListCategoryController(mContext, KEY) { + @Override + protected boolean isSatelliteEligible() { + return true; + } - int result = mController.getAvailabilityStatus(); + @Override + protected List getSatelliteDataOptimizedApps() { + return List.of(); + } + }; + mController.init(TEST_SUB_ID, mPersistableBundle, true, true); + + int result = mController.getAvailabilityStatus(TEST_SUB_ID); assertThat(result).isEqualTo(CONDITIONALLY_UNAVAILABLE); } + + @Test + @EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS) + public void getAvailabilityStatus_dataUnavailable_returnUnavailable() { + mController = new SatelliteAppListCategoryController(mContext, KEY) { + @Override + protected boolean isSatelliteEligible() { + return true; + } + + @Override + protected List getSatelliteDataOptimizedApps() { + return PACKAGE_NAMES; + } + }; + mController.init(TEST_SUB_ID, mPersistableBundle, true, false); + + int result = mController.getAvailabilityStatus(TEST_SUB_ID); + + assertThat(result).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + @EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS) + public void getAvailabilityStatus_entitlementSupportedButAccountIneligible_returnUnavailable() { + mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true); + mController = new SatelliteAppListCategoryController(mContext, KEY) { + @Override + protected boolean isSatelliteEligible() { + return false; + } + + @Override + protected List getSatelliteDataOptimizedApps() { + return PACKAGE_NAMES; + } + }; + mController.init(TEST_SUB_ID, mPersistableBundle, true, true); + + int result = mController.getAvailabilityStatus(TEST_SUB_ID); + + assertThat(result).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + @EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS) + public void getAvailabilityStatus_entitlementSupportedAndAccountEligible_returnAvailable() { + mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true); + mController = new SatelliteAppListCategoryController(mContext, KEY) { + @Override + protected boolean isSatelliteEligible() { + return true; + } + + @Override + protected List getSatelliteDataOptimizedApps() { + return PACKAGE_NAMES; + } + }; + mController.init(TEST_SUB_ID, mPersistableBundle, true, true); + + int result = mController.getAvailabilityStatus(TEST_SUB_ID); + + assertThat(result).isEqualTo(AVAILABLE_UNSEARCHABLE); + } } From 1d2b0b9aa0131018b2158027da0c1dfbe3ce394f Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 19 Mar 2025 17:45:13 +0800 Subject: [PATCH 17/22] Don't disable switch when changing status BUG: 404423669 Test: local tested Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: Ie2d48b4f0bbb224e62d2746d260b1026022ebf01 --- .../bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index 0658b1da40d..06e18f111af 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -296,7 +296,6 @@ class DeviceDetailsFragmentFormatterImpl( prefKey, if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF, ) - isEnabled = false model.onCheckedChange.invoke(newState) } return false @@ -332,7 +331,6 @@ class DeviceDetailsFragmentFormatterImpl( prefKey, if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF, ) - isSwitchEnabled = false model.onCheckedChange.invoke(newState) return false } From 08a3aa7f336ffdfc803f7835d235d6167c43ffdc Mon Sep 17 00:00:00 2001 From: Zaiyue Xue Date: Wed, 19 Mar 2025 17:49:16 +0800 Subject: [PATCH 18/22] Support phonetype control for Settings Battery database retention time Bug: 404740486 Fix: 404740486 Test: atest Flag: EXEMPT bug fix Change-Id: Ifc9032e19a5b26e307d1426ea5c07de84e1613f4 --- .../settings/fuelgauge/PowerUsageFeatureProvider.java | 3 +++ .../fuelgauge/PowerUsageFeatureProviderImpl.java | 5 +++++ .../settings/fuelgauge/batteryusage/DatabaseUtils.java | 7 +++++-- .../fuelgauge/batteryusage/PeriodicJobReceiverTest.java | 9 +++++++-- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java index e441b85c2d3..004fc74ae8f 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java @@ -64,6 +64,9 @@ public interface PowerUsageFeatureProvider { /** Returns an allowlist of app names combined into the system-apps item */ List getSystemAppsAllowlist(); + /** Returns the data retention days in the database */ + int getDataRetentionDays(); + /** Check whether location setting is enabled */ boolean isLocationSettingEnabled(String[] packages); diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java index bdb236a5192..903ae64b489 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java @@ -118,6 +118,11 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider return new ArrayList<>(); } + @Override + public int getDataRetentionDays() { + return 9; + } + @Override public boolean isLocationSettingEnabled(String[] packages) { return false; diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index 6cfe115f5c3..1d26d65c8bd 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java @@ -45,6 +45,7 @@ import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; +import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.fuelgauge.BatteryStatus; import java.io.PrintWriter; @@ -67,7 +68,6 @@ public final class DatabaseUtils { private static final String SHARED_PREFS_FILE = "battery_usage_shared_prefs"; private static final long INVALID_TIMESTAMP = 0L; - static final int DATA_RETENTION_INTERVAL_DAY = 9; static final String KEY_LAST_LOAD_FULL_CHARGE_TIME = "last_load_full_charge_time"; static final String KEY_LAST_UPLOAD_FULL_CHARGE_TIME = "last_upload_full_charge_time"; static final String KEY_LAST_USAGE_SOURCE = "last_usage_source"; @@ -468,11 +468,14 @@ public final class DatabaseUtils { AsyncTask.execute( () -> { try { + final int dataRetentionDays = + FeatureFactory.getFeatureFactory() + .getPowerUsageFeatureProvider().getDataRetentionDays(); final BatteryStateDatabase database = BatteryStateDatabase.getInstance(context.getApplicationContext()); final long earliestTimestamp = Clock.systemUTC().millis() - - Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis(); + - Duration.ofDays(dataRetentionDays).toMillis(); database.appUsageEventDao().clearAllBefore(earliestTimestamp); database.batteryEventDao().clearAllBefore(earliestTimestamp); database.batteryStateDao().clearAllBefore(earliestTimestamp); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java index 82121660c19..338bab89a48 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java @@ -32,6 +32,7 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; +import com.android.settings.overlay.FeatureFactory; import com.android.settings.testutils.BatteryTestUtils; import org.junit.After; @@ -104,7 +105,9 @@ public final class PeriodicJobReceiverTest { @Test public void onReceive_containsExpiredData_clearsExpiredDataFromDatabase() throws InterruptedException { - insertExpiredData(/* shiftDay= */ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY); + int dataRetentionDays = FeatureFactory.getFeatureFactory() + .getPowerUsageFeatureProvider().getDataRetentionDays(); + insertExpiredData(/* shiftDay= */ dataRetentionDays); mReceiver.onReceive(mContext, JOB_UPDATE_INTENT); @@ -115,7 +118,9 @@ public final class PeriodicJobReceiverTest { @Test public void onReceive_withoutExpiredData_notClearsExpiredDataFromDatabase() throws InterruptedException { - insertExpiredData(/* shiftDay= */ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY - 1); + int dataRetentionDays = FeatureFactory.getFeatureFactory() + .getPowerUsageFeatureProvider().getDataRetentionDays(); + insertExpiredData(dataRetentionDays - 1); mReceiver.onReceive(mContext, JOB_UPDATE_INTENT); From ce73cba979618d55dbea1f26dad42953a7841940 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 19 Mar 2025 19:27:00 +0800 Subject: [PATCH 19/22] Scroll to top when opening more settings page Test: local tested Flag: EXEMPT minor fix Bug: 343317785 Change-Id: I5e90e4b1b3bcd5329c6574655ad6a3dee537438c --- .../ui/view/DeviceDetailsFragmentFormatter.kt | 18 ++++++++++++++++++ .../view/DeviceDetailsFragmentFormatterTest.kt | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index 06e18f111af..d25dca4415a 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -26,9 +26,13 @@ import android.os.Bundle import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toDrawable import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -227,6 +231,7 @@ class DeviceDetailsFragmentFormatterImpl( dashboardFragment.lifecycleScope.launch { if (isLoading) { + scrollToTop() dashboardFragment.setLoading(false, false) isLoading = false } @@ -493,6 +498,19 @@ class DeviceDetailsFragmentFormatterImpl( } } + private fun scrollToTop() { + // Temporary fix to make sure the screen is scroll to the top when rendering. + ComposePreference(context).apply { + order = -1 + isEnabled = false + isSelectable = false + setContent { Spacer(modifier = Modifier.height(1.dp)) } + }.also { + dashboardFragment.preferenceScreen.addPreference(it) + dashboardFragment.scrollToPreference(it) + } + } + private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}" private class SpotlightPreference(context: Context) : Preference(context) { diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt index 1eb15e50f62..d31e4317624 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt @@ -404,7 +404,7 @@ class DeviceDetailsFragmentFormatterTest { for (i in 0.. Date: Wed, 19 Mar 2025 07:18:08 -0700 Subject: [PATCH 20/22] Fix default window type for Advanced Protection dialog The previous window type FIRST_APPLICATION_WINDOW was incorrect and could have caused the dialog to be dimmed and TalkBack to not be focused. The current window type TYPE_APPLICATION fixes these issues by correctly placing the dialog in the window hierarchy. Bug: 404595933 Bug: 404439964 Bug: 353531691 Test: visual Test: checked with TalkBack Flag: EXEMPT bug fix Change-Id: I520220c1527868b6fc928eaa35184ba9b0159e06 --- .../security/ActionDisabledByAdvancedProtectionDialog.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt b/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt index cf29c3d707e..0c96473b6d4 100644 --- a/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt +++ b/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt @@ -118,7 +118,7 @@ class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { } override fun getDialogWindowType(): Int? = if (intent.hasExtra(DIALOG_WINDOW_TYPE)) { - intent.getIntExtra(DIALOG_WINDOW_TYPE, WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW) + intent.getIntExtra(DIALOG_WINDOW_TYPE, WindowManager.LayoutParams.TYPE_APPLICATION) } else null private fun getIntentFeatureId(): Int { From 96c6ed59a63c01513dce7c33ef4341b68971b639 Mon Sep 17 00:00:00 2001 From: Azhara Assanova Date: Tue, 11 Mar 2025 16:35:22 +0000 Subject: [PATCH 21/22] Change the resource ID of advanced protection help URI Change-Id I3c6d24e3e9a4358ab1adb342dbee8fc56ac16794 added advanced protection help URI to frameworks/base/core/res/res/values/config.xml, so this change removes the duplicate resource from Settings and updates ActionDisabledByAdvancedProtectionDialog to use the framework resource. Bug: 401233918 Test: manual Test: atest ActionDisabledByAdvancedProtectionDialog Flag: EXEMPT bug fix Change-Id: Ic84909f4c16d3449bd9981be6659987b1963a26a --- res/values/strings.xml | 3 - ...ctionDisabledByAdvancedProtectionDialog.kt | 14 ++- tests/spa_unit/AndroidManifest.xml | 8 ++ ...nDisabledByAdvancedProtectionDialogTest.kt | 113 +++++++++++++++++- 4 files changed, 129 insertions(+), 9 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index db9c302edca..9a506b64c4d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12221,9 +12221,6 @@ Data usage charges may apply. - - - Update Do Not Disturb diff --git a/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt b/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt index cf29c3d707e..255bc985fcb 100644 --- a/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt +++ b/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt @@ -14,21 +14,23 @@ * limitations under the License. */ -package com.android.settings.security; +package com.android.settings.security import android.content.Intent +import android.content.pm.PackageManager +import android.security.advancedprotection.AdvancedProtectionManager import android.security.advancedprotection.AdvancedProtectionManager.EXTRA_SUPPORT_DIALOG_FEATURE import android.security.advancedprotection.AdvancedProtectionManager.EXTRA_SUPPORT_DIALOG_TYPE import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP -import android.content.pm.PackageManager import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_ENABLE_MTE import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_DISABLED_SETTING import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_UNKNOWN import android.util.Log import android.view.WindowManager +import androidx.annotation.VisibleForTesting import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -38,7 +40,6 @@ import com.android.settingslib.spa.SpaDialogWindowTypeActivity import com.android.settingslib.spa.widget.dialog.AlertDialogButton import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogContent import com.android.settingslib.wifi.WifiUtils.Companion.DIALOG_WINDOW_TYPE -import android.security.advancedprotection.AdvancedProtectionManager class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { @@ -85,9 +86,12 @@ class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { return getString(messageId) } - private fun getSupportButtonIfExists(): AlertDialogButton? { + @VisibleForTesting + fun getSupportButtonIfExists(): AlertDialogButton? { try { - val helpIntentUri = getString(R.string.help_url_action_disabled_by_advanced_protection) + val helpIntentUri = getString( + com.android.internal.R.string.config_help_url_action_disabled_by_advanced_protection + ) val helpIntent = Intent.parseUri(helpIntentUri, Intent.URI_INTENT_SCHEME) if (helpIntent == null) return null val helpActivityInfo = packageManager.resolveActivity(helpIntent, /* flags */ 0) diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml index 1950f209868..d225a03061a 100644 --- a/tests/spa_unit/AndroidManifest.xml +++ b/tests/spa_unit/AndroidManifest.xml @@ -30,6 +30,14 @@ + + + + + + + () + + private val context: Context = ApplicationProvider.getApplicationContext() @Test fun blockedInteractionDialog_showsCorrectTitleAndMessage() { @@ -159,6 +178,85 @@ class ActionDisabledByAdvancedProtectionDialogTest { } } + @Test + fun helpIntentDoesNotExist_getSupportButtonIfExists_returnsNull() { + launchDialogActivity(defaultIntent) { scenario -> + scenario.onActivity { activity -> + val spyActivity = spyOnActivityHelpIntentUri(activity, /* uriToReturn */ null) + + val button = spyActivity.getSupportButtonIfExists() + assertNull(button) + } + } + } + + @Test + fun helpIntentExistsAndDoesNotResolveToActivity_getSupportButtonIfExists_returnsNull() { + launchDialogActivity(defaultIntent) { scenario -> + scenario.onActivity { activity -> + val spyActivity = spyOnActivityHelpIntentUri(activity, helpIntentUri) + mockResolveActivity(spyActivity, /* resolveInfoToReturn */ null) + + val button = spyActivity.getSupportButtonIfExists() + assertNull(button) + } + } + } + + @Test + fun helpIntentExistsAndResolvesToActivity_getSupportButtonIfExists_returnsButton() { + launchDialogActivity(defaultIntent) { scenario -> + scenario.onActivity { activity -> + val spyActivity = spyOnActivityHelpIntentUri(activity, helpIntentUri) + val resolveInfoToReturn = ResolveInfo().apply { + activityInfo = ActivityInfo().apply { + packageName = HELP_INTENT_PKG_NAME + } + } + mockResolveActivity(spyActivity, resolveInfoToReturn) + + // 1. Check the button is returned. + val button = spyActivity.getSupportButtonIfExists() + assertNotNull(button) + + // 2. Check the button has correct text. + assertEquals(context.getString( + R.string.disabled_by_advanced_protection_help_button_title), button!!.text + ) + + // 3. Check the button's onClick launches the help activity and finishes the dialog. + button.onClick() + + val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) + verify(spyActivity).startActivity(intentCaptor.capture()) + val launchedIntent = intentCaptor.value + assertEquals(HELP_INTENT_ACTION, launchedIntent.action) + assertEquals(HELP_INTENT_PKG_NAME, launchedIntent.`package`) + + assertTrue(spyActivity.isFinishing) + } + } + } + + private fun spyOnActivityHelpIntentUri( + activity: ActionDisabledByAdvancedProtectionDialog, + uriToReturn: String? + ): ActionDisabledByAdvancedProtectionDialog { + val spyActivity = spy(activity) + val spyResources = spy(spyActivity.resources) + doReturn(spyResources).whenever(spyActivity).resources + doReturn(uriToReturn).whenever(spyResources).getString(helpUriResourceId) + return spyActivity + } + + private fun mockResolveActivity( + spyActivity: ActionDisabledByAdvancedProtectionDialog, + resolveInfoToReturn: ResolveInfo? + ) { + doReturn(mockPackageManager).whenever(spyActivity).packageManager + doReturn(resolveInfoToReturn).whenever(mockPackageManager).resolveActivity(any(), anyInt()) + } + private fun launchDialogActivity( intent: Intent, onScenario: (ActivityScenario) -> Unit @@ -172,10 +270,23 @@ class ActionDisabledByAdvancedProtectionDialogTest { launch(intent).use(onScenario) } + class HelpTestActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + finish() + } + } + private companion object { val defaultIntent = AdvancedProtectionManager.createSupportIntent( FEATURE_ID_DISALLOW_CELLULAR_2G, SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION ) + const val HELP_INTENT_PKG_NAME = "com.android.settings.tests.spa_unit" + const val HELP_INTENT_ACTION = "$HELP_INTENT_PKG_NAME.HELP_ACTION" + val helpIntent = Intent(HELP_INTENT_ACTION).setPackage(HELP_INTENT_PKG_NAME) + val helpIntentUri = helpIntent.toUri(Intent.URI_INTENT_SCHEME) + val helpUriResourceId = + com.android.internal.R.string.config_help_url_action_disabled_by_advanced_protection } } From f5aa74b0e83fb3325a88b1f3dd407ffe6176c2a2 Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Fri, 14 Mar 2025 14:14:24 -0400 Subject: [PATCH 22/22] Fixes 'capture more' on Settings home page This change excludes the contents of 'main_content_scrollable_container' from scroll capture search. Explanation: The inner-most child is selected whenever nested scrolling views are found. This is incorrect for Settings because the inner RecyclerView isn't actually scrollable (that is, the contents of it are not scrolled within it, the RecyclerView itself is scrolled by the outer ScrollView instead). View.canScrollVertically is unfortunately incorrect for RecyclerView in this case as well. This change excludes all children of the main ScrollView from the search. This ensures the outer ScrollView is chosen and this corrects the 'capture more' screenshot functionality on the Settings home page. Bug: 399810823 Test: manual; Open settings, screenshot, capture more Flag: com.android.settings.flags.extended_screenshots_exclude_nested_scrollables Change-Id: I3363d70b3649e2a34e541c45ab387d59547b8588 --- aconfig/settings_flag_declarations.aconfig | 10 ++++++++++ .../settings/homepage/SettingsHomepageActivity.java | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/aconfig/settings_flag_declarations.aconfig b/aconfig/settings_flag_declarations.aconfig index 2971d22b5de..548a733db7e 100644 --- a/aconfig/settings_flag_declarations.aconfig +++ b/aconfig/settings_flag_declarations.aconfig @@ -80,3 +80,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "extended_screenshots_exclude_nested_scrollables" + namespace: "systemui" + description: "Sets a flag on the main scrollable container to exclude any nested scrollable views as potential targets for extended screenshots." + bug: "399810823" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 08acbc74c7e..f89e8fd6c45 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -775,6 +775,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements // Prevent inner RecyclerView gets focus and invokes scrolling. view.setFocusableInTouchMode(true); view.requestFocus(); + + if (Flags.extendedScreenshotsExcludeNestedScrollables()) { + // Force scroll capture to select the NestedScrollView, instead of the non-scrollable + // RecyclerView which is contained inside it with no height constraint. + final View scrollableContainer = findViewById(R.id.main_content_scrollable_container); + if (scrollableContainer != null) { + scrollableContainer.setScrollCaptureHint( + View.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS); + } + } } private void updateHomepageAppBar() {