Move SystemUpdateManager.retrieveSystemUpdateInfo

Into Kotlin Coroutine for true async.

Bug: 300851543
Test: manual - on system page
Test: unit test
Change-Id: Ibec5c9d0934d71ed1a5808cadf3b3542eb3d5fa0
diff --git a/src/com/android/settings/system/SystemDashboardFragment.java b/src/com/android/settings/system/SystemDashboardFragment.java
index 79c5b9f..678b675 100644
--- a/src/com/android/settings/system/SystemDashboardFragment.java
+++ b/src/com/android/settings/system/SystemDashboardFragment.java
@@ -16,9 +16,7 @@
 package com.android.settings.system;
 
 import android.app.settings.SettingsEnums;
-import android.content.Context;
 import android.os.Bundle;
-import android.provider.SearchIndexableResource;
 
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
@@ -29,9 +27,6 @@
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settingslib.search.SearchIndexable;
 
-import java.util.Arrays;
-import java.util.List;
-
 @SearchIndexable
 public class SystemDashboardFragment extends DashboardFragment {
 
@@ -85,13 +80,5 @@
      * For Search.
      */
     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
-            new BaseSearchIndexProvider() {
-                @Override
-                public List<SearchIndexableResource> getXmlResourcesToIndex(
-                        Context context, boolean enabled) {
-                    final SearchIndexableResource sir = new SearchIndexableResource(context);
-                    sir.xmlResId = R.xml.system_dashboard_fragment;
-                    return Arrays.asList(sir);
-                }
-            };
-}
\ No newline at end of file
+            new BaseSearchIndexProvider(R.xml.system_dashboard_fragment);
+}
diff --git a/src/com/android/settings/system/SystemUpdateManagerExt.kt b/src/com/android/settings/system/SystemUpdateManagerExt.kt
new file mode 100644
index 0000000..8ddf174
--- /dev/null
+++ b/src/com/android/settings/system/SystemUpdateManagerExt.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.system
+
+import android.content.Context
+import android.os.Bundle
+import android.os.SystemUpdateManager
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+private const val TAG = "SystemUpdateManagerExt"
+
+/**
+ * Gets the system update status.
+ *
+ * Note: [SystemUpdateManager.retrieveSystemUpdateInfo] must be called on worker thread to avoid
+ * StrictMode violation.
+ */
+suspend fun Context.getSystemUpdateInfo(): Bundle? = withContext(Dispatchers.Default) {
+    val updateManager = getSystemService(SystemUpdateManager::class.java)!!
+    try {
+        updateManager.retrieveSystemUpdateInfo()
+    } catch (e: Exception) {
+        Log.w(TAG, "Error getting system update info.")
+        null
+    }
+}
diff --git a/src/com/android/settings/system/SystemUpdatePreferenceController.java b/src/com/android/settings/system/SystemUpdatePreferenceController.java
deleted file mode 100644
index b2a22ff..0000000
--- a/src/com/android/settings/system/SystemUpdatePreferenceController.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.settings.system;
-
-import static android.content.Context.CARRIER_CONFIG_SERVICE;
-import static android.content.Context.SYSTEM_UPDATE_SERVICE;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.PersistableBundle;
-import android.os.SystemUpdateManager;
-import android.os.UserManager;
-import android.telephony.CarrierConfigManager;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
-
-import com.android.settings.R;
-import com.android.settings.Utils;
-import com.android.settings.core.BasePreferenceController;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-
-public class SystemUpdatePreferenceController extends BasePreferenceController {
-
-    private static final String TAG = "SysUpdatePrefContr";
-
-    private static final String KEY_SYSTEM_UPDATE_SETTINGS = "system_update_settings";
-
-    private final UserManager mUm;
-    private final SystemUpdateManager mUpdateManager;
-
-    public SystemUpdatePreferenceController(Context context) {
-        super(context, KEY_SYSTEM_UPDATE_SETTINGS);
-        mUm = UserManager.get(context);
-        mUpdateManager = (SystemUpdateManager) context.getSystemService(SYSTEM_UPDATE_SERVICE);
-    }
-
-    @Override
-    public int getAvailabilityStatus() {
-        return mContext.getResources().getBoolean(R.bool.config_show_system_update_settings)
-                && mUm.isAdminUser()
-                ? AVAILABLE
-                : UNSUPPORTED_ON_DEVICE;
-    }
-
-    @Override
-    public void displayPreference(PreferenceScreen screen) {
-        super.displayPreference(screen);
-        if (isAvailable()) {
-            Utils.updatePreferenceToSpecificActivityOrRemove(mContext, screen,
-                    getPreferenceKey(),
-                    Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY);
-        }
-    }
-
-    @Override
-    public boolean handlePreferenceTreeClick(Preference preference) {
-        if (TextUtils.equals(getPreferenceKey(), preference.getKey())) {
-            CarrierConfigManager configManager =
-                    (CarrierConfigManager) mContext.getSystemService(CARRIER_CONFIG_SERVICE);
-            PersistableBundle b = configManager.getConfig();
-            if (b != null && b.getBoolean(CarrierConfigManager.KEY_CI_ACTION_ON_SYS_UPDATE_BOOL)) {
-                ciActionOnSysUpdate(b);
-            }
-        }
-        // always return false here because this handler does not want to block other handlers.
-        return false;
-    }
-
-    @Override
-    public CharSequence getSummary() {
-        CharSequence summary = mContext.getString(R.string.android_version_summary,
-                Build.VERSION.RELEASE_OR_PREVIEW_DISPLAY);
-        final FutureTask<Bundle> bundleFutureTask = new FutureTask<>(
-                // Put the API call in a future to avoid StrictMode violation.
-                () -> mUpdateManager.retrieveSystemUpdateInfo());
-        final Bundle updateInfo;
-        try {
-            bundleFutureTask.run();
-            updateInfo = bundleFutureTask.get();
-        } catch (InterruptedException | ExecutionException e) {
-            Log.w(TAG, "Error getting system update info.");
-            return summary;
-        }
-        switch (updateInfo.getInt(SystemUpdateManager.KEY_STATUS)) {
-            case SystemUpdateManager.STATUS_WAITING_DOWNLOAD:
-            case SystemUpdateManager.STATUS_IN_PROGRESS:
-            case SystemUpdateManager.STATUS_WAITING_INSTALL:
-            case SystemUpdateManager.STATUS_WAITING_REBOOT:
-                summary = mContext.getText(R.string.android_version_pending_update_summary);
-                break;
-            case SystemUpdateManager.STATUS_UNKNOWN:
-                Log.d(TAG, "Update statue unknown");
-                // fall through to next branch
-            case SystemUpdateManager.STATUS_IDLE:
-                final String version = updateInfo.getString(SystemUpdateManager.KEY_TITLE);
-                if (!TextUtils.isEmpty(version)) {
-                    summary = mContext.getString(R.string.android_version_summary, version);
-                }
-                break;
-        }
-        return summary;
-    }
-
-    /**
-     * Trigger client initiated action (send intent) on system update
-     */
-    private void ciActionOnSysUpdate(PersistableBundle b) {
-        String intentStr = b.getString(CarrierConfigManager.
-                KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING);
-        if (!TextUtils.isEmpty(intentStr)) {
-            String extra = b.getString(CarrierConfigManager.
-                    KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_STRING);
-            String extraVal = b.getString(CarrierConfigManager.
-                    KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_VAL_STRING);
-
-            Intent intent = new Intent(intentStr);
-            if (!TextUtils.isEmpty(extra)) {
-                intent.putExtra(extra, extraVal);
-            }
-            Log.d(TAG, "ciActionOnSysUpdate: broadcasting intent " + intentStr +
-                    " with extra " + extra + ", " + extraVal);
-            intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
-            mContext.getApplicationContext().sendBroadcast(intent);
-        }
-    }
-}
diff --git a/src/com/android/settings/system/SystemUpdatePreferenceController.kt b/src/com/android/settings/system/SystemUpdatePreferenceController.kt
new file mode 100644
index 0000000..01df065
--- /dev/null
+++ b/src/com/android/settings/system/SystemUpdatePreferenceController.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 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.system
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.PersistableBundle
+import android.os.SystemUpdateManager
+import android.os.UserManager
+import android.telephony.CarrierConfigManager
+import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.Utils
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import kotlinx.coroutines.launch
+
+open class SystemUpdatePreferenceController(context: Context, preferenceKey: String) :
+    BasePreferenceController(context, preferenceKey) {
+    private val userManager: UserManager = context.userManager
+    private lateinit var preference: Preference
+
+    override fun getAvailabilityStatus() =
+        if (mContext.resources.getBoolean(R.bool.config_show_system_update_settings) &&
+            userManager.isAdminUser
+        ) AVAILABLE else UNSUPPORTED_ON_DEVICE
+
+    override fun displayPreference(screen: PreferenceScreen) {
+        super.displayPreference(screen)
+        preference = screen.findPreference(preferenceKey)!!
+        if (isAvailable) {
+            Utils.updatePreferenceToSpecificActivityOrRemove(
+                mContext,
+                screen,
+                preferenceKey,
+                Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY,
+            )
+        }
+    }
+
+    override fun handlePreferenceTreeClick(preference: Preference): Boolean {
+        if (preferenceKey == preference.key) {
+            val configManager = mContext.getSystemService(CarrierConfigManager::class.java)!!
+            configManager.getConfig(CarrierConfigManager.KEY_CI_ACTION_ON_SYS_UPDATE_BOOL)?.let {
+                if (it.getBoolean(CarrierConfigManager.KEY_CI_ACTION_ON_SYS_UPDATE_BOOL)) {
+                    ciActionOnSysUpdate(it)
+                }
+            }
+        }
+        // always return false here because this handler does not want to block other handlers.
+        return false
+    }
+
+    override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
+        viewLifecycleOwner.lifecycleScope.launch {
+            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                preference.summary = calculateSummary()
+            }
+        }
+    }
+
+    private suspend fun calculateSummary(): String {
+        val updateInfo = mContext.getSystemUpdateInfo() ?: return getReleaseVersionSummary()
+
+        val status = updateInfo.getInt(SystemUpdateManager.KEY_STATUS)
+        if (status == SystemUpdateManager.STATUS_UNKNOWN) {
+            Log.d(TAG, "Update statue unknown")
+        }
+        when (status) {
+            SystemUpdateManager.STATUS_WAITING_DOWNLOAD,
+            SystemUpdateManager.STATUS_IN_PROGRESS,
+            SystemUpdateManager.STATUS_WAITING_INSTALL,
+            SystemUpdateManager.STATUS_WAITING_REBOOT -> {
+                return mContext.getString(R.string.android_version_pending_update_summary)
+            }
+
+            SystemUpdateManager.STATUS_IDLE,
+            SystemUpdateManager.STATUS_UNKNOWN -> {
+                val version = updateInfo.getString(SystemUpdateManager.KEY_TITLE)
+                if (!version.isNullOrEmpty()) {
+                    return mContext.getString(R.string.android_version_summary, version)
+                }
+            }
+        }
+        return getReleaseVersionSummary()
+    }
+
+    private fun getReleaseVersionSummary(): String = mContext.getString(
+        R.string.android_version_summary,
+        Build.VERSION.RELEASE_OR_PREVIEW_DISPLAY,
+    )
+
+    /**
+     * Trigger client initiated action (send intent) on system update
+     */
+    private fun ciActionOnSysUpdate(b: PersistableBundle) {
+        val intentStr = b.getString(CarrierConfigManager.KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING)
+        if (intentStr.isNullOrEmpty()) return
+        val extra = b.getString(CarrierConfigManager.KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_STRING)
+        val extraVal =
+            b.getString(CarrierConfigManager.KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_VAL_STRING)
+        Log.d(
+            TAG,
+            "ciActionOnSysUpdate: broadcasting intent $intentStr with extra $extra, $extraVal"
+        )
+        val intent = Intent(intentStr).apply {
+            if (!extra.isNullOrEmpty()) putExtra(extra, extraVal)
+            addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND)
+        }
+        mContext.applicationContext.sendBroadcast(intent)
+    }
+
+    companion object {
+        private const val TAG = "SysUpdatePrefContr"
+    }
+}
diff --git a/tests/robotests/res/values-mcc999/config.xml b/tests/robotests/res/values-mcc999/config.xml
index 82edea5..a5767e4 100644
--- a/tests/robotests/res/values-mcc999/config.xml
+++ b/tests/robotests/res/values-mcc999/config.xml
@@ -53,7 +53,6 @@
     <bool name="config_show_pointer_speed">false</bool>
     <bool name="config_show_vibrate_input_devices">false</bool>
     <bool name="config_show_reset_dashboard">false</bool>
