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 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);