From 67466de364b40fe5bad0a8d27ddbb8c7692fe1d2 Mon Sep 17 00:00:00 2001 From: Yurii Zubrytskyi Date: Thu, 25 Apr 2024 17:06:28 -0700 Subject: [PATCH 01/22] Don't overwrite the system global gender settings Developer setting shouldn't rewrite the proper user-selected value. This is best effort and will still overwrite it when the values used to be the same. Bug: 332955145 Test: manual Change-Id: I7c350ff84dfc7e216835ab3b45b7c1316ea78155 --- .../GrammaticalGenderPreferenceController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/development/GrammaticalGenderPreferenceController.java b/src/com/android/settings/development/GrammaticalGenderPreferenceController.java index 054097487e0..347894d7ee6 100644 --- a/src/com/android/settings/development/GrammaticalGenderPreferenceController.java +++ b/src/com/android/settings/development/GrammaticalGenderPreferenceController.java @@ -69,12 +69,19 @@ public class GrammaticalGenderPreferenceController extends DeveloperOptionsPrefe @Override public boolean onPreferenceChange(Preference preference, Object newValue) { + final var oldValue = SystemProperties.getInt(GRAMMATICAL_GENDER_PROPERTY, + Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED); SystemProperties.set(GRAMMATICAL_GENDER_PROPERTY, newValue.toString()); updateState(mPreference); try { Configuration config = mActivityManager.getConfiguration(); - config.setGrammaticalGender(Integer.parseInt(newValue.toString())); - mActivityManager.updatePersistentConfiguration(config); + // Only apply the developer settings value if it is the one currently used, + // otherwise it means there's some kind of override that we don't want to + // touch here. + if (config.getGrammaticalGender() == oldValue) { + config.setGrammaticalGender(Integer.parseInt(newValue.toString())); + mActivityManager.updatePersistentConfiguration(config); + } } catch (RemoteException ex) { // intentional no-op } From f6895743cf41f5c6d70bb72b5a14e7675ec7edd5 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Mon, 6 May 2024 22:44:42 +0000 Subject: [PATCH 02/22] Deprecate Settings panels and its infrastructure Bug: 328525899 Test: robotest Change-Id: I30bc423e31e9202e70c509459f4397c296c8423f --- src/com/android/settings/panel/PanelContent.java | 3 +++ src/com/android/settings/panel/PanelContentCallback.java | 3 +++ src/com/android/settings/panel/PanelFeatureProvider.java | 1 + .../android/settings/panel/PanelFeatureProviderImpl.java | 1 + src/com/android/settings/panel/PanelFragment.java | 2 ++ src/com/android/settings/panel/PanelLoggingContract.java | 3 +++ src/com/android/settings/panel/PanelSlicesAdapter.java | 6 ++++++ .../settings/panel/PanelSlicesLoaderCountdownLatch.java | 3 +++ src/com/android/settings/panel/SettingsPanelActivity.java | 3 +++ .../src/com/android/settings/panel/FakePanelContent.java | 3 +++ .../android/settings/panel/FakeSettingsPanelActivity.java | 1 + .../src/com/android/settings/panel/PanelFragmentTest.java | 1 + .../com/android/settings/panel/PanelSlicesAdapterTest.java | 1 + .../android/settings/panel/SettingsPanelActivityTest.java | 1 + .../settings/panel/PanelSlicesLoaderCountdownLatchTest.java | 1 + 15 files changed, 33 insertions(+) diff --git a/src/com/android/settings/panel/PanelContent.java b/src/com/android/settings/panel/PanelContent.java index 6b582288457..1bbe2dba83a 100644 --- a/src/com/android/settings/panel/PanelContent.java +++ b/src/com/android/settings/panel/PanelContent.java @@ -28,7 +28,10 @@ import java.util.List; /** * Represents the data class needed to create a Settings Panel. See {@link PanelFragment}. + * + * @deprecated this is no longer used after V and will be removed. */ +@Deprecated(forRemoval = true) public interface PanelContent extends Instrumentable { int VIEW_TYPE_SLIDER = 1; diff --git a/src/com/android/settings/panel/PanelContentCallback.java b/src/com/android/settings/panel/PanelContentCallback.java index e59d69913db..cceecd1e4ed 100644 --- a/src/com/android/settings/panel/PanelContentCallback.java +++ b/src/com/android/settings/panel/PanelContentCallback.java @@ -18,7 +18,10 @@ package com.android.settings.panel; /** * PanelContentCallback provides a callback interface for {@link PanelFragment} to receive * events from {@link PanelContent}. + * + * @deprecated this is no longer used after V and will be removed. */ +@Deprecated(forRemoval = true) public interface PanelContentCallback { /** diff --git a/src/com/android/settings/panel/PanelFeatureProvider.java b/src/com/android/settings/panel/PanelFeatureProvider.java index 402a562d53d..943c37def46 100644 --- a/src/com/android/settings/panel/PanelFeatureProvider.java +++ b/src/com/android/settings/panel/PanelFeatureProvider.java @@ -19,6 +19,7 @@ package com.android.settings.panel; import android.content.Context; import android.os.Bundle; +@Deprecated(forRemoval = true) public interface PanelFeatureProvider { /** diff --git a/src/com/android/settings/panel/PanelFeatureProviderImpl.java b/src/com/android/settings/panel/PanelFeatureProviderImpl.java index 71711f9228e..cafc9573b29 100644 --- a/src/com/android/settings/panel/PanelFeatureProviderImpl.java +++ b/src/com/android/settings/panel/PanelFeatureProviderImpl.java @@ -24,6 +24,7 @@ import android.util.FeatureFlagUtils; import com.android.settings.Utils; +@Deprecated(forRemoval = true) public class PanelFeatureProviderImpl implements PanelFeatureProvider { @Override diff --git a/src/com/android/settings/panel/PanelFragment.java b/src/com/android/settings/panel/PanelFragment.java index 159028369aa..b3a28844be5 100644 --- a/src/com/android/settings/panel/PanelFragment.java +++ b/src/com/android/settings/panel/PanelFragment.java @@ -66,6 +66,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +@Deprecated(forRemoval = true) public class PanelFragment extends Fragment { private static final String TAG = "PanelFragment"; @@ -519,6 +520,7 @@ public class PanelFragment extends Fragment { return mPanel.getViewType(); } + @Deprecated(forRemoval = true) class LocalPanelCallback implements PanelContentCallback { @Override diff --git a/src/com/android/settings/panel/PanelLoggingContract.java b/src/com/android/settings/panel/PanelLoggingContract.java index e6e3012abef..fd145f865fb 100644 --- a/src/com/android/settings/panel/PanelLoggingContract.java +++ b/src/com/android/settings/panel/PanelLoggingContract.java @@ -21,7 +21,10 @@ package com.android.settings.panel; *

* Constants should only be removed if underlying panel, or use case is removed. *

+ * + * @deprecated this is no longer used after V and will be removed. */ +@Deprecated(forRemoval = true) public class PanelLoggingContract { /** diff --git a/src/com/android/settings/panel/PanelSlicesAdapter.java b/src/com/android/settings/panel/PanelSlicesAdapter.java index a2360d8367b..2223cbb61ab 100644 --- a/src/com/android/settings/panel/PanelSlicesAdapter.java +++ b/src/com/android/settings/panel/PanelSlicesAdapter.java @@ -48,7 +48,10 @@ import java.util.Map; /** * RecyclerView adapter for Slices in Settings Panels. + * + * @deprecated this is no longer used after V and will be removed. */ +@Deprecated(forRemoval = true) public class PanelSlicesAdapter extends RecyclerView.Adapter { @@ -112,7 +115,10 @@ public class PanelSlicesAdapter /** * ViewHolder for binding Slices to SliceViews. + * + * @deprecated this is no longer used after V and will be removed. */ + @Deprecated(forRemoval = true) public class SliceRowViewHolder extends RecyclerView.ViewHolder implements DividerItemDecoration.DividedViewHolder { diff --git a/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatch.java b/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatch.java index 6137d6c564e..49fd8619941 100644 --- a/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatch.java +++ b/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatch.java @@ -36,7 +36,10 @@ import java.util.concurrent.CountDownLatch; * {@link Uri}. Then check if all of the Slices have loaded with * {@link #isPanelReadyToLoad()}, which will return {@code true} the first time after all * Slices have loaded. + * + * @deprecated this is no longer used after V and will be removed. */ +@Deprecated(forRemoval = true) public class PanelSlicesLoaderCountdownLatch { private final Set mLoadedSlices; private final CountDownLatch mCountDownLatch; diff --git a/src/com/android/settings/panel/SettingsPanelActivity.java b/src/com/android/settings/panel/SettingsPanelActivity.java index 60b8f887936..d539c433c58 100644 --- a/src/com/android/settings/panel/SettingsPanelActivity.java +++ b/src/com/android/settings/panel/SettingsPanelActivity.java @@ -42,7 +42,10 @@ import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; /** * Dialog Activity to host Settings Slices. + * + * @deprecated this is no longer used after V and will be removed. */ +@Deprecated(forRemoval = true) public class SettingsPanelActivity extends FragmentActivity { private static final String TAG = "SettingsPanelActivity"; diff --git a/tests/robotests/src/com/android/settings/panel/FakePanelContent.java b/tests/robotests/src/com/android/settings/panel/FakePanelContent.java index 06beb3ecf51..17787cd01ae 100644 --- a/tests/robotests/src/com/android/settings/panel/FakePanelContent.java +++ b/tests/robotests/src/com/android/settings/panel/FakePanelContent.java @@ -29,7 +29,10 @@ import java.util.List; /** * Fake PanelContent for testing. + * + * @deprecated this is no longer used after V and will be removed. */ +@Deprecated(forRemoval = true) public class FakePanelContent implements PanelContent { public static final String FAKE_ACTION = "fake_action"; diff --git a/tests/robotests/src/com/android/settings/panel/FakeSettingsPanelActivity.java b/tests/robotests/src/com/android/settings/panel/FakeSettingsPanelActivity.java index ba763ce3a06..fe19f287794 100644 --- a/tests/robotests/src/com/android/settings/panel/FakeSettingsPanelActivity.java +++ b/tests/robotests/src/com/android/settings/panel/FakeSettingsPanelActivity.java @@ -19,6 +19,7 @@ package com.android.settings.panel; import android.content.ComponentName; import android.content.Intent; +@Deprecated(forRemoval = true) public class FakeSettingsPanelActivity extends SettingsPanelActivity { @Override public ComponentName getCallingActivity() { diff --git a/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java b/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java index 42f3977a227..e77eeab569b 100644 --- a/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java +++ b/tests/robotests/src/com/android/settings/panel/PanelFragmentTest.java @@ -54,6 +54,7 @@ import org.robolectric.annotation.Config; import java.util.Objects; +@Deprecated(forRemoval = true) @Ignore("b/313576125") @RunWith(RobolectricTestRunner.class) @Config(shadows = { diff --git a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java index 87a798a2197..e778cb8cec8 100644 --- a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java +++ b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java @@ -67,6 +67,7 @@ import org.robolectric.annotation.Implements; import java.util.LinkedHashMap; import java.util.Map; +@Deprecated(forRemoval = true) @RunWith(RobolectricTestRunner.class) @Config(shadows = PanelSlicesAdapterTest.ShadowLayoutInflater.class) public class PanelSlicesAdapterTest { diff --git a/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java b/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java index e550284b028..4f03abb664b 100644 --- a/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java +++ b/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java @@ -59,6 +59,7 @@ import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; +@Deprecated(forRemoval = true) @RunWith(RobolectricTestRunner.class) @Config(shadows = { com.android.settings.testutils.shadow.ShadowFragment.class, diff --git a/tests/unit/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatchTest.java b/tests/unit/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatchTest.java index 3794e00ea10..e201f42e4b3 100644 --- a/tests/unit/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatchTest.java +++ b/tests/unit/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatchTest.java @@ -29,6 +29,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +@Deprecated(forRemoval = true) @RunWith(AndroidJUnit4.class) public class PanelSlicesLoaderCountdownLatchTest { From 0887ee4540e73553e20ae6419a511fb16960c4e2 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Wed, 8 May 2024 04:55:37 +0000 Subject: [PATCH 03/22] Use permission com.android.settings.BATTERY_DATA In SettingsSpaUnitTests Change-Id: Ic1bec601e773389813bce4e7663ce08c41fa3deb Fix: 338300477 Test: presubmit Merged-In: I5a1753835d2d47712ea249081c9a77c455eb3291 --- tests/spa_unit/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml index 5a7f5659ce6..e3bc5ad4a98 100644 --- a/tests/spa_unit/AndroidManifest.xml +++ b/tests/spa_unit/AndroidManifest.xml @@ -22,6 +22,7 @@ + Date: Tue, 30 Apr 2024 17:56:58 +0000 Subject: [PATCH 04/22] ARC++ PH: Rephrase the tooltip text Rephrase of the UI tool tip text. Making it more clear for the user. UI: https://screenshot.googleplex.com/5gkWMVmZn8zKDj4 Upstreaming-Plan: Will be merged to main once approved. Bug: 265471993 Test: Manual (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:d8c1b2bdef1b38e1cec6335d3a63042acd479442) Merged-In: I925aef7af63907e87a5295914cbb819c343fa168 Change-Id: I925aef7af63907e87a5295914cbb819c343fa168 --- res/values/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index e14a1073451..f999a3d618b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -672,8 +672,10 @@ Learn more about Location settings - - To change location access, go to Settings > Security and Privacy > Privacy controls + + + To change go to ChromeOS Settings > Privacy and security > Privacy controls > Location access + Accounts From 33aba0a6941b17b4a2ee23f9ab95074aeff68100 Mon Sep 17 00:00:00 2001 From: sunnyshao Date: Wed, 13 Mar 2024 14:45:00 +0800 Subject: [PATCH 05/22] Fix the flicker in the Lock screen page - Due to without summary text in xml, the UI render will skip it and it flicker while program adding the summary later. - Add the summary_placeholder in the shortcuts item. Fixes: 325989849 Test: manual test Change-Id: Icb7714c19ae73d15bccc9b6976a10cf343a16f53 Merged-In: Icb7714c19ae73d15bccc9b6976a10cf343a16f53 --- res/xml/security_lockscreen_settings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/xml/security_lockscreen_settings.xml b/res/xml/security_lockscreen_settings.xml index cb1ce4401d2..15d530357d9 100644 --- a/res/xml/security_lockscreen_settings.xml +++ b/res/xml/security_lockscreen_settings.xml @@ -69,9 +69,11 @@ android:summary="@string/lockscreen_trivial_controls_summary" settings:controller="com.android.settings.display.ControlsTrivialPrivacyPreferenceController"/> + Date: Sun, 5 May 2024 21:03:37 +0800 Subject: [PATCH 06/22] [Thread] update Thread settings screen Per b/327098435 the new Thread settings design proposed (go/android-thread) is approved. As a summary, this commit adds a new "connected devices > connection preference -> Thread" list item and decidated config page for Thread. Also, we simplified the airplane mode to delegate it to the mainline code to handle it Bug: 327098435 Test: atest SettingsUnitTests Change-Id: Iffbb2471f5a28ec57d30a378f22642fe6ac0b9cc --- res/values/strings.xml | 14 +- res/xml/connected_devices_advanced.xml | 20 +- res/xml/thread_network_settings.xml | 33 +++ .../BaseThreadNetworkController.kt | 46 ++++ .../ThreadNetworkFooterController.kt | 66 +++++ .../threadnetwork/ThreadNetworkFragment.kt | 39 +++ .../ThreadNetworkFragmentController.kt | 108 ++++++++ .../ThreadNetworkPreferenceController.kt | 236 ---------------- .../ThreadNetworkToggleController.kt | 146 ++++++++++ .../threadnetwork/ThreadNetworkUtils.kt | 59 ++++ .../ThreadNetworkPreferenceControllerTest.kt | 255 ------------------ .../FakeThreadNetworkController.kt | 74 +++++ .../threadnetwork/OWNERS | 0 .../ThreadNetworkFragmentControllerTest.kt | 112 ++++++++ .../ThreadNetworkToggleControllerTest.kt | 128 +++++++++ 15 files changed, 832 insertions(+), 504 deletions(-) create mode 100644 res/xml/thread_network_settings.xml create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt delete mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt delete mode 100644 tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt create mode 100644 tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt rename tests/unit/src/com/android/settings/{conecteddevice => connecteddevice}/threadnetwork/OWNERS (100%) create mode 100644 tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt create mode 100644 tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt diff --git a/res/values/strings.xml b/res/values/strings.xml index e14a1073451..4030b02bd97 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12579,11 +12579,17 @@ Thread - - Connect to compatible devices using Thread for a seamless smart home experience + + Use Thread - - Turn off airplane mode to use Thread + + Thread helps connect your smart home devices, boosting efficiency, and performance.\n\nWhen enabled, this device is eligible to join a Thread network, allowing control of Matter supported devices through this phone. + + + Learn more about Thread + + + https://developers.home.google.com Camera access diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index 87db61921c3..68b4c04572e 100644 --- a/res/xml/connected_devices_advanced.xml +++ b/res/xml/connected_devices_advanced.xml @@ -46,6 +46,17 @@ settings:controller="com.android.settings.wfd.WifiDisplayPreferenceController" settings:keywords="@string/keywords_wifi_display_settings" /> + + - - diff --git a/res/xml/thread_network_settings.xml b/res/xml/thread_network_settings.xml new file mode 100644 index 00000000000..549d6507e57 --- /dev/null +++ b/res/xml/thread_network_settings.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt new file mode 100644 index 00000000000..583706a63ac --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.os.OutcomeReceiver +import androidx.annotation.VisibleForTesting +import java.util.concurrent.Executor + +/** + * A testable interface for [ThreadNetworkController] which is `final`. + * + * We are in a awkward situation that Android API guideline suggest `final` for API classes + * while Robolectric test is being deprecated for platform testing (See + * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's + * conflicting with the default "mockito-target" which is somehow indirectly depended by the + * `SettingsUnitTests` target. + */ +@VisibleForTesting +interface BaseThreadNetworkController { + fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver + ) + + fun registerStateCallback(executor: Executor, callback: StateCallback) + + fun unregisterStateCallback(callback: StateCallback) +} \ No newline at end of file diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt new file mode 100644 index 00000000000..1e3b62484de --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.util.Log +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settingslib.HelpUtils +import com.android.settingslib.widget.FooterPreference + +/** + * The footer preference controller for Thread settings in + * "Connected devices > Connection preferences > Thread". + */ +class ThreadNetworkFooterController( + context: Context, + preferenceKey: String +) : BasePreferenceController(context, preferenceKey) { + override fun getAvailabilityStatus(): Int { + // The thread_network_settings screen won't be displayed and it doesn't matter if this + // controller always return AVAILABLE + return AVAILABLE + } + + override fun displayPreference(screen: PreferenceScreen) { + val footer: FooterPreference? = screen.findPreference(KEY_PREFERENCE_FOOTER) + if (footer != null) { + footer.setLearnMoreAction { _ -> openLocaleLearnMoreLink() } + footer.setLearnMoreText(mContext.getString(R.string.thread_network_settings_learn_more)) + } + } + + private fun openLocaleLearnMoreLink() { + val intent = HelpUtils.getHelpIntent( + mContext, + mContext.getString(R.string.thread_network_settings_learn_more_link), + mContext::class.java.name + ) + if (intent != null) { + mContext.startActivity(intent) + } else { + Log.w(TAG, "HelpIntent is null") + } + } + + companion object { + private const val TAG = "ThreadNetworkSettings" + private const val KEY_PREFERENCE_FOOTER = "thread_network_settings_footer" + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt new file mode 100644 index 00000000000..fd385d7ee1d --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.app.settings.SettingsEnums +import com.android.settings.R +import com.android.settings.dashboard.DashboardFragment +import com.android.settings.search.BaseSearchIndexProvider +import com.android.settingslib.search.SearchIndexable + +/** The fragment for Thread settings in "Connected devices > Connection preferences > Thread". */ +@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv()) +class ThreadNetworkFragment : DashboardFragment() { + override fun getPreferenceScreenResId() = R.xml.thread_network_settings + + override fun getLogTag() = "ThreadNetworkFragment" + + override fun getMetricsCategory() = SettingsEnums.CONNECTED_DEVICE_PREFERENCES_THREAD + + companion object { + /** For Search. */ + @JvmField + val SEARCH_INDEX_DATA_PROVIDER = BaseSearchIndexProvider(R.xml.thread_network_settings) + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt new file mode 100644 index 00000000000..beb824a36a0 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settings.flags.Flags +import java.util.concurrent.Executor + +/** + * The fragment controller for Thread settings in + * "Connected devices > Connection preferences > Thread". + */ +class ThreadNetworkFragmentController @VisibleForTesting constructor( + context: Context, + preferenceKey: String, + private val executor: Executor, + private val threadController: BaseThreadNetworkController? +) : BasePreferenceController(context, preferenceKey), LifecycleEventObserver { + private val stateCallback: StateCallback + private var threadEnabled = false + private var preference: Preference? = null + + constructor(context: Context, preferenceKey: String) : this( + context, + preferenceKey, + ContextCompat.getMainExecutor(context), + ThreadNetworkUtils.getThreadNetworkController(context) + ) + + init { + stateCallback = newStateCallback() + } + + override fun getAvailabilityStatus(): Int { + return if (!Flags.threadSettingsEnabled()) { + CONDITIONALLY_UNAVAILABLE + } else if (threadController == null) { + UNSUPPORTED_ON_DEVICE + } else { + AVAILABLE + } + } + + override fun getSummary(): CharSequence { + return if (threadEnabled) { + mContext.getText(R.string.switch_on_text) + } else { + mContext.getText(R.string.switch_off_text) + } + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (threadController == null) { + return + } + + when (event) { + Lifecycle.Event.ON_START -> + threadController.registerStateCallback(executor, stateCallback) + + Lifecycle.Event.ON_STOP -> + threadController.unregisterStateCallback(stateCallback) + + else -> {} + } + } + + private fun newStateCallback(): StateCallback { + return object : StateCallback { + override fun onThreadEnableStateChanged(enabledState: Int) { + threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED + preference?.let { preference -> refreshSummary(preference) } + } + + override fun onDeviceRoleChanged(role: Int) {} + } + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt deleted file mode 100644 index 1c0175036d5..00000000000 --- a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settings.connecteddevice.threadnetwork - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.pm.PackageManager -import android.net.thread.ThreadNetworkController -import android.net.thread.ThreadNetworkController.StateCallback -import android.net.thread.ThreadNetworkException -import android.net.thread.ThreadNetworkManager -import android.os.OutcomeReceiver -import android.provider.Settings -import android.util.Log -import androidx.annotation.VisibleForTesting -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner -import androidx.preference.Preference -import androidx.preference.PreferenceScreen -import com.android.settings.R -import com.android.settings.core.TogglePreferenceController -import com.android.settings.flags.Flags -import java.util.concurrent.Executor - -/** Controller for the "Thread" toggle in "Connected devices > Connection preferences". */ -class ThreadNetworkPreferenceController @VisibleForTesting constructor( - context: Context, - key: String, - private val executor: Executor, - private val threadController: BaseThreadNetworkController? -) : TogglePreferenceController(context, key), LifecycleEventObserver { - private val stateCallback: StateCallback - private val airplaneModeReceiver: BroadcastReceiver - private var threadEnabled = false - private var airplaneModeOn = false - private var preference: Preference? = null - - /** - * A testable interface for [ThreadNetworkController] which is `final`. - * - * We are in a awkward situation that Android API guideline suggest `final` for API classes - * while Robolectric test is being deprecated for platform testing (See - * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's - * conflicting with the default "mockito-target" which is somehow indirectly depended by the - * `SettingsUnitTests` target. - */ - @VisibleForTesting - interface BaseThreadNetworkController { - fun setEnabled( - enabled: Boolean, - executor: Executor, - receiver: OutcomeReceiver - ) - - fun registerStateCallback(executor: Executor, callback: StateCallback) - - fun unregisterStateCallback(callback: StateCallback) - } - - constructor(context: Context, key: String) : this( - context, - key, - ContextCompat.getMainExecutor(context), - getThreadNetworkController(context) - ) - - init { - stateCallback = newStateCallback() - airplaneModeReceiver = newAirPlaneModeReceiver() - } - - val isThreadSupportedOnDevice: Boolean - get() = threadController != null - - private fun newStateCallback(): StateCallback { - return object : StateCallback { - override fun onThreadEnableStateChanged(enabledState: Int) { - threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED - } - - override fun onDeviceRoleChanged(role: Int) {} - } - } - - private fun newAirPlaneModeReceiver(): BroadcastReceiver { - return object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - airplaneModeOn = isAirplaneModeOn(context) - Log.i(TAG, "Airplane mode is " + if (airplaneModeOn) "ON" else "OFF") - preference?.let { preference -> updateState(preference) } - } - } - } - - override fun getAvailabilityStatus(): Int { - return if (!Flags.threadSettingsEnabled()) { - CONDITIONALLY_UNAVAILABLE - } else if (!isThreadSupportedOnDevice) { - UNSUPPORTED_ON_DEVICE - } else if (airplaneModeOn) { - DISABLED_DEPENDENT_SETTING - } else { - AVAILABLE - } - } - - override fun displayPreference(screen: PreferenceScreen) { - super.displayPreference(screen) - preference = screen.findPreference(preferenceKey) - } - - override fun isChecked(): Boolean { - // TODO (b/322742298): - // Check airplane mode here because it's planned to disable Thread state in airplane mode - // (code in the mainline module). But it's currently not implemented yet (b/322742298). - // By design, the toggle should be unchecked in airplane mode, so explicitly check the - // airplane mode here to acchieve the same UX. - return !airplaneModeOn && threadEnabled - } - - override fun setChecked(isChecked: Boolean): Boolean { - if (threadController == null) { - return false - } - val action = if (isChecked) "enable" else "disable" - threadController.setEnabled( - isChecked, - executor, - object : OutcomeReceiver { - override fun onError(e: ThreadNetworkException) { - // TODO(b/327549838): gracefully handle the failure by resetting the UI state - Log.e(TAG, "Failed to $action Thread", e) - } - - override fun onResult(unused: Void?) { - Log.d(TAG, "Successfully $action Thread") - } - }) - return true - } - - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - if (threadController == null) { - return - } - - when (event) { - Lifecycle.Event.ON_START -> { - threadController.registerStateCallback(executor, stateCallback) - airplaneModeOn = isAirplaneModeOn(mContext) - mContext.registerReceiver( - airplaneModeReceiver, - IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED) - ) - preference?.let { preference -> updateState(preference) } - } - Lifecycle.Event.ON_STOP -> { - threadController.unregisterStateCallback(stateCallback) - mContext.unregisterReceiver(airplaneModeReceiver) - } - else -> {} - } - } - - override fun updateState(preference: Preference) { - super.updateState(preference) - preference.isEnabled = !airplaneModeOn - refreshSummary(preference) - } - - override fun getSummary(): CharSequence { - val resId: Int = if (airplaneModeOn) { - R.string.thread_network_settings_summary_airplane_mode - } else { - R.string.thread_network_settings_summary - } - return mContext.getResources().getString(resId) - } - - override fun getSliceHighlightMenuRes(): Int { - return R.string.menu_key_connected_devices - } - - companion object { - private const val TAG = "ThreadNetworkSettings" - private fun getThreadNetworkController(context: Context): BaseThreadNetworkController? { - if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) { - return null - } - val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null - val controller = manager.allThreadNetworkControllers[0] - return object : BaseThreadNetworkController { - override fun setEnabled( - enabled: Boolean, - executor: Executor, - receiver: OutcomeReceiver - ) { - controller.setEnabled(enabled, executor, receiver) - } - - override fun registerStateCallback(executor: Executor, callback: StateCallback) { - controller.registerStateCallback(executor, callback) - } - - override fun unregisterStateCallback(callback: StateCallback) { - controller.unregisterStateCallback(callback) - } - } - } - - private fun isAirplaneModeOn(context: Context): Boolean { - return Settings.Global.getInt( - context.contentResolver, - Settings.Global.AIRPLANE_MODE_ON, - 0 - ) == 1 - } - } -} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt new file mode 100644 index 00000000000..2af46759dd7 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.os.OutcomeReceiver +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.TogglePreferenceController +import com.android.settings.flags.Flags +import java.util.concurrent.Executor + +/** + * Controller for the "Use Thread" toggle in "Connected devices > Connection preferences > Thread". + */ +class ThreadNetworkToggleController @VisibleForTesting constructor( + context: Context, + key: String, + private val executor: Executor, + private val threadController: BaseThreadNetworkController? +) : TogglePreferenceController(context, key), LifecycleEventObserver { + private val stateCallback: StateCallback + private var threadEnabled = false + private var preference: Preference? = null + + constructor(context: Context, key: String) : this( + context, + key, + ContextCompat.getMainExecutor(context), + ThreadNetworkUtils.getThreadNetworkController(context) + ) + + init { + stateCallback = newStateCallback() + } + + val isThreadSupportedOnDevice: Boolean + get() = threadController != null + + private fun newStateCallback(): StateCallback { + return object : StateCallback { + override fun onThreadEnableStateChanged(enabledState: Int) { + threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED + preference?.let { preference -> updateState(preference) } + } + + override fun onDeviceRoleChanged(role: Int) {} + } + } + + override fun getAvailabilityStatus(): Int { + return if (!Flags.threadSettingsEnabled()) { + CONDITIONALLY_UNAVAILABLE + } else if (!isThreadSupportedOnDevice) { + UNSUPPORTED_ON_DEVICE + } else { + AVAILABLE + } + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun isChecked(): Boolean { + return threadEnabled + } + + override fun setChecked(isChecked: Boolean): Boolean { + if (threadController == null) { + return false + } + + // Avoids dead loop of setChecked -> threadController.setEnabled() -> + // StateCallback.onThreadEnableStateChanged -> updateState -> setChecked + if (isChecked == isChecked()) { + return true + } + + val action = if (isChecked) "enable" else "disable" + threadController.setEnabled( + isChecked, + executor, + object : OutcomeReceiver { + override fun onError(e: ThreadNetworkException) { + // TODO(b/327549838): gracefully handle the failure by resetting the UI state + Log.e(TAG, "Failed to $action Thread", e) + } + + override fun onResult(unused: Void?) { + Log.d(TAG, "Successfully $action Thread") + } + }) + return true + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (threadController == null) { + return + } + + when (event) { + Lifecycle.Event.ON_START -> { + threadController.registerStateCallback(executor, stateCallback) + } + + Lifecycle.Event.ON_STOP -> { + threadController.unregisterStateCallback(stateCallback) + } + + else -> {} + } + } + + override fun getSliceHighlightMenuRes(): Int { + return R.string.menu_key_connected_devices + } + + companion object { + private const val TAG = "ThreadNetworkSettings" + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt new file mode 100644 index 00000000000..70830ed3803 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.content.pm.PackageManager +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.net.thread.ThreadNetworkManager +import android.os.OutcomeReceiver +import androidx.annotation.VisibleForTesting +import java.util.concurrent.Executor + +/** Common utilities for Thread settings classes. */ +object ThreadNetworkUtils { + /** + * Retrieves the [BaseThreadNetworkController] instance that is backed by the Android + * [ThreadNetworkController]. + */ + fun getThreadNetworkController(context: Context): BaseThreadNetworkController? { + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) { + return null + } + val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null + val controller = manager.allThreadNetworkControllers[0] + return object : BaseThreadNetworkController { + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver + ) { + controller.setEnabled(enabled, executor, receiver) + } + + override fun registerStateCallback(executor: Executor, callback: StateCallback) { + controller.registerStateCallback(executor, callback) + } + + override fun unregisterStateCallback(callback: StateCallback) { + controller.unregisterStateCallback(callback) + } + } + } +} \ No newline at end of file diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt deleted file mode 100644 index 976096c7cc4..00000000000 --- a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settings.connecteddevice.threadnetwork - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.net.thread.ThreadNetworkController.STATE_DISABLED -import android.net.thread.ThreadNetworkController.STATE_DISABLING -import android.net.thread.ThreadNetworkController.STATE_ENABLED -import android.net.thread.ThreadNetworkController.StateCallback -import android.net.thread.ThreadNetworkException -import android.os.OutcomeReceiver -import android.platform.test.flag.junit.SetFlagsRule -import android.provider.Settings -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.preference.PreferenceManager -import androidx.preference.SwitchPreference -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settings.R -import com.android.settings.core.BasePreferenceController.AVAILABLE -import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE -import com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING -import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE -import com.android.settings.connecteddevice.threadnetwork.ThreadNetworkPreferenceController.BaseThreadNetworkController -import com.android.settings.flags.Flags -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.ArgumentCaptor -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import java.util.concurrent.Executor - -/** Unit tests for [ThreadNetworkPreferenceController]. */ -@RunWith(AndroidJUnit4::class) -class ThreadNetworkPreferenceControllerTest { - @get:Rule - val mSetFlagsRule = SetFlagsRule() - private lateinit var context: Context - private lateinit var executor: Executor - private lateinit var controller: ThreadNetworkPreferenceController - private lateinit var fakeThreadNetworkController: FakeThreadNetworkController - private lateinit var preference: SwitchPreference - private val broadcastReceiverArgumentCaptor = ArgumentCaptor.forClass( - BroadcastReceiver::class.java - ) - - @Before - fun setUp() { - mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) - context = spy(ApplicationProvider.getApplicationContext()) - executor = ContextCompat.getMainExecutor(context) - fakeThreadNetworkController = FakeThreadNetworkController(executor) - controller = newControllerWithThreadFeatureSupported(true) - val preferenceManager = PreferenceManager(context) - val preferenceScreen = preferenceManager.createPreferenceScreen(context) - preference = SwitchPreference(context) - preference.key = "thread_network_settings" - preferenceScreen.addPreference(preference) - controller.displayPreference(preferenceScreen) - - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - } - - private fun newControllerWithThreadFeatureSupported( - present: Boolean - ): ThreadNetworkPreferenceController { - return ThreadNetworkPreferenceController( - context, - "thread_network_settings" /* key */, - executor, - if (present) fakeThreadNetworkController else null - ) - } - - @Test - fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() { - mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) - assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE) - } - - @Test - fun availabilityStatus_airPlaneModeOn_returnsDisabledDependentSetting() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING) - } - - @Test - fun availabilityStatus_airPlaneModeOff_returnsAvailable() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE) - } - - @Test - fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() { - controller = newControllerWithThreadFeatureSupported(false) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(fakeThreadNetworkController.registeredStateCallback).isNull() - assertThat(controller.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE) - } - - @Test - fun isChecked_threadSetEnabled_returnsTrue() { - fakeThreadNetworkController.setEnabled(true, executor) { } - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.isChecked).isTrue() - } - - @Test - fun isChecked_threadSetDisabled_returnsFalse() { - fakeThreadNetworkController.setEnabled(false, executor) { } - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.isChecked).isFalse() - } - - @Test - fun setChecked_setChecked_threadIsEnabled() { - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - controller.setChecked(true) - - assertThat(fakeThreadNetworkController.isEnabled).isTrue() - } - - @Test - fun setChecked_setUnchecked_threadIsDisabled() { - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - controller.setChecked(false) - - assertThat(fakeThreadNetworkController.isEnabled).isFalse() - } - - @Test - fun updatePreference_airPlaneModeOff_preferenceEnabled() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(preference.isEnabled).isTrue() - assertThat(preference.summary).isEqualTo( - context.resources.getString(R.string.thread_network_settings_summary) - ) - } - - @Test - fun updatePreference_airPlaneModeOn_preferenceDisabled() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(preference.isEnabled).isFalse() - assertThat(preference.summary).isEqualTo( - context.resources.getString(R.string.thread_network_settings_summary_airplane_mode) - ) - } - - @Test - fun updatePreference_airPlaneModeTurnedOn_preferenceDisabled() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - startControllerAndCaptureCallbacks() - - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) - broadcastReceiverArgumentCaptor.value.onReceive(context, Intent()) - - assertThat(preference.isEnabled).isFalse() - assertThat(preference.summary).isEqualTo( - context.resources.getString(R.string.thread_network_settings_summary_airplane_mode) - ) - } - - private fun startControllerAndCaptureCallbacks() { - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - verify(context)!!.registerReceiver(broadcastReceiverArgumentCaptor.capture(), any()) - } - - private class FakeThreadNetworkController(private val executor: Executor) : - BaseThreadNetworkController { - var isEnabled = true - private set - var registeredStateCallback: StateCallback? = null - private set - - override fun setEnabled( - enabled: Boolean, - executor: Executor, - receiver: OutcomeReceiver - ) { - isEnabled = enabled - if (registeredStateCallback != null) { - if (!isEnabled) { - executor.execute { - registeredStateCallback!!.onThreadEnableStateChanged( - STATE_DISABLING - ) - } - executor.execute { - registeredStateCallback!!.onThreadEnableStateChanged( - STATE_DISABLED - ) - } - } else { - executor.execute { - registeredStateCallback!!.onThreadEnableStateChanged( - STATE_ENABLED - ) - } - } - } - executor.execute { receiver.onResult(null) } - } - - override fun registerStateCallback( - executor: Executor, - callback: StateCallback - ) { - require(callback !== registeredStateCallback) { "callback is already registered" } - registeredStateCallback = callback - val enabledState = - if (isEnabled) STATE_ENABLED else STATE_DISABLED - executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) } - } - - override fun unregisterStateCallback(callback: StateCallback) { - requireNotNull(registeredStateCallback) { "callback is already unregistered" } - registeredStateCallback = null - } - } -} diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt new file mode 100644 index 00000000000..8cb717dbb9b --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkException +import android.os.OutcomeReceiver +import java.util.concurrent.Executor + +/** A fake implementation of [BaseThreadNetworkController] for unit tests. */ +class FakeThreadNetworkController : BaseThreadNetworkController { + var isEnabled = true + private set + var registeredStateCallback: ThreadNetworkController.StateCallback? = null + private set + + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver + ) { + isEnabled = enabled + if (registeredStateCallback != null) { + if (!isEnabled) { + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + ThreadNetworkController.STATE_DISABLING + ) + } + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + ThreadNetworkController.STATE_DISABLED + ) + } + } else { + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + ThreadNetworkController.STATE_ENABLED + ) + } + } + } + executor.execute { receiver.onResult(null) } + } + + override fun registerStateCallback( + executor: Executor, + callback: ThreadNetworkController.StateCallback + ) { + require(callback !== registeredStateCallback) { "callback is already registered" } + registeredStateCallback = callback + val enabledState = + if (isEnabled) ThreadNetworkController.STATE_ENABLED else ThreadNetworkController.STATE_DISABLED + executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) } + } + + override fun unregisterStateCallback(callback: ThreadNetworkController.StateCallback) { + requireNotNull(registeredStateCallback) { "callback is already unregistered" } + registeredStateCallback = null + } +} diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS similarity index 100% rename from tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS rename to tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt new file mode 100644 index 00000000000..0d57dafc144 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.core.BasePreferenceController.AVAILABLE +import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE +import com.android.settings.flags.Flags +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.mock +import org.mockito.Mockito.spy +import java.util.concurrent.Executor + +/** Unit tests for [ThreadNetworkFragmentController]. */ +@RunWith(AndroidJUnit4::class) +class ThreadNetworkFragmentControllerTest { + @get:Rule + val mSetFlagsRule = SetFlagsRule() + private lateinit var context: Context + private lateinit var executor: Executor + private lateinit var controller: ThreadNetworkFragmentController + private lateinit var fakeThreadNetworkController: FakeThreadNetworkController + + @Before + fun setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + context = spy(ApplicationProvider.getApplicationContext()) + executor = ContextCompat.getMainExecutor(context) + fakeThreadNetworkController = FakeThreadNetworkController() + controller = newControllerWithThreadFeatureSupported(true) + } + + private fun newControllerWithThreadFeatureSupported( + present: Boolean + ): ThreadNetworkFragmentController { + return ThreadNetworkFragmentController( + context, + "thread_network_settings" /* key */, + executor, + if (present) fakeThreadNetworkController else null + ) + } + + @Test + fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() { + mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + startController(controller) + + assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() { + controller = newControllerWithThreadFeatureSupported(false) + startController(controller) + + assertThat(fakeThreadNetworkController.registeredStateCallback).isNull() + assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE) + } + + @Test + fun availabilityStatus_threadFeatureSupported_returnsAvailable() { + controller = newControllerWithThreadFeatureSupported(true) + startController(controller) + + assertThat(controller.availabilityStatus).isEqualTo(AVAILABLE) + } + + @Test + fun getSummary_ThreadIsEnabled_returnsOn() { + startController(controller) + fakeThreadNetworkController.setEnabled(true, executor) {} + + assertThat(controller.summary).isEqualTo("On") + } + + @Test + fun getSummary_ThreadIsDisabled_returnsOff() { + startController(controller) + fakeThreadNetworkController.setEnabled(false, executor) {} + + assertThat(controller.summary).isEqualTo("Off") + } + + private fun startController(controller: ThreadNetworkFragmentController) { + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt new file mode 100644 index 00000000000..329e7416d44 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreference +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE +import com.android.settings.flags.Flags +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.mock +import org.mockito.Mockito.spy +import java.util.concurrent.Executor + +/** Unit tests for [ThreadNetworkToggleController]. */ +@RunWith(AndroidJUnit4::class) +class ThreadNetworkToggleControllerTest { + @get:Rule + val mSetFlagsRule = SetFlagsRule() + private lateinit var context: Context + private lateinit var executor: Executor + private lateinit var controller: ThreadNetworkToggleController + private lateinit var fakeThreadNetworkController: FakeThreadNetworkController + private lateinit var preference: SwitchPreference + + @Before + fun setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + context = spy(ApplicationProvider.getApplicationContext()) + executor = ContextCompat.getMainExecutor(context) + fakeThreadNetworkController = FakeThreadNetworkController() + controller = newControllerWithThreadFeatureSupported(true) + val preferenceManager = PreferenceManager(context) + val preferenceScreen = preferenceManager.createPreferenceScreen(context) + preference = SwitchPreference(context) + preference.key = "toggle_thread_network" + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + } + + private fun newControllerWithThreadFeatureSupported( + present: Boolean + ): ThreadNetworkToggleController { + return ThreadNetworkToggleController( + context, + "toggle_thread_network" /* key */, + executor, + if (present) fakeThreadNetworkController else null + ) + } + + @Test + fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() { + mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() { + controller = newControllerWithThreadFeatureSupported(false) + startController(controller) + + assertThat(fakeThreadNetworkController.registeredStateCallback).isNull() + assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE) + } + + @Test + fun isChecked_threadSetEnabled_returnsTrue() { + fakeThreadNetworkController.setEnabled(true, executor) { } + startController(controller) + + assertThat(controller.isChecked).isTrue() + } + + @Test + fun isChecked_threadSetDisabled_returnsFalse() { + fakeThreadNetworkController.setEnabled(false, executor) { } + startController(controller) + + assertThat(controller.isChecked).isFalse() + } + + @Test + fun setChecked_setChecked_threadIsEnabled() { + startController(controller) + + controller.setChecked(true) + + assertThat(fakeThreadNetworkController.isEnabled).isTrue() + } + + @Test + fun setChecked_setUnchecked_threadIsDisabled() { + startController(controller) + + controller.setChecked(false) + + assertThat(fakeThreadNetworkController.isEnabled).isFalse() + } + + private fun startController(controller: ThreadNetworkToggleController) { + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + } +} From ab7f48dcdec78caafe5710508bfc891930efb017 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Sun, 5 May 2024 21:03:37 +0800 Subject: [PATCH 07/22] [Thread] update Thread settings screen Per b/327098435 the new Thread settings design proposed (go/android-thread) is approved. As a summary, this commit adds a new "connected devices > connection preference -> Thread" list item and decidated config page for Thread. Also, we simplified the airplane mode to delegate it to the mainline code to handle it Bug: 327098435 Test: atest SettingsUnitTests Merged-In: Iffbb2471f5a28ec57d30a378f22642fe6ac0b9cc Change-Id: Iffbb2471f5a28ec57d30a378f22642fe6ac0b9cc --- res/values/strings.xml | 14 +- res/xml/connected_devices_advanced.xml | 30 ++- res/xml/thread_network_settings.xml | 33 +++ .../BaseThreadNetworkController.kt | 46 ++++ .../ThreadNetworkFooterController.kt | 66 +++++ .../threadnetwork/ThreadNetworkFragment.kt | 39 +++ .../ThreadNetworkFragmentController.kt | 108 ++++++++ .../ThreadNetworkPreferenceController.kt | 236 ---------------- .../ThreadNetworkToggleController.kt | 146 ++++++++++ .../threadnetwork/ThreadNetworkUtils.kt | 59 ++++ .../ThreadNetworkPreferenceControllerTest.kt | 255 ------------------ .../FakeThreadNetworkController.kt | 74 +++++ .../threadnetwork/OWNERS | 0 .../ThreadNetworkFragmentControllerTest.kt | 112 ++++++++ .../ThreadNetworkToggleControllerTest.kt | 128 +++++++++ 15 files changed, 837 insertions(+), 509 deletions(-) create mode 100644 res/xml/thread_network_settings.xml create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt delete mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt create mode 100644 src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt delete mode 100644 tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt create mode 100644 tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt rename tests/unit/src/com/android/settings/{conecteddevice => connecteddevice}/threadnetwork/OWNERS (100%) create mode 100644 tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt create mode 100644 tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt diff --git a/res/values/strings.xml b/res/values/strings.xml index d920f1cbbd9..bac6af36748 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12076,11 +12076,17 @@ Thread - - Connect to compatible devices using Thread for a seamless smart home experience + + Use Thread - - Turn off airplane mode to use Thread + + Thread helps connect your smart home devices, boosting efficiency, and performance.\n\nWhen enabled, this device is eligible to join a Thread network, allowing control of Matter supported devices through this phone. + + + Learn more about Thread + + + https://developers.home.google.com Camera access diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index b1276d89d7f..49bdbaab2c7 100644 --- a/res/xml/connected_devices_advanced.xml +++ b/res/xml/connected_devices_advanced.xml @@ -54,12 +54,23 @@ settings:keywords="@string/keywords_wifi_display_settings"/> + + + android:icon="@*android:drawable/ic_settings_print" + android:key="connected_device_printing" + android:order="-3" + android:summary="@string/summary_placeholder" + android:title="@string/print_settings" /> - - diff --git a/res/xml/thread_network_settings.xml b/res/xml/thread_network_settings.xml new file mode 100644 index 00000000000..549d6507e57 --- /dev/null +++ b/res/xml/thread_network_settings.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt new file mode 100644 index 00000000000..583706a63ac --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.os.OutcomeReceiver +import androidx.annotation.VisibleForTesting +import java.util.concurrent.Executor + +/** + * A testable interface for [ThreadNetworkController] which is `final`. + * + * We are in a awkward situation that Android API guideline suggest `final` for API classes + * while Robolectric test is being deprecated for platform testing (See + * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's + * conflicting with the default "mockito-target" which is somehow indirectly depended by the + * `SettingsUnitTests` target. + */ +@VisibleForTesting +interface BaseThreadNetworkController { + fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver + ) + + fun registerStateCallback(executor: Executor, callback: StateCallback) + + fun unregisterStateCallback(callback: StateCallback) +} \ No newline at end of file diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt new file mode 100644 index 00000000000..1e3b62484de --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.util.Log +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settingslib.HelpUtils +import com.android.settingslib.widget.FooterPreference + +/** + * The footer preference controller for Thread settings in + * "Connected devices > Connection preferences > Thread". + */ +class ThreadNetworkFooterController( + context: Context, + preferenceKey: String +) : BasePreferenceController(context, preferenceKey) { + override fun getAvailabilityStatus(): Int { + // The thread_network_settings screen won't be displayed and it doesn't matter if this + // controller always return AVAILABLE + return AVAILABLE + } + + override fun displayPreference(screen: PreferenceScreen) { + val footer: FooterPreference? = screen.findPreference(KEY_PREFERENCE_FOOTER) + if (footer != null) { + footer.setLearnMoreAction { _ -> openLocaleLearnMoreLink() } + footer.setLearnMoreText(mContext.getString(R.string.thread_network_settings_learn_more)) + } + } + + private fun openLocaleLearnMoreLink() { + val intent = HelpUtils.getHelpIntent( + mContext, + mContext.getString(R.string.thread_network_settings_learn_more_link), + mContext::class.java.name + ) + if (intent != null) { + mContext.startActivity(intent) + } else { + Log.w(TAG, "HelpIntent is null") + } + } + + companion object { + private const val TAG = "ThreadNetworkSettings" + private const val KEY_PREFERENCE_FOOTER = "thread_network_settings_footer" + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt new file mode 100644 index 00000000000..fd385d7ee1d --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.app.settings.SettingsEnums +import com.android.settings.R +import com.android.settings.dashboard.DashboardFragment +import com.android.settings.search.BaseSearchIndexProvider +import com.android.settingslib.search.SearchIndexable + +/** The fragment for Thread settings in "Connected devices > Connection preferences > Thread". */ +@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv()) +class ThreadNetworkFragment : DashboardFragment() { + override fun getPreferenceScreenResId() = R.xml.thread_network_settings + + override fun getLogTag() = "ThreadNetworkFragment" + + override fun getMetricsCategory() = SettingsEnums.CONNECTED_DEVICE_PREFERENCES_THREAD + + companion object { + /** For Search. */ + @JvmField + val SEARCH_INDEX_DATA_PROVIDER = BaseSearchIndexProvider(R.xml.thread_network_settings) + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt new file mode 100644 index 00000000000..beb824a36a0 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settings.flags.Flags +import java.util.concurrent.Executor + +/** + * The fragment controller for Thread settings in + * "Connected devices > Connection preferences > Thread". + */ +class ThreadNetworkFragmentController @VisibleForTesting constructor( + context: Context, + preferenceKey: String, + private val executor: Executor, + private val threadController: BaseThreadNetworkController? +) : BasePreferenceController(context, preferenceKey), LifecycleEventObserver { + private val stateCallback: StateCallback + private var threadEnabled = false + private var preference: Preference? = null + + constructor(context: Context, preferenceKey: String) : this( + context, + preferenceKey, + ContextCompat.getMainExecutor(context), + ThreadNetworkUtils.getThreadNetworkController(context) + ) + + init { + stateCallback = newStateCallback() + } + + override fun getAvailabilityStatus(): Int { + return if (!Flags.threadSettingsEnabled()) { + CONDITIONALLY_UNAVAILABLE + } else if (threadController == null) { + UNSUPPORTED_ON_DEVICE + } else { + AVAILABLE + } + } + + override fun getSummary(): CharSequence { + return if (threadEnabled) { + mContext.getText(R.string.switch_on_text) + } else { + mContext.getText(R.string.switch_off_text) + } + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (threadController == null) { + return + } + + when (event) { + Lifecycle.Event.ON_START -> + threadController.registerStateCallback(executor, stateCallback) + + Lifecycle.Event.ON_STOP -> + threadController.unregisterStateCallback(stateCallback) + + else -> {} + } + } + + private fun newStateCallback(): StateCallback { + return object : StateCallback { + override fun onThreadEnableStateChanged(enabledState: Int) { + threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED + preference?.let { preference -> refreshSummary(preference) } + } + + override fun onDeviceRoleChanged(role: Int) {} + } + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt deleted file mode 100644 index 1c0175036d5..00000000000 --- a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settings.connecteddevice.threadnetwork - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.pm.PackageManager -import android.net.thread.ThreadNetworkController -import android.net.thread.ThreadNetworkController.StateCallback -import android.net.thread.ThreadNetworkException -import android.net.thread.ThreadNetworkManager -import android.os.OutcomeReceiver -import android.provider.Settings -import android.util.Log -import androidx.annotation.VisibleForTesting -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner -import androidx.preference.Preference -import androidx.preference.PreferenceScreen -import com.android.settings.R -import com.android.settings.core.TogglePreferenceController -import com.android.settings.flags.Flags -import java.util.concurrent.Executor - -/** Controller for the "Thread" toggle in "Connected devices > Connection preferences". */ -class ThreadNetworkPreferenceController @VisibleForTesting constructor( - context: Context, - key: String, - private val executor: Executor, - private val threadController: BaseThreadNetworkController? -) : TogglePreferenceController(context, key), LifecycleEventObserver { - private val stateCallback: StateCallback - private val airplaneModeReceiver: BroadcastReceiver - private var threadEnabled = false - private var airplaneModeOn = false - private var preference: Preference? = null - - /** - * A testable interface for [ThreadNetworkController] which is `final`. - * - * We are in a awkward situation that Android API guideline suggest `final` for API classes - * while Robolectric test is being deprecated for platform testing (See - * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's - * conflicting with the default "mockito-target" which is somehow indirectly depended by the - * `SettingsUnitTests` target. - */ - @VisibleForTesting - interface BaseThreadNetworkController { - fun setEnabled( - enabled: Boolean, - executor: Executor, - receiver: OutcomeReceiver - ) - - fun registerStateCallback(executor: Executor, callback: StateCallback) - - fun unregisterStateCallback(callback: StateCallback) - } - - constructor(context: Context, key: String) : this( - context, - key, - ContextCompat.getMainExecutor(context), - getThreadNetworkController(context) - ) - - init { - stateCallback = newStateCallback() - airplaneModeReceiver = newAirPlaneModeReceiver() - } - - val isThreadSupportedOnDevice: Boolean - get() = threadController != null - - private fun newStateCallback(): StateCallback { - return object : StateCallback { - override fun onThreadEnableStateChanged(enabledState: Int) { - threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED - } - - override fun onDeviceRoleChanged(role: Int) {} - } - } - - private fun newAirPlaneModeReceiver(): BroadcastReceiver { - return object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - airplaneModeOn = isAirplaneModeOn(context) - Log.i(TAG, "Airplane mode is " + if (airplaneModeOn) "ON" else "OFF") - preference?.let { preference -> updateState(preference) } - } - } - } - - override fun getAvailabilityStatus(): Int { - return if (!Flags.threadSettingsEnabled()) { - CONDITIONALLY_UNAVAILABLE - } else if (!isThreadSupportedOnDevice) { - UNSUPPORTED_ON_DEVICE - } else if (airplaneModeOn) { - DISABLED_DEPENDENT_SETTING - } else { - AVAILABLE - } - } - - override fun displayPreference(screen: PreferenceScreen) { - super.displayPreference(screen) - preference = screen.findPreference(preferenceKey) - } - - override fun isChecked(): Boolean { - // TODO (b/322742298): - // Check airplane mode here because it's planned to disable Thread state in airplane mode - // (code in the mainline module). But it's currently not implemented yet (b/322742298). - // By design, the toggle should be unchecked in airplane mode, so explicitly check the - // airplane mode here to acchieve the same UX. - return !airplaneModeOn && threadEnabled - } - - override fun setChecked(isChecked: Boolean): Boolean { - if (threadController == null) { - return false - } - val action = if (isChecked) "enable" else "disable" - threadController.setEnabled( - isChecked, - executor, - object : OutcomeReceiver { - override fun onError(e: ThreadNetworkException) { - // TODO(b/327549838): gracefully handle the failure by resetting the UI state - Log.e(TAG, "Failed to $action Thread", e) - } - - override fun onResult(unused: Void?) { - Log.d(TAG, "Successfully $action Thread") - } - }) - return true - } - - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - if (threadController == null) { - return - } - - when (event) { - Lifecycle.Event.ON_START -> { - threadController.registerStateCallback(executor, stateCallback) - airplaneModeOn = isAirplaneModeOn(mContext) - mContext.registerReceiver( - airplaneModeReceiver, - IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED) - ) - preference?.let { preference -> updateState(preference) } - } - Lifecycle.Event.ON_STOP -> { - threadController.unregisterStateCallback(stateCallback) - mContext.unregisterReceiver(airplaneModeReceiver) - } - else -> {} - } - } - - override fun updateState(preference: Preference) { - super.updateState(preference) - preference.isEnabled = !airplaneModeOn - refreshSummary(preference) - } - - override fun getSummary(): CharSequence { - val resId: Int = if (airplaneModeOn) { - R.string.thread_network_settings_summary_airplane_mode - } else { - R.string.thread_network_settings_summary - } - return mContext.getResources().getString(resId) - } - - override fun getSliceHighlightMenuRes(): Int { - return R.string.menu_key_connected_devices - } - - companion object { - private const val TAG = "ThreadNetworkSettings" - private fun getThreadNetworkController(context: Context): BaseThreadNetworkController? { - if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) { - return null - } - val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null - val controller = manager.allThreadNetworkControllers[0] - return object : BaseThreadNetworkController { - override fun setEnabled( - enabled: Boolean, - executor: Executor, - receiver: OutcomeReceiver - ) { - controller.setEnabled(enabled, executor, receiver) - } - - override fun registerStateCallback(executor: Executor, callback: StateCallback) { - controller.registerStateCallback(executor, callback) - } - - override fun unregisterStateCallback(callback: StateCallback) { - controller.unregisterStateCallback(callback) - } - } - } - - private fun isAirplaneModeOn(context: Context): Boolean { - return Settings.Global.getInt( - context.contentResolver, - Settings.Global.AIRPLANE_MODE_ON, - 0 - ) == 1 - } - } -} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt new file mode 100644 index 00000000000..2af46759dd7 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.os.OutcomeReceiver +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.TogglePreferenceController +import com.android.settings.flags.Flags +import java.util.concurrent.Executor + +/** + * Controller for the "Use Thread" toggle in "Connected devices > Connection preferences > Thread". + */ +class ThreadNetworkToggleController @VisibleForTesting constructor( + context: Context, + key: String, + private val executor: Executor, + private val threadController: BaseThreadNetworkController? +) : TogglePreferenceController(context, key), LifecycleEventObserver { + private val stateCallback: StateCallback + private var threadEnabled = false + private var preference: Preference? = null + + constructor(context: Context, key: String) : this( + context, + key, + ContextCompat.getMainExecutor(context), + ThreadNetworkUtils.getThreadNetworkController(context) + ) + + init { + stateCallback = newStateCallback() + } + + val isThreadSupportedOnDevice: Boolean + get() = threadController != null + + private fun newStateCallback(): StateCallback { + return object : StateCallback { + override fun onThreadEnableStateChanged(enabledState: Int) { + threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED + preference?.let { preference -> updateState(preference) } + } + + override fun onDeviceRoleChanged(role: Int) {} + } + } + + override fun getAvailabilityStatus(): Int { + return if (!Flags.threadSettingsEnabled()) { + CONDITIONALLY_UNAVAILABLE + } else if (!isThreadSupportedOnDevice) { + UNSUPPORTED_ON_DEVICE + } else { + AVAILABLE + } + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun isChecked(): Boolean { + return threadEnabled + } + + override fun setChecked(isChecked: Boolean): Boolean { + if (threadController == null) { + return false + } + + // Avoids dead loop of setChecked -> threadController.setEnabled() -> + // StateCallback.onThreadEnableStateChanged -> updateState -> setChecked + if (isChecked == isChecked()) { + return true + } + + val action = if (isChecked) "enable" else "disable" + threadController.setEnabled( + isChecked, + executor, + object : OutcomeReceiver { + override fun onError(e: ThreadNetworkException) { + // TODO(b/327549838): gracefully handle the failure by resetting the UI state + Log.e(TAG, "Failed to $action Thread", e) + } + + override fun onResult(unused: Void?) { + Log.d(TAG, "Successfully $action Thread") + } + }) + return true + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (threadController == null) { + return + } + + when (event) { + Lifecycle.Event.ON_START -> { + threadController.registerStateCallback(executor, stateCallback) + } + + Lifecycle.Event.ON_STOP -> { + threadController.unregisterStateCallback(stateCallback) + } + + else -> {} + } + } + + override fun getSliceHighlightMenuRes(): Int { + return R.string.menu_key_connected_devices + } + + companion object { + private const val TAG = "ThreadNetworkSettings" + } +} diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt new file mode 100644 index 00000000000..70830ed3803 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.content.pm.PackageManager +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.net.thread.ThreadNetworkManager +import android.os.OutcomeReceiver +import androidx.annotation.VisibleForTesting +import java.util.concurrent.Executor + +/** Common utilities for Thread settings classes. */ +object ThreadNetworkUtils { + /** + * Retrieves the [BaseThreadNetworkController] instance that is backed by the Android + * [ThreadNetworkController]. + */ + fun getThreadNetworkController(context: Context): BaseThreadNetworkController? { + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) { + return null + } + val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null + val controller = manager.allThreadNetworkControllers[0] + return object : BaseThreadNetworkController { + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver + ) { + controller.setEnabled(enabled, executor, receiver) + } + + override fun registerStateCallback(executor: Executor, callback: StateCallback) { + controller.registerStateCallback(executor, callback) + } + + override fun unregisterStateCallback(callback: StateCallback) { + controller.unregisterStateCallback(callback) + } + } + } +} \ No newline at end of file diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt deleted file mode 100644 index 976096c7cc4..00000000000 --- a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settings.connecteddevice.threadnetwork - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.net.thread.ThreadNetworkController.STATE_DISABLED -import android.net.thread.ThreadNetworkController.STATE_DISABLING -import android.net.thread.ThreadNetworkController.STATE_ENABLED -import android.net.thread.ThreadNetworkController.StateCallback -import android.net.thread.ThreadNetworkException -import android.os.OutcomeReceiver -import android.platform.test.flag.junit.SetFlagsRule -import android.provider.Settings -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.preference.PreferenceManager -import androidx.preference.SwitchPreference -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settings.R -import com.android.settings.core.BasePreferenceController.AVAILABLE -import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE -import com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING -import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE -import com.android.settings.connecteddevice.threadnetwork.ThreadNetworkPreferenceController.BaseThreadNetworkController -import com.android.settings.flags.Flags -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.ArgumentCaptor -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import java.util.concurrent.Executor - -/** Unit tests for [ThreadNetworkPreferenceController]. */ -@RunWith(AndroidJUnit4::class) -class ThreadNetworkPreferenceControllerTest { - @get:Rule - val mSetFlagsRule = SetFlagsRule() - private lateinit var context: Context - private lateinit var executor: Executor - private lateinit var controller: ThreadNetworkPreferenceController - private lateinit var fakeThreadNetworkController: FakeThreadNetworkController - private lateinit var preference: SwitchPreference - private val broadcastReceiverArgumentCaptor = ArgumentCaptor.forClass( - BroadcastReceiver::class.java - ) - - @Before - fun setUp() { - mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) - context = spy(ApplicationProvider.getApplicationContext()) - executor = ContextCompat.getMainExecutor(context) - fakeThreadNetworkController = FakeThreadNetworkController(executor) - controller = newControllerWithThreadFeatureSupported(true) - val preferenceManager = PreferenceManager(context) - val preferenceScreen = preferenceManager.createPreferenceScreen(context) - preference = SwitchPreference(context) - preference.key = "thread_network_settings" - preferenceScreen.addPreference(preference) - controller.displayPreference(preferenceScreen) - - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - } - - private fun newControllerWithThreadFeatureSupported( - present: Boolean - ): ThreadNetworkPreferenceController { - return ThreadNetworkPreferenceController( - context, - "thread_network_settings" /* key */, - executor, - if (present) fakeThreadNetworkController else null - ) - } - - @Test - fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() { - mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) - assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE) - } - - @Test - fun availabilityStatus_airPlaneModeOn_returnsDisabledDependentSetting() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING) - } - - @Test - fun availabilityStatus_airPlaneModeOff_returnsAvailable() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE) - } - - @Test - fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() { - controller = newControllerWithThreadFeatureSupported(false) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(fakeThreadNetworkController.registeredStateCallback).isNull() - assertThat(controller.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE) - } - - @Test - fun isChecked_threadSetEnabled_returnsTrue() { - fakeThreadNetworkController.setEnabled(true, executor) { } - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.isChecked).isTrue() - } - - @Test - fun isChecked_threadSetDisabled_returnsFalse() { - fakeThreadNetworkController.setEnabled(false, executor) { } - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(controller.isChecked).isFalse() - } - - @Test - fun setChecked_setChecked_threadIsEnabled() { - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - controller.setChecked(true) - - assertThat(fakeThreadNetworkController.isEnabled).isTrue() - } - - @Test - fun setChecked_setUnchecked_threadIsDisabled() { - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - controller.setChecked(false) - - assertThat(fakeThreadNetworkController.isEnabled).isFalse() - } - - @Test - fun updatePreference_airPlaneModeOff_preferenceEnabled() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(preference.isEnabled).isTrue() - assertThat(preference.summary).isEqualTo( - context.resources.getString(R.string.thread_network_settings_summary) - ) - } - - @Test - fun updatePreference_airPlaneModeOn_preferenceDisabled() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - - assertThat(preference.isEnabled).isFalse() - assertThat(preference.summary).isEqualTo( - context.resources.getString(R.string.thread_network_settings_summary_airplane_mode) - ) - } - - @Test - fun updatePreference_airPlaneModeTurnedOn_preferenceDisabled() { - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) - startControllerAndCaptureCallbacks() - - Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) - broadcastReceiverArgumentCaptor.value.onReceive(context, Intent()) - - assertThat(preference.isEnabled).isFalse() - assertThat(preference.summary).isEqualTo( - context.resources.getString(R.string.thread_network_settings_summary_airplane_mode) - ) - } - - private fun startControllerAndCaptureCallbacks() { - controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) - verify(context)!!.registerReceiver(broadcastReceiverArgumentCaptor.capture(), any()) - } - - private class FakeThreadNetworkController(private val executor: Executor) : - BaseThreadNetworkController { - var isEnabled = true - private set - var registeredStateCallback: StateCallback? = null - private set - - override fun setEnabled( - enabled: Boolean, - executor: Executor, - receiver: OutcomeReceiver - ) { - isEnabled = enabled - if (registeredStateCallback != null) { - if (!isEnabled) { - executor.execute { - registeredStateCallback!!.onThreadEnableStateChanged( - STATE_DISABLING - ) - } - executor.execute { - registeredStateCallback!!.onThreadEnableStateChanged( - STATE_DISABLED - ) - } - } else { - executor.execute { - registeredStateCallback!!.onThreadEnableStateChanged( - STATE_ENABLED - ) - } - } - } - executor.execute { receiver.onResult(null) } - } - - override fun registerStateCallback( - executor: Executor, - callback: StateCallback - ) { - require(callback !== registeredStateCallback) { "callback is already registered" } - registeredStateCallback = callback - val enabledState = - if (isEnabled) STATE_ENABLED else STATE_DISABLED - executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) } - } - - override fun unregisterStateCallback(callback: StateCallback) { - requireNotNull(registeredStateCallback) { "callback is already unregistered" } - registeredStateCallback = null - } - } -} diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt new file mode 100644 index 00000000000..8cb717dbb9b --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkException +import android.os.OutcomeReceiver +import java.util.concurrent.Executor + +/** A fake implementation of [BaseThreadNetworkController] for unit tests. */ +class FakeThreadNetworkController : BaseThreadNetworkController { + var isEnabled = true + private set + var registeredStateCallback: ThreadNetworkController.StateCallback? = null + private set + + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver + ) { + isEnabled = enabled + if (registeredStateCallback != null) { + if (!isEnabled) { + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + ThreadNetworkController.STATE_DISABLING + ) + } + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + ThreadNetworkController.STATE_DISABLED + ) + } + } else { + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + ThreadNetworkController.STATE_ENABLED + ) + } + } + } + executor.execute { receiver.onResult(null) } + } + + override fun registerStateCallback( + executor: Executor, + callback: ThreadNetworkController.StateCallback + ) { + require(callback !== registeredStateCallback) { "callback is already registered" } + registeredStateCallback = callback + val enabledState = + if (isEnabled) ThreadNetworkController.STATE_ENABLED else ThreadNetworkController.STATE_DISABLED + executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) } + } + + override fun unregisterStateCallback(callback: ThreadNetworkController.StateCallback) { + requireNotNull(registeredStateCallback) { "callback is already unregistered" } + registeredStateCallback = null + } +} diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS similarity index 100% rename from tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS rename to tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt new file mode 100644 index 00000000000..0d57dafc144 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.core.BasePreferenceController.AVAILABLE +import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE +import com.android.settings.flags.Flags +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.mock +import org.mockito.Mockito.spy +import java.util.concurrent.Executor + +/** Unit tests for [ThreadNetworkFragmentController]. */ +@RunWith(AndroidJUnit4::class) +class ThreadNetworkFragmentControllerTest { + @get:Rule + val mSetFlagsRule = SetFlagsRule() + private lateinit var context: Context + private lateinit var executor: Executor + private lateinit var controller: ThreadNetworkFragmentController + private lateinit var fakeThreadNetworkController: FakeThreadNetworkController + + @Before + fun setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + context = spy(ApplicationProvider.getApplicationContext()) + executor = ContextCompat.getMainExecutor(context) + fakeThreadNetworkController = FakeThreadNetworkController() + controller = newControllerWithThreadFeatureSupported(true) + } + + private fun newControllerWithThreadFeatureSupported( + present: Boolean + ): ThreadNetworkFragmentController { + return ThreadNetworkFragmentController( + context, + "thread_network_settings" /* key */, + executor, + if (present) fakeThreadNetworkController else null + ) + } + + @Test + fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() { + mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + startController(controller) + + assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() { + controller = newControllerWithThreadFeatureSupported(false) + startController(controller) + + assertThat(fakeThreadNetworkController.registeredStateCallback).isNull() + assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE) + } + + @Test + fun availabilityStatus_threadFeatureSupported_returnsAvailable() { + controller = newControllerWithThreadFeatureSupported(true) + startController(controller) + + assertThat(controller.availabilityStatus).isEqualTo(AVAILABLE) + } + + @Test + fun getSummary_ThreadIsEnabled_returnsOn() { + startController(controller) + fakeThreadNetworkController.setEnabled(true, executor) {} + + assertThat(controller.summary).isEqualTo("On") + } + + @Test + fun getSummary_ThreadIsDisabled_returnsOff() { + startController(controller) + fakeThreadNetworkController.setEnabled(false, executor) {} + + assertThat(controller.summary).isEqualTo("Off") + } + + private fun startController(controller: ThreadNetworkFragmentController) { + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt new file mode 100644 index 00000000000..329e7416d44 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreference +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE +import com.android.settings.flags.Flags +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.mock +import org.mockito.Mockito.spy +import java.util.concurrent.Executor + +/** Unit tests for [ThreadNetworkToggleController]. */ +@RunWith(AndroidJUnit4::class) +class ThreadNetworkToggleControllerTest { + @get:Rule + val mSetFlagsRule = SetFlagsRule() + private lateinit var context: Context + private lateinit var executor: Executor + private lateinit var controller: ThreadNetworkToggleController + private lateinit var fakeThreadNetworkController: FakeThreadNetworkController + private lateinit var preference: SwitchPreference + + @Before + fun setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + context = spy(ApplicationProvider.getApplicationContext()) + executor = ContextCompat.getMainExecutor(context) + fakeThreadNetworkController = FakeThreadNetworkController() + controller = newControllerWithThreadFeatureSupported(true) + val preferenceManager = PreferenceManager(context) + val preferenceScreen = preferenceManager.createPreferenceScreen(context) + preference = SwitchPreference(context) + preference.key = "toggle_thread_network" + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + } + + private fun newControllerWithThreadFeatureSupported( + present: Boolean + ): ThreadNetworkToggleController { + return ThreadNetworkToggleController( + context, + "toggle_thread_network" /* key */, + executor, + if (present) fakeThreadNetworkController else null + ) + } + + @Test + fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() { + mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) + assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() { + controller = newControllerWithThreadFeatureSupported(false) + startController(controller) + + assertThat(fakeThreadNetworkController.registeredStateCallback).isNull() + assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE) + } + + @Test + fun isChecked_threadSetEnabled_returnsTrue() { + fakeThreadNetworkController.setEnabled(true, executor) { } + startController(controller) + + assertThat(controller.isChecked).isTrue() + } + + @Test + fun isChecked_threadSetDisabled_returnsFalse() { + fakeThreadNetworkController.setEnabled(false, executor) { } + startController(controller) + + assertThat(controller.isChecked).isFalse() + } + + @Test + fun setChecked_setChecked_threadIsEnabled() { + startController(controller) + + controller.setChecked(true) + + assertThat(fakeThreadNetworkController.isEnabled).isTrue() + } + + @Test + fun setChecked_setUnchecked_threadIsDisabled() { + startController(controller) + + controller.setChecked(false) + + assertThat(fakeThreadNetworkController.isEnabled).isFalse() + } + + private fun startController(controller: ThreadNetworkToggleController) { + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + } +} From 24289fa08434d67e2d93c2f5baff422a192465fc Mon Sep 17 00:00:00 2001 From: marcusge Date: Sat, 20 Apr 2024 00:13:24 +0000 Subject: [PATCH 08/22] [Contrast] Migrate contrast settings into Display Test: local raven device Bug: 333905689 Change-Id: Ie94633c23ebe024b8fb48d7ffebdd7b1dfa588da --- AndroidManifest.xml | 4 +- res/drawable/ic_color_contrast.xml | 35 ----------- res/values/strings.xml | 2 - res/xml/accessibility_color_and_motion.xml | 10 ---- res/xml/accessibility_color_contrast.xml | 4 +- res/xml/display_settings.xml | 8 +++ .../ContrastPreferenceController.java | 39 ------------ .../core/gateway/SettingsGateway.java | 2 +- ...lorContrastFooterPreferenceController.java | 3 +- .../ColorContrastFragment.java | 3 +- .../display/ContrastPreferenceController.java | 60 +++++++++++++++++++ .../ContrastSelectorPreferenceController.java | 2 +- .../ColorContrastFragmentTest.java | 3 +- .../ContrastPreferenceControllerTest.java | 28 +++++++-- ...trastSelectorPreferenceControllerTest.java | 2 +- 15 files changed, 103 insertions(+), 102 deletions(-) delete mode 100644 res/drawable/ic_color_contrast.xml delete mode 100644 src/com/android/settings/accessibility/ContrastPreferenceController.java rename src/com/android/settings/{accessibility => display}/ColorContrastFooterPreferenceController.java (90%) rename src/com/android/settings/{accessibility => display}/ColorContrastFragment.java (97%) create mode 100644 src/com/android/settings/display/ContrastPreferenceController.java rename src/com/android/settings/{accessibility => display}/ContrastSelectorPreferenceController.java (99%) rename tests/robotests/src/com/android/settings/{accessibility => display}/ColorContrastFragmentTest.java (96%) rename tests/robotests/src/com/android/settings/{accessibility => display}/ContrastPreferenceControllerTest.java (55%) rename tests/robotests/src/com/android/settings/{accessibility => display}/ContrastSelectorPreferenceControllerTest.java (98%) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index daadd35d213..bf7d4a16063 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2551,9 +2551,9 @@ + android:value="com.android.settings.display.ColorContrastFragment" /> + android:value="@string/menu_key_display"/> diff --git a/res/drawable/ic_color_contrast.xml b/res/drawable/ic_color_contrast.xml deleted file mode 100644 index 9d56ada2f0c..00000000000 --- a/res/drawable/ic_color_contrast.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index e14a1073451..a1428a6ad14 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4688,8 +4688,6 @@ Higher contrast makes text, buttons, and icons stand out more. Choose the contrast that looks best to you. Some apps may not support all color and text contrast settings - - Adjust how colors and text look against your screen\'s background color Preview diff --git a/res/xml/accessibility_color_and_motion.xml b/res/xml/accessibility_color_and_motion.xml index 35222347a29..a500b72d958 100644 --- a/res/xml/accessibility_color_and_motion.xml +++ b/res/xml/accessibility_color_and_motion.xml @@ -21,16 +21,6 @@ android:persistent="false" android:title="@string/accessibility_color_and_motion_title"> - - + settings:controller="com.android.settings.display.ContrastSelectorPreferenceController" /> + settings:controller="com.android.settings.display.ColorContrastFooterPreferenceController" /> diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index 0c6d673fb20..abf0cc6d3d9 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -111,6 +111,14 @@ android:fragment="com.android.settings.display.ColorModePreferenceFragment" settings:controller="com.android.settings.display.ColorModePreferenceController" settings:keywords="@string/keywords_color_mode"/> + + mContrastLevelToResId = Map.ofEntries( + Map.entry(CONTRAST_LEVEL_STANDARD, R.string.contrast_default), + Map.entry(CONTRAST_LEVEL_MEDIUM, R.string.contrast_medium), + Map.entry(CONTRAST_LEVEL_HIGH, R.string.contrast_high) + ); + + float contrastLevel = mContext.getSystemService(UiModeManager.class).getContrast(); + return mContext.getString(mContrastLevelToResId.get(toContrastLevel(contrastLevel))); + } +} diff --git a/src/com/android/settings/accessibility/ContrastSelectorPreferenceController.java b/src/com/android/settings/display/ContrastSelectorPreferenceController.java similarity index 99% rename from src/com/android/settings/accessibility/ContrastSelectorPreferenceController.java rename to src/com/android/settings/display/ContrastSelectorPreferenceController.java index 5b746cdd705..ba98601d603 100644 --- a/src/com/android/settings/accessibility/ContrastSelectorPreferenceController.java +++ b/src/com/android/settings/display/ContrastSelectorPreferenceController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.accessibility; +package com.android.settings.display; import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_HIGH; import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_MEDIUM; diff --git a/tests/robotests/src/com/android/settings/accessibility/ColorContrastFragmentTest.java b/tests/robotests/src/com/android/settings/display/ColorContrastFragmentTest.java similarity index 96% rename from tests/robotests/src/com/android/settings/accessibility/ColorContrastFragmentTest.java rename to tests/robotests/src/com/android/settings/display/ColorContrastFragmentTest.java index 3077637a8e4..47a7363b531 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ColorContrastFragmentTest.java +++ b/tests/robotests/src/com/android/settings/display/ColorContrastFragmentTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.accessibility; +package com.android.settings.display; import static com.google.common.truth.Truth.assertThat; @@ -28,6 +28,7 @@ import android.content.Context; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; +import com.android.settings.accessibility.ShortcutsSettingsFragment; import com.android.settings.testutils.XmlTestUtils; import org.junit.Before; diff --git a/tests/robotests/src/com/android/settings/accessibility/ContrastPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/ContrastPreferenceControllerTest.java similarity index 55% rename from tests/robotests/src/com/android/settings/accessibility/ContrastPreferenceControllerTest.java rename to tests/robotests/src/com/android/settings/display/ContrastPreferenceControllerTest.java index 07c3b54f946..1ddc81960b6 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ContrastPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/display/ContrastPreferenceControllerTest.java @@ -14,15 +14,22 @@ * limitations under the License. */ -package com.android.settings.accessibility; +package com.android.settings.display; import static com.google.common.truth.Truth.assertThat; +import android.content.Context; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + import androidx.test.core.app.ApplicationProvider; +import com.android.settings.accessibility.Flags; import com.android.settings.core.BasePreferenceController; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -31,19 +38,30 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class ContrastPreferenceControllerTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final String PREFERENCE_KEY = "preference_key"; + private Context mContext; private ContrastPreferenceController mController; @Before public void setUp() { - mController = new ContrastPreferenceController(ApplicationProvider.getApplicationContext(), - PREFERENCE_KEY); + mContext = ApplicationProvider.getApplicationContext(); + mController = new ContrastPreferenceController(mContext, PREFERENCE_KEY); } @Test - public void getAvailabilityStatus_shouldReturnUnavailable() { + @EnableFlags(Flags.FLAG_ENABLE_COLOR_CONTRAST_CONTROL) + public void getAvailabilityStatus_flagsEnabled_shouldReturnAvailable() { assertThat(mController.getAvailabilityStatus()) - .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + .isEqualTo(BasePreferenceController.AVAILABLE); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_COLOR_CONTRAST_CONTROL) + public void getAvailabilityStatus_flagsDisabled_shouldReturnUnsupported() { + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.UNSUPPORTED_ON_DEVICE); } } diff --git a/tests/robotests/src/com/android/settings/accessibility/ContrastSelectorPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/ContrastSelectorPreferenceControllerTest.java similarity index 98% rename from tests/robotests/src/com/android/settings/accessibility/ContrastSelectorPreferenceControllerTest.java rename to tests/robotests/src/com/android/settings/display/ContrastSelectorPreferenceControllerTest.java index 83d9cb957b4..0d490a88347 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ContrastSelectorPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/display/ContrastSelectorPreferenceControllerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.accessibility; +package com.android.settings.display; import static com.google.common.truth.Truth.assertThat; From 47400df7aed2f8e9be4a5b7a85ee7b4b3d202a8b Mon Sep 17 00:00:00 2001 From: Edgar Wang Date: Tue, 16 Apr 2024 14:51:42 +0000 Subject: [PATCH 09/22] Homepage UX revamp - unified Search and Suggestion behavior between regular phone and two pane - don't adjust padding - update new icon drawable - support group homepage preference with round corner on phone - Remove avator from homepage - Adjust homepage preference order Bug: 333989622 Bug: 334130370 Test: visual Change-Id: I9880b52553f164745766c8b9d5c996585285e52a --- .../ic_settings_about_device_filled.xml | 25 ++ ...omepage_highlighted_item_background_v2.xml | 29 +++ ...homepage_selectable_item_background_v2.xml | 29 +++ res/drawable/ic_apps_filled.xml | 25 ++ res/drawable/ic_devices_other_filled.xml | 28 ++ res/drawable/ic_help_filled.xml | 26 ++ res/drawable/ic_notifications_filled.xml | 26 ++ .../ic_settings_about_device_filled.xml | 25 ++ .../ic_settings_accessibility_filled.xml | 25 ++ res/drawable/ic_settings_battery_filled.xml | 25 ++ res/drawable/ic_settings_display_filled.xml | 25 ++ res/drawable/ic_settings_emergency_filled.xml | 25 ++ res/drawable/ic_settings_location_filled.xml | 25 ++ res/drawable/ic_settings_passwords_filled.xml | 25 ++ res/drawable/ic_settings_privacy_filled.xml | 25 ++ .../ic_settings_safety_center_filled.xml | 29 +++ res/drawable/ic_settings_security_filled.xml | 25 ++ .../ic_settings_system_dashboard_filled.xml | 25 ++ res/drawable/ic_settings_wallpaper_filled.xml | 25 ++ res/drawable/ic_settings_wireless_filled.xml | 25 ++ res/drawable/ic_storage_filled.xml | 25 ++ res/drawable/ic_volume_up_filled.xml | 26 ++ res/layout/homepage_preference_v2.xml | 90 +++++++ res/layout/search_bar_unified_version.xml | 44 ++++ ...ttings_homepage_app_bar_unified_layout.xml | 29 +++ res/layout/settings_homepage_container_v2.xml | 80 ++++++ res/values/config.xml | 2 +- res/xml/top_level_settings_v2.xml | 241 ++++++++++++++++++ .../core/RoundCornerPreferenceAdapter.java | 151 +++++++++++ .../DashboardFeatureProviderImpl.java | 4 +- .../homepage/SettingsHomepageActivity.java | 70 +++-- .../settings/homepage/TopLevelSettings.java | 19 +- ...ighlightableTopLevelPreferenceAdapter.java | 9 +- .../HomepagePreferenceLayoutHelper.java | 6 +- .../homepage/TopLevelSettingsTest.java | 7 - 35 files changed, 1284 insertions(+), 36 deletions(-) create mode 100644 res/drawable-sw600dp/ic_settings_about_device_filled.xml create mode 100644 res/drawable/homepage_highlighted_item_background_v2.xml create mode 100644 res/drawable/homepage_selectable_item_background_v2.xml create mode 100644 res/drawable/ic_apps_filled.xml create mode 100644 res/drawable/ic_devices_other_filled.xml create mode 100644 res/drawable/ic_help_filled.xml create mode 100644 res/drawable/ic_notifications_filled.xml create mode 100644 res/drawable/ic_settings_about_device_filled.xml create mode 100644 res/drawable/ic_settings_accessibility_filled.xml create mode 100644 res/drawable/ic_settings_battery_filled.xml create mode 100644 res/drawable/ic_settings_display_filled.xml create mode 100644 res/drawable/ic_settings_emergency_filled.xml create mode 100644 res/drawable/ic_settings_location_filled.xml create mode 100644 res/drawable/ic_settings_passwords_filled.xml create mode 100644 res/drawable/ic_settings_privacy_filled.xml create mode 100644 res/drawable/ic_settings_safety_center_filled.xml create mode 100644 res/drawable/ic_settings_security_filled.xml create mode 100644 res/drawable/ic_settings_system_dashboard_filled.xml create mode 100644 res/drawable/ic_settings_wallpaper_filled.xml create mode 100644 res/drawable/ic_settings_wireless_filled.xml create mode 100644 res/drawable/ic_storage_filled.xml create mode 100644 res/drawable/ic_volume_up_filled.xml create mode 100644 res/layout/homepage_preference_v2.xml create mode 100644 res/layout/search_bar_unified_version.xml create mode 100644 res/layout/settings_homepage_app_bar_unified_layout.xml create mode 100644 res/layout/settings_homepage_container_v2.xml create mode 100644 res/xml/top_level_settings_v2.xml create mode 100644 src/com/android/settings/core/RoundCornerPreferenceAdapter.java diff --git a/res/drawable-sw600dp/ic_settings_about_device_filled.xml b/res/drawable-sw600dp/ic_settings_about_device_filled.xml new file mode 100644 index 00000000000..33ec5fe3f79 --- /dev/null +++ b/res/drawable-sw600dp/ic_settings_about_device_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/homepage_highlighted_item_background_v2.xml b/res/drawable/homepage_highlighted_item_background_v2.xml new file mode 100644 index 00000000000..7aa489527a0 --- /dev/null +++ b/res/drawable/homepage_highlighted_item_background_v2.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/homepage_selectable_item_background_v2.xml b/res/drawable/homepage_selectable_item_background_v2.xml new file mode 100644 index 00000000000..d2f79ff9bf6 --- /dev/null +++ b/res/drawable/homepage_selectable_item_background_v2.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/res/drawable/ic_apps_filled.xml b/res/drawable/ic_apps_filled.xml new file mode 100644 index 00000000000..5f86a92e013 --- /dev/null +++ b/res/drawable/ic_apps_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_devices_other_filled.xml b/res/drawable/ic_devices_other_filled.xml new file mode 100644 index 00000000000..a2ded48c57f --- /dev/null +++ b/res/drawable/ic_devices_other_filled.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/ic_help_filled.xml b/res/drawable/ic_help_filled.xml new file mode 100644 index 00000000000..79cbb0b131f --- /dev/null +++ b/res/drawable/ic_help_filled.xml @@ -0,0 +1,26 @@ + + + + diff --git a/res/drawable/ic_notifications_filled.xml b/res/drawable/ic_notifications_filled.xml new file mode 100644 index 00000000000..3f539132f22 --- /dev/null +++ b/res/drawable/ic_notifications_filled.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_settings_about_device_filled.xml b/res/drawable/ic_settings_about_device_filled.xml new file mode 100644 index 00000000000..fb6b2be5884 --- /dev/null +++ b/res/drawable/ic_settings_about_device_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_accessibility_filled.xml b/res/drawable/ic_settings_accessibility_filled.xml new file mode 100644 index 00000000000..24a5304be8f --- /dev/null +++ b/res/drawable/ic_settings_accessibility_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_battery_filled.xml b/res/drawable/ic_settings_battery_filled.xml new file mode 100644 index 00000000000..122fb0a40f8 --- /dev/null +++ b/res/drawable/ic_settings_battery_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_display_filled.xml b/res/drawable/ic_settings_display_filled.xml new file mode 100644 index 00000000000..ef61cbbb3d2 --- /dev/null +++ b/res/drawable/ic_settings_display_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_emergency_filled.xml b/res/drawable/ic_settings_emergency_filled.xml new file mode 100644 index 00000000000..af58127fac3 --- /dev/null +++ b/res/drawable/ic_settings_emergency_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_location_filled.xml b/res/drawable/ic_settings_location_filled.xml new file mode 100644 index 00000000000..264952122ad --- /dev/null +++ b/res/drawable/ic_settings_location_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_passwords_filled.xml b/res/drawable/ic_settings_passwords_filled.xml new file mode 100644 index 00000000000..eee77afbf21 --- /dev/null +++ b/res/drawable/ic_settings_passwords_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_privacy_filled.xml b/res/drawable/ic_settings_privacy_filled.xml new file mode 100644 index 00000000000..95127351a72 --- /dev/null +++ b/res/drawable/ic_settings_privacy_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_safety_center_filled.xml b/res/drawable/ic_settings_safety_center_filled.xml new file mode 100644 index 00000000000..8b6bb6c314a --- /dev/null +++ b/res/drawable/ic_settings_safety_center_filled.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/res/drawable/ic_settings_security_filled.xml b/res/drawable/ic_settings_security_filled.xml new file mode 100644 index 00000000000..fa2a42b6192 --- /dev/null +++ b/res/drawable/ic_settings_security_filled.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/drawable/ic_settings_system_dashboard_filled.xml b/res/drawable/ic_settings_system_dashboard_filled.xml new file mode 100644 index 00000000000..aa2756ebaa0 --- /dev/null +++ b/res/drawable/ic_settings_system_dashboard_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_wallpaper_filled.xml b/res/drawable/ic_settings_wallpaper_filled.xml new file mode 100644 index 00000000000..cbcc3b2d5f6 --- /dev/null +++ b/res/drawable/ic_settings_wallpaper_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_settings_wireless_filled.xml b/res/drawable/ic_settings_wireless_filled.xml new file mode 100644 index 00000000000..ec85a8b8146 --- /dev/null +++ b/res/drawable/ic_settings_wireless_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_storage_filled.xml b/res/drawable/ic_storage_filled.xml new file mode 100644 index 00000000000..2fa3c74a2a9 --- /dev/null +++ b/res/drawable/ic_storage_filled.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_volume_up_filled.xml b/res/drawable/ic_volume_up_filled.xml new file mode 100644 index 00000000000..da3a867c501 --- /dev/null +++ b/res/drawable/ic_volume_up_filled.xml @@ -0,0 +1,26 @@ + + + + diff --git a/res/layout/homepage_preference_v2.xml b/res/layout/homepage_preference_v2.xml new file mode 100644 index 00000000000..4d441d36842 --- /dev/null +++ b/res/layout/homepage_preference_v2.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/layout/search_bar_unified_version.xml b/res/layout/search_bar_unified_version.xml new file mode 100644 index 00000000000..eec8406af7d --- /dev/null +++ b/res/layout/search_bar_unified_version.xml @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/res/layout/settings_homepage_app_bar_unified_layout.xml b/res/layout/settings_homepage_app_bar_unified_layout.xml new file mode 100644 index 00000000000..3e254186822 --- /dev/null +++ b/res/layout/settings_homepage_app_bar_unified_layout.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/res/layout/settings_homepage_container_v2.xml b/res/layout/settings_homepage_container_v2.xml new file mode 100644 index 00000000000..73b8f21cebf --- /dev/null +++ b/res/layout/settings_homepage_container_v2.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/config.xml b/res/values/config.xml index 6d9d784cb84..4d3a23348e4 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -257,7 +257,7 @@ true - true + false false diff --git a/res/xml/top_level_settings_v2.xml b/res/xml/top_level_settings_v2.xml new file mode 100644 index 00000000000..9cd8dbe1d70 --- /dev/null +++ b/res/xml/top_level_settings_v2.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/core/RoundCornerPreferenceAdapter.java b/src/com/android/settings/core/RoundCornerPreferenceAdapter.java new file mode 100644 index 00000000000..e5f3763a641 --- /dev/null +++ b/src/com/android/settings/core/RoundCornerPreferenceAdapter.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.core; + +import android.os.Handler; +import android.os.Looper; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceGroupAdapter; +import androidx.preference.PreferenceViewHolder; + +import com.android.settingslib.widget.theme.R; + +import java.util.ArrayList; +import java.util.List; + +public class RoundCornerPreferenceAdapter extends PreferenceGroupAdapter { + + private static final int ROUND_CORNER_CENTER = 1; + private static final int ROUND_CORNER_TOP = 1 << 1; + private static final int ROUND_CORNER_BOTTOM = 1 << 2; + + private final PreferenceGroup mPreferenceGroup; + + private List mRoundCornerMappingList; + + private final Handler mHandler; + + private final Runnable mSyncRunnable = new Runnable() { + @Override + public void run() { + updatePreferences(); + } + }; + + public RoundCornerPreferenceAdapter(@NonNull PreferenceGroup preferenceGroup) { + super(preferenceGroup); + mPreferenceGroup = preferenceGroup; + mHandler = new Handler(Looper.getMainLooper()); + updatePreferences(); + } + + @Override + public void onPreferenceHierarchyChange(@NonNull Preference preference) { + super.onPreferenceHierarchyChange(preference); + mHandler.removeCallbacks(mSyncRunnable); + mHandler.post(mSyncRunnable); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) { + super.onBindViewHolder(holder, position); + updateBackground(holder, position); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + private void updatePreferences() { + mRoundCornerMappingList = new ArrayList<>(); + mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup); + } + private void mappingPreferenceGroup(List visibleList, PreferenceGroup group) { + int groupSize = group.getPreferenceCount(); + int firstVisible = 0; + int lastVisible = 0; + for (int i = 0; i < groupSize; i++) { + Preference pref = group.getPreference(i); + if (!pref.isVisible()) { + continue; + } + + //the first visible preference. + Preference firstVisiblePref = group.getPreference(firstVisible); + if (!firstVisiblePref.isVisible()) { + firstVisible = i; + } + + int value = 0; + if (group instanceof PreferenceCategory) { + if (pref instanceof PreferenceCategory) { + visibleList.add(value); + mappingPreferenceGroup(visibleList, (PreferenceCategory) pref); + } else { + if (i == firstVisible) { + value |= ROUND_CORNER_TOP; + } + + value |= ROUND_CORNER_BOTTOM; + if (i > lastVisible) { + // the last + int lastIndex = visibleList.size() - 1; + int newValue = visibleList.get(lastIndex) & ~ROUND_CORNER_BOTTOM; + visibleList.set(lastIndex, newValue); + lastVisible = i; + } + + value |= ROUND_CORNER_CENTER; + visibleList.add(value); + } + } else { + visibleList.add(value); + if (pref instanceof PreferenceCategory) { + mappingPreferenceGroup(visibleList, (PreferenceCategory) pref); + } + } + } + } + + /** handle roundCorner background */ + private void updateBackground(PreferenceViewHolder holder, int position) { + int CornerType = mRoundCornerMappingList.get(position); + + if ((CornerType & ROUND_CORNER_CENTER) == 0) { + return; + } + + View v = holder.itemView; + if (((CornerType & ROUND_CORNER_TOP) != 0) && ((CornerType & ROUND_CORNER_BOTTOM) == 0)) { + // the first + v.setBackgroundResource(R.drawable.settingslib_round_background_top); + } else if (((CornerType & ROUND_CORNER_BOTTOM) != 0) + && ((CornerType & ROUND_CORNER_TOP) == 0)) { + // the last + v.setBackgroundResource(R.drawable.settingslib_round_background_bottom); + } else if (((CornerType & ROUND_CORNER_TOP) != 0) + && ((CornerType & ROUND_CORNER_BOTTOM) != 0)) { + // the only one preference + v.setBackgroundResource(R.drawable.settingslib_round_background); + } else { + // in the center + v.setBackgroundResource(R.drawable.settingslib_round_background_center); + } + } +} diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java index b95d927414a..ffc97dc722c 100644 --- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java @@ -444,7 +444,9 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { } if (TextUtils.equals(tile.getCategory(), CategoryKey.CATEGORY_HOMEPAGE)) { iconDrawable.setTint(Utils.getHomepageIconColor(preference.getContext())); - } else if (forceRoundedIcon && !TextUtils.equals(mContext.getPackageName(), iconPackage)) { + } + + if (forceRoundedIcon && !TextUtils.equals(mContext.getPackageName(), iconPackage)) { iconDrawable = new AdaptiveIcon(mContext, iconDrawable, R.dimen.dashboard_tile_foreground_image_inset); ((AdaptiveIcon) iconDrawable).setBackgroundColor(mContext, tile); diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index a2ca9ae2824..94408d64a87 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -72,6 +72,7 @@ import com.android.settings.activityembedding.ActivityEmbeddingRulesController; import com.android.settings.activityembedding.ActivityEmbeddingUtils; import com.android.settings.core.CategoryMixin; import com.android.settings.core.FeatureFlags; +import com.android.settings.flags.Flags; import com.android.settings.homepage.contextualcards.ContextualCardsFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.safetycenter.SafetyCenterManagerWrapper; @@ -159,8 +160,12 @@ public class SettingsHomepageActivity extends FragmentActivity implements if (mAllowUpdateSuggestion) { Log.i(TAG, "showHomepageWithSuggestion: " + showSuggestion); mAllowUpdateSuggestion = false; - mSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); - mTwoPaneSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); + if (Flags.homepageRevamp()) { + mSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); + } else { + mSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); + mTwoPaneSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); + } } if (mHomepageView == null) { @@ -244,7 +249,10 @@ public class SettingsHomepageActivity extends FragmentActivity implements } setupEdgeToEdge(); - setContentView(R.layout.settings_homepage_container); + setContentView( + Flags.homepageRevamp() + ? R.layout.settings_homepage_container_v2 + : R.layout.settings_homepage_container); mIsTwoPane = ActivityEmbeddingUtils.isAlreadyEmbedded(this); @@ -396,19 +404,31 @@ public class SettingsHomepageActivity extends FragmentActivity implements } private void initSearchBarView() { - final Toolbar toolbar = findViewById(R.id.search_action_bar); - FeatureFactory.getFeatureFactory().getSearchFeatureProvider() - .initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE); - - if (mIsEmbeddingActivityEnabled) { - final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane); + if (Flags.homepageRevamp()) { + Toolbar toolbar = findViewById(R.id.search_action_bar_unified); FeatureFactory.getFeatureFactory().getSearchFeatureProvider() - .initSearchToolbar(this /* activity */, toolbarTwoPaneVersion, + .initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE); + } else { + final Toolbar toolbar = findViewById(R.id.search_action_bar); + FeatureFactory.getFeatureFactory().getSearchFeatureProvider() + .initSearchToolbar(this /* activity */, toolbar, + SettingsEnums.SETTINGS_HOMEPAGE); + + if (mIsEmbeddingActivityEnabled) { + final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane); + FeatureFactory.getFeatureFactory().getSearchFeatureProvider() + .initSearchToolbar(this /* activity */, toolbarTwoPaneVersion, + SettingsEnums.SETTINGS_HOMEPAGE); + } } } private void initAvatarView() { + if (Flags.homepageRevamp()) { + return; + } + final ImageView avatarView = findViewById(R.id.account_avatar); final ImageView avatarTwoPaneView = findViewById(R.id.account_avatar_two_pane_version); if (AvatarViewMixin.isAvatarSupported(this)) { @@ -457,8 +477,12 @@ public class SettingsHomepageActivity extends FragmentActivity implements return; } - mSuggestionView = findViewById(R.id.suggestion_content); - mTwoPaneSuggestionView = findViewById(R.id.two_pane_suggestion_content); + if (Flags.homepageRevamp()) { + mSuggestionView = findViewById(R.id.unified_suggestion_content); + } else { + mSuggestionView = findViewById(R.id.suggestion_content); + mTwoPaneSuggestionView = findViewById(R.id.two_pane_suggestion_content); + } mHomepageView = findViewById(R.id.settings_homepage_container); // Hide the homepage for preparing the suggestion. If scrolling is needed, the list views // should be initialized in the invisible homepage view to prevent a scroll flicker. @@ -466,11 +490,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements // Schedule a timer to show the homepage and hide the suggestion on timeout. mHomepageView.postDelayed(() -> showHomepageWithSuggestion(false), HOMEPAGE_LOADING_TIMEOUT_MS); - showFragment(new SuggestionFragCreator(fragmentClass, /* isTwoPaneLayout= */ false), - R.id.suggestion_content); - if (mIsEmbeddingActivityEnabled) { - showFragment(new SuggestionFragCreator(fragmentClass, /* isTwoPaneLayout= */ true), - R.id.two_pane_suggestion_content); + if (Flags.homepageRevamp()) { + showFragment(new SuggestionFragCreator(fragmentClass, true), + R.id.unified_suggestion_content); + } else { + showFragment(new SuggestionFragCreator(fragmentClass, /* isTwoPaneLayout= */ false), + R.id.suggestion_content); + if (mIsEmbeddingActivityEnabled) { + showFragment(new SuggestionFragCreator(fragmentClass, /* isTwoPaneLayout= */ true), + R.id.two_pane_suggestion_content); + } } } @@ -735,7 +764,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements } private void updateHomepageAppBar() { - if (!mIsEmbeddingActivityEnabled) { + if (Flags.homepageRevamp() || !mIsEmbeddingActivityEnabled) { return; } updateAppBarMinHeight(); @@ -751,7 +780,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements } private void updateHomepagePaddings() { - if (!mIsEmbeddingActivityEnabled) { + if (Flags.homepageRevamp() || !mIsEmbeddingActivityEnabled) { return; } if (mIsTwoPane) { @@ -765,6 +794,9 @@ public class SettingsHomepageActivity extends FragmentActivity implements } private void updateAppBarMinHeight() { + if (Flags.homepageRevamp()) { + return; + } final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height); final int margin = getResources().getDimensionPixelSize( mIsEmbeddingActivityEnabled && mIsTwoPane diff --git a/src/com/android/settings/homepage/TopLevelSettings.java b/src/com/android/settings/homepage/TopLevelSettings.java index d1fa7601322..66428611a5b 100644 --- a/src/com/android/settings/homepage/TopLevelSettings.java +++ b/src/com/android/settings/homepage/TopLevelSettings.java @@ -42,8 +42,10 @@ import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.activityembedding.ActivityEmbeddingRulesController; import com.android.settings.activityembedding.ActivityEmbeddingUtils; +import com.android.settings.core.RoundCornerPreferenceAdapter; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.flags.Flags; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.support.SupportPreferenceController; @@ -84,7 +86,7 @@ public class TopLevelSettings extends DashboardFragment implements SplitLayoutLi @Override protected int getPreferenceScreenResId() { - return R.xml.top_level_settings; + return Flags.homepageRevamp() ? R.xml.top_level_settings_v2 : R.xml.top_level_settings; } @Override @@ -331,10 +333,14 @@ public class TopLevelSettings extends DashboardFragment implements SplitLayoutLi @Override protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { - if (!mIsEmbeddingActivityEnabled || !(getActivity() instanceof SettingsHomepageActivity)) { - return super.onCreateAdapter(preferenceScreen); + if (mIsEmbeddingActivityEnabled && (getActivity() instanceof SettingsHomepageActivity)) { + return mHighlightMixin.onCreateAdapter(this, preferenceScreen, mScrollNeeded); } - return mHighlightMixin.onCreateAdapter(this, preferenceScreen, mScrollNeeded); + + if (Flags.homepageRevamp()) { + return new RoundCornerPreferenceAdapter(preferenceScreen); + } + return super.onCreateAdapter(preferenceScreen); } @Override @@ -376,7 +382,10 @@ public class TopLevelSettings extends DashboardFragment implements SplitLayoutLi } public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider(R.xml.top_level_settings) { + new BaseSearchIndexProvider( + Flags.homepageRevamp() + ? R.xml.top_level_settings_v2 + : R.xml.top_level_settings) { @Override protected boolean isPageSearchEnabled(Context context) { diff --git a/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java b/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java index 8084a4811d6..4ba12056630 100644 --- a/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java +++ b/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java @@ -34,6 +34,7 @@ import androidx.window.embedding.ActivityEmbeddingController; import com.android.settings.R; import com.android.settings.Utils; +import com.android.settings.flags.Flags; import com.android.settings.homepage.SettingsHomepageActivity; /** @@ -46,9 +47,13 @@ public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapt static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 100L; private static final int RES_NORMAL_BACKGROUND = - R.drawable.homepage_selectable_item_background; + Flags.homepageRevamp() + ? R.drawable.homepage_selectable_item_background_v2 + : R.drawable.homepage_selectable_item_background; private static final int RES_HIGHLIGHTED_BACKGROUND = - R.drawable.homepage_highlighted_item_background; + Flags.homepageRevamp() + ? R.drawable.homepage_highlighted_item_background_v2 + : R.drawable.homepage_highlighted_item_background; private final int mTitleColorNormal; private final int mTitleColorHighlight; diff --git a/src/com/android/settings/widget/HomepagePreferenceLayoutHelper.java b/src/com/android/settings/widget/HomepagePreferenceLayoutHelper.java index 6242e23000c..2251180e5f6 100644 --- a/src/com/android/settings/widget/HomepagePreferenceLayoutHelper.java +++ b/src/com/android/settings/widget/HomepagePreferenceLayoutHelper.java @@ -22,6 +22,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; import com.android.settings.R; +import com.android.settings.flags.Flags; /** Helper for homepage preference to manage layout. */ public class HomepagePreferenceLayoutHelper { @@ -39,7 +40,10 @@ public class HomepagePreferenceLayoutHelper { } public HomepagePreferenceLayoutHelper(Preference preference) { - preference.setLayoutResource(R.layout.homepage_preference); + preference.setLayoutResource( + Flags.homepageRevamp() + ? R.layout.homepage_preference_v2 + : R.layout.homepage_preference); } /** Sets whether the icon should be visible */ diff --git a/tests/robotests/src/com/android/settings/homepage/TopLevelSettingsTest.java b/tests/robotests/src/com/android/settings/homepage/TopLevelSettingsTest.java index 44f44aa8589..36c48e3cdd7 100644 --- a/tests/robotests/src/com/android/settings/homepage/TopLevelSettingsTest.java +++ b/tests/robotests/src/com/android/settings/homepage/TopLevelSettingsTest.java @@ -16,8 +16,6 @@ package com.android.settings.homepage; -import static com.google.common.truth.Truth.assertThat; - import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doReturn; @@ -59,11 +57,6 @@ public class TopLevelSettingsTest { mSettings.onAttach(mContext); } - @Test - public void shouldForceRoundedIcon_true() { - assertThat(mSettings.shouldForceRoundedIcon()).isTrue(); - } - @Test public void onCreatePreferences_shouldTintPreferenceIcon() { final Preference preference = new Preference(mContext); From 180b2d956127a50be8cc5ab87bdfbb8616a5ccc1 Mon Sep 17 00:00:00 2001 From: tomhsu Date: Thu, 9 May 2024 06:16:34 +0000 Subject: [PATCH 10/22] Expose SatelliteWarningDialogActivity Bug: 337154438 Test: Manual test Test: Build pass Change-Id: I530c554433d99b4732416b8f1b83677ddc0ff6d8 Merged-In: I530c554433d99b4732416b8f1b83677ddc0ff6d8 --- AndroidManifest.xml | 3 ++- .../settings/network/SatelliteWarningDialogActivity.kt | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2282cdbda3d..b8dd331823a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5146,7 +5146,8 @@ android:name="com.android.settings.network.SatelliteWarningDialogActivity" android:configChanges="orientation|keyboard|keyboardHidden|screenSize|screenLayout|smallestScreenSize" android:excludeFromRecents="true" - android:exported="false" + android:exported="true" + android:permission="android.permission.NETWORK_SETTINGS" android:theme="@style/Theme.SpaLib.Dialog"> diff --git a/src/com/android/settings/network/SatelliteWarningDialogActivity.kt b/src/com/android/settings/network/SatelliteWarningDialogActivity.kt index 0702e4fbc66..3f1d416951d 100644 --- a/src/com/android/settings/network/SatelliteWarningDialogActivity.kt +++ b/src/com/android/settings/network/SatelliteWarningDialogActivity.kt @@ -27,6 +27,7 @@ import com.android.settings.R 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 /** A dialog to show the warning message when device is under satellite mode. */ class SatelliteWarningDialogActivity : SpaDialogWindowTypeActivity() { @@ -41,7 +42,10 @@ class SatelliteWarningDialogActivity : SpaDialogWindowTypeActivity() { } override fun getDialogWindowType(): Int { - return WindowManager.LayoutParams.LAST_APPLICATION_WINDOW + return intent.getIntExtra( + WifiUtils.DIALOG_WINDOW_TYPE, + WindowManager.LayoutParams.LAST_APPLICATION_WINDOW + ) } @Composable From 51dd526faf9dfb3e83bdf2fb2bf27702f7d569ac Mon Sep 17 00:00:00 2001 From: Manish Singh Date: Tue, 7 May 2024 18:20:21 +0000 Subject: [PATCH 11/22] Support multi-pane deeplink in PrivateSpaceAuthenticationActivity Bug: 334792208 Fix: 336996032 Test: manual Change-Id: I0cffe64faa1f0981ee09e3775249efcd7073d7ce --- AndroidManifest.xml | 2 - .../PrivateSpaceAuthenticationActivity.java | 62 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ad815516c61..4fa6e0b6297 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5138,8 +5138,6 @@ - diff --git a/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivity.java b/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivity.java index 623816a01b4..0b35bd3fbc2 100644 --- a/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivity.java +++ b/src/com/android/settings/privatespace/PrivateSpaceAuthenticationActivity.java @@ -19,6 +19,7 @@ package com.android.settings.privatespace; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; import static com.android.internal.app.SetScreenLockDialogActivity.LAUNCH_REASON_PRIVATE_SPACE_SETTINGS_ACCESS; +import static com.android.settings.activityembedding.EmbeddedDeepLinkUtils.tryStartMultiPaneDeepLink; import android.app.ActivityOptions; import android.app.AlertDialog; @@ -36,11 +37,12 @@ import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.SetScreenLockDialogActivity; import com.android.settings.R; -import com.android.settings.SettingsActivity; +import com.android.settings.activityembedding.ActivityEmbeddingUtils; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.transition.SettingsTransitionHelper; @@ -52,7 +54,7 @@ import com.google.android.setupdesign.util.ThemeHelper; * user to set a device lock if not set with an alert dialog. This can be launched using the intent * com.android.settings.action.OPEN_PRIVATE_SPACE_SETTINGS. */ -public class PrivateSpaceAuthenticationActivity extends SettingsActivity { +public class PrivateSpaceAuthenticationActivity extends FragmentActivity { private static final String TAG = "PrivateSpaceAuthCheck"; public static final String EXTRA_SHOW_PRIVATE_SPACE_UNLOCKED = "extra_show_private_space_unlocked"; @@ -76,31 +78,55 @@ public class PrivateSpaceAuthenticationActivity extends SettingsActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (isFinishing()) { + if (!(Flags.allowPrivateProfile() + && android.multiuser.Flags.enablePrivateSpaceFeatures())) { + finish(); return; } - if (Flags.allowPrivateProfile() - && android.multiuser.Flags.enablePrivateSpaceFeatures()) { - ThemeHelper.trySetDynamicColor(this); - mPrivateSpaceMaintainer = - new Injector().injectPrivateSpaceMaintainer(getApplicationContext()); - if (getKeyguardManager().isDeviceSecure()) { - if (savedInstanceState == null) { - if (mPrivateSpaceMaintainer.doesPrivateSpaceExist()) { - unlockAndLaunchPrivateSpaceSettings(this); - } else { - authenticatePrivateSpaceEntry(); - } + Intent intent = getIntent(); + String highlightMenuKey = getString(R.string.menu_key_security); + if (shouldShowMultiPaneDeepLink(intent) + && tryStartMultiPaneDeepLink(this, intent, highlightMenuKey)) { + finish(); + return; + } + + ThemeHelper.trySetDynamicColor(this); + mPrivateSpaceMaintainer = + new Injector().injectPrivateSpaceMaintainer(getApplicationContext()); + if (getKeyguardManager().isDeviceSecure()) { + if (savedInstanceState == null) { + if (mPrivateSpaceMaintainer.doesPrivateSpaceExist()) { + unlockAndLaunchPrivateSpaceSettings(this); + } else { + authenticatePrivateSpaceEntry(); } - } else { - promptToSetDeviceLock(); } } else { - finish(); + promptToSetDeviceLock(); } } + private boolean shouldShowMultiPaneDeepLink(Intent intent) { + if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) { + return false; + } + + // If the activity is task root, starting trampoline is needed in order to show two-pane UI. + // If FLAG_ACTIVITY_NEW_TASK is set, the activity will become the start of a new task on + // this history stack, so starting trampoline is needed in order to notify the homepage that + // the highlight key is changed. + if (!isTaskRoot() && (intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0) { + return false; + } + + // Only starts trampoline for deep links. Should return false for all the cases that + // Settings app starts SettingsActivity or SubSetting by itself. + // Other apps should send deep link intent which matches intent filter of the Activity. + return intent.getAction() != null; + } + /** Starts private space setup flow or the PS settings page on device lock authentication */ @VisibleForTesting public void onLockAuthentication(Context context) { From 8c507e871be722369e74d71c933e7728799daa4f Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Wed, 8 May 2024 17:55:30 +0800 Subject: [PATCH 12/22] Catch exception in telephonyRepository.isDataEnabledFlow And migrate BillingCycleRepository to use it. Fix: 339197552 Test: manual - on data usage Test: unit test Change-Id: Ieac295f37fdbf75d184d66ea11f170652af3ec5f --- .../datausage/BillingCyclePreference.kt | 6 ++---- .../settings/datausage/DataUsageList.kt | 16 +++++++--------- .../datausage/lib/BillingCycleRepository.kt | 19 +++++++++++-------- .../network/telephony/TelephonyRepository.kt | 17 +++++++++++------ .../network/NetworkCellularGroupProvider.kt | 5 ++--- .../datausage/BillingCyclePreferenceTest.kt | 10 +++++++--- .../lib/BillingCycleRepositoryTest.kt | 18 ++++++++++-------- .../telephony/TelephonyRepositoryTest.kt | 6 ++---- 8 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/com/android/settings/datausage/BillingCyclePreference.kt b/src/com/android/settings/datausage/BillingCyclePreference.kt index a6904bc4eb6..8dd7d0f7030 100644 --- a/src/com/android/settings/datausage/BillingCyclePreference.kt +++ b/src/com/android/settings/datausage/BillingCyclePreference.kt @@ -26,11 +26,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R import com.android.settings.core.SubSettingLauncher import com.android.settings.datausage.lib.BillingCycleRepository -import com.android.settings.network.mobileDataEnabledFlow import com.android.settings.spa.preference.ComposePreference import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -import kotlinx.coroutines.flow.map /** * Preference which displays billing cycle of subscription @@ -46,8 +44,8 @@ class BillingCyclePreference @JvmOverloads constructor( override fun setTemplate(template: NetworkTemplate, subId: Int) { setContent { - val isModifiable by remember { - context.mobileDataEnabledFlow(subId).map { repository.isModifiable(subId) } + val isModifiable by remember(subId) { + repository.isModifiableFlow(subId) }.collectAsStateWithLifecycle(initialValue = false) Preference(object : PreferenceModel { diff --git a/src/com/android/settings/datausage/DataUsageList.kt b/src/com/android/settings/datausage/DataUsageList.kt index 1995097d6f1..a8f5460a18c 100644 --- a/src/com/android/settings/datausage/DataUsageList.kt +++ b/src/com/android/settings/datausage/DataUsageList.kt @@ -35,7 +35,7 @@ import com.android.settings.datausage.lib.BillingCycleRepository import com.android.settings.datausage.lib.NetworkUsageData import com.android.settings.network.MobileNetworkRepository import com.android.settings.network.SubscriptionUtil -import com.android.settings.network.mobileDataEnabledFlow +import com.android.settings.network.telephony.requireSubscriptionManager import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spaprivileged.framework.common.userManager @@ -113,8 +113,8 @@ open class DataUsageList : DashboardFragment() { override fun onViewCreated(v: View, savedInstanceState: Bundle?) { super.onViewCreated(v, savedInstanceState) - requireContext().mobileDataEnabledFlow(subId) - .collectLatestWithLifecycle(viewLifecycleOwner) { updatePolicy() } + billingCycleRepository.isModifiableFlow(subId) + .collectLatestWithLifecycle(viewLifecycleOwner, action = ::updatePolicy) val template = template ?: return viewModel.templateFlow.value = template @@ -163,16 +163,14 @@ open class DataUsageList : DashboardFragment() { } /** Update chart sweeps and cycle list to reflect [NetworkPolicy] for current [template]. */ - private fun updatePolicy() { - val isBillingCycleModifiable = isBillingCycleModifiable() + private fun updatePolicy(isModifiable: Boolean) { + val isBillingCycleModifiable = isModifiable && isActiveSubscription() dataUsageListHeaderController?.setConfigButtonVisible(isBillingCycleModifiable) chartDataUsagePreferenceController?.setBillingCycleModifiable(isBillingCycleModifiable) } - private fun isBillingCycleModifiable(): Boolean = - billingCycleRepository.isModifiable(subId) && - requireContext().getSystemService(SubscriptionManager::class.java)!! - .getActiveSubscriptionInfo(subId) != null + private fun isActiveSubscription(): Boolean = + requireContext().requireSubscriptionManager().getActiveSubscriptionInfo(subId) != null /** * Updates the chart and detail data when initial loaded or selected cycle changed. diff --git a/src/com/android/settings/datausage/lib/BillingCycleRepository.kt b/src/com/android/settings/datausage/lib/BillingCycleRepository.kt index bd6aa273ad2..d324c75d6f9 100644 --- a/src/com/android/settings/datausage/lib/BillingCycleRepository.kt +++ b/src/com/android/settings/datausage/lib/BillingCycleRepository.kt @@ -19,10 +19,15 @@ package com.android.settings.datausage.lib import android.content.Context import android.os.INetworkManagementService import android.os.ServiceManager -import android.telephony.TelephonyManager import android.util.Log import androidx.annotation.OpenForTesting +import com.android.settings.network.telephony.TelephonyRepository import com.android.settingslib.spaprivileged.framework.common.userManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map @OpenForTesting open class BillingCycleRepository @JvmOverloads constructor( @@ -31,12 +36,14 @@ open class BillingCycleRepository @JvmOverloads constructor( INetworkManagementService.Stub.asInterface( ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE) ), + private val telephonyRepository: TelephonyRepository = TelephonyRepository(context), ) { private val userManager = context.userManager - private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!! - fun isModifiable(subId: Int): Boolean = - isBandwidthControlEnabled() && userManager.isAdminUser && isDataEnabled(subId) + fun isModifiableFlow(subId: Int): Flow = + telephonyRepository.isDataEnabledFlow(subId).map { isDataEnabled -> + isDataEnabled && isBandwidthControlEnabled() && userManager.isAdminUser + }.conflate().flowOn(Dispatchers.Default) open fun isBandwidthControlEnabled(): Boolean = try { networkService.isBandwidthControlEnabled @@ -45,10 +52,6 @@ open class BillingCycleRepository @JvmOverloads constructor( false } - private fun isDataEnabled(subId: Int): Boolean = - telephonyManager.createForSubscriptionId(subId) - .isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) - companion object { private const val TAG = "BillingCycleRepository" } diff --git a/src/com/android/settings/network/telephony/TelephonyRepository.kt b/src/com/android/settings/network/telephony/TelephonyRepository.kt index cc9b53dba82..d0d53b7be95 100644 --- a/src/com/android/settings/network/telephony/TelephonyRepository.kt +++ b/src/com/android/settings/network/telephony/TelephonyRepository.kt @@ -29,10 +29,12 @@ import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach class TelephonyRepository( private val context: Context, @@ -64,19 +66,21 @@ class TelephonyRepository( telephonyManager.setMobileDataPolicyEnabled(policy, enabled) } - fun isDataEnabled( - subId: Int, - ): Flow { + fun isDataEnabledFlow(subId: Int): Flow { if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false) - Log.d(TAG, "register mobileDataEnabledFlow: [$subId]") return context.mobileDataEnabledFlow(subId) .map { - Log.d(TAG, "mobileDataEnabledFlow: receive mobile data [$subId] start") val telephonyManager = context.telephonyManager(subId) telephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) - .also { Log.d(TAG, "mobileDataEnabledFlow: [$subId] isDataEnabled(): $it") } } + .catch { + Log.w(TAG, "[$subId] isDataEnabledFlow: exception", it) + emit(false) + } + .onEach { Log.d(TAG, "[$subId] isDataEnabledFlow: isDataEnabled() = $it") } + .conflate() + .flowOn(Dispatchers.Default) } fun setMobileData( @@ -100,6 +104,7 @@ class TelephonyRepository( wifiPickerTrackerHelper.setCarrierNetworkEnabled(enabled) } } + private companion object { private const val TAG = "TelephonyRepository" } diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt index 28b7a9e1cab..98d83402339 100644 --- a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt +++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt @@ -36,11 +36,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settings.R @@ -62,7 +62,6 @@ import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBool import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOf @@ -207,7 +206,7 @@ fun MobileDataSectionImpl( }.collectAsStateWithLifecycle(initialValue = null) val mobileDataStateChanged by remember(mobileDataSelectedId.intValue) { - TelephonyRepository(context).isDataEnabled(mobileDataSelectedId.intValue) + TelephonyRepository(context).isDataEnabledFlow(mobileDataSelectedId.intValue) }.collectAsStateWithLifecycle(initialValue = false) val coroutineScope = rememberCoroutineScope() diff --git a/tests/spa_unit/src/com/android/settings/datausage/BillingCyclePreferenceTest.kt b/tests/spa_unit/src/com/android/settings/datausage/BillingCyclePreferenceTest.kt index 4bf385169d3..1db0d48f211 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/BillingCyclePreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/BillingCyclePreferenceTest.kt @@ -27,6 +27,8 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R import com.android.settings.datausage.lib.BillingCycleRepository +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -39,7 +41,9 @@ class BillingCyclePreferenceTest { @get:Rule val composeTestRule = createComposeRule() - private val mockBillingCycleRepository = mock() + private val mockBillingCycleRepository = mock { + on { isModifiableFlow(SUB_ID) } doReturn emptyFlow() + } private val context: Context = ApplicationProvider.getApplicationContext() @@ -56,7 +60,7 @@ class BillingCyclePreferenceTest { @Test fun setTemplate_modifiable_enabled() { mockBillingCycleRepository.stub { - on { isModifiable(SUB_ID) } doReturn true + on { isModifiableFlow(SUB_ID) } doReturn flowOf(true) } setTemplate() @@ -67,7 +71,7 @@ class BillingCyclePreferenceTest { @Test fun setTemplate_notModifiable_notEnabled() { mockBillingCycleRepository.stub { - on { isModifiable(SUB_ID) } doReturn false + on { isModifiableFlow(SUB_ID) } doReturn flowOf(false) } setTemplate() diff --git a/tests/spa_unit/src/com/android/settings/datausage/lib/BillingCycleRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/lib/BillingCycleRepositoryTest.kt index deaaf2dd1f0..22e5dfe14b4 100644 --- a/tests/spa_unit/src/com/android/settings/datausage/lib/BillingCycleRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/datausage/lib/BillingCycleRepositoryTest.kt @@ -22,8 +22,10 @@ import android.os.UserManager import android.telephony.TelephonyManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull import com.android.settingslib.spaprivileged.framework.common.userManager import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.doReturn @@ -55,43 +57,43 @@ class BillingCycleRepositoryTest { private val repository = BillingCycleRepository(context, mockNetworkManagementService) @Test - fun isModifiable_bandwidthControlDisabled_returnFalse() { + fun isModifiable_bandwidthControlDisabled_returnFalse() = runBlocking { whenever(mockNetworkManagementService.isBandwidthControlEnabled).thenReturn(false) - val modifiable = repository.isModifiable(SUB_ID) + val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull() assertThat(modifiable).isFalse() } @Test - fun isModifiable_notAdminUser_returnFalse() { + fun isModifiable_notAdminUser_returnFalse() = runBlocking { whenever(mockUserManager.isAdminUser).thenReturn(false) - val modifiable = repository.isModifiable(SUB_ID) + val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull() assertThat(modifiable).isFalse() } @Test - fun isModifiable_dataDisabled_returnFalse() { + fun isModifiable_dataDisabled_returnFalse() = runBlocking { whenever( mockTelephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) ).thenReturn(false) - val modifiable = repository.isModifiable(SUB_ID) + val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull() assertThat(modifiable).isFalse() } @Test - fun isModifiable_meetAllRequirements_returnTrue() { + fun isModifiable_meetAllRequirements_returnTrue() = runBlocking { whenever(mockNetworkManagementService.isBandwidthControlEnabled).thenReturn(true) whenever(mockUserManager.isAdminUser).thenReturn(true) whenever( mockTelephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) ).thenReturn(true) - val modifiable = repository.isModifiable(SUB_ID) + val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull() assertThat(modifiable).isTrue() } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt index 60589358e3a..65e8c47023d 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt @@ -93,7 +93,7 @@ class TelephonyRepositoryTest { @Test fun isDataEnabled_invalidSub_returnFalse() = runBlocking { - val state = repository.isDataEnabled( + val state = repository.isDataEnabledFlow( subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID, ) @@ -108,9 +108,7 @@ class TelephonyRepositoryTest { } doReturn true } - val state = repository.isDataEnabled( - subId = SUB_ID, - ) + val state = repository.isDataEnabledFlow(subId = SUB_ID) assertThat(state.firstWithTimeoutOrNull()).isTrue() } From d97b7812510c504b5210f1babf9becbff8aa846f Mon Sep 17 00:00:00 2001 From: Edgar Wang Date: Thu, 9 May 2024 18:51:27 +0000 Subject: [PATCH 13/22] Fix the touch area of the edit box is not large enough in Fingerprint unlock page Bug: 315405247 Change-Id: Ic15f051e12ccc04575e5c0801104633d4d733dab Test: manual --- res/layout/fingerprint_rename_dialog.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/layout/fingerprint_rename_dialog.xml b/res/layout/fingerprint_rename_dialog.xml index 1e1ef112a5c..070d9249d51 100644 --- a/res/layout/fingerprint_rename_dialog.xml +++ b/res/layout/fingerprint_rename_dialog.xml @@ -39,6 +39,7 @@ android:id="@+id/fingerprint_rename_field" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="textCapWords"/> + android:inputType="textCapWords" + android:minHeight = "48dp"/> From 225bb7be88bd3df6e3d19ce991bd411e70da426a Mon Sep 17 00:00:00 2001 From: Edgar Wang Date: Thu, 9 May 2024 18:57:15 +0000 Subject: [PATCH 14/22] Allow WebViewAppPicker can be launched when dev-option disabled Bug: 333135859 Change-Id: If91e0299ef7a0ccf92b489a1c85b04972119e075 Test: manual --- src/com/android/settings/webview/WebViewAppPicker.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/com/android/settings/webview/WebViewAppPicker.java b/src/com/android/settings/webview/WebViewAppPicker.java index 0060fa0c7b7..b1dfd1454f9 100644 --- a/src/com/android/settings/webview/WebViewAppPicker.java +++ b/src/com/android/settings/webview/WebViewAppPicker.java @@ -33,14 +33,12 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; -import com.android.settings.development.DeveloperOptionAwareMixin; import com.android.settingslib.applications.DefaultAppInfo; import java.util.ArrayList; import java.util.List; -public class WebViewAppPicker extends DefaultAppPickerFragment implements - DeveloperOptionAwareMixin { +public class WebViewAppPicker extends DefaultAppPickerFragment { private WebViewUpdateServiceWrapper mWebViewUpdateServiceWrapper; private WebViewUpdateServiceWrapper getWebViewUpdateServiceWrapper() { From 2028535d06ebc341ef2fdf3be482629835fc03a7 Mon Sep 17 00:00:00 2001 From: Srinivas Patibandla Date: Tue, 30 Apr 2024 23:35:03 +0000 Subject: [PATCH 15/22] [Hide DCK Device] Update unit tests per change in exclusive manager verification Test: atest: com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdaterTest Test: atest: com.android.settings.bluetooth.SavedBluetoothDeviceUpdaterTest Bug: 322285078 Bug: 324475542 Flag: ACONFIG com.android.settingslib.flags.enable_hide_exclusively_managed_bluetooth_device NEXTFOOD Change-Id: I5a4f9eccc461033aeca79ea657af61958af0c660 --- .../ConnectedBluetoothDeviceUpdaterTest.java | 87 +++++++------------ .../SavedBluetoothDeviceUpdaterTest.java | 83 ++++++++---------- 2 files changed, 68 insertions(+), 102 deletions(-) diff --git a/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java index ee000686b2a..b2449dab39e 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java @@ -30,7 +30,7 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; -import android.content.pm.PackageInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.media.AudioManager; @@ -44,7 +44,6 @@ import com.android.settings.dashboard.DashboardFragment; import com.android.settings.testutils.shadow.ShadowAudioManager; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowCachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.flags.Flags; @@ -68,7 +67,7 @@ import java.util.Collection; public class ConnectedBluetoothDeviceUpdaterTest { private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C"; - private static final String FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name"; + private static final String TEST_EXCLUSIVE_MANAGER = "com.test.manager"; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -355,13 +354,16 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_notAllowedExclusiveManagedDevice_addDevice() { + public void update_exclusivelyManagedDevice_packageNotInstalled_addDevice() + throws Exception { mAudioManager.setMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - FAKE_EXCLUSIVE_MANAGER_NAME.getBytes()); + TEST_EXCLUSIVE_MANAGER.getBytes()); + doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager) + .getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0); mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); @@ -370,64 +372,39 @@ public class ConnectedBluetoothDeviceUpdaterTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_existingExclusivelyManagedDeviceWithPackageInstalled_removePreference() + public void update_exclusivelyManagedDevice_packageNotEnabled_addDevice() throws Exception { - final String exclusiveManagerName = - BluetoothUtils.getExclusiveManagers().stream().findAny().orElse( - FAKE_EXCLUSIVE_MANAGER_NAME); + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.enabled = false; mAudioManager.setMode(AudioManager.MODE_NORMAL); when(mBluetoothDeviceUpdater - .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); + .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - exclusiveManagerName.getBytes()); - doReturn(new PackageInfo()).when(mPackageManager).getPackageInfo(exclusiveManagerName, 0); - - mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); - - verify(mBluetoothDeviceUpdater).removePreference(mCachedBluetoothDevice); - verify(mBluetoothDeviceUpdater, never()).addPreference(mCachedBluetoothDevice); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_newExclusivelyManagedDeviceWithPackageInstalled_doNotAddPreference() - throws Exception { - final String exclusiveManagerName = - BluetoothUtils.getExclusiveManagers().stream().findAny().orElse( - FAKE_EXCLUSIVE_MANAGER_NAME); - mAudioManager.setMode(AudioManager.MODE_NORMAL); - when(mBluetoothDeviceUpdater - .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); - when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); - when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - exclusiveManagerName.getBytes()); - doReturn(new PackageInfo()).when(mPackageManager).getPackageInfo(exclusiveManagerName, 0); - - mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); - - verify(mBluetoothDeviceUpdater).removePreference(mCachedBluetoothDevice); - verify(mBluetoothDeviceUpdater, never()).addPreference(mCachedBluetoothDevice); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_exclusivelyManagedDeviceWithoutPackageInstalled_addDevice() - throws Exception { - final String exclusiveManagerName = - BluetoothUtils.getExclusiveManagers().stream().findAny().orElse( - FAKE_EXCLUSIVE_MANAGER_NAME); - mAudioManager.setMode(AudioManager.MODE_NORMAL); - when(mBluetoothDeviceUpdater - .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); - when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); - when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - exclusiveManagerName.getBytes()); - doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager).getPackageInfo( - exclusiveManagerName, 0); + TEST_EXCLUSIVE_MANAGER.getBytes()); + doReturn(appInfo).when(mPackageManager).getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0); mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice); } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + public void update_exclusivelyManagedDevice_packageInstalledAndEnabled_removePreference() + throws Exception { + mAudioManager.setMode(AudioManager.MODE_NORMAL); + when(mBluetoothDeviceUpdater + .isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true); + when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true); + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( + TEST_EXCLUSIVE_MANAGER.getBytes()); + doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo( + TEST_EXCLUSIVE_MANAGER, 0); + + mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); + + verify(mBluetoothDeviceUpdater).removePreference(mCachedBluetoothDevice); + verify(mBluetoothDeviceUpdater, never()).addPreference(mCachedBluetoothDevice); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdaterTest.java index 796120d43f5..e2cf14810c4 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdaterTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdaterTest.java @@ -29,7 +29,7 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; -import android.content.pm.PackageInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.platform.test.annotations.RequiresFlagsDisabled; @@ -41,7 +41,6 @@ import android.util.Pair; import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; -import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -66,7 +65,7 @@ import java.util.List; public class SavedBluetoothDeviceUpdaterTest { private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C"; - private static final String FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name"; + private static final String TEST_EXCLUSIVE_MANAGER = "com.test.manager"; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -339,42 +338,18 @@ public class SavedBluetoothDeviceUpdaterTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_notAllowedExclusivelyManagedDevice_addDevice() { - final Collection cachedDevices = new ArrayList<>(); - cachedDevices.add(mCachedBluetoothDevice); - - when(mBluetoothAdapter.isEnabled()).thenReturn(true); - when(mBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); - when(mDeviceManager.getCachedDevicesCopy()).thenReturn(cachedDevices); - when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - when(mBluetoothDevice.isConnected()).thenReturn(false); - when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - FAKE_EXCLUSIVE_MANAGER_NAME.getBytes()); - - mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); - - verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice, - BluetoothDevicePreference.SortType.TYPE_NO_SORT); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_existingExclusivelyManagedDeviceWithPackageInstalled_removePreference() + public void update_existingExclusivelyManagedDevice_packageEnabled_removePreference() throws Exception { final Collection cachedDevices = new ArrayList<>(); - final String exclusiveManagerName = - BluetoothUtils.getExclusiveManagers().stream().findAny().orElse( - FAKE_EXCLUSIVE_MANAGER_NAME); - when(mBluetoothAdapter.isEnabled()).thenReturn(true); when(mBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mDeviceManager.getCachedDevicesCopy()).thenReturn(cachedDevices); when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mBluetoothDevice.isConnected()).thenReturn(false); when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - exclusiveManagerName.getBytes()); - - doReturn(new PackageInfo()).when(mPackageManager).getPackageInfo(exclusiveManagerName, 0); + TEST_EXCLUSIVE_MANAGER.getBytes()); + doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo( + TEST_EXCLUSIVE_MANAGER, 0); mBluetoothDeviceUpdater.mPreferenceMap.put(mBluetoothDevice, mPreference); mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); @@ -386,23 +361,19 @@ public class SavedBluetoothDeviceUpdaterTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_newExclusivelyManagedDeviceWithPackageInstalled_doNotAddPreference() + public void update_newExclusivelyManagedDevice_packageEnabled_doNotAddPreference() throws Exception { final Collection cachedDevices = new ArrayList<>(); - final String exclusiveManagerName = - BluetoothUtils.getExclusiveManagers().stream().findAny().orElse( - FAKE_EXCLUSIVE_MANAGER_NAME); cachedDevices.add(mCachedBluetoothDevice); - when(mBluetoothAdapter.isEnabled()).thenReturn(true); when(mBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mDeviceManager.getCachedDevicesCopy()).thenReturn(cachedDevices); when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mBluetoothDevice.isConnected()).thenReturn(false); when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - exclusiveManagerName.getBytes()); - - doReturn(new PackageInfo()).when(mPackageManager).getPackageInfo(exclusiveManagerName, 0); + TEST_EXCLUSIVE_MANAGER.getBytes()); + doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo( + TEST_EXCLUSIVE_MANAGER, 0); mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); @@ -413,24 +384,42 @@ public class SavedBluetoothDeviceUpdaterTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - public void update_exclusivelyManagedDeviceWithoutPackageInstalled_addDevice() + public void update_exclusivelyManagedDevice_packageNotInstalled_addDevice() throws Exception { final Collection cachedDevices = new ArrayList<>(); - final String exclusiveManagerName = - BluetoothUtils.getExclusiveManagers().stream().findAny().orElse( - FAKE_EXCLUSIVE_MANAGER_NAME); cachedDevices.add(mCachedBluetoothDevice); - when(mBluetoothAdapter.isEnabled()).thenReturn(true); when(mBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mDeviceManager.getCachedDevicesCopy()).thenReturn(cachedDevices); when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mBluetoothDevice.isConnected()).thenReturn(false); when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( - exclusiveManagerName.getBytes()); + TEST_EXCLUSIVE_MANAGER.getBytes()); + doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager) + .getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0); - doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager).getPackageInfo( - exclusiveManagerName, 0); + mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); + + verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice, + BluetoothDevicePreference.SortType.TYPE_NO_SORT); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + public void update_exclusivelyManagedDevice_packageNotEnabled_addDevice() + throws Exception { + final Collection cachedDevices = new ArrayList<>(); + cachedDevices.add(mCachedBluetoothDevice); + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.enabled = false; + when(mBluetoothAdapter.isEnabled()).thenReturn(true); + when(mBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); + when(mDeviceManager.getCachedDevicesCopy()).thenReturn(cachedDevices); + when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mBluetoothDevice.isConnected()).thenReturn(false); + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn( + TEST_EXCLUSIVE_MANAGER.getBytes()); + doReturn(appInfo).when(mPackageManager).getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0); mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); From 658bc03d4ff0efbe0bb8964f7bdd60339a9d916b Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Wed, 8 May 2024 16:24:01 +0800 Subject: [PATCH 16/22] Update time format for the first timestamp on usage chartview. - If usage data start from the time-change event rather than full-charged event [Before] https://screenshot.googleplex.com/BokAvKHXmt2Mmwn [After] https://screenshot.googleplex.com/8thpgVrVt8kqo37 Bug: 336423923 Test: atest SettingsRoboTests:com.android.settings.fuelgauge.batteryusage Change-Id: I66f8b384938f55852e28bd9f50d1a99c7fc9e41b --- .../BatteryChartPreferenceController.java | 9 +- .../batteryusage/BatteryLevelData.java | 27 +++++- .../BatteryChartPreferenceControllerTest.java | 84 ++++++++++++++----- .../batteryusage/DataProcessManagerTest.java | 6 +- .../batteryusage/DataProcessorTest.java | 12 ++- 5 files changed, 103 insertions(+), 35 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java index b938c72e4b2..5e17f4b4871 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java @@ -649,9 +649,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll private final class HourlyChartLabelTextGenerator extends BaseLabelTextGenerator implements BatteryChartViewModel.LabelTextGenerator { - private static final int FULL_CHARGE_BATTERY_LEVEL = 100; - - private boolean mIsFromFullCharge; + private boolean mIsStartTimestamp; private long mFistTimestamp; private long mLatestTimestamp; @@ -664,7 +662,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll long timestamp = timestamps.get(index); boolean showMinute = false; if (Objects.equal(timestamp, mFistTimestamp)) { - if (mIsFromFullCharge) { + if (mIsStartTimestamp) { showMinute = true; } else { // starts from 7 days ago @@ -699,8 +697,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll @NonNull final BatteryLevelData batteryLevelData) { BatteryLevelData.PeriodBatteryLevelData firstDayLevelData = batteryLevelData.getHourlyBatteryLevelsPerDay().get(0); - this.mIsFromFullCharge = - firstDayLevelData.getLevels().get(0) == FULL_CHARGE_BATTERY_LEVEL; + this.mIsStartTimestamp = firstDayLevelData.isStartTimestamp(); this.mFistTimestamp = firstDayLevelData.getTimestamps().get(0); this.mLatestTimestamp = getLast( diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java index 231c730a176..d1bf49b6cc5 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java @@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting; import androidx.core.util.Preconditions; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -39,17 +40,24 @@ public final class BatteryLevelData { private static final long MIN_SIZE = 2; private static final long TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2; + // For testing only. + @VisibleForTesting @Nullable static Calendar sTestCalendar; + /** A container for the battery timestamp and level data. */ public static final class PeriodBatteryLevelData { // The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when // there is no level data for the corresponding timestamp. private final List mTimestamps; private final List mLevels; + private final boolean mIsStartTimestamp; public PeriodBatteryLevelData( - @NonNull Map batteryLevelMap, @NonNull List timestamps) { + @NonNull Map batteryLevelMap, + @NonNull List timestamps, + boolean isStartTimestamp) { mTimestamps = timestamps; mLevels = new ArrayList<>(timestamps.size()); + mIsStartTimestamp = isStartTimestamp; for (Long timestamp : timestamps) { mLevels.add( batteryLevelMap.containsKey(timestamp) @@ -66,6 +74,10 @@ public final class BatteryLevelData { return mLevels; } + public boolean isStartTimestamp() { + return mIsStartTimestamp; + } + @Override public String toString() { return String.format( @@ -105,14 +117,21 @@ public final class BatteryLevelData { final List timestampList = new ArrayList<>(batteryLevelMap.keySet()); Collections.sort(timestampList); + final long minTimestamp = timestampList.get(0); + final long sixDaysAgoTimestamp = + DatabaseUtils.getTimestampSixDaysAgo(sTestCalendar != null ? sTestCalendar : null); + final boolean isStartTimestamp = minTimestamp > sixDaysAgoTimestamp; final List dailyTimestamps = getDailyTimestamps(timestampList); final List> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps); - mDailyBatteryLevels = new PeriodBatteryLevelData(batteryLevelMap, dailyTimestamps); + mDailyBatteryLevels = + new PeriodBatteryLevelData(batteryLevelMap, dailyTimestamps, isStartTimestamp); mHourlyBatteryLevelsPerDay = new ArrayList<>(hourlyTimestamps.size()); - for (List hourlyTimestampsPerDay : hourlyTimestamps) { + for (int i = 0; i < hourlyTimestamps.size(); i++) { + final List hourlyTimestampsPerDay = hourlyTimestamps.get(i); mHourlyBatteryLevelsPerDay.add( - new PeriodBatteryLevelData(batteryLevelMap, hourlyTimestampsPerDay)); + new PeriodBatteryLevelData( + batteryLevelMap, hourlyTimestampsPerDay, isStartTimestamp && i == 0)); } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java index f62fdb8ce6f..44a16f19cca 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java @@ -50,6 +50,7 @@ import android.widget.TextView; import com.android.settings.SettingsActivity; import com.android.settings.testutils.FakeFeatureFactory; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -58,6 +59,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.util.Calendar; import java.util.List; import java.util.Locale; import java.util.Map; @@ -84,10 +86,13 @@ public final class BatteryChartPreferenceControllerTest { MockitoAnnotations.initMocks(this); Locale.setDefault(new Locale("en_US")); org.robolectric.shadows.ShadowSettings.set24HourTimeFormat(false); - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + final TimeZone timeZone = TimeZone.getTimeZone("UTC"); + TimeZone.setDefault(timeZone); DataProcessor.sTestSystemAppsPackageNames = Set.of(); mFeatureFactory = FakeFeatureFactory.setupForTest(); mContext = spy(RuntimeEnvironment.application); + BatteryLevelData.sTestCalendar = Calendar.getInstance(); + BatteryLevelData.sTestCalendar.setTimeZone(timeZone); doReturn(mContext).when(mContext).getApplicationContext(); doReturn(mUserManager).when(mContext).getSystemService(UserManager.class); doReturn(true).when(mUserManager).isUserUnlocked(anyInt()); @@ -115,6 +120,11 @@ public final class BatteryChartPreferenceControllerTest { new BatteryEntry.NameAndIcon("fakeName", /* icon= */ null, /* iconId= */ 1)); } + @After + public void tearDown() { + BatteryLevelData.sTestCalendar = null; + } + @Test public void onDestroy_activityIsChanging_clearBatteryEntryCache() { doReturn(true).when(mSettingsActivity).isChangingConfigurations(); @@ -141,7 +151,8 @@ public final class BatteryChartPreferenceControllerTest { reset(mHourlyChartView); setupHourlyChartViewAnimationMock(); - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 0)); verify(mDailyChartView, atLeastOnce()).setVisibility(View.GONE); // Ignore fast refresh ui from the data processor callback. @@ -178,7 +189,8 @@ public final class BatteryChartPreferenceControllerTest { BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS, mBatteryChartPreferenceController.mDailyChartLabelTextGenerator); - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 60, /* levelOffset= */ 0)); verify(mDailyChartView, atLeastOnce()).setVisibility(View.VISIBLE); verify(mViewPropertyAnimator, atLeastOnce()).alpha(0f); @@ -283,7 +295,8 @@ public final class BatteryChartPreferenceControllerTest { public void onBatteryLevelDataUpdate_oneDay_showHourlyChartOnly() { doReturn(View.GONE).when(mHourlyChartView).getVisibility(); - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 0)); verify(mChartSummaryTextView).setVisibility(View.VISIBLE); verify(mDailyChartView).setVisibility(View.GONE); @@ -295,7 +308,8 @@ public final class BatteryChartPreferenceControllerTest { doReturn(View.GONE).when(mHourlyChartView).getVisibility(); mBatteryChartPreferenceController.mDailyChartIndex = SELECTED_INDEX_ALL; - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 60, /* levelOffset= */ 0)); verify(mChartSummaryTextView).setVisibility(View.VISIBLE); verify(mDailyChartView).setVisibility(View.VISIBLE); @@ -307,7 +321,8 @@ public final class BatteryChartPreferenceControllerTest { doReturn(View.GONE).when(mHourlyChartView).getVisibility(); mBatteryChartPreferenceController.mDailyChartIndex = 0; - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 60, /* levelOffset= */ 0)); verify(mChartSummaryTextView).setVisibility(View.VISIBLE); verify(mDailyChartView).setVisibility(View.VISIBLE); @@ -379,7 +394,8 @@ public final class BatteryChartPreferenceControllerTest { @Test public void selectedSlotText_selectAllDaysAllHours_returnNull() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 60, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = SELECTED_INDEX_ALL; mBatteryChartPreferenceController.mHourlyChartIndex = SELECTED_INDEX_ALL; @@ -390,7 +406,8 @@ public final class BatteryChartPreferenceControllerTest { @Test public void selectedSlotText_onlyOneDayDataSelectAllHours_returnNull() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = 0; mBatteryChartPreferenceController.mHourlyChartIndex = SELECTED_INDEX_ALL; @@ -401,7 +418,8 @@ public final class BatteryChartPreferenceControllerTest { @Test public void selectedSlotText_selectADayAllHours_onlyDayText() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 60, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = 1; mBatteryChartPreferenceController.mHourlyChartIndex = SELECTED_INDEX_ALL; @@ -412,7 +430,8 @@ public final class BatteryChartPreferenceControllerTest { @Test public void selectedSlotText_onlyOneDayDataSelectAnHour_onlyHourText() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = 0; mBatteryChartPreferenceController.mHourlyChartIndex = 2; @@ -426,7 +445,8 @@ public final class BatteryChartPreferenceControllerTest { @Test public void selectedSlotText_SelectADayAnHour_dayAndHourText() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(60)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 60, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = 1; mBatteryChartPreferenceController.mHourlyChartIndex = 8; @@ -439,8 +459,9 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void selectedSlotText_selectFirstSlot_withMinuteText() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6)); + public void selectedSlotText_selectFirstSlotAfterFullCharged_withMinuteText() { + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = 0; mBatteryChartPreferenceController.mHourlyChartIndex = 0; @@ -452,9 +473,29 @@ public final class BatteryChartPreferenceControllerTest { .isEqualTo("Battery level percentage from 100% to 99%"); } + @Test + public void selectedSlotText_selectFirstSlotAfterTimeUpdated_withMinuteText() { + BatteryLevelData batteryLevelData = + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 10); + assertThat(batteryLevelData.getHourlyBatteryLevelsPerDay().get(0).isStartTimestamp()) + .isTrue(); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 10)); + mBatteryChartPreferenceController.mDailyChartIndex = 0; + mBatteryChartPreferenceController.mHourlyChartIndex = 0; + + assertThat(mBatteryChartPreferenceController.getSlotInformation(false)) + .isEqualTo("7:01 AM - 8 AM"); + assertThat(mBatteryChartPreferenceController.getSlotInformation(true)) + .isEqualTo("7:01 AM to 8 AM"); + assertThat(mBatteryChartPreferenceController.getBatteryLevelPercentageInfo()) + .isEqualTo("Battery level percentage from 90% to 89%"); + } + @Test public void selectedSlotText_selectLastSlot_withNowText() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(6)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 6, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = 0; mBatteryChartPreferenceController.mHourlyChartIndex = 3; @@ -468,7 +509,8 @@ public final class BatteryChartPreferenceControllerTest { @Test public void selectedSlotText_selectOnlySlot_withMinuteAndNowText() { - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(1)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 1, /* levelOffset= */ 0)); mBatteryChartPreferenceController.mDailyChartIndex = 0; mBatteryChartPreferenceController.mHourlyChartIndex = 0; @@ -493,7 +535,8 @@ public final class BatteryChartPreferenceControllerTest { mBatteryChartPreferenceController.mHourlyChartIndex = -1; mBatteryChartPreferenceController.onCreate(bundle); - mBatteryChartPreferenceController.onBatteryLevelDataUpdate(createBatteryLevelData(25)); + mBatteryChartPreferenceController.onBatteryLevelDataUpdate( + createBatteryLevelData(/* numOfHours= */ 25, /* levelOffset= */ 0)); assertThat(mBatteryChartPreferenceController.mDailyChartIndex) .isEqualTo(expectedDailyIndex); @@ -503,7 +546,8 @@ public final class BatteryChartPreferenceControllerTest { @Test public void getTotalHours_getExpectedResult() { - BatteryLevelData batteryLevelData = createBatteryLevelData(60); + BatteryLevelData batteryLevelData = + createBatteryLevelData(/* numOfHours= */ 60, /* levelOffset= */ 0); final int totalHour = BatteryChartPreferenceController.getTotalHours(batteryLevelData); @@ -516,10 +560,10 @@ public final class BatteryChartPreferenceControllerTest { return 1619247600000L + index * DateUtils.HOUR_IN_MILLIS; } - private static BatteryLevelData createBatteryLevelData(int numOfHours) { + private static BatteryLevelData createBatteryLevelData(int numOfHours, int levelOffset) { Map batteryLevelMap = new ArrayMap<>(); for (int index = 0; index < numOfHours; index += 2) { - final Integer level = 100 - index; + final Integer level = 100 - index - levelOffset; Long timestamp = generateTimestamp(index); if (index == 0) { timestamp += DateUtils.MINUTE_IN_MILLIS; @@ -529,6 +573,8 @@ public final class BatteryChartPreferenceControllerTest { } long current = generateTimestamp(numOfHours - 1) + DateUtils.MINUTE_IN_MILLIS * 2; batteryLevelMap.put(current, 66); + + BatteryLevelData.sTestCalendar.setTimeInMillis(current); DataProcessor.sTestCurrentTimeMillis = current; return new BatteryLevelData(batteryLevelMap); } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java index 7faca0d0960..60428014048 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java @@ -170,7 +170,8 @@ public final class DataProcessManagerTest { final Map batteryLevelMap1 = Map.of(timestamps1.get(0), 100, timestamps1.get(1), 100, timestamps1.get(2), 100); hourlyBatteryLevelsPerDay.add( - new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap1, timestamps1)); + new BatteryLevelData.PeriodBatteryLevelData( + batteryLevelMap1, timestamps1, /* isStartTimestamp= */ false)); // Adds the day 2 data. hourlyBatteryLevelsPerDay.add(null); // Adds the day 3 data. @@ -178,7 +179,8 @@ public final class DataProcessManagerTest { final Map batteryLevelMap2 = Map.of(timestamps2.get(0), 100, timestamps2.get(1), 100); hourlyBatteryLevelsPerDay.add( - new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap2, timestamps2)); + new BatteryLevelData.PeriodBatteryLevelData( + batteryLevelMap2, timestamps2, /* isStartTimestamp= */ false)); // Fake current usage data. final UsageEvents.Event event1 = getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /* timestamp= */ 1, packageName); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java index 28973430ce9..ae4c56d035a 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java @@ -209,7 +209,8 @@ public final class DataProcessorTest { final Map batteryLevelMap1 = Map.of(timestamps1.get(0), 100, timestamps1.get(1), 100, timestamps1.get(2), 100); hourlyBatteryLevelsPerDay.add( - new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap1, timestamps1)); + new BatteryLevelData.PeriodBatteryLevelData( + batteryLevelMap1, timestamps1, /* isStartTimestamp= */ false)); // Adds the day 2 data. hourlyBatteryLevelsPerDay.add(null); // Adds the day 3 data. @@ -217,7 +218,8 @@ public final class DataProcessorTest { final Map batteryLevelMap2 = Map.of(timestamps2.get(0), 100, timestamps2.get(1), 100); hourlyBatteryLevelsPerDay.add( - new BatteryLevelData.PeriodBatteryLevelData(batteryLevelMap2, timestamps2)); + new BatteryLevelData.PeriodBatteryLevelData( + batteryLevelMap2, timestamps2, /* isStartTimestamp= */ false)); final List appUsageEventList = new ArrayList<>(); // Adds some events before the start timestamp. appUsageEventList.add( @@ -365,7 +367,8 @@ public final class DataProcessorTest { final List hourlyBatteryLevelsPerDay = new ArrayList<>(); hourlyBatteryLevelsPerDay.add( - new BatteryLevelData.PeriodBatteryLevelData(new ArrayMap<>(), new ArrayList<>())); + new BatteryLevelData.PeriodBatteryLevelData( + new ArrayMap<>(), new ArrayList<>(), /* isStartTimestamp= */ false)); assertThat( DataProcessor.generateAppUsagePeriodMap( mContext, @@ -858,7 +861,8 @@ public final class DataProcessorTest { new ArrayList<>(); hourlyBatteryLevelsPerDay.add( - new BatteryLevelData.PeriodBatteryLevelData(new ArrayMap<>(), new ArrayList<>())); + new BatteryLevelData.PeriodBatteryLevelData( + new ArrayMap<>(), new ArrayList<>(), /* isStartTimestamp= */ false)); assertThat( DataProcessor.getBatteryDiffDataMap( From 798340fafdb3bf9aa83e89b22a7a73ece429955e Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Wed, 8 May 2024 17:08:16 +0800 Subject: [PATCH 17/22] Update database clear & job refresh mechanism for time change intent - Ignore time change intent for time format update - Clear data after current time in DB and refresh periodic job - Take a snapshot of current battery usage stats if no periodic job in DB Bug: 336423923 Bug: 314921894 Fix: 314921894 Test: atest SettingsRoboTests:com.android.settings.fuelgauge.batteryusagei Change-Id: Iec0f5e8e97f18c4603de711a5884336ba0af23a9 --- .../batteryusage/BootBroadcastReceiver.java | 2 +- .../fuelgauge/batteryusage/DatabaseUtils.java | 62 ++++++++++--------- .../batteryusage/db/AppUsageEventDao.java | 4 ++ .../batteryusage/db/BatteryEventDao.java | 4 ++ .../batteryusage/db/BatteryStateDao.java | 4 ++ .../batteryusage/db/BatteryUsageSlotDao.java | 4 ++ .../BootBroadcastReceiverTest.java | 43 +++++++++++-- 7 files changed, 87 insertions(+), 36 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java index e407c636ddf..5fa04eb0959 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java @@ -67,7 +67,7 @@ public final class BootBroadcastReceiver extends BroadcastReceiver { refreshJobs(context); break; case Intent.ACTION_TIME_CHANGED: - Log.d(TAG, "refresh job and clear all data from action=" + action); + Log.d(TAG, "refresh job and clear data from action=" + action); DatabaseUtils.clearDataAfterTimeChangedIfNeeded(context, intent); break; default: diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index a41e9bd0388..b40f71ac16e 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java @@ -16,8 +16,6 @@ package com.android.settings.fuelgauge.batteryusage; -import static android.content.Intent.FLAG_RECEIVER_REPLACE_PENDING; - import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTimeForLogging; import android.app.usage.IUsageStatsManager; @@ -436,6 +434,23 @@ public final class DatabaseUtils { }); } + /** Clears data after a specific startTimestamp in the battery usage database. */ + public static void clearAllAfter(Context context, long startTimestamp) { + AsyncTask.execute( + () -> { + try { + final BatteryStateDatabase database = + BatteryStateDatabase.getInstance(context.getApplicationContext()); + database.appUsageEventDao().clearAllAfter(startTimestamp); + database.batteryEventDao().clearAllAfter(startTimestamp); + database.batteryStateDao().clearAllAfter(startTimestamp); + database.batteryUsageSlotDao().clearAllAfter(startTimestamp); + } catch (RuntimeException e) { + Log.e(TAG, "clearAllAfter() failed", e); + } + }); + } + /** Clears all out-of-date data in the battery usage database. */ public static void clearExpiredDataIfNeeded(Context context) { AsyncTask.execute( @@ -456,14 +471,14 @@ public final class DatabaseUtils { }); } - /** Clears all data and jobs if current timestamp is out of the range of last recorded job. */ + /** Clears data after new updated time and refresh periodic job. */ public static void clearDataAfterTimeChangedIfNeeded(Context context, Intent intent) { - if ((intent.getFlags() & FLAG_RECEIVER_REPLACE_PENDING) != 0) { + if ((intent.hasExtra(Intent.EXTRA_TIME_PREF_24_HOUR_FORMAT))) { BatteryUsageLogUtils.writeLog( context, Action.TIME_UPDATED, - "Database is not cleared because the time change intent is only" - + " for the existing pending receiver."); + "Database is not cleared because the time change intent is" + + " for time format change"); return; } AsyncTask.execute( @@ -861,36 +876,23 @@ public final class DatabaseUtils { } private static void clearDataAfterTimeChangedIfNeededInternal(Context context) { + final long currentTime = System.currentTimeMillis(); + final String logInfo = + String.format(Locale.ENGLISH, "clear data after current time = %d", currentTime); + Log.d(TAG, logInfo); + BatteryUsageLogUtils.writeLog(context, Action.TIME_UPDATED, logInfo); + DatabaseUtils.clearAllAfter(context, currentTime); + PeriodicJobManager.getInstance(context).refreshJob(/* fromBoot= */ false); + final List batteryLevelRecordEvents = DatabaseUtils.getBatteryEvents( context, Calendar.getInstance(), getLastFullChargeTime(context), BATTERY_LEVEL_RECORD_EVENTS); - final long lastRecordTimestamp = - batteryLevelRecordEvents.isEmpty() - ? INVALID_TIMESTAMP - : batteryLevelRecordEvents.get(0).getTimestamp(); - final long nextRecordTimestamp = - TimestampUtils.getNextEvenHourTimestamp(lastRecordTimestamp); - final long currentTime = System.currentTimeMillis(); - final boolean isOutOfTimeRange = - lastRecordTimestamp == INVALID_TIMESTAMP - || currentTime < lastRecordTimestamp - || currentTime > nextRecordTimestamp; - final String logInfo = - String.format( - Locale.ENGLISH, - "clear database = %b, current time = %d, last record time = %d", - isOutOfTimeRange, - currentTime, - lastRecordTimestamp); - Log.d(TAG, logInfo); - BatteryUsageLogUtils.writeLog(context, Action.TIME_UPDATED, logInfo); - if (isOutOfTimeRange) { - DatabaseUtils.clearAll(context); - PeriodicJobManager.getInstance(context) - .refreshJob(/* fromBoot= */ false); + if (batteryLevelRecordEvents.isEmpty()) { + // Take a snapshot of battery usage data immediately if there's no battery events. + BatteryUsageDataLoader.enqueueWork(context, /* isFullChargeStart= */ true); } } diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDao.java index d220b15968f..249780125f2 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDao.java +++ b/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDao.java @@ -55,6 +55,10 @@ public interface AppUsageEventDao { @Query("DELETE FROM AppUsageEventEntity WHERE timestamp <= :timestamp") void clearAllBefore(long timestamp); + /** Deletes all recorded data after a specific timestamp. */ + @Query("DELETE FROM AppUsageEventEntity WHERE timestamp >= :timestamp") + void clearAllAfter(long timestamp); + /** Clears all recorded data in the database. */ @Query("DELETE FROM AppUsageEventEntity") void clearAll(); diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java index 8b696fe96c0..19d20438afa 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java @@ -65,6 +65,10 @@ public interface BatteryEventDao { @Query("DELETE FROM BatteryEventEntity WHERE timestamp <= :timestamp") void clearAllBefore(long timestamp); + /** Deletes all recorded data after a specific timestamp. */ + @Query("DELETE FROM BatteryEventEntity WHERE timestamp >= :timestamp") + void clearAllAfter(long timestamp); + /** Clears all recorded data in the database. */ @Query("DELETE FROM BatteryEventEntity") void clearAll(); diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java index 520c6bed484..049251eb718 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDao.java @@ -61,6 +61,10 @@ public interface BatteryStateDao { @Query("DELETE FROM BatteryState WHERE timestamp <= :timestamp") void clearAllBefore(long timestamp); + /** Deletes all recorded data after a specific timestamp. */ + @Query("DELETE FROM BatteryState WHERE timestamp >= :timestamp") + void clearAllAfter(long timestamp); + /** Clears all recorded data in the database. */ @Query("DELETE FROM BatteryState") void clearAll(); diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDao.java index d8cf41d0ce8..d53b0cf2532 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDao.java +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryUsageSlotDao.java @@ -52,6 +52,10 @@ public interface BatteryUsageSlotDao { @Query("DELETE FROM BatteryUsageSlotEntity WHERE timestamp <= :timestamp") void clearAllBefore(long timestamp); + /** Deletes all recorded data after a specific timestamp. */ + @Query("DELETE FROM BatteryUsageSlotEntity WHERE timestamp >= :timestamp") + void clearAllAfter(long timestamp); + /** Clears all recorded data in the database. */ @Query("DELETE FROM BatteryUsageSlotEntity") void clearAll(); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java index df330a36ac5..3e53b036edd 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java @@ -35,7 +35,6 @@ import com.android.settings.testutils.BatteryTestUtils; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -64,9 +63,8 @@ public final class BootBroadcastReceiverTest { // Inserts fake data into database for testing. final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext); - BatteryTestUtils.insertDataToBatteryStateTable( - mContext, Clock.systemUTC().millis(), "com.android.systemui"); mDao = database.batteryStateDao(); + mDao.clearAll(); clearSharedPreferences(); } @@ -129,10 +127,13 @@ public final class BootBroadcastReceiverTest { assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); } - @Ignore("b/314921894") @Test - public void onReceive_withTimeChangedIntent_clearsAllDataAndRefreshesJob() + public void onReceive_withTimeChangedIntentSetEarlierTime_refreshesJob() throws InterruptedException { + BatteryTestUtils.insertDataToBatteryStateTable( + mContext, Clock.systemUTC().millis() + 60000, "com.android.systemui"); + assertThat(mDao.getAllAfter(0).size()).isEqualTo(1); + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_TIME_CHANGED)); TimeUnit.MILLISECONDS.sleep(100); @@ -140,6 +141,38 @@ public final class BootBroadcastReceiverTest { assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); } + @Test + public void onReceive_withTimeChangedIntentSetLaterTime_clearNoDataAndRefreshesJob() + throws InterruptedException { + BatteryTestUtils.insertDataToBatteryStateTable( + mContext, Clock.systemUTC().millis() - 60000, "com.android.systemui"); + assertThat(mDao.getAllAfter(0).size()).isEqualTo(1); + + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_TIME_CHANGED)); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0).size()).isEqualTo(1); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void onReceive_withTimeFormatChangedIntent_skipRefreshJob() throws InterruptedException { + BatteryTestUtils.insertDataToBatteryStateTable( + mContext, Clock.systemUTC().millis() + 60000, "com.android.systemui"); + assertThat(mDao.getAllAfter(0).size()).isEqualTo(1); + + mReceiver.onReceive( + mContext, + new Intent(Intent.EXTRA_INTENT) + .putExtra( + Intent.EXTRA_TIME_PREF_24_HOUR_FORMAT, + Intent.EXTRA_TIME_PREF_VALUE_USE_12_HOUR)); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0).size()).isEqualTo(1); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + @Test public void invokeJobRecheck_broadcastsIntent() { BootBroadcastReceiver.invokeJobRecheck(mContext); From d3ce90347bb29b33fcbd0456c2e0b8d8c6f7147f Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Wed, 8 May 2024 17:34:22 +0800 Subject: [PATCH 18/22] Update database clear & job refresh mechanism for time zone change intent - Clear database and reset periodic job - Take a snapshot of current battery usage stats Bug: 336423923 Test: atest SettingsRoboTests:com.android.settings.fuelgauge.batteryusage Change-Id: I4aade9db950b508e2190605371f246904f131da3 --- AndroidManifest.xml | 1 + protos/fuelgauge_log.proto | 1 + .../batteryusage/BootBroadcastReceiver.java | 4 +++ .../fuelgauge/batteryusage/DatabaseUtils.java | 31 +++++++++++++++++++ .../BootBroadcastReceiverTest.java | 14 +++++++++ 5 files changed, 51 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ad815516c61..e085be86407 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3343,6 +3343,7 @@ + diff --git a/protos/fuelgauge_log.proto b/protos/fuelgauge_log.proto index 4bee75ca058..b16958d8e2b 100644 --- a/protos/fuelgauge_log.proto +++ b/protos/fuelgauge_log.proto @@ -45,6 +45,7 @@ message BatteryUsageHistoricalLogEntry { FETCH_USAGE_DATA = 4; INSERT_USAGE_DATA = 5; TIME_UPDATED = 6; + TIMEZONE_UPDATED = 7; } optional int64 timestamp = 1; diff --git a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java index 5fa04eb0959..b758df4bd6b 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java @@ -70,6 +70,10 @@ public final class BootBroadcastReceiver extends BroadcastReceiver { Log.d(TAG, "refresh job and clear data from action=" + action); DatabaseUtils.clearDataAfterTimeChangedIfNeeded(context, intent); break; + case Intent.ACTION_TIMEZONE_CHANGED: + Log.d(TAG, "refresh job and clear all data from action=" + action); + DatabaseUtils.clearDataAfterTimeZoneChangedIfNeeded(context); + break; default: Log.w(TAG, "receive unsupported action=" + action); } diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index b40f71ac16e..5b28abb422f 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java @@ -57,6 +57,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -495,6 +496,22 @@ public final class DatabaseUtils { }); } + /** Clears all data and reset jobs if timezone changed. */ + public static void clearDataAfterTimeZoneChangedIfNeeded(Context context) { + AsyncTask.execute( + () -> { + try { + clearDataAfterTimeZoneChangedIfNeededInternal(context); + } catch (RuntimeException e) { + Log.e(TAG, "clearDataAfterTimeZoneChangedIfNeeded() failed", e); + BatteryUsageLogUtils.writeLog( + context, + Action.TIMEZONE_UPDATED, + "clearDataAfterTimeZoneChangedIfNeeded() failed" + e); + } + }); + } + /** Returns the timestamp for 00:00 6 days before the calendar date. */ public static long getTimestampSixDaysAgo(Calendar calendar) { Calendar startCalendar = @@ -896,6 +913,20 @@ public final class DatabaseUtils { } } + private static void clearDataAfterTimeZoneChangedIfNeededInternal(Context context) { + final String logInfo = + String.format( + Locale.ENGLISH, + "clear database for new time zone = %s", + TimeZone.getDefault().toString()); + BatteryUsageLogUtils.writeLog(context, Action.TIMEZONE_UPDATED, logInfo); + Log.d(TAG, logInfo); + DatabaseUtils.clearAll(context); + PeriodicJobManager.getInstance(context).refreshJob(/* fromBoot= */ false); + // Take a snapshot of battery usage data immediately + BatteryUsageDataLoader.enqueueWork(context, /* isFullChargeStart= */ true); + } + private static long loadLongFromContentProvider( Context context, Uri uri, final long defaultValue) { return loadFromContentProvider( diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java index 3e53b036edd..545f7733f41 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java @@ -173,6 +173,20 @@ public final class BootBroadcastReceiverTest { assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); } + @Test + public void onReceive_withTimeZoneChangedIntent_clearAllDataAndRefreshesJob() + throws InterruptedException { + BatteryTestUtils.insertDataToBatteryStateTable( + mContext, Clock.systemUTC().millis(), "com.android.systemui"); + assertThat(mDao.getAllAfter(0).size()).isEqualTo(1); + + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_TIMEZONE_CHANGED)); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0)).isEmpty(); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + @Test public void invokeJobRecheck_broadcastsIntent() { BootBroadcastReceiver.invokeJobRecheck(mContext); From e216fd4248ba17afce8f3544cbea012b0f3d2275 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 6 May 2024 17:53:18 +0800 Subject: [PATCH 19/22] No show DisableSimFooterPreference when sub not found DisableSimFooterPreferenceController.getAvailabilityStatus() used to return AVAILABLE when the subscription is not found in selectable subscription info list. Return CONDITIONALLY_UNAVAILABLE in this case to fix. Bug: 336232487 Test: manual - on Mobile Settings Change-Id: I88642fc9853ce6603a78dfdc0e5fed1da1553adc --- .../DisableSimFooterPreferenceController.java | 62 ----------- .../DisableSimFooterPreferenceController.kt | 54 ++++++++++ .../telephony/SubscriptionRepository.kt | 3 + ...isableSimFooterPreferenceControllerTest.kt | 102 ++++++++++++++++++ ...ableSimFooterPreferenceControllerTest.java | 94 ---------------- 5 files changed, 159 insertions(+), 156 deletions(-) delete mode 100644 src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.java create mode 100644 src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.kt delete mode 100644 tests/unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.java diff --git a/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.java b/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.java deleted file mode 100644 index d14c8d09e44..00000000000 --- a/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2019 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; - -import android.content.Context; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; - -import com.android.settings.network.SubscriptionUtil; - -/** - * Shows information about disable a physical SIM. - */ -public class DisableSimFooterPreferenceController extends TelephonyBasePreferenceController { - - /** - * Constructor - */ - public DisableSimFooterPreferenceController(Context context, String preferenceKey) { - super(context, preferenceKey); - } - - /** - * re-init for SIM based on given subscription ID. - * @param subId is the given subscription ID - */ - public void init(int subId) { - mSubId = subId; - } - - @Override - public int getAvailabilityStatus(int subId) { - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - return CONDITIONALLY_UNAVAILABLE; - } - - SubscriptionManager subManager = mContext.getSystemService(SubscriptionManager.class); - for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions(mContext)) { - if (info.getSubscriptionId() == subId) { - if (info.isEmbedded() || SubscriptionUtil.showToggleForPhysicalSim(subManager)) { - return CONDITIONALLY_UNAVAILABLE; - } - break; - } - } - return AVAILABLE; - } -} diff --git a/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.kt b/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.kt new file mode 100644 index 00000000000..8e3e398db8e --- /dev/null +++ b/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony + +import android.content.Context +import android.telephony.SubscriptionManager + +/** + * Shows information about disable a physical SIM. + */ +class DisableSimFooterPreferenceController @JvmOverloads constructor( + context: Context, + preferenceKey: String, + private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), +) : TelephonyBasePreferenceController(context, preferenceKey) { + + /** + * Re-init for SIM based on given subscription ID. + * + * @param subId is the given subscription ID + */ + fun init(subId: Int) { + mSubId = subId + } + + override fun getAvailabilityStatus(subId: Int): Int { + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID || + subscriptionRepository.canDisablePhysicalSubscription() + ) { + return CONDITIONALLY_UNAVAILABLE + } + + val isAvailable = + subscriptionRepository.getSelectableSubscriptionInfoList().any { subInfo -> + subInfo.subscriptionId == subId && !subInfo.isEmbedded + } + + return if (isAvailable) AVAILABLE else CONDITIONALLY_UNAVAILABLE + } +} diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index f4bbc763fb9..05cfad884b3 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -36,6 +36,8 @@ import kotlinx.coroutines.flow.onEach private const val TAG = "SubscriptionRepository" class SubscriptionRepository(private val context: Context) { + private val subscriptionManager = context.requireSubscriptionManager() + /** * Return a list of subscriptions that are available and visible to the user. * @@ -55,6 +57,7 @@ class SubscriptionRepository(private val context: Context) { isSubscriptionEnabledFlow(subId).collectLatestWithLifecycle(lifecycleOwner, action = action) } + fun canDisablePhysicalSubscription() = subscriptionManager.canDisablePhysicalSubscription() } val Context.subscriptionManager: SubscriptionManager? diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.kt new file mode 100644 index 00000000000..0ddaa520baf --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony + +import android.content.Context +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.core.BasePreferenceController.AVAILABLE +import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class DisableSimFooterPreferenceControllerTest { + + private val subscriptionInfo = mock { + on { subscriptionId } doReturn SUB_ID + } + + private var context: Context = ApplicationProvider.getApplicationContext() + + private val mockSubscriptionRepository = mock { + on { getSelectableSubscriptionInfoList() } doReturn listOf(subscriptionInfo) + } + + private var controller = DisableSimFooterPreferenceController( + context = context, + preferenceKey = PREFERENCE_KEY, + subscriptionRepository = mockSubscriptionRepository, + ).apply { init(SUB_ID) } + + @Test + fun getAvailabilityStatus_invalidId_notAvailable() { + val availabilityStatus = controller.getAvailabilityStatus(INVALID_SUBSCRIPTION_ID) + + assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun getAvailabilityStatus_eSim_notAvailable() { + subscriptionInfo.stub { + on { isEmbedded } doReturn true + } + + val availabilityStatus = controller.getAvailabilityStatus(SUB_ID) + + assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun getAvailabilityStatus_pSimAndCannotDisable_available() { + mockSubscriptionRepository.stub { + on { canDisablePhysicalSubscription() } doReturn false + } + subscriptionInfo.stub { + on { isEmbedded } doReturn false + } + + val availabilityStatus = controller.getAvailabilityStatus(SUB_ID) + + assertThat(availabilityStatus).isEqualTo(AVAILABLE) + } + + @Test + fun getAvailabilityStatus_pSimAndCanDisable_notAvailable() { + mockSubscriptionRepository.stub { + on { canDisablePhysicalSubscription() } doReturn true + } + subscriptionInfo.stub { + on { isEmbedded } doReturn false + } + + val availabilityStatus = controller.getAvailabilityStatus(SUB_ID) + + assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + private companion object { + const val PREFERENCE_KEY = "preference_key" + const val SUB_ID = 111 + } +} diff --git a/tests/unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.java deleted file mode 100644 index bbbee216994..00000000000 --- a/tests/unit/src/com/android/settings/network/telephony/DisableSimFooterPreferenceControllerTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2020 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; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.android.settings.network.SubscriptionUtil; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Arrays; - -@RunWith(AndroidJUnit4.class) -public class DisableSimFooterPreferenceControllerTest { - private static final String PREF_KEY = "pref_key"; - private static final int SUB_ID = 111; - - @Mock - private SubscriptionInfo mInfo; - - private Context mContext; - @Mock - private SubscriptionManager mSubscriptionManager; - private DisableSimFooterPreferenceController mController; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = spy(ApplicationProvider.getApplicationContext()); - when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); - when(mSubscriptionManager.createForAllUserProfiles()).thenReturn(mSubscriptionManager); - - when(mInfo.getSubscriptionId()).thenReturn(SUB_ID); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mInfo)); - mController = new DisableSimFooterPreferenceController(mContext, PREF_KEY); - } - - @Test - public void isAvailable_noInit_notAvailable() { - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void isAvailable_eSIM_notAvailable() { - when(mInfo.isEmbedded()).thenReturn(true); - mController.init(SUB_ID); - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void isAvailable_pSIM_available_cannot_disable_pSIM() { - when(mInfo.isEmbedded()).thenReturn(false); - mController.init(SUB_ID); - doReturn(false).when(mSubscriptionManager).canDisablePhysicalSubscription(); - assertThat(mController.isAvailable()).isTrue(); - } - - @Test - public void isAvailable_pSIM_available_can_disable_pSIM() { - when(mInfo.isEmbedded()).thenReturn(false); - mController.init(SUB_ID); - doReturn(true).when(mSubscriptionManager).canDisablePhysicalSubscription(); - assertThat(mController.isAvailable()).isFalse(); - } -} From 6e4ac4bdc03079c898687855667e02f23dc3c1a2 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Fri, 10 May 2024 14:59:04 +0800 Subject: [PATCH 20/22] Fix empty network scan result Settings use TelephonyScanManager.NetworkScanCallback to scan, and its onComplete() could happens before onResults(). We will change Settings to fix. Fix: 338986191 Test: manual - on NetworkSelectSettings Test: unit test Change-Id: If41d957f916a99eacc1becb6b460e58722a4dca7 --- .../telephony/NetworkSelectSettings.java | 92 ++++--------------- .../telephony/scan/NetworkScanRepository.kt | 42 +++++---- .../scan/NetworkScanRepositoryTest.kt | 36 +++++--- .../telephony/NetworkSelectSettingsTest.java | 14 ++- 4 files changed, 79 insertions(+), 105 deletions(-) diff --git a/src/com/android/settings/network/telephony/NetworkSelectSettings.java b/src/com/android/settings/network/telephony/NetworkSelectSettings.java index f29a5efd2d6..9455b70d091 100644 --- a/src/com/android/settings/network/telephony/NetworkSelectSettings.java +++ b/src/com/android/settings/network/telephony/NetworkSelectSettings.java @@ -48,13 +48,12 @@ import com.android.internal.telephony.flags.Flags; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.network.telephony.scan.NetworkScanRepository; -import com.android.settings.network.telephony.scan.NetworkScanRepository.NetworkScanCellInfos; -import com.android.settings.network.telephony.scan.NetworkScanRepository.NetworkScanComplete; -import com.android.settings.network.telephony.scan.NetworkScanRepository.NetworkScanError; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.utils.ThreadUtils; +import com.google.common.collect.ImmutableList; + import kotlin.Unit; import java.util.ArrayList; @@ -83,7 +82,8 @@ public class NetworkSelectSettings extends DashboardFragment { private View mProgressHeader; private Preference mStatusMessagePreference; @VisibleForTesting - List mCellInfoList; + @NonNull + List mCellInfoList = ImmutableList.of(); private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private TelephonyManager mTelephonyManager; private SatelliteManager mSatelliteManager; @@ -96,7 +96,6 @@ public class NetworkSelectSettings extends DashboardFragment { private AtomicBoolean mShouldFilterOutSatellitePlmn = new AtomicBoolean(); private NetworkScanRepository mNetworkScanRepository; - private boolean mUpdateScanResult = false; private NetworkSelectRepository mNetworkSelectRepository; @@ -213,38 +212,16 @@ public class NetworkSelectSettings extends DashboardFragment { } private void launchNetworkScan() { + setProgressBarVisible(true); mNetworkScanRepository.launchNetworkScan(getViewLifecycleOwner(), (networkScanResult) -> { - if (!mUpdateScanResult) { - // Not update UI if not in scan mode. - return Unit.INSTANCE; - } - if (networkScanResult instanceof NetworkScanCellInfos networkScanCellInfos) { - scanResultHandler(networkScanCellInfos.getCellInfos()); - return Unit.INSTANCE; - } - if (!isPreferenceScreenEnabled()) { - clearPreferenceSummary(); - enablePreferenceScreen(true); - } else if (networkScanResult instanceof NetworkScanComplete - && mCellInfoList == null) { - // In case the scan timeout before getting any results - addMessagePreference(R.string.empty_networks_list); - } else if (networkScanResult instanceof NetworkScanError) { - addMessagePreference(R.string.network_query_error); + if (isPreferenceScreenEnabled()) { + scanResultHandler(networkScanResult); } return Unit.INSTANCE; }); } - @Override - public void onStart() { - super.onStart(); - - setProgressBarVisible(true); - mUpdateScanResult = true; - } - /** * Update forbidden PLMNs from the USIM App */ @@ -268,8 +245,6 @@ public class NetworkSelectSettings extends DashboardFragment { return false; } - mUpdateScanResult = false; - // Refresh the last selected item in case users reselect network. clearPreferenceSummary(); if (mSelectedPreference != null) { @@ -380,27 +355,19 @@ public class NetworkSelectSettings extends DashboardFragment { } } - @Keep @VisibleForTesting - protected void scanResultHandler(List results) { - mCellInfoList = filterOutSatellitePlmn(results); + protected void scanResultHandler(NetworkScanRepository.NetworkScanResult results) { + mCellInfoList = filterOutSatellitePlmn(results.getCellInfos()); Log.d(TAG, "CellInfoList: " + CellInfoUtil.cellInfoListToString(mCellInfoList)); - if (mCellInfoList != null && mCellInfoList.size() != 0) { - final NetworkOperatorPreference connectedPref = updateAllPreferenceCategory(); - if (connectedPref != null) { - // update selected preference instance into connected preference - if (mSelectedPreference != null) { - mSelectedPreference = connectedPref; - } - } else if (!isPreferenceScreenEnabled()) { - mSelectedPreference.setSummary(R.string.network_connecting); - } - enablePreferenceScreen(true); - } else if (isPreferenceScreenEnabled()) { + updateAllPreferenceCategory(); + NetworkScanRepository.NetworkScanState state = results.getState(); + if (state == NetworkScanRepository.NetworkScanState.ERROR) { + addMessagePreference(R.string.network_query_error); + } else if (mCellInfoList.isEmpty()) { addMessagePreference(R.string.empty_networks_list); - // keep showing progress bar, it will be stopped when error or completed - setProgressBarVisible(true); } + // keep showing progress bar, it will be stopped when error or completed + setProgressBarVisible(state == NetworkScanRepository.NetworkScanState.ACTIVE); } @Keep @@ -417,11 +384,8 @@ public class NetworkSelectSettings extends DashboardFragment { /** * Update the content of network operators list. - * - * @return preference which shows connected */ - @Nullable - private NetworkOperatorPreference updateAllPreferenceCategory() { + private void updateAllPreferenceCategory() { int numberOfPreferences = mPreferenceCategory.getPreferenceCount(); // remove unused preferences @@ -432,7 +396,6 @@ public class NetworkSelectSettings extends DashboardFragment { } // update the content of preference - NetworkOperatorPreference connectedPref = null; for (int index = 0; index < mCellInfoList.size(); index++) { final CellInfo cellInfo = mCellInfoList.get(index); @@ -457,23 +420,10 @@ public class NetworkSelectSettings extends DashboardFragment { if (mCellInfoList.get(index).isRegistered()) { pref.setSummary(R.string.network_connected); - connectedPref = pref; } else { pref.setSummary(null); } } - - // update selected preference instance by index - for (int index = 0; index < mCellInfoList.size(); index++) { - final CellInfo cellInfo = mCellInfoList.get(index); - - if ((mSelectedPreference != null) && mSelectedPreference.isSameCell(cellInfo)) { - mSelectedPreference = (NetworkOperatorPreference) - (mPreferenceCategory.getPreference(index)); - } - } - - return connectedPref; } /** @@ -524,13 +474,6 @@ public class NetworkSelectSettings extends DashboardFragment { } } - private boolean isProgressBarVisible() { - if (mProgressHeader == null) { - return false; - } - return (mProgressHeader.getVisibility() == View.VISIBLE); - } - protected void setProgressBarVisible(boolean visible) { if (mProgressHeader != null) { mProgressHeader.setVisibility(visible ? View.VISIBLE : View.GONE); @@ -538,7 +481,6 @@ public class NetworkSelectSettings extends DashboardFragment { } private void addMessagePreference(int messageId) { - setProgressBarVisible(false); mStatusMessagePreference.setTitle(messageId); mPreferenceCategory.removeAll(); mPreferenceCategory.addPreference(mStatusMessagePreference); diff --git a/src/com/android/settings/network/telephony/scan/NetworkScanRepository.kt b/src/com/android/settings/network/telephony/scan/NetworkScanRepository.kt index dfa79cbc869..4ae5842c513 100644 --- a/src/com/android/settings/network/telephony/scan/NetworkScanRepository.kt +++ b/src/com/android/settings/network/telephony/scan/NetworkScanRepository.kt @@ -27,25 +27,29 @@ import android.telephony.TelephonyScanManager import android.util.Log import androidx.lifecycle.LifecycleOwner import com.android.settings.R -import com.android.settings.network.telephony.CellInfoUtil import com.android.settings.network.telephony.CellInfoUtil.getNetworkTitle +import com.android.settings.network.telephony.telephonyManager import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach class NetworkScanRepository(private val context: Context, subId: Int) { - sealed interface NetworkScanResult + enum class NetworkScanState { + ACTIVE, COMPLETE, ERROR + } - data class NetworkScanCellInfos(val cellInfos: List) : NetworkScanResult - data object NetworkScanComplete : NetworkScanResult - data class NetworkScanError(val error: Int) : NetworkScanResult + data class NetworkScanResult( + val state: NetworkScanState, + val cellInfos: List, + ) - private val telephonyManager = - context.getSystemService(TelephonyManager::class.java)!!.createForSubscriptionId(subId) + private val telephonyManager = context.telephonyManager(subId) /** TODO: Move this to UI layer, when UI layer migrated to Kotlin. */ fun launchNetworkScan(lifecycleOwner: LifecycleOwner, onResult: (NetworkScanResult) -> Unit) { @@ -65,23 +69,29 @@ class NetworkScanRepository(private val context: Context, subId: Int) { } fun networkScanFlow(): Flow = callbackFlow { + var state = NetworkScanState.ACTIVE + var cellInfos: List = emptyList() + val callback = object : TelephonyScanManager.NetworkScanCallback() { override fun onResults(results: List) { - val cellInfos = results.distinctBy { CellInfoScanKey(it) } - trySend(NetworkScanCellInfos(cellInfos)) - Log.d(TAG, "CellInfoList: ${CellInfoUtil.cellInfoListToString(cellInfos)}") + cellInfos = results.distinctBy { CellInfoScanKey(it) } + sendResult() } override fun onComplete() { - trySend(NetworkScanComplete) - close() - Log.d(TAG, "onComplete") + state = NetworkScanState.COMPLETE + sendResult() + // Don't call close() here since onComplete() could happens before onResults() } override fun onError(error: Int) { - trySend(NetworkScanError(error)) + state = NetworkScanState.ERROR + sendResult() close() - Log.d(TAG, "onError: $error") + } + + private fun sendResult() { + trySend(NetworkScanResult(state, cellInfos)) } } @@ -92,7 +102,7 @@ class NetworkScanRepository(private val context: Context, subId: Int) { ) awaitClose { networkScan.stopScan() } - }.flowOn(Dispatchers.Default) + }.conflate().onEach { Log.d(TAG, "networkScanFlow: $it") }.flowOn(Dispatchers.Default) /** Create network scan for allowed network types. */ private fun createNetworkScan(): NetworkScanRequest { diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/scan/NetworkScanRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/scan/NetworkScanRepositoryTest.kt index 070c779b5ea..c0b918fcf5d 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/scan/NetworkScanRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/scan/NetworkScanRepositoryTest.kt @@ -32,9 +32,6 @@ import android.telephony.TelephonyManager.NETWORK_CLASS_BITMASK_5G import android.telephony.TelephonyScanManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settings.network.telephony.scan.NetworkScanRepository.NetworkScanCellInfos -import com.android.settings.network.telephony.scan.NetworkScanRepository.NetworkScanComplete -import com.android.settings.network.telephony.scan.NetworkScanRepository.NetworkScanError import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull import com.android.settingslib.spa.testutils.toListWithTimeout import com.google.common.truth.Truth.assertThat @@ -88,7 +85,12 @@ class NetworkScanRepositoryTest { callback?.onResults(cellInfos) - assertThat(listDeferred.await()).containsExactly(NetworkScanCellInfos(cellInfos)) + assertThat(listDeferred.await()).containsExactly( + NetworkScanRepository.NetworkScanResult( + state = NetworkScanRepository.NetworkScanState.ACTIVE, + cellInfos = cellInfos, + ) + ) } @Test @@ -100,7 +102,12 @@ class NetworkScanRepositoryTest { callback?.onComplete() - assertThat(listDeferred.await()).containsExactly(NetworkScanComplete) + assertThat(listDeferred.await()).containsExactly( + NetworkScanRepository.NetworkScanResult( + state = NetworkScanRepository.NetworkScanState.COMPLETE, + cellInfos = emptyList(), + ) + ) } @Test @@ -112,7 +119,12 @@ class NetworkScanRepositoryTest { callback?.onError(1) - assertThat(listDeferred.await()).containsExactly(NetworkScanError(1)) + assertThat(listDeferred.await()).containsExactly( + NetworkScanRepository.NetworkScanResult( + state = NetworkScanRepository.NetworkScanState.ERROR, + cellInfos = emptyList(), + ) + ) } @Test @@ -133,12 +145,13 @@ class NetworkScanRepositoryTest { callback?.onResults(cellInfos) assertThat(listDeferred.await()).containsExactly( - NetworkScanCellInfos( - listOf( + NetworkScanRepository.NetworkScanResult( + state = NetworkScanRepository.NetworkScanState.ACTIVE, + cellInfos = listOf( createCellInfoLte("123", false), createCellInfoLte("124", true), createCellInfoGsm("123", false), - ) + ), ) ) } @@ -162,8 +175,9 @@ class NetworkScanRepositoryTest { callback?.onResults(cellInfos) assertThat(listDeferred.await()).containsExactly( - NetworkScanCellInfos( - listOf( + NetworkScanRepository.NetworkScanResult( + state = NetworkScanRepository.NetworkScanState.ACTIVE, + cellInfos = listOf( createCellInfoLte("123", false), createCellInfoLte("123", true), createCellInfoLte("124", false), diff --git a/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java b/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java index a4657cee8a0..d71af84dcf8 100644 --- a/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java +++ b/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java @@ -44,8 +44,12 @@ import androidx.preference.PreferenceManager; import androidx.test.annotation.UiThreadTest; import androidx.test.core.app.ApplicationProvider; +import com.android.settings.network.telephony.scan.NetworkScanRepository; +import com.android.settings.network.telephony.scan.NetworkScanRepository.NetworkScanResult; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.google.common.collect.ImmutableList; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -163,8 +167,7 @@ public class NetworkSelectSettingsTest { } @Override - protected NetworkOperatorPreference - createNetworkOperatorPreference(CellInfo cellInfo) { + protected NetworkOperatorPreference createNetworkOperatorPreference(CellInfo cellInfo) { NetworkOperatorPreference pref = super.createNetworkOperatorPreference(cellInfo); if (cellInfo == mTestEnv.mCellInfo1) { pref.updateCell(cellInfo, mTestEnv.mCellId1); @@ -183,9 +186,14 @@ public class NetworkSelectSettingsTest { @Test @UiThreadTest public void updateAllPreferenceCategory_correctOrderingPreference() { + NetworkScanResult result = new NetworkScanResult( + NetworkScanRepository.NetworkScanState.COMPLETE, + ImmutableList.of(mCellInfo1, mCellInfo2)); mNetworkSelectSettings.onCreateInitialization(); mNetworkSelectSettings.enablePreferenceScreen(true); - mNetworkSelectSettings.scanResultHandler(Arrays.asList(mCellInfo1, mCellInfo2)); + + mNetworkSelectSettings.scanResultHandler(result); + assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2); final NetworkOperatorPreference preference = (NetworkOperatorPreference) mPreferenceCategory.getPreference(1); From 257e1645622710a911243bc6872deacf350a9ca4 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Fri, 10 May 2024 17:02:49 +0800 Subject: [PATCH 21/22] [Thread] fix Thread unit test failures It looks like SettingsUnitTests is not in presubmit and the existing Thread settings unit tests are failing in postsubmit. This commit fixes two small issues: 1. The default Thread enabled state in FakeThreadNetworkController and ThreadNetworkToggleController is not consistent 2. The executor in ThreadNetworkToggleControllerTest is reusing the main thread which could result in flaky as the callbacks are not guaranteed to be invoked before the API returns Bug: 339767488 Bug: 339145155 Test: atest SettingsUnitTests:com.android.settings.connecteddevice.threadnetwork.ThreadNetworkToggleControllerTest Change-Id: Id8b453cf6d7bef698d08dbf106e48250369d0832 --- .../threadnetwork/FakeThreadNetworkController.kt | 2 +- .../threadnetwork/ThreadNetworkToggleControllerTest.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt index 8cb717dbb9b..e30226e25fb 100644 --- a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt @@ -22,7 +22,7 @@ import java.util.concurrent.Executor /** A fake implementation of [BaseThreadNetworkController] for unit tests. */ class FakeThreadNetworkController : BaseThreadNetworkController { - var isEnabled = true + var isEnabled = false private set var registeredStateCallback: ThreadNetworkController.StateCallback? = null private set diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt index 329e7416d44..04ebc9252ef 100644 --- a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt +++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt @@ -17,7 +17,6 @@ package com.android.settings.connecteddevice.threadnetwork import android.content.Context import android.platform.test.flag.junit.SetFlagsRule -import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.preference.PreferenceManager @@ -51,7 +50,7 @@ class ThreadNetworkToggleControllerTest { fun setUp() { mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED) context = spy(ApplicationProvider.getApplicationContext()) - executor = ContextCompat.getMainExecutor(context) + executor = Executor { runnable: Runnable -> runnable.run() } fakeThreadNetworkController = FakeThreadNetworkController() controller = newControllerWithThreadFeatureSupported(true) val preferenceManager = PreferenceManager(context) From 7b5c1bf7663e3a514c7187f8e072cf56eb72f89d Mon Sep 17 00:00:00 2001 From: Manish Singh Date: Fri, 10 May 2024 09:11:49 +0000 Subject: [PATCH 22/22] Move mock init before fragment is created Bug: 336518132 Test: atest PrivateSpaceDeletionProgressFragmentTest Change-Id: Ic7d81e59515ad9ad1a354fff8884133e7cc8ea64 --- .../delete/PrivateSpaceDeletionProgressFragmentTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/src/com/android/settings/privatespace/delete/PrivateSpaceDeletionProgressFragmentTest.java b/tests/unit/src/com/android/settings/privatespace/delete/PrivateSpaceDeletionProgressFragmentTest.java index 9806540d540..62505400c8d 100644 --- a/tests/unit/src/com/android/settings/privatespace/delete/PrivateSpaceDeletionProgressFragmentTest.java +++ b/tests/unit/src/com/android/settings/privatespace/delete/PrivateSpaceDeletionProgressFragmentTest.java @@ -64,6 +64,11 @@ public class PrivateSpaceDeletionProgressFragmentTest { public void setup() { MockitoAnnotations.initMocks(this); mContext = ApplicationProvider.getApplicationContext(); + final FakeFeatureFactory featureFactory = FakeFeatureFactory.setupForTest(); + when(featureFactory.securityFeatureProvider.getLockPatternUtils(mContext)) + .thenReturn(mLockPatternUtils); + doReturn(true).when(mLockPatternUtils).isSecure(anyInt()); + mFragment = new PrivateSpaceDeletionProgressFragment(); PrivateSpaceDeletionProgressFragment.Injector injector = new PrivateSpaceDeletionProgressFragment.Injector() { @@ -74,10 +79,6 @@ public class PrivateSpaceDeletionProgressFragmentTest { }; mPrivateSpaceMaintainer = PrivateSpaceMaintainer.getInstance(mContext); mFragment.setPrivateSpaceMaintainer(injector); - final FakeFeatureFactory featureFactory = FakeFeatureFactory.setupForTest(); - when(featureFactory.securityFeatureProvider.getLockPatternUtils(mContext)) - .thenReturn(mLockPatternUtils); - doReturn(true).when(mLockPatternUtils).isSecure(anyInt()); } @After