-    <bool name="config_show_system_update_settings">false</bool>
     <bool name="config_show_device_model">false</bool>
     <bool name="config_show_top_level_accessibility">false</bool>
     <bool name="config_show_top_level_battery">false</bool>
diff --git a/tests/robotests/src/com/android/settings/system/SystemUpdatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/system/SystemUpdatePreferenceControllerTest.java
deleted file mode 100644
index 61aa294..0000000
--- a/tests/robotests/src/com/android/settings/system/SystemUpdatePreferenceControllerTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.settings.system;
-
-import static android.os.SystemUpdateManager.KEY_STATUS;
-import static android.os.SystemUpdateManager.KEY_TITLE;
-import static android.os.SystemUpdateManager.STATUS_IDLE;
-import static android.os.SystemUpdateManager.STATUS_UNKNOWN;
-import static android.os.SystemUpdateManager.STATUS_WAITING_DOWNLOAD;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.SystemUpdateManager;
-
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
-
-import com.android.settings.R;
-import com.android.settings.testutils.shadow.ShadowUserManager;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowApplication;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = ShadowUserManager.class)
-public class SystemUpdatePreferenceControllerTest {
-
-    @Mock
-    private PreferenceScreen mScreen;
-    @Mock
-    private SystemUpdateManager mSystemUpdateManager;
-
-    private Context mContext;
-    private ShadowUserManager mShadowUserManager;
-    private SystemUpdatePreferenceController mController;
-    private Preference mPreference;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mContext = RuntimeEnvironment.application;
-        mShadowUserManager = ShadowUserManager.getShadow();
-
-        ShadowApplication.getInstance().setSystemService(Context.SYSTEM_UPDATE_SERVICE,
-                mSystemUpdateManager);
-        mController = new SystemUpdatePreferenceController(mContext);
-        mPreference = new Preference(RuntimeEnvironment.application);
-        mPreference.setKey(mController.getPreferenceKey());
-        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
-    }
-
-    @After
-    public void cleanUp() {
-        mShadowUserManager.setIsAdminUser(false);
-    }
-
-    @Test
-    public void updateNonIndexable_ifAvailable_shouldNotUpdate() {
-        final List<String> keys = new ArrayList<>();
-        mShadowUserManager.setIsAdminUser(true);
-
-        mController.updateNonIndexableKeys(keys);
-
-        assertThat(keys).isEmpty();
-    }
-
-    @Test
-    public void updateNonIndexable_ifNotAvailable_shouldUpdate() {
-        mShadowUserManager.setIsAdminUser(false);
-        final List<String> keys = new ArrayList<>();
-
-        mController.updateNonIndexableKeys(keys);
-
-        assertThat(keys).hasSize(1);
-    }
-
-    @Test
-    public void displayPrefs_ifVisible_butNotAdminUser_shouldNotDisplay() {
-        mShadowUserManager.setIsAdminUser(false);
-        mController.displayPreference(mScreen);
-
-        assertThat(mPreference.isVisible()).isFalse();
-    }
-
-    @Test
-    @Config(qualifiers = "mcc999")
-    public void displayPrefs_ifAdminUser_butNotVisible_shouldNotDisplay() {
-        mShadowUserManager.setIsAdminUser(true);
-        mController.displayPreference(mScreen);
-
-        assertThat(mPreference.isVisible()).isFalse();
-    }
-
-    @Test
-    public void displayPrefs_ifAvailable_shouldDisplay() {
-        mShadowUserManager.setIsAdminUser(true);
-
-        mController.displayPreference(mScreen);
-
-        assertThat(mPreference.isVisible()).isTrue();
-    }
-
-    @Test
-    public void updateState_systemUpdateStatusUnknown_shouldSetToAndroidVersion() {
-        final Bundle bundle = new Bundle();
-        bundle.putInt(KEY_STATUS, STATUS_UNKNOWN);
-        when(mSystemUpdateManager.retrieveSystemUpdateInfo()).thenReturn(bundle);
-
-        mController.updateState(mPreference);
-
-        assertThat(mPreference.getSummary())
-                .isEqualTo(mContext.getString(R.string.android_version_summary,
-                        Build.VERSION.RELEASE_OR_PREVIEW_DISPLAY));
-    }
-
-    @Test
-    public void updateState_systemUpdateStatusIdle_shouldSetToAndroidVersion() {
-        final String testReleaseName = "ANDROID TEST VERSION";
-
-        final Bundle bundle = new Bundle();
-        bundle.putInt(KEY_STATUS, STATUS_IDLE);
-        bundle.putString(KEY_TITLE, testReleaseName);
-        when(mSystemUpdateManager.retrieveSystemUpdateInfo()).thenReturn(bundle);
-
-        mController.updateState(mPreference);
-
-        assertThat(mPreference.getSummary())
-                .isEqualTo(mContext.getString(R.string.android_version_summary, testReleaseName));
-    }
-
-    @Test
-    public void updateState_systemUpdateInProgress_shouldSetToUpdatePending() {
-        final Bundle bundle = new Bundle();
-        bundle.putInt(KEY_STATUS, STATUS_WAITING_DOWNLOAD);
-        when(mSystemUpdateManager.retrieveSystemUpdateInfo()).thenReturn(bundle);
-
-        mController.updateState(mPreference);
-
-        assertThat(mPreference.getSummary())
-                .isEqualTo(mContext.getString(R.string.android_version_pending_update_summary));
-    }
-}
diff --git a/tests/spa_unit/src/com/android/settings/system/SystemUpdateManagerExtKtTest.kt b/tests/spa_unit/src/com/android/settings/system/SystemUpdateManagerExtKtTest.kt
new file mode 100644
index 0000000..0ba91df
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/system/SystemUpdateManagerExtKtTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 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.system
+
+import android.content.Context
+import android.os.Bundle
+import android.os.SystemUpdateManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class SystemUpdateManagerExtKtTest {
+
+    private val mockSystemUpdateManager = mock<SystemUpdateManager>()
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { getSystemService(SystemUpdateManager::class.java) } doReturn mockSystemUpdateManager
+    }
+
+    @Test
+    fun getSystemUpdateInfo() = runTest {
+        val bundle = Bundle()
+        whenever(mockSystemUpdateManager.retrieveSystemUpdateInfo()).thenReturn(bundle)
+
+        val info = context.getSystemUpdateInfo()
+
+        assertThat(info).isSameInstanceAs(bundle)
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/system/SystemUpdatePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/system/SystemUpdatePreferenceControllerTest.kt
new file mode 100644
index 0000000..17cdf04
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/system/SystemUpdatePreferenceControllerTest.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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.system
+
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import android.os.SystemClock
+import android.os.SystemUpdateManager
+import android.os.UserManager
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class SystemUpdatePreferenceControllerTest {
+    private val mockUserManager = mock<UserManager>()
+    private val mockSystemUpdateManager = mock<SystemUpdateManager>()
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { userManager } doReturn mockUserManager
+        on { getSystemService(SystemUpdateManager::class.java) } doReturn mockSystemUpdateManager
+    }
+
+    private val resources = spy(context.resources) {
+        on { getBoolean(R.bool.config_show_system_update_settings) } doReturn true
+    }
+
+    private val preference = Preference(context).apply { key = KEY }
+    private val preferenceScreen = mock<PreferenceScreen> {
+        onGeneric { findPreference(KEY) } doReturn preference
+    }
+    private val controller = SystemUpdatePreferenceController(context, KEY)
+
+    @Before
+    fun setUp() {
+        whenever(context.resources).thenReturn(resources)
+    }
+
+    @Test
+    fun updateNonIndexable_ifAvailable_shouldNotUpdate() {
+        whenever(mockUserManager.isAdminUser).thenReturn(true)
+        val keys = mutableListOf<String>()
+
+        controller.updateNonIndexableKeys(keys)
+
+        assertThat(keys).isEmpty()
+    }
+
+    @Test
+    fun updateNonIndexable_ifNotAvailable_shouldUpdate() {
+        whenever(mockUserManager.isAdminUser).thenReturn(false)
+        val keys = mutableListOf<String>()
+
+        controller.updateNonIndexableKeys(keys)
+
+        assertThat(keys).containsExactly(KEY)
+    }
+
+    @Test
+    fun displayPrefs_ifVisible_butNotAdminUser_shouldNotDisplay() {
+        whenever(mockUserManager.isAdminUser).thenReturn(false)
+
+        controller.displayPreference(preferenceScreen)
+
+        assertThat(preference.isVisible).isFalse()
+    }
+
+    @Test
+    fun displayPrefs_ifAdminUser_butNotVisible_shouldNotDisplay() {
+        whenever(mockUserManager.isAdminUser).thenReturn(true)
+        whenever(resources.getBoolean(R.bool.config_show_system_update_settings)).thenReturn(false)
+
+        controller.displayPreference(preferenceScreen)
+
+        assertThat(preference.isVisible).isFalse()
+    }
+
+    @Test
+    fun displayPrefs_ifAvailable_shouldDisplay() {
+        whenever(mockUserManager.isAdminUser).thenReturn(true)
+
+        controller.displayPreference(preferenceScreen)
+
+        assertThat(preference.isVisible).isTrue()
+    }
+
+    @Test
+    fun updateState_systemUpdateStatusUnknown_shouldSetToAndroidVersion() {
+        val bundle = Bundle().apply {
+            putInt(SystemUpdateManager.KEY_STATUS, SystemUpdateManager.STATUS_UNKNOWN)
+        }
+        whenever(mockSystemUpdateManager.retrieveSystemUpdateInfo()).thenReturn(bundle)
+        controller.displayPreference(preferenceScreen)
+
+        controller.onViewCreated(TestLifecycleOwner())
+        SystemClock.sleep(100)
+
+        assertThat(preference.summary).isEqualTo(
+            context.getString(
+                R.string.android_version_summary,
+                Build.VERSION.RELEASE_OR_PREVIEW_DISPLAY,
+            )
+        )
+    }
+
+    @Test
+    fun updateState_systemUpdateStatusIdle_shouldSetToAndroidVersion() {
+        val testReleaseName = "ANDROID TEST VERSION"
+        val bundle = Bundle().apply {
+            putInt(SystemUpdateManager.KEY_STATUS, SystemUpdateManager.STATUS_IDLE)
+            putString(SystemUpdateManager.KEY_TITLE, testReleaseName)
+        }
+        whenever(mockSystemUpdateManager.retrieveSystemUpdateInfo()).thenReturn(bundle)
+        controller.displayPreference(preferenceScreen)
+
+        controller.onViewCreated(TestLifecycleOwner())
+        SystemClock.sleep(100)
+
+        assertThat(preference.summary)
+            .isEqualTo(context.getString(R.string.android_version_summary, testReleaseName))
+    }
+
+    @Test
+    fun updateState_systemUpdateInProgress_shouldSetToUpdatePending() {
+        val bundle = Bundle().apply {
+            putInt(SystemUpdateManager.KEY_STATUS, SystemUpdateManager.STATUS_WAITING_DOWNLOAD)
+        }
+        whenever(mockSystemUpdateManager.retrieveSystemUpdateInfo()).thenReturn(bundle)
+        controller.displayPreference(preferenceScreen)
+
+        controller.onViewCreated(TestLifecycleOwner())
+        SystemClock.sleep(100)
+
+        assertThat(preference.summary)
+            .isEqualTo(context.getString(R.string.android_version_pending_update_summary))
+    }
+
+    private companion object {
+        const val KEY = "test_key"
+    }
+}