Snap for 11876238 from 7f541e460b to 24Q3-release

Change-Id: I6c71eff6d3db72ea15720ae25064d666e142a9f7
This commit is contained in:
Android Build Coastguard Worker
2024-05-22 23:22:57 +00:00
37 changed files with 1435 additions and 273 deletions

View File

@@ -17,9 +17,6 @@
},
{
"exclude-filter": "com.android.settings.regionalpreferences"
},
{
"exclude-filter": "com.android.settings.vpn2"
}
]
}

View File

@@ -84,11 +84,11 @@
<!-- Introduction detail message shown in face enrollment dialog [CHAR LIMIT=NONE]-->
<string name="security_settings_face_enroll_introduction_message" product="device">Use your face to unlock your device, authorize purchases, or sign in to apps.</string>
<!-- Subtitle shown on the face enrollment introduction screen with in-app authentication. [CHAR LIMIT=NONE] -->
<string name="security_settings_face_enroll_introduction_message_class3" product="default">Use your face to unlock your phone or for authentication in apps, like when you sign in to apps or approve a purchase.</string>
<string name="security_settings_face_enroll_introduction_message_class3" product="default">Use your face to unlock your phone or for authentication in apps, like when you sign in to apps or approve a purchase</string>
<!-- Subtitle shown on the face enrollment introduction screen with in-app authentication. [CHAR LIMIT=NONE] -->
<string name="security_settings_face_enroll_introduction_message_class3" product="tablet">Use your face to unlock your tablet or for authentication in apps, like when you sign in to apps or approve a purchase.</string>
<string name="security_settings_face_enroll_introduction_message_class3" product="tablet">Use your face to unlock your tablet or for authentication in apps, like when you sign in to apps or approve a purchase</string>
<!-- Subtitle shown on the face enrollment introduction screen with in-app authentication. [CHAR LIMIT=NONE] -->
<string name="security_settings_face_enroll_introduction_message_class3" product="device">Use your face to unlock your device or for authentication in apps, like when you sign in to apps or approve a purchase.</string>
<string name="security_settings_face_enroll_introduction_message_class3" product="device">Use your face to unlock your device or for authentication in apps, like when you sign in to apps or approve a purchase</string>
<!-- Introduction detail message shown in face enrollment dialog when asking for parental consent [CHAR LIMIT=NONE]-->
<string name="security_settings_face_enroll_introduction_consent_message_0" product="default">Allow your child to use their face to unlock their phone</string>
<!-- Introduction detail message shown in face enrollment dialog when asking for parental consent [CHAR LIMIT=NONE]-->

View File

@@ -469,8 +469,8 @@
<dimen name="screen_flash_color_button_frame_size">48dp</dimen>
<dimen name="screen_flash_color_button_outer_circle_size">48dp</dimen>
<dimen name="screen_flash_color_button_outer_circle_stroke_width">2dp</dimen>
<dimen name="screen_flash_color_button_inner_circle_size">42dp</dimen>
<dimen name="screen_flash_color_button_inner_circle_padding">3dp</dimen>
<dimen name="screen_flash_color_button_inner_circle_size">38dp</dimen>
<dimen name="screen_flash_color_button_inner_circle_padding">5dp</dimen>
<dimen name="screen_flash_color_button_inner_circle_stroke">1dp</dimen>
<!-- An arbitrarily large number to make the max size fit the parent -->

View File

@@ -13385,10 +13385,8 @@
<string name="audio_streams_add_source_bad_code_state_summary">Check password and try again</string>
<!-- The preference summary when add source response results in general failure [CHAR LIMIT=NONE] -->
<string name="audio_streams_add_source_failed_state_summary">Can\u0027t connect. Try again.</string>
<!-- The preference summary when waiting for add source response [CHAR LIMIT=NONE] -->
<string name="audio_streams_add_source_wait_for_response_summary">Connecting\u2026</string>
<!-- The preference summary when waiting for sync [CHAR LIMIT=NONE] -->
<string name="audio_streams_wait_for_sync_state_summary">Scanning\u2026</string>
<!-- The preference summary when connecting [CHAR LIMIT=NONE] -->
<string name="audio_streams_connecting_summary">Connecting\u2026</string>
<!-- Le audio streams audio lost dialog title [CHAR LIMIT=NONE] -->
<string name="audio_streams_dialog_stream_is_not_available">Audio stream isn\u0027t available</string>
<!-- Le audio streams audio lost dialog subtitle [CHAR LIMIT=NONE] -->
@@ -13408,7 +13406,7 @@
<!-- Le audio streams confirm dialog default device [CHAR LIMIT=NONE] -->
<string name="audio_streams_dialog_default_device">connected compatible headphones</string>
<!-- Le audio streams activity title [CHAR LIMIT=NONE] -->
<string name="audio_streams_activity_title">Broadcasts</string>
<string name="audio_streams_activity_title">Audio streams</string>
<!-- Le audio streams no password summary [CHAR LIMIT=NONE] -->
<string name="audio_streams_no_password_summary">No password</string>
<!-- Le audio streams failure dialog subtitle [CHAR LIMIT=NONE] -->

