diff --git a/res/values/strings.xml b/res/values/strings.xml
index 650ba3ef469..349a4ab5768 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1031,13 +1031,21 @@
Touch the sensor on the back of your phone. Use your index finger.
- Enrollment was not completed
+ Can\u2019t complete fingerprint setup
+
+ You can try again now or set up your fingerprint later in Settings.
+
+ You can try again now or set up your fingerprint later.
Fingerprint setup timed out
+
+ You can set up your fingerprint later in Settings.
+
+ Something went wrong. You can set up your fingerprint later in Settings.
- You can set up your fingerprint later in Settings.
+ You can set up your fingerprint later.
- Fingerprint enrollment didn\'t work. Try again or use a different finger.
+ Something went wrong. You can set up your fingerprint later.
Add another
@@ -5509,6 +5517,8 @@
< %1$s
+
+ Due to quality inspections before shipping, the cycle count may not be zero on first use
Process Stats
diff --git a/res/xml/battery_info.xml b/res/xml/battery_info.xml
index 186b4fe3ebf..8e3c31f22aa 100644
--- a/res/xml/battery_info.xml
+++ b/res/xml/battery_info.xml
@@ -42,4 +42,9 @@
settings:controller="com.android.settings.deviceinfo.batteryinfo.BatteryCycleCountPreferenceController"
settings:enableCopying="true"/>
+
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
index 9aee087f3e0..f653942f3a9 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -511,7 +511,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
// showErrorDialog() will cause onWindowFocusChanged(false), set mIsCanceled to false
// before showErrorDialog() to prevent that another error dialog is triggered again.
mIsCanceled = true;
- FingerprintErrorDialog.showErrorDialog(this, errorMsgId);
+ FingerprintErrorDialog.showErrorDialog(this, errorMsgId,
+ this instanceof SetupFingerprintEnrollEnrolling);
cancelEnrollment();
stopIconAnimation();
stopListenOrientationEvent();
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
index e54403170eb..aadc9329e4c 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
@@ -329,7 +329,8 @@ public class FingerprintEnrollFindSensor extends BiometricEnrollBase implements
if (mNextClicked && errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
proceedToEnrolling(false /* cancelEnrollment */);
} else {
- FingerprintErrorDialog.showErrorDialog(this, errMsgId);
+ FingerprintErrorDialog.showErrorDialog(this, errMsgId,
+ this instanceof SetupFingerprintEnrollFindSensor);
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java b/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java
index f0a87a4a889..55745d043f6 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java
@@ -18,11 +18,14 @@ package com.android.settings.biometrics.fingerprint;
import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
+import static com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling.KEY_STATE_CANCELED;
import android.app.Activity;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
+import android.content.Intent;
import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
@@ -48,6 +51,8 @@ public class FingerprintErrorDialog extends InstrumentedDialogFragment {
final CharSequence errorTitle = getArguments().getCharSequence(KEY_ERROR_TITLE);
final int errMsgId = getArguments().getInt(KEY_ERROR_ID);
final boolean wasTimeout = errMsgId == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT;
+ final boolean showTryAgain = errMsgId
+ == BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS;
builder.setTitle(errorTitle)
.setMessage(errorString)
@@ -64,12 +69,33 @@ public class FingerprintErrorDialog extends InstrumentedDialogFragment {
}
activity.finish();
});
+
+ if (showTryAgain) {
+ builder.setPositiveButton(
+ R.string.security_settings_fingerprint_enroll_dialog_try_again,
+ (dialog, which) -> {
+ dialog.dismiss();
+ final Activity activity = getActivity();
+ final Intent intent = activity.getIntent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ intent.putExtra(KEY_STATE_CANCELED, false);
+ activity.startActivity(intent);
+ activity.finish();
+ })
+ .setNegativeButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
+ (dialog, which) -> {
+ dialog.dismiss();
+ final Activity activity = getActivity();
+ activity.setResult(RESULT_FINISHED);
+ activity.finish();
+ });
+ }
final AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
- public static void showErrorDialog(BiometricEnrollBase host, int errMsgId) {
+ public static void showErrorDialog(BiometricEnrollBase host, int errMsgId, boolean isSetup) {
if (host.isFinishing()) {
return;
}
@@ -77,12 +103,39 @@ public class FingerprintErrorDialog extends InstrumentedDialogFragment {
if (fragmentManager.isDestroyed() || fragmentManager.isStateSaved()) {
return;
}
- CharSequence errMsg = host.getText(getErrorMessage(errMsgId));
+ CharSequence errMsg;
+ if (isSetup) {
+ errMsg = host.getText(getSetupErrorMessage(errMsgId));
+ } else {
+ errMsg = host.getText(getErrorMessage(errMsgId));
+ }
final CharSequence errTitle = host.getText(getErrorTitle(errMsgId));
final FingerprintErrorDialog dialog = newInstance(errMsg, errTitle, errMsgId);
dialog.show(fragmentManager, FingerprintErrorDialog.class.getName());
}
+ /**
+ * Gets dialog message as error id inside {@link FingerprintManager}
+ */
+ public static int getSetupErrorMessage(int errMsgId) {
+ switch (errMsgId) {
+ case FingerprintManager.FINGERPRINT_ERROR_TIMEOUT:
+ // This message happens when the underlying crypto layer decides to revoke
+ // the enrollment auth token.
+ return R.string
+ .security_settings_fingerprint_enroll_error_timeout_dialog_message_setup;
+ case FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION:
+ return R.string.security_settings_fingerprint_bad_calibration;
+ case FingerprintManager.FINGERPRINT_ERROR_UNABLE_TO_PROCESS:
+ return R.string
+ .security_settings_fingerprint_enroll_error_unable_to_process_message_setup;
+ default:
+ // There's nothing specific to tell the user about. Ask them to try again.
+ return R.string
+ .security_settings_fingerprint_enroll_error_generic_dialog_message_setup;
+ }
+ }
+
/**
* Gets dialog message as error id inside {@link FingerprintManager}
*/
@@ -94,6 +147,9 @@ public class FingerprintErrorDialog extends InstrumentedDialogFragment {
return R.string.security_settings_fingerprint_enroll_error_timeout_dialog_message;
case FingerprintManager.FINGERPRINT_ERROR_BAD_CALIBRATION:
return R.string.security_settings_fingerprint_bad_calibration;
+ case FingerprintManager.FINGERPRINT_ERROR_UNABLE_TO_PROCESS:
+ return R.string
+ .security_settings_fingerprint_enroll_error_unable_to_process_message;
default:
// There's nothing specific to tell the user about. Ask them to try again.
return R.string.security_settings_fingerprint_enroll_error_generic_dialog_message;
@@ -105,11 +161,11 @@ public class FingerprintErrorDialog extends InstrumentedDialogFragment {
*/
public static int getErrorTitle(int errMsgId) {
switch (errMsgId) {
- case FingerprintManager.FINGERPRINT_ERROR_UNABLE_TO_PROCESS:
+ case FingerprintManager.FINGERPRINT_ERROR_TIMEOUT:
+ return R.string.security_settings_fingerprint_enroll_error_dialog_title;
+ default:
return R.string
.security_settings_fingerprint_enroll_error_unable_to_process_dialog_title;
- default:
- return R.string.security_settings_fingerprint_enroll_error_dialog_title;
}
}
diff --git a/src/com/android/settings/datausage/DataSaverBackend.java b/src/com/android/settings/datausage/DataSaverBackend.java
index e47ecbdc997..6a392341122 100644
--- a/src/com/android/settings/datausage/DataSaverBackend.java
+++ b/src/com/android/settings/datausage/DataSaverBackend.java
@@ -196,8 +196,10 @@ public class DataSaverBackend {
public interface Listener {
void onDataSaverChanged(boolean isDataSaving);
- void onAllowlistStatusChanged(int uid, boolean isAllowlisted);
+ /** This is called when allow list status is changed. */
+ default void onAllowlistStatusChanged(int uid, boolean isAllowlisted) {}
- void onDenylistStatusChanged(int uid, boolean isDenylisted);
+ /** This is called when deny list status is changed. */
+ default void onDenylistStatusChanged(int uid, boolean isDenylisted) {}
}
}
diff --git a/src/com/android/settings/datausage/DataSaverSummary.kt b/src/com/android/settings/datausage/DataSaverSummary.kt
index 1d9cbb73a66..7f38900bbd2 100644
--- a/src/com/android/settings/datausage/DataSaverSummary.kt
+++ b/src/com/android/settings/datausage/DataSaverSummary.kt
@@ -15,33 +15,36 @@
*/
package com.android.settings.datausage
-import android.app.Application
import android.app.settings.SettingsEnums
import android.content.Context
+import android.net.NetworkPolicyManager
import android.os.Bundle
+import android.os.UserHandle
import android.telephony.SubscriptionManager
import android.widget.Switch
+import androidx.annotation.VisibleForTesting
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.android.settings.R
import com.android.settings.SettingsActivity
import com.android.settings.SettingsPreferenceFragment
-import com.android.settings.applications.AppStateBaseBridge
-import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState
import com.android.settings.search.BaseSearchIndexProvider
import com.android.settings.widget.SettingsMainSwitchBar
-import com.android.settingslib.applications.ApplicationsState
import com.android.settingslib.search.SearchIndexable
import com.android.settingslib.spa.framework.util.formatString
+import com.android.settingslib.spaprivileged.model.app.AppListRepository
+import com.android.settingslib.spaprivileged.model.app.AppListRepositoryImpl
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
@SearchIndexable
class DataSaverSummary : SettingsPreferenceFragment() {
private lateinit var switchBar: SettingsMainSwitchBar
private lateinit var dataSaverBackend: DataSaverBackend
private lateinit var unrestrictedAccess: Preference
- private var dataUsageBridge: AppStateDataUsageBridge? = null
- private var session: ApplicationsState.Session? = null
// Flag used to avoid infinite loop due if user switch it on/off too quick.
private var switching = false
@@ -72,27 +75,15 @@ class DataSaverSummary : SettingsPreferenceFragment() {
override fun onResume() {
super.onResume()
- dataSaverBackend.refreshAllowlist()
- dataSaverBackend.refreshDenylist()
dataSaverBackend.addListener(dataSaverBackendListener)
- dataUsageBridge?.resume(/* forceLoadAllApps= */ true)
- ?: viewLifecycleOwner.lifecycleScope.launch {
- val applicationsState = ApplicationsState.getInstance(
- requireContext().applicationContext as Application
- )
- dataUsageBridge = AppStateDataUsageBridge(
- applicationsState, dataUsageBridgeCallbacks, dataSaverBackend
- )
- session =
- applicationsState.newSession(applicationsStateCallbacks, settingsLifecycle)
- dataUsageBridge?.resume(/* forceLoadAllApps= */ true)
- }
+ viewLifecycleOwner.lifecycleScope.launch {
+ unrestrictedAccess.summary = getUnrestrictedSummary(requireContext())
+ }
}
override fun onPause() {
super.onPause()
dataSaverBackend.remListener(dataSaverBackendListener)
- dataUsageBridge?.pause()
}
private fun onSwitchChanged(isChecked: Boolean) {
@@ -115,52 +106,36 @@ class DataSaverSummary : SettingsPreferenceFragment() {
switching = false
}
}
-
- override fun onAllowlistStatusChanged(uid: Int, isAllowlisted: Boolean) {}
-
- override fun onDenylistStatusChanged(uid: Int, isDenylisted: Boolean) {}
- }
-
- private val dataUsageBridgeCallbacks = AppStateBaseBridge.Callback {
- updateUnrestrictedAccessSummary()
- }
-
- private val applicationsStateCallbacks = object : ApplicationsState.Callbacks {
- override fun onRunningStateChanged(running: Boolean) {}
-
- override fun onPackageListChanged() {}
-
- override fun onRebuildComplete(apps: ArrayList?) {}
-
- override fun onPackageIconChanged() {}
-
- override fun onPackageSizeChanged(packageName: String?) {}
-
- override fun onAllSizesComputed() {
- updateUnrestrictedAccessSummary()
- }
-
- override fun onLauncherInfoChanged() {
- updateUnrestrictedAccessSummary()
- }
-
- override fun onLoadEntriesCompleted() {}
- }
-
- private fun updateUnrestrictedAccessSummary() {
- if (!isAdded || isFinishingOrDestroyed) return
- val allApps = session?.allApps ?: return
- val count = allApps.count {
- ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(it) &&
- (it.extraInfo as? DataUsageState)?.isDataSaverAllowlisted == true
- }
- unrestrictedAccess.summary =
- resources.formatString(R.string.data_saver_unrestricted_summary, "count" to count)
}
companion object {
private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access"
+ @VisibleForTesting
+ suspend fun getUnrestrictedSummary(
+ context: Context,
+ appListRepository: AppListRepository =
+ AppListRepositoryImpl(context.applicationContext),
+ ) = context.formatString(
+ R.string.data_saver_unrestricted_summary,
+ "count" to getAllowCount(context.applicationContext, appListRepository),
+ )
+
+ private suspend fun getAllowCount(context: Context, appListRepository: AppListRepository) =
+ withContext(Dispatchers.IO) {
+ coroutineScope {
+ val appsDeferred = async {
+ appListRepository.loadAndFilterApps(
+ userId = UserHandle.myUserId(),
+ isSystemApp = false,
+ )
+ }
+ val uidsAllowed = NetworkPolicyManager.from(context)
+ .getUidsWithPolicy(NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND)
+ appsDeferred.await().count { app -> app.uid in uidsAllowed }
+ }
+ }
+
private fun Context.isDataSaverVisible(): Boolean =
resources.getBoolean(R.bool.config_show_data_saver)
diff --git a/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceController.java b/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceController.java
index b585ecec6a4..5786dfc99cd 100644
--- a/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceController.java
+++ b/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceController.java
@@ -17,6 +17,8 @@
package com.android.settings.network.telephony;
import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
@@ -33,9 +35,10 @@ import com.android.internal.telephony.util.ArrayUtils;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
+import com.android.settingslib.utils.ThreadUtils;
/**
- * Preference controller for "Enhanced 4G LTE"
+ * Preference controller for "Voice over NR".
*/
public class NrAdvancedCallingPreferenceController extends TelephonyTogglePreferenceController
implements LifecycleObserver, OnStart, OnStop {
@@ -50,8 +53,11 @@ public class NrAdvancedCallingPreferenceController extends TelephonyTogglePrefer
private boolean mIsVonrVisibleFromCarrierConfig = false;
private boolean mIsNrEnableFromCarrierConfig = false;
private boolean mHas5gCapability = false;
+ private boolean mIsVoNrEnabled = false;
private Integer mCallState;
+ private Handler mHandler = new Handler(Looper.getMainLooper());
+
public NrAdvancedCallingPreferenceController(Context context, String key) {
super(context, key);
mTelephonyManager = context.getSystemService(TelephonyManager.class);
@@ -94,6 +100,8 @@ public class NrAdvancedCallingPreferenceController extends TelephonyTogglePrefer
CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY);
mIsNrEnableFromCarrierConfig = !ArrayUtils.isEmpty(nrAvailabilities);
+ updateVoNrState();
+
Log.d(TAG, "mHas5gCapability: " + mHas5gCapability
+ ",mIsNrEnabledFromCarrierConfig: " + mIsNrEnableFromCarrierConfig
+ ",mIsVonrEnabledFromCarrierConfig: " + mIsVonrEnabledFromCarrierConfig
@@ -162,7 +170,7 @@ public class NrAdvancedCallingPreferenceController extends TelephonyTogglePrefer
@Override
public boolean isChecked() {
- return mTelephonyManager.isVoNrEnabled();
+ return mIsVoNrEnabled;
}
@VisibleForTesting
@@ -174,6 +182,19 @@ public class NrAdvancedCallingPreferenceController extends TelephonyTogglePrefer
return isCallStateIdle();
}
+ private void updateVoNrState() {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ boolean result = mTelephonyManager.isVoNrEnabled();
+ if (result != mIsVoNrEnabled) {
+ Log.i(TAG, "VoNr state : " + result);
+ mIsVoNrEnabled = result;
+ mHandler.post(() -> {
+ updateState(mPreference);
+ });
+ }
+ });
+ }
+
private class PhoneCallStateTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.CallStateListener {
diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
index 9fc358b5cf8..5ed361560d7 100644
--- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
+++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
@@ -23,6 +23,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager.GET_ACTIVITIES
import android.content.pm.PackageManager.PackageInfoFlags
+import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import com.android.settings.R
@@ -56,26 +57,21 @@ class PictureInPictureListModel(private val context: Context) :
private val packageManager = context.packageManager
override fun transform(userIdFlow: Flow, appListFlow: Flow>) =
- userIdFlow.map(::getPictureInPicturePackages).combine(appListFlow) {
- pictureInPicturePackages,
- appList ->
- appList.map { app ->
- createPictureInPictureRecord(
- app = app,
- isSupport = app.packageName in pictureInPicturePackages,
- )
+ userIdFlow.map(::getPictureInPicturePackages)
+ .combine(appListFlow) { pictureInPicturePackages, appList ->
+ appList.map { app ->
+ createPictureInPictureRecord(
+ app = app,
+ isSupport = app.packageName in pictureInPicturePackages,
+ )
+ }
}
- }
- override fun transformItem(app: ApplicationInfo): PictureInPictureRecord {
- return createPictureInPictureRecord(
- app = app,
- isSupport = app.installed &&
- packageManager
- .getPackageInfoAsUser(app.packageName, GET_ACTIVITIES_FLAGS, app.userId)
- .supportsPictureInPicture(),
- )
- }
+ override fun transformItem(app: ApplicationInfo) = createPictureInPictureRecord(
+ app = app,
+ isSupport = app.installed &&
+ getPackageAndActivityInfo(app)?.supportsPictureInPicture() == true,
+ )
private fun createPictureInPictureRecord(app: ApplicationInfo, isSupport: Boolean) =
PictureInPictureRecord(
@@ -103,13 +99,36 @@ class PictureInPictureListModel(private val context: Context) :
}
private fun getPictureInPicturePackages(userId: Int): Set =
- packageManager
- .getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId)
+ getPackageAndActivityInfoList(userId)
.filter { it.supportsPictureInPicture() }
.map { it.packageName }
.toSet()
+ private fun getPackageAndActivityInfo(app: ApplicationInfo): PackageInfo? = try {
+ packageManager.getPackageInfoAsUser(app.packageName, GET_ACTIVITIES_FLAGS, app.userId)
+ } catch (e: Exception) {
+ // Query PackageManager.getPackageInfoAsUser() with GET_ACTIVITIES_FLAGS could cause
+ // exception sometimes. Since we reply on this flag to retrieve the Picture In Picture
+ // packages, we need to catch the exception to alleviate the impact before PackageManager
+ // fixing this issue or provide a better api.
+ Log.e(TAG, "Exception while getPackageInfoAsUser", e)
+ null
+ }
+
+ private fun getPackageAndActivityInfoList(userId: Int): List = try {
+ packageManager.getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId)
+ } catch (e: Exception) {
+ // Query PackageManager.getPackageInfoAsUser() with GET_ACTIVITIES_FLAGS could cause
+ // exception sometimes. Since we reply on this flag to retrieve the Picture In Picture
+ // packages, we need to catch the exception to alleviate the impact before PackageManager
+ // fixing this issue or provide a better api.
+ Log.e(TAG, "Exception while getInstalledPackagesAsUser", e)
+ emptyList()
+ }
+
companion object {
+ private const val TAG = "PictureInPictureListModel"
+
private fun PackageInfo.supportsPictureInPicture() =
activities?.any(ActivityInfo::supportsPictureInPicture) ?: false
diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.kt
new file mode 100644
index 00000000000..3c88d8e875b
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/datausage/DataSaverSummaryTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.datausage
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.net.NetworkPolicyManager
+import android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.datausage.DataSaverSummary.Companion.getUnrestrictedSummary
+import com.android.settingslib.spaprivileged.model.app.AppListRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class DataSaverSummaryTest {
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Mock
+ private lateinit var networkPolicyManager: NetworkPolicyManager
+
+ @Before
+ fun setUp() {
+ whenever(context.applicationContext).thenReturn(context)
+ whenever(NetworkPolicyManager.from(context)).thenReturn(networkPolicyManager)
+ }
+
+ @Test
+ fun getUnrestrictedSummary_whenTwoAppsAllowed() = runTest {
+ whenever(
+ networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND)
+ ).thenReturn(intArrayOf(APP1.uid, APP2.uid))
+
+ val summary =
+ getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository)
+
+ assertThat(summary)
+ .isEqualTo("2 apps allowed to use unrestricted data when Data Saver is on")
+ }
+
+ @Test
+ fun getUnrestrictedSummary_whenNoAppsAllowed() = runTest {
+ whenever(
+ networkPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND)
+ ).thenReturn(intArrayOf())
+
+ val summary =
+ getUnrestrictedSummary(context = context, appListRepository = FakeAppListRepository)
+
+ assertThat(summary)
+ .isEqualTo("0 apps allowed to use unrestricted data when Data Saver is on")
+ }
+
+ private companion object {
+ val APP1 = ApplicationInfo().apply { uid = 10001 }
+ val APP2 = ApplicationInfo().apply { uid = 10002 }
+ val APP3 = ApplicationInfo().apply { uid = 10003 }
+
+ object FakeAppListRepository : AppListRepository {
+ override suspend fun loadApps(
+ userId: Int,
+ loadInstantApps: Boolean,
+ matchAnyUserForAdmin: Boolean,
+ ) = emptyList()
+
+ override fun showSystemPredicate(
+ userIdFlow: Flow,
+ showSystemFlow: Flow,
+ ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { false }
+
+ override fun getSystemPackageNamesBlocking(userId: Int): Set = emptySet()
+
+ override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) =
+ listOf(APP1, APP2, APP3)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt
index f90d63947d0..fb0fb698045 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt
@@ -23,6 +23,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags
+import android.os.DeadSystemRuntimeException
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
@@ -100,6 +101,23 @@ class PictureInPictureTest {
assertThat(record.isSupport).isTrue()
}
+ @Test
+ fun transform_getInstalledPackagesAsUserThrowsException_treatAsNotSupported() = runTest {
+ whenever(packageManager.getInstalledPackagesAsUser(any(), anyInt()))
+ .thenThrow(DeadSystemRuntimeException())
+
+ val recordListFlow = listModel.transform(
+ userIdFlow = flowOf(USER_ID),
+ appListFlow = flowOf(listOf(PICTURE_IN_PICTURE_APP)),
+ )
+
+ val recordList = recordListFlow.first()
+ assertThat(recordList).hasSize(1)
+ val record = recordList[0]
+ assertThat(record.app).isSameInstanceAs(PICTURE_IN_PICTURE_APP)
+ assertThat(record.isSupport).isFalse()
+ }
+
@Test
fun transformItem() {
whenever(
@@ -114,6 +132,20 @@ class PictureInPictureTest {
assertThat(record.isSupport).isTrue()
}
+ @Test
+ fun transformItem_getPackageInfoAsUserThrowsException_treatAsNotSupported() {
+ whenever(
+ packageManager.getPackageInfoAsUser(
+ eq(PICTURE_IN_PICTURE_PACKAGE_NAME), any(), eq(USER_ID)
+ )
+ ).thenThrow(DeadSystemRuntimeException())
+
+ val record = listModel.transformItem(PICTURE_IN_PICTURE_APP)
+
+ assertThat(record.app).isSameInstanceAs(PICTURE_IN_PICTURE_APP)
+ assertThat(record.isSupport).isFalse()
+ }
+
@Test
fun filter_isSupport() = runTest {
val record = createRecord(isSupport = true)
diff --git a/tests/unit/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceControllerTest.java
index 9d4082ecff6..053fb4d8916 100644
--- a/tests/unit/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/network/telephony/NrAdvancedCallingPreferenceControllerTest.java
@@ -186,6 +186,7 @@ public class NrAdvancedCallingPreferenceControllerTest {
doReturn(true).when(mTelephonyManager).isVoNrEnabled();
mPreference.setChecked(false);
+ mController.init(SUB_ID);
mController.updateState(mPreference);
assertThat(mPreference.isChecked()).isTrue();