diff --git a/TEST_MAPPING b/TEST_MAPPING
index 4b857526fd8..ec049732781 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -17,9 +17,6 @@
},
{
"exclude-filter": "com.android.settings.regionalpreferences"
- },
- {
- "exclude-filter": "com.android.settings.vpn2"
}
]
}
diff --git a/res-product/values/strings.xml b/res-product/values/strings.xml
index a6db4d9f1dd..987548acf33 100644
--- a/res-product/values/strings.xml
+++ b/res-product/values/strings.xml
@@ -84,11 +84,11 @@
Use your face to unlock your device, authorize purchases, or sign in to apps.
- Use your face to unlock your phone or for authentication in apps, like when you sign in to apps or approve a purchase.
+ Use your face to unlock your phone or for authentication in apps, like when you sign in to apps or approve a purchase
- Use your face to unlock your tablet or for authentication in apps, like when you sign in to apps or approve a purchase.
+ Use your face to unlock your tablet or for authentication in apps, like when you sign in to apps or approve a purchase
- Use your face to unlock your device or for authentication in apps, like when you sign in to apps or approve a purchase.
+ Use your face to unlock your device or for authentication in apps, like when you sign in to apps or approve a purchase
Allow your child to use their face to unlock their phone
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index dcfc4102f56..402fd042fab 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -469,8 +469,8 @@
48dp
48dp
2dp
- 42dp
- 3dp
+ 38dp
+ 5dp
1dp
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ff585c3173f..7a1ce115274 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -13385,10 +13385,8 @@
Check password and try again
Can\u0027t connect. Try again.
-
- Connecting\u2026
-
- Scanning\u2026
+
+ Connecting\u2026
Audio stream isn\u0027t available
@@ -13408,7 +13406,7 @@
connected compatible headphones
- Broadcasts
+ Audio streams
No password
diff --git a/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java b/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java
index 43d11b0148d..ff271c1e016 100644
--- a/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java
+++ b/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java
@@ -67,16 +67,12 @@ public class MagnificationAlwaysOnPreferenceController extends
@Override
public void onResume() {
- if (Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
- MagnificationCapabilities.registerObserver(mContext, mContentObserver);
- }
+ MagnificationCapabilities.registerObserver(mContext, mContentObserver);
}
@Override
public void onPause() {
- if (Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
- MagnificationCapabilities.unregisterObserver(mContext, mContentObserver);
- }
+ MagnificationCapabilities.unregisterObserver(mContext, mContentObserver);
}
@Override
@@ -111,10 +107,6 @@ public class MagnificationAlwaysOnPreferenceController extends
@Override
public CharSequence getSummary() {
- if (!Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
- return super.getSummary();
- }
-
@StringRes int resId = mPreference.isEnabled()
? R.string.accessibility_screen_magnification_always_on_summary
: R.string.accessibility_screen_magnification_always_on_unavailable_summary;
@@ -124,9 +116,6 @@ public class MagnificationAlwaysOnPreferenceController extends
@Override
public void updateState(Preference preference) {
super.updateState(preference);
- if (!Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
- return;
- }
if (preference == null) {
return;
diff --git a/src/com/android/settings/applications/credentials/PrimaryProviderPreference.java b/src/com/android/settings/applications/credentials/PrimaryProviderPreference.java
index 4a45ea4ed06..fcf6f90a277 100644
--- a/src/com/android/settings/applications/credentials/PrimaryProviderPreference.java
+++ b/src/com/android/settings/applications/credentials/PrimaryProviderPreference.java
@@ -168,7 +168,7 @@ public class PrimaryProviderPreference extends GearPreference {
mButtonFrameView.setPadding(
paddingLeft,
mButtonFrameView.getPaddingTop(),
- mButtonFrameView.getPaddingRight(),
+ paddingLeft,
mButtonFrameView.getPaddingBottom());
}
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index d776b9af29a..414c5450297 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -170,22 +170,25 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
infoMessageRequireEyes.setText(getInfoMessageRequireEyes());
}
- mFaceManager.addAuthenticatorsRegisteredCallback(
- new IFaceAuthenticatorsRegisteredCallback.Stub() {
- @Override
- public void onAllAuthenticatorsRegistered(
- @NonNull List sensors) {
- if (sensors.isEmpty()) {
- Log.e(TAG, "No sensors");
- return;
- }
- boolean isFaceStrong = sensors.get(0).sensorStrength
- == SensorProperties.STRENGTH_STRONG;
- mIsFaceStrong = isFaceStrong;
- onFaceStrengthChanged();
- }
- });
+ if (mFaceManager != null) {
+ mFaceManager.addAuthenticatorsRegisteredCallback(
+ new IFaceAuthenticatorsRegisteredCallback.Stub() {
+ @Override
+ public void onAllAuthenticatorsRegistered(
+ @NonNull List sensors) {
+ if (sensors.isEmpty()) {
+ Log.e(TAG, "No sensors");
+ return;
+ }
+
+ boolean isFaceStrong = sensors.get(0).sensorStrength
+ == SensorProperties.STRENGTH_STRONG;
+ mIsFaceStrong = isFaceStrong;
+ onFaceStrengthChanged();
+ }
+ });
+ }
// This path is an entry point for SetNewPasswordController, e.g.
// adb shell am start -a android.app.action.SET_NEW_PASSWORD
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
index 4d6a7f9d80e..d314d3f1fe9 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
@@ -28,7 +28,7 @@ import com.android.settingslib.utils.ThreadUtils;
class AddSourceWaitForResponseState extends AudioStreamStateHandler {
@VisibleForTesting
static final int AUDIO_STREAM_ADD_SOURCE_WAIT_FOR_RESPONSE_STATE_SUMMARY =
- R.string.audio_streams_add_source_wait_for_response_summary;
+ R.string.audio_streams_connecting_summary;
@VisibleForTesting static final int ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS = 20000;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
index 94e6644f2a9..1f71b73e379 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
@@ -19,13 +19,15 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import android.os.Bundle;
+import androidx.annotation.VisibleForTesting;
+
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
public class AudioStreamDetailsFragment extends DashboardFragment {
static final String BROADCAST_NAME_ARG = "broadcast_name";
static final String BROADCAST_ID_ARG = "broadcast_id";
- private static final String TAG = "AudioStreamDetailsFragment";
+ @VisibleForTesting static final String TAG = "AudioStreamDetailsFragment";
@Override
public void onAttach(Context context) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index 0334e055036..ab9612529dd 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -23,6 +23,7 @@ import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
@@ -60,7 +61,8 @@ class AudioStreamPreference extends TwoTargetPreference {
notifyChanged();
}
- private AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
+ @VisibleForTesting
+ AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing);
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
index ac4d9a1b1b3..e7197f6d023 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
@@ -33,7 +33,7 @@ import com.android.settingslib.utils.ThreadUtils;
class WaitForSyncState extends AudioStreamStateHandler {
@VisibleForTesting
static final int AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY =
- R.string.audio_streams_wait_for_sync_state_summary;
+ R.string.audio_streams_connecting_summary;
@VisibleForTesting static final int WAIT_FOR_SYNC_TIMEOUT_MILLIS = 15000;
diff --git a/src/com/android/settings/deviceinfo/simstatus/ImsRegistrationStateController.kt b/src/com/android/settings/deviceinfo/simstatus/ImsRegistrationStateController.kt
new file mode 100644
index 00000000000..3d17ec0d9ce
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/simstatus/ImsRegistrationStateController.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.deviceinfo.simstatus
+
+import android.content.Context
+import android.telephony.SubscriptionManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.settings.network.telephony.SimSlotRepository
+import com.android.settings.network.telephony.ims.ImsMmTelRepository
+import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ImsRegistrationStateController @JvmOverloads constructor(
+ private val context: Context,
+ private val simSlotRepository: SimSlotRepository = SimSlotRepository(context),
+ private val imsMmTelRepositoryFactory: (subId: Int) -> ImsMmTelRepository = { subId ->
+ ImsMmTelRepositoryImpl(context, subId)
+ },
+) {
+ fun collectImsRegistered(
+ lifecycleOwner: LifecycleOwner,
+ simSlotIndex: Int,
+ action: (imsRegistered: Boolean) -> Unit,
+ ) {
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ imsRegisteredFlow(simSlotIndex).collect(action)
+ }
+ }
+ }
+
+ private fun imsRegisteredFlow(simSlotIndex: Int): Flow =
+ simSlotRepository.subIdInSimSlotFlow(simSlotIndex)
+ .flatMapLatest { subId ->
+ if (SubscriptionManager.isValidSubscriptionId(subId)) {
+ imsMmTelRepositoryFactory(subId).imsRegisteredFlow()
+ } else {
+ flowOf(false)
+ }
+ }
+ .conflate()
+ .flowOn(Dispatchers.Default)
+}
diff --git a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java
index 6ac0d267f02..e5882dd9759 100644
--- a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java
+++ b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java
@@ -16,8 +16,6 @@
package com.android.settings.deviceinfo.simstatus;
-import static androidx.lifecycle.Lifecycle.Event;
-
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -30,7 +28,6 @@ import android.content.res.Resources;
import android.os.IBinder;
import android.os.PersistableBundle;
import android.os.RemoteException;
-import android.telephony.AccessNetworkConstants;
import android.telephony.Annotation;
import android.telephony.CarrierConfigManager;
import android.telephony.CellBroadcastIntents;
@@ -46,29 +43,28 @@ import android.telephony.TelephonyCallback;
import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import android.telephony.euicc.EuiccManager;
-import android.telephony.ims.ImsException;
-import android.telephony.ims.ImsMmTelManager;
-import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.LifecycleObserver;
-import androidx.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import com.android.settings.R;
import com.android.settings.network.SubscriptionUtil;
import com.android.settingslib.Utils;
import com.android.settingslib.core.lifecycle.Lifecycle;
+import kotlin.Unit;
+
import java.util.List;
/**
* Controller for Sim Status information within the About Phone Settings page.
*/
-public class SimStatusDialogController implements LifecycleObserver {
+public class SimStatusDialogController implements DefaultLifecycleObserver {
private final static String TAG = "SimStatusDialogCtrl";
@@ -110,26 +106,7 @@ public class SimStatusDialogController implements LifecycleObserver {
new OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
- final int prevSubId = (mSubscriptionInfo != null)
- ? mSubscriptionInfo.getSubscriptionId()
- : SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-
mSubscriptionInfo = getPhoneSubscriptionInfo(mSlotIndex);
-
- final int nextSubId = (mSubscriptionInfo != null)
- ? mSubscriptionInfo.getSubscriptionId()
- : SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-
- if (prevSubId != nextSubId) {
- if (SubscriptionManager.isValidSubscriptionId(prevSubId)) {
- unregisterImsRegistrationCallback(prevSubId);
- }
- if (SubscriptionManager.isValidSubscriptionId(nextSubId)) {
- mTelephonyManager =
- getTelephonyManager().createForSubscriptionId(nextSubId);
- registerImsRegistrationCallback(nextSubId);
- }
- }
updateSubscriptionStatus();
}
};
@@ -269,8 +246,8 @@ public class SimStatusDialogController implements LifecycleObserver {
/**
* OnResume lifecycle event, resume listening for phone state or subscription changes.
*/
- @OnLifecycleEvent(Event.ON_RESUME)
- public void onResume() {
+ @Override
+ public void onResume(@NonNull LifecycleOwner owner) {
if (mSubscriptionInfo == null) {
return;
}
@@ -280,7 +257,7 @@ public class SimStatusDialogController implements LifecycleObserver {
.registerTelephonyCallback(mContext.getMainExecutor(), mTelephonyCallback);
mSubscriptionManager.addOnSubscriptionsChangedListener(
mContext.getMainExecutor(), mOnSubscriptionsChangedListener);
- registerImsRegistrationCallback(mSubscriptionInfo.getSubscriptionId());
+ collectImsRegistered(owner);
if (mShowLatestAreaInfo) {
updateAreaInfoText();
@@ -295,8 +272,8 @@ public class SimStatusDialogController implements LifecycleObserver {
/**
* onPause lifecycle event, no longer listen for phone state or subscription changes.
*/
- @OnLifecycleEvent(Event.ON_PAUSE)
- public void onPause() {
+ @Override
+ public void onPause(@NonNull LifecycleOwner owner) {
if (mSubscriptionInfo == null) {
if (mIsRegisteredListener) {
mSubscriptionManager.removeOnSubscriptionsChangedListener(
@@ -310,7 +287,6 @@ public class SimStatusDialogController implements LifecycleObserver {
return;
}
- unregisterImsRegistrationCallback(mSubscriptionInfo.getSubscriptionId());
mSubscriptionManager.removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
getTelephonyManager().unregisterTelephonyCallback(mTelephonyCallback);
@@ -625,51 +601,22 @@ public class SimStatusDialogController implements LifecycleObserver {
mDialog.removeSettingFromScreen(IMS_REGISTRATION_STATE_VALUE_ID);
}
- private ImsMmTelManager.RegistrationCallback mImsRegStateCallback =
- new ImsMmTelManager.RegistrationCallback() {
- @Override
- public void onRegistered(@AccessNetworkConstants.TransportType int imsTransportType) {
- mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
- com.android.settingslib.R.string.ims_reg_status_registered));
- }
- @Override
- public void onRegistering(@AccessNetworkConstants.TransportType int imsTransportType) {
- mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
- com.android.settingslib.R.string.ims_reg_status_not_registered));
- }
- @Override
- public void onUnregistered(@Nullable ImsReasonInfo info) {
- mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
- com.android.settingslib.R.string.ims_reg_status_not_registered));
- }
- @Override
- public void onTechnologyChangeFailed(
- @AccessNetworkConstants.TransportType int imsTransportType,
- @Nullable ImsReasonInfo info) {
- mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
- com.android.settingslib.R.string.ims_reg_status_not_registered));
- }
- };
-
- private void registerImsRegistrationCallback(int subId) {
+ private void collectImsRegistered(@NonNull LifecycleOwner owner) {
if (!isImsRegistrationStateShowUp()) {
return;
}
- try {
- final ImsMmTelManager imsMmTelMgr = ImsMmTelManager.createForSubscriptionId(subId);
- imsMmTelMgr.registerImsRegistrationCallback(mDialog.getContext().getMainExecutor(),
- mImsRegStateCallback);
- } catch (ImsException exception) {
- Log.w(TAG, "fail to register IMS status for subId=" + subId, exception);
- }
- }
-
- private void unregisterImsRegistrationCallback(int subId) {
- if (!isImsRegistrationStateShowUp()) {
- return;
- }
- final ImsMmTelManager imsMmTelMgr = ImsMmTelManager.createForSubscriptionId(subId);
- imsMmTelMgr.unregisterImsRegistrationCallback(mImsRegStateCallback);
+ new ImsRegistrationStateController(mContext).collectImsRegistered(
+ owner, mSlotIndex, (Boolean imsRegistered) -> {
+ if (imsRegistered) {
+ mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
+ com.android.settingslib.R.string.ims_reg_status_registered));
+ } else {
+ mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
+ com.android.settingslib.R.string.ims_reg_status_not_registered));
+ }
+ return Unit.INSTANCE;
+ }
+ );
}
private SubscriptionInfo getPhoneSubscriptionInfo(int slotId) {
diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java
index 8a84e47b364..7a2f52a8674 100644
--- a/src/com/android/settings/homepage/SettingsHomepageActivity.java
+++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java
@@ -455,7 +455,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements
}
private void updateHomepageBackground() {
- if (!mIsEmbeddingActivityEnabled) {
+ if (!Flags.homepageRevamp() && !mIsEmbeddingActivityEnabled) {
return;
}
diff --git a/src/com/android/settings/network/telephony/SimSlotRepository.kt b/src/com/android/settings/network/telephony/SimSlotRepository.kt
new file mode 100644
index 00000000000..3859d785233
--- /dev/null
+++ b/src/com/android/settings/network/telephony/SimSlotRepository.kt
@@ -0,0 +1,45 @@
+/*
+ * 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
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+class SimSlotRepository(private val context: Context) {
+ private val subscriptionManager = context.requireSubscriptionManager()
+
+ fun subIdInSimSlotFlow(simSlotIndex: Int) =
+ context.subscriptionsChangedFlow()
+ .map {
+ subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(simSlotIndex)
+ ?.subscriptionId
+ ?: SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ }
+ .conflate()
+ .onEach { Log.d(TAG, "sub id in sim slot $simSlotIndex: $it") }
+ .flowOn(Dispatchers.Default)
+
+ private companion object {
+ private const val TAG = "SimSlotRepository"
+ }
+}
diff --git a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
index 822c20a2e0f..dd1cbd5bc11 100644
--- a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
+++ b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
@@ -21,7 +21,10 @@ import android.telephony.AccessNetworkConstants
import android.telephony.ims.ImsManager
import android.telephony.ims.ImsMmTelManager
import android.telephony.ims.ImsMmTelManager.WiFiCallingMode
+import android.telephony.ims.ImsReasonInfo
+import android.telephony.ims.ImsRegistrationAttributes
import android.telephony.ims.ImsStateCallback
+import android.telephony.ims.RegistrationManager
import android.telephony.ims.feature.MmTelFeature
import android.util.Log
import kotlin.coroutines.resume
@@ -39,7 +42,11 @@ import kotlinx.coroutines.withContext
interface ImsMmTelRepository {
@WiFiCallingMode
fun getWiFiCallingMode(useRoamingMode: Boolean): Int
+
+ fun imsRegisteredFlow(): Flow
+
fun imsReadyFlow(): Flow
+
suspend fun isSupported(
@MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
@AccessNetworkConstants.TransportType transportType: Int,
@@ -64,6 +71,36 @@ class ImsMmTelRepositoryImpl(
ImsMmTelManager.WIFI_MODE_UNKNOWN
}
+ override fun imsRegisteredFlow(): Flow = callbackFlow {
+ val callback = object : RegistrationManager.RegistrationCallback() {
+ override fun onRegistered(attributes: ImsRegistrationAttributes) {
+ Log.d(TAG, "[$subId] IMS onRegistered")
+ trySend(true)
+ }
+
+ override fun onRegistering(imsTransportType: Int) {
+ Log.d(TAG, "[$subId] IMS onRegistering")
+ trySend(false)
+ }
+
+ override fun onTechnologyChangeFailed(imsTransportType: Int, info: ImsReasonInfo) {
+ Log.d(TAG, "[$subId] IMS onTechnologyChangeFailed")
+ trySend(false)
+ }
+
+ override fun onUnregistered(info: ImsReasonInfo) {
+ Log.d(TAG, "[$subId] IMS onUnregistered")
+ trySend(false)
+ }
+ }
+
+ imsMmTelManager.registerImsRegistrationCallback(Dispatchers.Default.asExecutor(), callback)
+
+ awaitClose { imsMmTelManager.unregisterImsRegistrationCallback(callback) }
+ }.catch { e ->
+ Log.w(TAG, "[$subId] error while imsRegisteredFlow", e)
+ }.conflate().flowOn(Dispatchers.Default)
+
override fun imsReadyFlow(): Flow = callbackFlow {
val callback = object : ImsStateCallback() {
override fun onAvailable() {
diff --git a/src/com/android/settings/notification/modes/FutureUtil.java b/src/com/android/settings/notification/modes/FutureUtil.java
new file mode 100644
index 00000000000..e7bf8b9a75f
--- /dev/null
+++ b/src/com/android/settings/notification/modes/FutureUtil.java
@@ -0,0 +1,50 @@
+/*
+ * 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.notification.modes;
+
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+class FutureUtil {
+
+ private static final String TAG = "ZenFutureUtil";
+
+ static void whenDone(ListenableFuture future, Consumer consumer, Executor executor) {
+ whenDone(future, consumer, executor, "Error in future");
+ }
+
+ static void whenDone(ListenableFuture future, Consumer consumer, Executor executor,
+ String errorLogMessage, Object... errorLogMessageArgs) {
+ Futures.addCallback(future, new FutureCallback() {
+ @Override
+ public void onSuccess(V v) {
+ consumer.accept(v);
+ }
+
+ @Override
+ public void onFailure(Throwable throwable) {
+ Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
+ }
+ }, executor);
+ }
+}
diff --git a/src/com/android/settings/notification/modes/IconLoader.java b/src/com/android/settings/notification/modes/IconLoader.java
new file mode 100644
index 00000000000..b7a6c9526ac
--- /dev/null
+++ b/src/com/android/settings/notification/modes/IconLoader.java
@@ -0,0 +1,159 @@
+/*
+ * 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.notification.modes;
+
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.Nullable;
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.service.notification.SystemZenRules;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
+
+import com.android.settings.R;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+class IconLoader {
+
+ private static final String TAG = "ZenIconLoader";
+
+ private static final Drawable MISSING = new ColorDrawable();
+
+ @Nullable // Until first usage
+ private static IconLoader sInstance;
+
+ private final Context mContext;
+ private final LruCache mCache;
+ private final ListeningExecutorService mBackgroundExecutor;
+
+ static IconLoader getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new IconLoader(context);
+ }
+ return sInstance;
+ }
+
+ private IconLoader(Context context) {
+ this(context, Executors.newFixedThreadPool(4));
+ }
+
+ @VisibleForTesting
+ IconLoader(Context context, ExecutorService backgroundExecutor) {
+ mContext = context.getApplicationContext();
+ mCache = new LruCache<>(50);
+ mBackgroundExecutor =
+ MoreExecutors.listeningDecorator(backgroundExecutor);
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ @NonNull
+ ListenableFuture getIcon(@NonNull AutomaticZenRule rule) {
+ if (rule.getIconResId() == 0) {
+ return Futures.immediateFuture(getFallbackIcon(rule.getType()));
+ }
+
+ return FluentFuture.from(loadIcon(rule.getPackageName(), rule.getIconResId()))
+ .transform(icon ->
+ icon != null ? icon : getFallbackIcon(rule.getType()),
+ MoreExecutors.directExecutor());
+ }
+
+ @NonNull
+ private ListenableFuture* @Nullable */ Drawable> loadIcon(String pkg, int iconResId) {
+ String cacheKey = pkg + ":" + iconResId;
+ synchronized (mCache) {
+ Drawable cachedValue = mCache.get(cacheKey);
+ if (cachedValue != null) {
+ return immediateFuture(cachedValue != MISSING ? cachedValue : null);
+ }
+ }
+
+ return FluentFuture.from(mBackgroundExecutor.submit(() -> {
+ if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
+ return mContext.getDrawable(iconResId);
+ } else {
+ Context appContext = mContext.createPackageContext(pkg, 0);
+ Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
+ return getMonochromeIconIfPresent(appDrawable);
+ }
+ })).catching(Exception.class, ex -> {
+ // If we cannot resolve the icon, then store MISSING in the cache below, so
+ // we don't try again.
+ Log.e(TAG, "Error while loading icon " + cacheKey, ex);
+ return null;
+ }, MoreExecutors.directExecutor()).transform(drawable -> {
+ synchronized (mCache) {
+ mCache.put(cacheKey, drawable != null ? drawable : MISSING);
+ }
+ return drawable;
+ }, MoreExecutors.directExecutor());
+ }
+
+ private Drawable getFallbackIcon(int ruleType) {
+ int iconResIdFromType = switch (ruleType) {
+ // TODO: b/333528437 - continue replacing with proper default icons
+ case AutomaticZenRule.TYPE_UNKNOWN -> R.drawable.ic_do_not_disturb_on_24dp;
+ case AutomaticZenRule.TYPE_OTHER -> R.drawable.ic_do_not_disturb_on_24dp;
+ case AutomaticZenRule.TYPE_SCHEDULE_TIME -> R.drawable.ic_modes_time;
+ case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR -> R.drawable.ic_modes_event;
+ case AutomaticZenRule.TYPE_BEDTIME -> R.drawable.ic_do_not_disturb_on_24dp;
+ case AutomaticZenRule.TYPE_DRIVING -> R.drawable.ic_do_not_disturb_on_24dp;
+ case AutomaticZenRule.TYPE_IMMERSIVE -> R.drawable.ic_do_not_disturb_on_24dp;
+ case AutomaticZenRule.TYPE_THEATER -> R.drawable.ic_do_not_disturb_on_24dp;
+ case AutomaticZenRule.TYPE_MANAGED -> R.drawable.ic_do_not_disturb_on_24dp;
+ default -> R.drawable.ic_do_not_disturb_on_24dp;
+ };
+ return requireNonNull(mContext.getDrawable(iconResIdFromType));
+ }
+
+ private static Drawable getMonochromeIconIfPresent(Drawable icon) {
+ // For created rules, the app should've provided a monochrome Drawable. However, implicit
+ // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
+ // we choose it, if present.
+ if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
+ if (adaptiveIcon.getMonochrome() != null) {
+ // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
+ return new InsetDrawable(adaptiveIcon.getMonochrome(),
+ -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
+ }
+ }
+ return icon;
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenMode.java b/src/com/android/settings/notification/modes/ZenMode.java
index 51c92e6eae5..ca9bec5b7c0 100644
--- a/src/com/android/settings/notification/modes/ZenMode.java
+++ b/src/com/android/settings/notification/modes/ZenMode.java
@@ -25,15 +25,12 @@ import android.annotation.SuppressLint;
import android.app.AutomaticZenRule;
import android.app.NotificationManager;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
-import android.service.notification.SystemZenRules;
import android.service.notification.ZenPolicy;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.content.res.AppCompatResources;
import com.android.settings.R;
@@ -94,8 +91,6 @@ class ZenMode {
private final boolean mIsActive;
private final boolean mIsManualDnd;
-// private ZenPolicy mPreviousPolicy;
-
ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
this(id, rule, isActive, false);
}
@@ -122,49 +117,14 @@ class ZenMode {
}
@NonNull
- public ListenableFuture getIcon(@NonNull Context context) {
- // TODO: b/333528586 - Load the icons asynchronously, and cache them
+ public ListenableFuture getIcon(@NonNull IconLoader iconLoader) {
+ Context context = iconLoader.getContext();
if (mIsManualDnd) {
- return Futures.immediateFuture(
- requireNonNull(context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+ return Futures.immediateFuture(requireNonNull(
+ context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
}
- int iconResId = mRule.getIconResId();
- Drawable customIcon = null;
- if (iconResId != 0) {
- if (SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName())) {
- customIcon = context.getDrawable(mRule.getIconResId());
- } else {
- try {
- Context appContext = context.createPackageContext(mRule.getPackageName(), 0);
- customIcon = AppCompatResources.getDrawable(appContext, mRule.getIconResId());
- } catch (PackageManager.NameNotFoundException e) {
- Log.wtf(TAG,
- "Package " + mRule.getPackageName() + " used in rule " + mId
- + " not found?", e);
- // Continue down to use a default icon.
- }
- }
- }
- if (customIcon != null) {
- return Futures.immediateFuture(customIcon);
- }
-
- // Derive a default icon from the rule type.
- // TODO: b/333528437 - Use correct icons
- int iconResIdFromType = switch (mRule.getType()) {
- case AutomaticZenRule.TYPE_UNKNOWN -> R.drawable.ic_do_not_disturb_on_24dp;
- case AutomaticZenRule.TYPE_OTHER -> R.drawable.ic_do_not_disturb_on_24dp;
- case AutomaticZenRule.TYPE_SCHEDULE_TIME -> R.drawable.ic_modes_time;
- case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR -> R.drawable.ic_modes_event;
- case AutomaticZenRule.TYPE_BEDTIME -> R.drawable.ic_do_not_disturb_on_24dp;
- case AutomaticZenRule.TYPE_DRIVING -> R.drawable.ic_do_not_disturb_on_24dp;
- case AutomaticZenRule.TYPE_IMMERSIVE -> R.drawable.ic_do_not_disturb_on_24dp;
- case AutomaticZenRule.TYPE_THEATER -> R.drawable.ic_do_not_disturb_on_24dp;
- case AutomaticZenRule.TYPE_MANAGED -> R.drawable.ic_do_not_disturb_on_24dp;
- default -> R.drawable.ic_do_not_disturb_on_24dp;
- };
- return Futures.immediateFuture(requireNonNull(context.getDrawable(iconResIdFromType)));
+ return iconLoader.getIcon(mRule);
}
@NonNull
diff --git a/src/com/android/settings/notification/modes/ZenModeHeaderController.java b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
index 246eee85d77..f55c02d7daa 100644
--- a/src/com/android/settings/notification/modes/ZenModeHeaderController.java
+++ b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
@@ -17,7 +17,6 @@ package com.android.settings.notification.modes;
import android.app.Flags;
import android.content.Context;
-import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -28,9 +27,7 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.widget.LayoutPreference;
-import java.util.concurrent.TimeUnit;
-
-public class ZenModeHeaderController extends AbstractZenModePreferenceController {
+class ZenModeHeaderController extends AbstractZenModePreferenceController {
private final DashboardFragment mFragment;
private EntityHeaderController mHeaderController;
@@ -51,7 +48,8 @@ public class ZenModeHeaderController extends AbstractZenModePreferenceController
@Override
public void updateState(Preference preference) {
- if (getAZR() == null || mFragment == null) {
+ ZenMode mode = getMode();
+ if (mode == null || mFragment == null) {
return;
}
@@ -62,14 +60,12 @@ public class ZenModeHeaderController extends AbstractZenModePreferenceController
mFragment,
pref.findViewById(R.id.entity_header));
}
- Drawable icon = null;
- try {
- icon = getMode().getIcon(mContext).get(200, TimeUnit.MILLISECONDS);
- } catch (Exception e) {
- // no icon
- }
- mHeaderController.setIcon(icon)
- .setLabel(getAZR().getName())
- .done(false /* rebindActions */);
+
+ FutureUtil.whenDone(
+ mode.getIcon(IconLoader.getInstance(mContext)),
+ icon -> mHeaderController.setIcon(icon)
+ .setLabel(mode.getRule().getName())
+ .done(false /* rebindActions */),
+ mContext.getMainExecutor());
}
}
diff --git a/src/com/android/settings/notification/modes/ZenModeListPreference.java b/src/com/android/settings/notification/modes/ZenModeListPreference.java
index 2a95dffc5e0..0f4728f05de 100644
--- a/src/com/android/settings/notification/modes/ZenModeListPreference.java
+++ b/src/com/android/settings/notification/modes/ZenModeListPreference.java
@@ -25,15 +25,11 @@ import com.android.settings.core.SubSettingLauncher;
import com.android.settings.notification.zen.ZenModeSettings;
import com.android.settingslib.RestrictedPreference;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
/**
* Preference representing a single mode item on the modes aggregator page. Clicking on this
* preference leads to an individual mode's configuration page.
*/
-public class ZenModeListPreference extends RestrictedPreference {
+class ZenModeListPreference extends RestrictedPreference {
final Context mContext;
ZenMode mZenMode;
@@ -68,10 +64,10 @@ public class ZenModeListPreference extends RestrictedPreference {
mZenMode = zenMode;
setTitle(mZenMode.getRule().getName());
setSummary(mZenMode.getRule().getTriggerDescription());
- try {
- setIcon(mZenMode.getIcon(mContext).get(200, TimeUnit.MILLISECONDS));
- } catch (Exception e) {
- // no icon
- }
+
+ FutureUtil.whenDone(
+ mZenMode.getIcon(IconLoader.getInstance(mContext)),
+ icon -> setIcon(icon),
+ mContext.getMainExecutor());
}
}
diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java
index ac170c6d3d9..8df282bfd5c 100644
--- a/src/com/android/settings/notification/modes/ZenModesBackend.java
+++ b/src/com/android/settings/notification/modes/ZenModesBackend.java
@@ -41,7 +41,6 @@ import com.android.settings.R;
import java.time.Duration;
import java.util.ArrayList;
-import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -104,7 +103,6 @@ class ZenModesBackend {
ZenMode getMode(String id) {
ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
- // Regardless of its contents, non-null manualRule means that manual rule is active.
return getManualDndMode(currentConfig);
} else {
AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
@@ -177,8 +175,9 @@ class ZenModesBackend {
.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
.build();
+ // Regardless of its contents, non-null manualRule means that manual rule is active.
return ZenMode.manualDndMode(manualDndRule,
- config != null && config.manualRule != null); // isActive
+ config != null && config.manualRule != null);
}
private static boolean isRuleActive(String id, ZenModeConfig config) {
diff --git a/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt b/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt
index c3183a7494f..e612d1d2c8c 100644
--- a/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppForceStopButton.kt
@@ -35,18 +35,14 @@ import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spa.widget.dialog.AlertDialogButton
import com.android.settingslib.spa.widget.dialog.AlertDialogPresenter
import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter
-import com.android.settingslib.spaprivileged.model.app.hasFlag
-import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
import com.android.settingslib.spaprivileged.model.app.userId
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
class AppForceStopButton(
private val packageInfoPresenter: PackageInfoPresenter,
+ private val appForceStopRepository: AppForceStopRepository =
+ AppForceStopRepository(packageInfoPresenter),
) {
private val context = packageInfoPresenter.context
- private val appButtonRepository = AppButtonRepository(context)
private val packageManager = context.packageManager
@Composable
@@ -55,27 +51,11 @@ class AppForceStopButton(
return ActionButton(
text = stringResource(R.string.force_stop),
imageVector = Icons.Outlined.Report,
- enabled = remember(app) {
- flow {
- emit(isForceStopButtonEnable(app))
- }.flowOn(Dispatchers.Default)
- }.collectAsStateWithLifecycle(false).value,
+ enabled = remember(app) { appForceStopRepository.canForceStopFlow() }
+ .collectAsStateWithLifecycle(false).value,
) { onForceStopButtonClicked(app, dialogPresenter) }
}
- /**
- * Gets whether a package can be force stopped.
- */
- private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when {
- // User can't force stop device admin.
- app.isActiveAdmin(context) -> false
-
- appButtonRepository.isDisallowControl(app) -> false
-
- // If the app isn't explicitly stopped, then always show the force stop button.
- else -> !app.hasFlag(ApplicationInfo.FLAG_STOPPED)
- }
-
private fun onForceStopButtonClicked(
app: ApplicationInfo,
dialogPresenter: AlertDialogPresenter,
diff --git a/src/com/android/settings/spa/app/appinfo/AppForceStopRepository.kt b/src/com/android/settings/spa/app/appinfo/AppForceStopRepository.kt
new file mode 100644
index 00000000000..e929df1b976
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/AppForceStopRepository.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appinfo
+
+import android.Manifest
+import android.app.Activity
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.net.Uri
+import android.os.UserHandle
+import android.util.Log
+import com.android.settingslib.spaprivileged.model.app.hasFlag
+import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
+import com.android.settingslib.spaprivileged.model.app.userId
+import kotlin.coroutines.resume
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+class AppForceStopRepository(
+ private val packageInfoPresenter: PackageInfoPresenter,
+ private val appButtonRepository: AppButtonRepository =
+ AppButtonRepository(packageInfoPresenter.context),
+) {
+ private val context = packageInfoPresenter.context
+
+ /**
+ * Flow of whether a package can be force stopped.
+ */
+ fun canForceStopFlow(): Flow = packageInfoPresenter.flow
+ .map { packageInfo ->
+ val app = packageInfo?.applicationInfo ?: return@map false
+ canForceStop(app)
+ }
+ .conflate()
+ .onEach { Log.d(TAG, "canForceStopFlow: $it") }
+ .flowOn(Dispatchers.Default)
+
+ /**
+ * Gets whether a package can be force stopped.
+ */
+ private suspend fun canForceStop(app: ApplicationInfo): Boolean = when {
+ // User can't force stop device admin.
+ app.isActiveAdmin(context) -> false
+
+ appButtonRepository.isDisallowControl(app) -> false
+
+ // If the app isn't explicitly stopped, then always show the force stop button.
+ !app.hasFlag(ApplicationInfo.FLAG_STOPPED) -> true
+
+ else -> queryAppRestart(app)
+ }
+
+ /**
+ * Queries if app has restarted.
+ *
+ * @return true means app can be force stop again.
+ */
+ private suspend fun queryAppRestart(app: ApplicationInfo): Boolean {
+ val packageName = app.packageName
+ val intent = Intent(
+ Intent.ACTION_QUERY_PACKAGE_RESTART,
+ Uri.fromParts("package", packageName, null)
+ ).apply {
+ putExtra(Intent.EXTRA_PACKAGES, arrayOf(packageName))
+ putExtra(Intent.EXTRA_UID, app.uid)
+ putExtra(Intent.EXTRA_USER_HANDLE, app.userId)
+ }
+ Log.d(TAG, "Sending broadcast to query restart status for $packageName")
+
+ return suspendCancellableCoroutine { continuation ->
+ val receiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val enabled = resultCode != Activity.RESULT_CANCELED
+ Log.d(TAG, "Got broadcast response: Restart status for $packageName $enabled")
+ continuation.resume(enabled)
+ }
+ }
+ context.sendOrderedBroadcastAsUser(
+ intent,
+ UserHandle.CURRENT,
+ Manifest.permission.HANDLE_QUERY_PACKAGE_RESTART,
+ receiver,
+ null,
+ Activity.RESULT_CANCELED,
+ null,
+ null,
+ )
+ }
+ }
+
+ private companion object {
+ private const val TAG = "AppForceStopRepository"
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java
index f0decabccee..64b16fa5d66 100644
--- a/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java
@@ -28,9 +28,6 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.content.Context;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import androidx.preference.PreferenceManager;
@@ -39,7 +36,6 @@ import androidx.preference.SwitchPreference;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@@ -49,8 +45,6 @@ import org.robolectric.shadows.ShadowContentResolver;
@RunWith(RobolectricTestRunner.class)
public class MagnificationAlwaysOnPreferenceControllerTest {
- @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
private static final String KEY_ALWAYS_ON =
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED;
@@ -99,8 +93,7 @@ public class MagnificationAlwaysOnPreferenceControllerTest {
}
@Test
- @EnableFlags(Flags.FLAG_HIDE_MAGNIFICATION_ALWAYS_ON_TOGGLE_WHEN_WINDOW_MODE_ONLY)
- public void onResume_flagOn_verifyRegisterCapabilityObserver() {
+ public void onResume_verifyRegisterCapabilityObserver() {
mController.onResume();
assertThat(mShadowContentResolver.getContentObservers(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY)))
@@ -108,8 +101,7 @@ public class MagnificationAlwaysOnPreferenceControllerTest {
}
@Test
- @EnableFlags(Flags.FLAG_HIDE_MAGNIFICATION_ALWAYS_ON_TOGGLE_WHEN_WINDOW_MODE_ONLY)
- public void onPause_flagOn_verifyUnregisterCapabilityObserver() {
+ public void onPause_verifyUnregisterCapabilityObserver() {
mController.onResume();
mController.onPause();
assertThat(mShadowContentResolver.getContentObservers(
@@ -118,17 +110,7 @@ public class MagnificationAlwaysOnPreferenceControllerTest {
}
@Test
- @DisableFlags(Flags.FLAG_HIDE_MAGNIFICATION_ALWAYS_ON_TOGGLE_WHEN_WINDOW_MODE_ONLY)
- public void updateState_windowModeOnlyAndFlagOff_preferenceIsAvailable() {
- MagnificationCapabilities.setCapabilities(mContext, MagnificationMode.WINDOW);
-
- mController.updateState(mSwitchPreference);
- assertThat(mSwitchPreference.isEnabled()).isTrue();
- }
-
- @Test
- @EnableFlags(Flags.FLAG_HIDE_MAGNIFICATION_ALWAYS_ON_TOGGLE_WHEN_WINDOW_MODE_ONLY)
- public void updateState_windowModeOnlyAndFlagOn_preferenceBecomesUnavailable() {
+ public void updateState_windowModeOnly_preferenceBecomesUnavailable() {
MagnificationCapabilities.setCapabilities(mContext, MagnificationMode.WINDOW);
mController.updateState(mSwitchPreference);
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivityTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivityTest.java
new file mode 100644
index 00000000000..e967a12bb1c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivityTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.audiosharing.audiostreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioStreamConfirmDialogActivityTest {
+ private AudioStreamConfirmDialogActivity mActivity;
+
+ @Before
+ public void setUp() {
+ mActivity = Robolectric.buildActivity(AudioStreamConfirmDialogActivity.class).get();
+ }
+
+ @Test
+ public void isValidFragment_returnsTrue() {
+ assertThat(mActivity.isValidFragment(AudioStreamConfirmDialog.class.getName())).isTrue();
+ }
+
+ @Test
+ public void isValidFragment_returnsFalse() {
+ assertThat(mActivity.isValidFragment("")).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragmentTest.java
new file mode 100644
index 00000000000..724c7721f11
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragmentTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.audiosharing.audiostreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioStreamDetailsFragmentTest {
+ private AudioStreamDetailsFragment mFragment;
+
+ @Before
+ public void setUp() {
+ mFragment = new AudioStreamDetailsFragment();
+ }
+
+ @Test
+ public void getPreferenceScreenResId_returnsCorrectXml() {
+ assertThat(mFragment.getPreferenceScreenResId())
+ .isEqualTo(R.xml.bluetooth_le_audio_stream_details_fragment);
+ }
+
+ @Test
+ public void getLogTag_returnsCorrectTag() {
+ assertThat(mFragment.getLogTag()).isEqualTo(AudioStreamDetailsFragment.TAG);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreferenceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreferenceTest.java
new file mode 100644
index 00000000000..0c93e3e1879
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreferenceTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.audiosharing.audiostreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothLeAudioContentMetadata;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.preference.Preference.OnPreferenceClickListener;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioStreamPreferenceTest {
+ private static final int BROADCAST_ID = 1;
+ private static final String BROADCAST_NAME = "broadcast_name";
+ private static final String PROGRAM_NAME = "program_name";
+ private static final int BROADCAST_RSSI = 1;
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ private Context mContext;
+ private AudioStreamPreference mPreference;
+ @Mock private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
+ @Mock private BluetoothLeBroadcastReceiveState mBluetoothLeBroadcastReceiveState;
+ @Mock private BluetoothLeAudioContentMetadata mBluetoothLeAudioContentMetadata;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mPreference = new AudioStreamPreference(mContext, null);
+ when(mBluetoothLeBroadcastMetadata.getBroadcastId()).thenReturn(BROADCAST_ID);
+ when(mBluetoothLeBroadcastMetadata.getBroadcastName()).thenReturn(BROADCAST_NAME);
+ when(mBluetoothLeBroadcastMetadata.getRssi()).thenReturn(BROADCAST_RSSI);
+ when(mBluetoothLeBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
+ when(mBluetoothLeBroadcastReceiveState.getSubgroupMetadata())
+ .thenReturn(Collections.singletonList(mBluetoothLeAudioContentMetadata));
+ when(mBluetoothLeAudioContentMetadata.getProgramInfo()).thenReturn(PROGRAM_NAME);
+ }
+
+ @Test
+ public void createNewPreference_shouldSetIcon() {
+ assertThat(mPreference.getIcon()).isNotNull();
+ }
+
+ @Test
+ public void onBind_shouldHideDivider() {
+ PreferenceViewHolder holder =
+ PreferenceViewHolder.createInstanceForTests(
+ LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), null));
+ View divider =
+ holder.findViewById(
+ com.android.settingslib.widget.preference.twotarget.R.id
+ .two_target_divider);
+ assertThat(divider).isNotNull();
+
+ mPreference.onBindViewHolder(holder);
+
+ assertThat(divider.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void setConnected_shouldUpdatePreferenceUI() {
+ String summary = "Connected";
+ OnPreferenceClickListener listener = mock(OnPreferenceClickListener.class);
+ mPreference.setIsConnected(true, summary, listener);
+
+ assertThat(mPreference.getSummary()).isNotNull();
+ assertThat(mPreference.getSummary().toString()).isEqualTo(summary);
+ assertThat(mPreference.getOnPreferenceClickListener()).isEqualTo(listener);
+ }
+
+ @Test
+ public void setAudioStreamMetadata_shouldUpdateMetadata() {
+ AudioStreamPreference p =
+ AudioStreamPreference.fromMetadata(mContext, mBluetoothLeBroadcastMetadata);
+ BluetoothLeBroadcastMetadata metadata = mock(BluetoothLeBroadcastMetadata.class);
+ p.setAudioStreamMetadata(metadata);
+
+ assertThat(p.getAudioStreamMetadata()).isEqualTo(metadata);
+ }
+
+ @Test
+ public void setAudioStreamState_shouldUpdateState() {
+ AudioStreamPreference p =
+ AudioStreamPreference.fromMetadata(mContext, mBluetoothLeBroadcastMetadata);
+ AudioStreamState state = AudioStreamState.SOURCE_ADDED;
+ p.setAudioStreamState(state);
+
+ assertThat(p.getAudioStreamState()).isEqualTo(state);
+ }
+
+ @Test
+ public void fromMetadata_shouldReturnBroadcastInfo() {
+ AudioStreamPreference p =
+ AudioStreamPreference.fromMetadata(mContext, mBluetoothLeBroadcastMetadata);
+ assertThat(p.getAudioStreamBroadcastId()).isEqualTo(BROADCAST_ID);
+ assertThat(p.getAudioStreamBroadcastName()).isEqualTo(BROADCAST_NAME);
+ assertThat(p.getAudioStreamRssi()).isEqualTo(BROADCAST_RSSI);
+ }
+
+ @Test
+ public void fromReceiveState_shouldReturnBroadcastInfo() {
+ AudioStreamPreference p =
+ AudioStreamPreference.fromReceiveState(mContext, mBluetoothLeBroadcastReceiveState);
+ assertThat(p.getAudioStreamBroadcastId()).isEqualTo(BROADCAST_ID);
+ assertThat(p.getAudioStreamBroadcastName()).isEqualTo(PROGRAM_NAME);
+ assertThat(p.getAudioStreamRssi()).isEqualTo(Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void shouldHideSecondTarget_connected() {
+ mPreference.setIsConnected(true, "", null);
+ assertThat(mPreference.shouldHideSecondTarget()).isTrue();
+ }
+
+ @Test
+ public void shouldHideSecondTarget_notEncrypted() {
+ when(mBluetoothLeBroadcastMetadata.isEncrypted()).thenReturn(false);
+ AudioStreamPreference p =
+ AudioStreamPreference.fromMetadata(mContext, mBluetoothLeBroadcastMetadata);
+ assertThat(p.shouldHideSecondTarget()).isTrue();
+ }
+
+ @Test
+ public void shouldShowSecondTarget_encrypted() {
+ when(mBluetoothLeBroadcastMetadata.isEncrypted()).thenReturn(true);
+ AudioStreamPreference p =
+ AudioStreamPreference.fromMetadata(mContext, mBluetoothLeBroadcastMetadata);
+ assertThat(p.shouldHideSecondTarget()).isFalse();
+ }
+
+ @Test
+ public void secondTargetResId_shouldReturnLockLayoutId() {
+ assertThat(mPreference.getSecondTargetResId()).isEqualTo(R.layout.preference_widget_lock);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceControllerTest.java
new file mode 100644
index 00000000000..c0296350db4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceControllerTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.audiosharing.audiostreams;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioStreamsActiveDeviceControllerTest {
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ private AudioStreamsActiveDeviceController mController;
+ @Mock private PreferenceScreen mScreen;
+ @Mock private Preference mPreference;
+
+ @Before
+ public void setUp() {
+ Context context = RuntimeEnvironment.application;
+ mController =
+ new AudioStreamsActiveDeviceController(
+ context, AudioStreamsActiveDeviceController.KEY);
+ when(mScreen.findPreference(anyString())).thenReturn(mPreference);
+ }
+
+ @Test
+ public void getAvailabilityStatus() {
+ assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+ }
+
+ @Test
+ public void onSummaryChanged_shouldSetPreferenceSummary() {
+ String summary = "summary";
+ mController.displayPreference(mScreen);
+ mController.onSummaryChanged(summary);
+
+ verify(mPreference).setSummary(summary);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdaterTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdaterTest.java
new file mode 100644
index 00000000000..3bcc9a3d079
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdaterTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.audiosharing.audiostreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+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.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+ shadows = {
+ ShadowAudioStreamsHelper.class,
+ })
+public class AudioStreamsActiveDeviceSummaryUpdaterTest {
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ private static final String DEVICE_NAME = "device_name";
+ @Spy private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final AudioStreamsActiveDeviceSummaryUpdater.OnSummaryChangeListener mFakeListener =
+ summary -> mUpdatedSummary = summary;
+ @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
+ @Mock private AudioStreamsHelper mAudioStreamsHelper;
+ private @Nullable String mUpdatedSummary;
+ private AudioStreamsActiveDeviceSummaryUpdater mUpdater;
+
+ @Before
+ public void setUp() {
+ ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
+ ShadowAudioStreamsHelper.resetCachedBluetoothDevice();
+ mUpdater = new AudioStreamsActiveDeviceSummaryUpdater(mContext, mFakeListener);
+ }
+
+ @Test
+ public void register_summaryUpdated() {
+ mUpdater.register(true);
+
+ assertThat(mUpdatedSummary).isNotNull();
+ }
+
+ @Test
+ public void onActiveDeviceChanged_notLeProfile_doNothing() {
+ mUpdater.onActiveDeviceChanged(mCachedBluetoothDevice, 0);
+
+ assertThat(mUpdatedSummary).isNull();
+ }
+
+ @Test
+ public void onActiveDeviceChanged_leProfile_summaryUpdated() {
+ ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(
+ mCachedBluetoothDevice);
+ when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME);
+ mUpdater.onActiveDeviceChanged(mCachedBluetoothDevice, BluetoothProfile.LE_AUDIO);
+
+ assertThat(mUpdatedSummary).isEqualTo(DEVICE_NAME);
+ }
+
+ @Test
+ public void onActiveDeviceChanged_leProfile_noDevice_summaryUpdated() {
+ mUpdater.onActiveDeviceChanged(mCachedBluetoothDevice, BluetoothProfile.LE_AUDIO);
+
+ assertThat(mUpdatedSummary)
+ .isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_title));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/IconLoaderTest.java b/tests/robotests/src/com/android/settings/notification/modes/IconLoaderTest.java
new file mode 100644
index 00000000000..a92e6187aef
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/IconLoaderTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.ZenPolicy;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class IconLoaderTest {
+
+ private IconLoader mLoader;
+
+ @Before
+ public void setUp() {
+ mLoader = new IconLoader(RuntimeEnvironment.application,
+ MoreExecutors.newDirectExecutorService());
+ }
+
+ @Test
+ public void getIcon_systemOwnedRuleWithIcon_loads() throws Exception {
+ AutomaticZenRule systemRule = newRuleBuilder()
+ .setPackage("android")
+ .setIconResId(android.R.drawable.ic_media_play)
+ .build();
+
+ ListenableFuture loadFuture = mLoader.getIcon(systemRule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithoutSpecificIcon_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .build();
+
+ ListenableFuture loadFuture = mLoader.getIcon(rule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithAppIconWithLoadFailure_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .setIconResId(-123456)
+ .build();
+
+ ListenableFuture loadFuture = mLoader.getIcon(rule);
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ private static AutomaticZenRule.Builder newRuleBuilder() {
+ return new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().build());
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/deviceinfo/simstatus/ImsRegistrationStateControllerTest.kt b/tests/spa_unit/src/com/android/settings/deviceinfo/simstatus/ImsRegistrationStateControllerTest.kt
new file mode 100644
index 00000000000..5e486dd733e
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/deviceinfo/simstatus/ImsRegistrationStateControllerTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.deviceinfo.simstatus
+
+import android.content.Context
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.network.telephony.SimSlotRepository
+import com.android.settings.network.telephony.ims.ImsMmTelRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+class ImsRegistrationStateControllerTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val mockSimSlotRepository = mock {
+ on { subIdInSimSlotFlow(SIM_SLOT_INDEX) } doReturn flowOf(SUB_ID)
+ }
+
+ private val mockImsMmTelRepository = mock {
+ on { imsRegisteredFlow() } doReturn flowOf(true)
+ }
+
+ private val controller = ImsRegistrationStateController(
+ context = context,
+ simSlotRepository = mockSimSlotRepository,
+ imsMmTelRepositoryFactory = { subId ->
+ assertThat(subId).isEqualTo(SUB_ID)
+ mockImsMmTelRepository
+ },
+ )
+
+ @Test
+ fun collectImsRegistered() = runBlocking {
+ var imsRegistered = false
+
+ controller.collectImsRegistered(TestLifecycleOwner(), SIM_SLOT_INDEX) {
+ imsRegistered = it
+ }
+ delay(100)
+
+ assertThat(imsRegistered).isTrue()
+ }
+
+ private companion object {
+ const val SIM_SLOT_INDEX = 0
+ const val SUB_ID = 1
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SimSlotRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SimSlotRepositoryTest.kt
new file mode 100644
index 00000000000..88e0ad498da
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/SimSlotRepositoryTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class SimSlotRepositoryTest {
+
+ private val mockSubscriptionManager = mock {
+ on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer {
+ val listener = it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener
+ listener.onSubscriptionsChanged()
+ }
+ }
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { getSystemService(SubscriptionManager::class.java) } doReturn mockSubscriptionManager
+ }
+
+ private val repository = SimSlotRepository(context)
+
+ @Test
+ fun subIdInSimSlotFlow_valid() = runBlocking {
+ mockSubscriptionManager.stub {
+ on { getActiveSubscriptionInfoForSimSlotIndex(SIM_SLOT_INDEX) } doReturn
+ SubscriptionInfo.Builder().setId(SUB_ID).build()
+ }
+
+ val subId = repository.subIdInSimSlotFlow(SIM_SLOT_INDEX).firstWithTimeoutOrNull()
+
+ assertThat(subId).isEqualTo(SUB_ID)
+ }
+
+ @Test
+ fun subIdInSimSlotFlow_invalid() = runBlocking {
+ mockSubscriptionManager.stub {
+ on { getActiveSubscriptionInfoForSimSlotIndex(SIM_SLOT_INDEX) } doReturn null
+ }
+
+ val subId = repository.subIdInSimSlotFlow(SIM_SLOT_INDEX).firstWithTimeoutOrNull()
+
+ assertThat(subId).isEqualTo(SubscriptionManager.INVALID_SIM_SLOT_INDEX)
+ }
+
+ private companion object {
+ const val SIM_SLOT_INDEX = 0
+ const val SUB_ID = 1
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt
index 24b081a704f..f198660e5da 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt
@@ -19,10 +19,14 @@ package com.android.settings.network.telephony.ims
import android.content.Context
import android.telephony.AccessNetworkConstants
import android.telephony.ims.ImsMmTelManager
+import android.telephony.ims.ImsReasonInfo
+import android.telephony.ims.ImsRegistrationAttributes
import android.telephony.ims.ImsStateCallback
+import android.telephony.ims.RegistrationManager.RegistrationCallback
import android.telephony.ims.feature.MmTelFeature
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spa.testutils.toListWithTimeout
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
@@ -44,12 +48,17 @@ import org.mockito.kotlin.stub
class ImsMmTelRepositoryTest {
private val context: Context = ApplicationProvider.getApplicationContext()
+ private var registrationCallback: RegistrationCallback? = null
private var stateCallback: ImsStateCallback? = null
private val mockImsMmTelManager = mock {
on { isVoWiFiSettingEnabled } doReturn true
on { getVoWiFiRoamingModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED
on { getVoWiFiModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED
+ on { registerImsRegistrationCallback(any(), any()) } doAnswer {
+ registrationCallback = it.arguments[1] as RegistrationCallback
+ registrationCallback?.onRegistered(mock())
+ }
on { registerImsStateCallback(any(), any()) } doAnswer {
stateCallback = it.arguments[1] as ImsStateCallback
stateCallback?.onAvailable()
@@ -99,6 +108,25 @@ class ImsMmTelRepositoryTest {
assertThat(wiFiCallingMode).isEqualTo(ImsMmTelManager.WIFI_MODE_UNKNOWN)
}
+ @Test
+ fun imsRegisteredFlow_sendInitialValue() = runBlocking {
+ val imsRegistered = repository.imsRegisteredFlow().firstWithTimeoutOrNull()
+
+ assertThat(imsRegistered).isTrue()
+ }
+
+ @Test
+ fun imsRegisteredFlow_changed(): Unit = runBlocking {
+ val listDeferred = async {
+ repository.imsRegisteredFlow().toListWithTimeout()
+ }
+ delay(100)
+
+ registrationCallback?.onUnregistered(ImsReasonInfo())
+
+ assertThat(listDeferred.await().last()).isFalse()
+ }
+
@Test
fun imsReadyFlow_sendInitialValue() = runBlocking {
val flow = repository.imsReadyFlow()
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt
index 84d6651c396..186acd941d8 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopButtonTest.kt
@@ -29,6 +29,7 @@ import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.util.trace
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
@@ -36,10 +37,10 @@ import com.android.settingslib.spa.testutils.delay
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
import com.android.settingslib.spaprivileged.model.app.userId
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
@@ -54,51 +55,30 @@ class AppForceStopButtonTest {
private val mockDevicePolicyManager = mock()
- private val mockUserManager = mock {
- on { getUserRestrictionSources(any(), any()) } doReturn emptyList()
- }
-
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { packageManager } doReturn mockPackageManager
on { devicePolicyManager } doReturn mockDevicePolicyManager
on { getSystemService(Context.DEVICE_POLICY_SERVICE) } doReturn mockDevicePolicyManager
- on { getSystemService(Context.USER_SERVICE) } doReturn mockUserManager
}
private val packageInfoPresenter = mock {
on { context } doReturn context
}
- private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
-
- @Test
- fun getActionButton_isActiveAdmin_buttonDisabled() {
- val app = createApp()
- mockDevicePolicyManager.stub {
- on { packageHasActiveAdmins(PACKAGE_NAME, app.userId) } doReturn true
- }
-
- setForceStopButton(app)
-
- composeTestRule.onNodeWithText(context.getString(R.string.force_stop)).assertIsNotEnabled()
+ private val mockAppForceStopRepository = mock {
+ on { canForceStopFlow() } doReturn flowOf(false)
}
- @Test
- fun getActionButton_isUninstallInQueue_buttonDisabled() {
- val app = createApp()
- mockDevicePolicyManager.stub {
- on { isUninstallInQueue(PACKAGE_NAME) } doReturn true
- }
-
- setForceStopButton(app)
-
- composeTestRule.onNodeWithText(context.getString(R.string.force_stop)).assertIsNotEnabled()
- }
+ private val appForceStopButton = AppForceStopButton(
+ packageInfoPresenter = packageInfoPresenter,
+ appForceStopRepository = mockAppForceStopRepository,
+ )
@Test
fun getActionButton_isStopped_buttonDisabled() {
- val app = createApp {
- flags = ApplicationInfo.FLAG_STOPPED
+ val app = createApp()
+ mockAppForceStopRepository.stub {
+ on { canForceStopFlow() } doReturn flowOf(false)
}
setForceStopButton(app)
@@ -109,6 +89,9 @@ class AppForceStopButtonTest {
@Test
fun getActionButton_regularApp_buttonEnabled() {
val app = createApp()
+ mockAppForceStopRepository.stub {
+ on { canForceStopFlow() } doReturn flowOf(true)
+ }
setForceStopButton(app)
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopRepositoryTest.kt
new file mode 100644
index 00000000000..0bcd2490ea9
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppForceStopRepositoryTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appinfo
+
+import android.Manifest
+import android.app.Activity
+import android.app.admin.DevicePolicyManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.os.UserHandle
+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.devicePolicyManager
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class AppForceStopRepositoryTest {
+
+ private val mockDevicePolicyManager = mock()
+
+ private var resultCode = Activity.RESULT_CANCELED
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { devicePolicyManager } doReturn mockDevicePolicyManager
+ onGeneric {
+ sendOrderedBroadcastAsUser(
+ argThat { action == Intent.ACTION_QUERY_PACKAGE_RESTART },
+ eq(UserHandle.CURRENT),
+ eq(Manifest.permission.HANDLE_QUERY_PACKAGE_RESTART),
+ any(),
+ isNull(),
+ eq(Activity.RESULT_CANCELED),
+ isNull(),
+ isNull(),
+ )
+ } doAnswer {
+ val broadcastReceiver = spy(it.arguments[3] as BroadcastReceiver) {
+ on { resultCode } doReturn resultCode
+ }
+ broadcastReceiver.onReceive(mock, it.arguments[0] as Intent)
+ }
+ }
+
+ private val packageInfoPresenter = mock {
+ on { context } doReturn context
+ }
+
+ private val repository = AppForceStopRepository(packageInfoPresenter)
+
+ @Test
+ fun getActionButton_isActiveAdmin_returnFalse() = runBlocking {
+ val app = mockApp {}
+ mockDevicePolicyManager.stub {
+ on { packageHasActiveAdmins(PACKAGE_NAME, app.userId) } doReturn true
+ }
+
+ val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
+
+ assertThat(canForceStop).isFalse()
+ }
+
+ @Test
+ fun getActionButton_isUninstallInQueue_returnFalse() = runBlocking {
+ mockApp {}
+ mockDevicePolicyManager.stub {
+ on { isUninstallInQueue(PACKAGE_NAME) } doReturn true
+ }
+
+ val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
+
+ assertThat(canForceStop).isFalse()
+ }
+
+ @Test
+ fun canForceStopFlow_notStopped_returnTrue() = runBlocking {
+ mockApp { flags = 0 }
+
+ val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
+
+ assertThat(canForceStop).isTrue()
+ }
+
+ @Test
+ fun canForceStopFlow_isStoppedAndQueryReturnCancel_returnFalse() = runBlocking {
+ mockApp {
+ flags = ApplicationInfo.FLAG_STOPPED
+ }
+ resultCode = Activity.RESULT_CANCELED
+
+ val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
+
+ assertThat(canForceStop).isFalse()
+ }
+
+ @Test
+ fun canForceStopFlow_isStoppedAndQueryReturnOk_returnTrue() = runBlocking {
+ mockApp {
+ flags = ApplicationInfo.FLAG_STOPPED
+ }
+ resultCode = Activity.RESULT_OK
+
+ val canForceStop = repository.canForceStopFlow().firstWithTimeoutOrNull()
+
+ assertThat(canForceStop).isTrue()
+ }
+
+ private fun mockApp(builder: ApplicationInfo.() -> Unit = {}) = packageInfoPresenter.stub {
+ on { flow } doReturn MutableStateFlow(PackageInfo().apply {
+ applicationInfo = createApp(builder)
+ })
+ }
+
+ private fun createApp(builder: ApplicationInfo.() -> Unit = {}) =
+ ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ uid = UID
+ enabled = true
+ }.apply(builder)
+
+ private companion object {
+ const val PACKAGE_NAME = "package.name"
+ const val UID = 10000
+ }
+}
diff --git a/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java
index 556e1a7284e..f65480d3911 100644
--- a/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java
+++ b/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java
@@ -418,7 +418,6 @@ public class SimStatusDialogControllerTest {
}
@Test
- @Ignore
public void initialize_showImsRegistration_shouldNotRemoveImsRegistrationStateSetting() {
mPersistableBundle.putBoolean(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, true);
@@ -429,7 +428,6 @@ public class SimStatusDialogControllerTest {
}
@Test
- @Ignore
public void initialize_doNotShowImsRegistration_shouldRemoveImsRegistrationStateSetting() {
mPersistableBundle.putBoolean(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false);