View File

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

View File

@@ -168,7 +168,7 @@ public class PrimaryProviderPreference extends GearPreference {
mButtonFrameView.setPadding(
paddingLeft,
mButtonFrameView.getPaddingTop(),
mButtonFrameView.getPaddingRight(),
paddingLeft,
mButtonFrameView.getPaddingBottom());
}

View File

@@ -170,22 +170,25 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
infoMessageRequireEyes.setText(getInfoMessageRequireEyes());
}
mFaceManager.addAuthenticatorsRegisteredCallback(
new IFaceAuthenticatorsRegisteredCallback.Stub() {
@Override
public void onAllAuthenticatorsRegistered(
@NonNull List<FaceSensorPropertiesInternal> 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<FaceSensorPropertiesInternal> 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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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<Boolean> =
simSlotRepository.subIdInSimSlotFlow(simSlotIndex)
.flatMapLatest { subId ->
if (SubscriptionManager.isValidSubscriptionId(subId)) {
imsMmTelRepositoryFactory(subId).imsRegisteredFlow()
} else {
flowOf(false)
}
}
.conflate()
.flowOn(Dispatchers.Default)
}

View File

@@ -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) {

View File

@@ -455,7 +455,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements
}
private void updateHomepageBackground() {
if (!mIsEmbeddingActivityEnabled) {
if (!Flags.homepageRevamp() && !mIsEmbeddingActivityEnabled) {
return;
}

View File

@@ -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"
}
}

View File

@@ -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<Boolean>
fun imsReadyFlow(): Flow<Boolean>
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<Boolean> = 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<Boolean> = callbackFlow {
val callback = object : ImsStateCallback() {
override fun onAvailable() {

View File

@@ -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 <V> void whenDone(ListenableFuture<V> future, Consumer<V> consumer, Executor executor) {
whenDone(future, consumer, executor, "Error in future");
}
static <V> void whenDone(ListenableFuture<V> future, Consumer<V> consumer, Executor executor,
String errorLogMessage, Object... errorLogMessageArgs) {
Futures.addCallback(future, new FutureCallback<V>() {
@Override
public void onSuccess(V v) {
consumer.accept(v);
}
@Override
public void onFailure(Throwable throwable) {
Log.e(TAG, String.format(errorLogMessage, errorLogMessageArgs), throwable);
}
}, executor);
}
}

View File

@@ -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<String, Drawable> 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<Drawable> 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;
}
}

View File

@@ -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<Drawable> getIcon(@NonNull Context context) {
// TODO: b/333528586 - Load the icons asynchronously, and cache them
public ListenableFuture<Drawable> 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

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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<Boolean> = 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"
}
}

View File

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

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Drawable> 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<Drawable> 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<Drawable> 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());
}
}

View File

@@ -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<SimSlotRepository> {
on { subIdInSimSlotFlow(SIM_SLOT_INDEX) } doReturn flowOf(SUB_ID)
}
private val mockImsMmTelRepository = mock<ImsMmTelRepository> {
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
}
}

View File

@@ -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<SubscriptionManager> {
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
}
}

View File

@@ -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<ImsMmTelManager> {
on { isVoWiFiSettingEnabled } doReturn true
on { getVoWiFiRoamingModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED
on { getVoWiFiModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED
on { registerImsRegistrationCallback(any(), any<RegistrationCallback>()) } doAnswer {
registrationCallback = it.arguments[1] as RegistrationCallback
registrationCallback?.onRegistered(mock<ImsRegistrationAttributes>())
}
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()

View File

@@ -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<DevicePolicyManager>()
private val mockUserManager = mock<UserManager> {
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<PackageInfoPresenter> {
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<AppForceStopRepository> {
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)

View File

@@ -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<DevicePolicyManager>()
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<PackageInfoPresenter> {
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
}
}

View File

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