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.regionalpreferences"
},
{
"exclude-filter": "com.android.settings.vpn2"
} }
] ]
} }

View File

@@ -84,11 +84,11 @@
<!-- Introduction detail message shown in face enrollment dialog [CHAR LIMIT=NONE]--> <!-- 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> <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] --> <!-- 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] --> <!-- 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] --> <!-- 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]--> <!-- 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> <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]--> <!-- 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_frame_size">48dp</dimen>
<dimen name="screen_flash_color_button_outer_circle_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_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_size">38dp</dimen>
<dimen name="screen_flash_color_button_inner_circle_padding">3dp</dimen> <dimen name="screen_flash_color_button_inner_circle_padding">5dp</dimen>
<dimen name="screen_flash_color_button_inner_circle_stroke">1dp</dimen> <dimen name="screen_flash_color_button_inner_circle_stroke">1dp</dimen>
<!-- An arbitrarily large number to make the max size fit the parent --> <!-- 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> <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] --> <!-- 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> <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] --> <!-- The preference summary when connecting [CHAR LIMIT=NONE] -->
<string name="audio_streams_add_source_wait_for_response_summary">Connecting\u2026</string> <string name="audio_streams_connecting_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>
<!-- Le audio streams audio lost dialog title [CHAR LIMIT=NONE] --> <!-- 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> <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] --> <!-- Le audio streams audio lost dialog subtitle [CHAR LIMIT=NONE] -->
@@ -13408,7 +13406,7 @@
<!-- Le audio streams confirm dialog default device [CHAR LIMIT=NONE] --> <!-- Le audio streams confirm dialog default device [CHAR LIMIT=NONE] -->
<string name="audio_streams_dialog_default_device">connected compatible headphones</string> <string name="audio_streams_dialog_default_device">connected compatible headphones</string>
<!-- Le audio streams activity title [CHAR LIMIT=NONE] --> <!-- 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] --> <!-- Le audio streams no password summary [CHAR LIMIT=NONE] -->
<string name="audio_streams_no_password_summary">No password</string> <string name="audio_streams_no_password_summary">No password</string>
<!-- Le audio streams failure dialog subtitle [CHAR LIMIT=NONE] --> <!-- Le audio streams failure dialog subtitle [CHAR LIMIT=NONE] -->

View File

@@ -67,17 +67,13 @@ public class MagnificationAlwaysOnPreferenceController extends
@Override @Override
public void onResume() { public void onResume() {
if (Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
MagnificationCapabilities.registerObserver(mContext, mContentObserver); MagnificationCapabilities.registerObserver(mContext, mContentObserver);
} }
}
@Override @Override
public void onPause() { public void onPause() {
if (Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
MagnificationCapabilities.unregisterObserver(mContext, mContentObserver); MagnificationCapabilities.unregisterObserver(mContext, mContentObserver);
} }
}
@Override @Override
public void displayPreference(PreferenceScreen screen) { public void displayPreference(PreferenceScreen screen) {
@@ -111,10 +107,6 @@ public class MagnificationAlwaysOnPreferenceController extends
@Override @Override
public CharSequence getSummary() { public CharSequence getSummary() {
if (!Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
return super.getSummary();
}
@StringRes int resId = mPreference.isEnabled() @StringRes int resId = mPreference.isEnabled()
? R.string.accessibility_screen_magnification_always_on_summary ? R.string.accessibility_screen_magnification_always_on_summary
: R.string.accessibility_screen_magnification_always_on_unavailable_summary; : R.string.accessibility_screen_magnification_always_on_unavailable_summary;
@@ -124,9 +116,6 @@ public class MagnificationAlwaysOnPreferenceController extends
@Override @Override
public void updateState(Preference preference) { public void updateState(Preference preference) {
super.updateState(preference); super.updateState(preference);
if (!Flags.hideMagnificationAlwaysOnToggleWhenWindowModeOnly()) {
return;
}
if (preference == null) { if (preference == null) {
return; return;

View File

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

View File

@@ -170,6 +170,8 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
infoMessageRequireEyes.setText(getInfoMessageRequireEyes()); infoMessageRequireEyes.setText(getInfoMessageRequireEyes());
} }
if (mFaceManager != null) {
mFaceManager.addAuthenticatorsRegisteredCallback( mFaceManager.addAuthenticatorsRegisteredCallback(
new IFaceAuthenticatorsRegisteredCallback.Stub() { new IFaceAuthenticatorsRegisteredCallback.Stub() {
@Override @Override
@@ -186,6 +188,7 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
onFaceStrengthChanged(); onFaceStrengthChanged();
} }
}); });
}
// This path is an entry point for SetNewPasswordController, e.g. // This path is an entry point for SetNewPasswordController, e.g.
// adb shell am start -a android.app.action.SET_NEW_PASSWORD // 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 { class AddSourceWaitForResponseState extends AudioStreamStateHandler {
@VisibleForTesting @VisibleForTesting
static final int AUDIO_STREAM_ADD_SOURCE_WAIT_FOR_RESPONSE_STATE_SUMMARY = 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; @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.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.DashboardFragment;
public class AudioStreamDetailsFragment extends DashboardFragment { public class AudioStreamDetailsFragment extends DashboardFragment {
static final String BROADCAST_NAME_ARG = "broadcast_name"; static final String BROADCAST_NAME_ARG = "broadcast_name";
static final String BROADCAST_ID_ARG = "broadcast_id"; static final String BROADCAST_ID_ARG = "broadcast_id";
private static final String TAG = "AudioStreamDetailsFragment"; @VisibleForTesting static final String TAG = "AudioStreamDetailsFragment";
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {

View File

@@ -23,6 +23,7 @@ import android.util.AttributeSet;
import android.view.View; import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceViewHolder; import androidx.preference.PreferenceViewHolder;
import com.android.settings.R; import com.android.settings.R;
@@ -60,7 +61,8 @@ class AudioStreamPreference extends TwoTargetPreference {
notifyChanged(); notifyChanged();
} }
private AudioStreamPreference(Context context, @Nullable AttributeSet attrs) { @VisibleForTesting
AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
super(context, attrs); super(context, attrs);
setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing); 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 { class WaitForSyncState extends AudioStreamStateHandler {
@VisibleForTesting @VisibleForTesting
static final int AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY = 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; @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; package com.android.settings.deviceinfo.simstatus;
import static androidx.lifecycle.Lifecycle.Event;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -30,7 +28,6 @@ import android.content.res.Resources;
import android.os.IBinder; import android.os.IBinder;
import android.os.PersistableBundle; import android.os.PersistableBundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.telephony.AccessNetworkConstants;
import android.telephony.Annotation; import android.telephony.Annotation;
import android.telephony.CarrierConfigManager; import android.telephony.CarrierConfigManager;
import android.telephony.CellBroadcastIntents; import android.telephony.CellBroadcastIntents;
@@ -46,29 +43,28 @@ import android.telephony.TelephonyCallback;
import android.telephony.TelephonyDisplayInfo; import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.telephony.euicc.EuiccManager; 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.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.LifecycleOwner;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.network.SubscriptionUtil; import com.android.settings.network.SubscriptionUtil;
import com.android.settingslib.Utils; import com.android.settingslib.Utils;
import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.Lifecycle;
import kotlin.Unit;
import java.util.List; import java.util.List;
/** /**
* Controller for Sim Status information within the About Phone Settings page. * 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"; private final static String TAG = "SimStatusDialogCtrl";
@@ -110,26 +106,7 @@ public class SimStatusDialogController implements LifecycleObserver {
new OnSubscriptionsChangedListener() { new OnSubscriptionsChangedListener() {
@Override @Override
public void onSubscriptionsChanged() { public void onSubscriptionsChanged() {
final int prevSubId = (mSubscriptionInfo != null)
? mSubscriptionInfo.getSubscriptionId()
: SubscriptionManager.INVALID_SUBSCRIPTION_ID;
mSubscriptionInfo = getPhoneSubscriptionInfo(mSlotIndex); 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(); updateSubscriptionStatus();
} }
}; };
@@ -269,8 +246,8 @@ public class SimStatusDialogController implements LifecycleObserver {
/** /**
* OnResume lifecycle event, resume listening for phone state or subscription changes. * OnResume lifecycle event, resume listening for phone state or subscription changes.
*/ */
@OnLifecycleEvent(Event.ON_RESUME) @Override
public void onResume() { public void onResume(@NonNull LifecycleOwner owner) {
if (mSubscriptionInfo == null) { if (mSubscriptionInfo == null) {
return; return;
} }
@@ -280,7 +257,7 @@ public class SimStatusDialogController implements LifecycleObserver {
.registerTelephonyCallback(mContext.getMainExecutor(), mTelephonyCallback); .registerTelephonyCallback(mContext.getMainExecutor(), mTelephonyCallback);
mSubscriptionManager.addOnSubscriptionsChangedListener( mSubscriptionManager.addOnSubscriptionsChangedListener(
mContext.getMainExecutor(), mOnSubscriptionsChangedListener); mContext.getMainExecutor(), mOnSubscriptionsChangedListener);
registerImsRegistrationCallback(mSubscriptionInfo.getSubscriptionId()); collectImsRegistered(owner);
if (mShowLatestAreaInfo) { if (mShowLatestAreaInfo) {
updateAreaInfoText(); updateAreaInfoText();
@@ -295,8 +272,8 @@ public class SimStatusDialogController implements LifecycleObserver {
/** /**
* onPause lifecycle event, no longer listen for phone state or subscription changes. * onPause lifecycle event, no longer listen for phone state or subscription changes.
*/ */
@OnLifecycleEvent(Event.ON_PAUSE) @Override
public void onPause() { public void onPause(@NonNull LifecycleOwner owner) {
if (mSubscriptionInfo == null) { if (mSubscriptionInfo == null) {
if (mIsRegisteredListener) { if (mIsRegisteredListener) {
mSubscriptionManager.removeOnSubscriptionsChangedListener( mSubscriptionManager.removeOnSubscriptionsChangedListener(
@@ -310,7 +287,6 @@ public class SimStatusDialogController implements LifecycleObserver {
return; return;
} }
unregisterImsRegistrationCallback(mSubscriptionInfo.getSubscriptionId());
mSubscriptionManager.removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener); mSubscriptionManager.removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
getTelephonyManager().unregisterTelephonyCallback(mTelephonyCallback); getTelephonyManager().unregisterTelephonyCallback(mTelephonyCallback);
@@ -625,51 +601,22 @@ public class SimStatusDialogController implements LifecycleObserver {
mDialog.removeSettingFromScreen(IMS_REGISTRATION_STATE_VALUE_ID); mDialog.removeSettingFromScreen(IMS_REGISTRATION_STATE_VALUE_ID);
} }
private ImsMmTelManager.RegistrationCallback mImsRegStateCallback = private void collectImsRegistered(@NonNull LifecycleOwner owner) {
new ImsMmTelManager.RegistrationCallback() { if (!isImsRegistrationStateShowUp()) {
@Override return;
public void onRegistered(@AccessNetworkConstants.TransportType int imsTransportType) { }
new ImsRegistrationStateController(mContext).collectImsRegistered(
owner, mSlotIndex, (Boolean imsRegistered) -> {
if (imsRegistered) {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString( mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_registered)); com.android.settingslib.R.string.ims_reg_status_registered));
} } else {
@Override
public void onRegistering(@AccessNetworkConstants.TransportType int imsTransportType) {
mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString( mDialog.setText(IMS_REGISTRATION_STATE_VALUE_ID, mRes.getString(
com.android.settingslib.R.string.ims_reg_status_not_registered)); com.android.settingslib.R.string.ims_reg_status_not_registered));
} }
@Override return Unit.INSTANCE;
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) {
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);
} }
private SubscriptionInfo getPhoneSubscriptionInfo(int slotId) { private SubscriptionInfo getPhoneSubscriptionInfo(int slotId) {

View File

@@ -455,7 +455,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements
} }
private void updateHomepageBackground() { private void updateHomepageBackground() {
if (!mIsEmbeddingActivityEnabled) { if (!Flags.homepageRevamp() && !mIsEmbeddingActivityEnabled) {
return; 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.ImsManager
import android.telephony.ims.ImsMmTelManager import android.telephony.ims.ImsMmTelManager
import android.telephony.ims.ImsMmTelManager.WiFiCallingMode 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.ImsStateCallback
import android.telephony.ims.RegistrationManager
import android.telephony.ims.feature.MmTelFeature import android.telephony.ims.feature.MmTelFeature
import android.util.Log import android.util.Log
import kotlin.coroutines.resume import kotlin.coroutines.resume
@@ -39,7 +42,11 @@ import kotlinx.coroutines.withContext
interface ImsMmTelRepository { interface ImsMmTelRepository {
@WiFiCallingMode @WiFiCallingMode
fun getWiFiCallingMode(useRoamingMode: Boolean): Int fun getWiFiCallingMode(useRoamingMode: Boolean): Int
fun imsRegisteredFlow(): Flow<Boolean>
fun imsReadyFlow(): Flow<Boolean> fun imsReadyFlow(): Flow<Boolean>
suspend fun isSupported( suspend fun isSupported(
@MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int, @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
@AccessNetworkConstants.TransportType transportType: Int, @AccessNetworkConstants.TransportType transportType: Int,
@@ -64,6 +71,36 @@ class ImsMmTelRepositoryImpl(
ImsMmTelManager.WIFI_MODE_UNKNOWN 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 { override fun imsReadyFlow(): Flow<Boolean> = callbackFlow {
val callback = object : ImsStateCallback() { val callback = object : ImsStateCallback() {
override fun onAvailable() { 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.AutomaticZenRule;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.service.notification.SystemZenRules;
import android.service.notification.ZenPolicy; import android.service.notification.ZenPolicy;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import com.android.settings.R; import com.android.settings.R;
@@ -94,8 +91,6 @@ class ZenMode {
private final boolean mIsActive; private final boolean mIsActive;
private final boolean mIsManualDnd; private final boolean mIsManualDnd;
// private ZenPolicy mPreviousPolicy;
ZenMode(String id, AutomaticZenRule rule, boolean isActive) { ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
this(id, rule, isActive, false); this(id, rule, isActive, false);
} }
@@ -122,49 +117,14 @@ class ZenMode {
} }
@NonNull @NonNull
public ListenableFuture<Drawable> getIcon(@NonNull Context context) { public ListenableFuture<Drawable> getIcon(@NonNull IconLoader iconLoader) {
// TODO: b/333528586 - Load the icons asynchronously, and cache them Context context = iconLoader.getContext();
if (mIsManualDnd) { if (mIsManualDnd) {
return Futures.immediateFuture( return Futures.immediateFuture(requireNonNull(
requireNonNull(context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp))); context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
} }
int iconResId = mRule.getIconResId(); return iconLoader.getIcon(mRule);
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)));
} }
@NonNull @NonNull

View File

@@ -17,7 +17,6 @@ package com.android.settings.notification.modes;
import android.app.Flags; import android.app.Flags;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -28,9 +27,7 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.EntityHeaderController; import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.widget.LayoutPreference;
import java.util.concurrent.TimeUnit; class ZenModeHeaderController extends AbstractZenModePreferenceController {
public class ZenModeHeaderController extends AbstractZenModePreferenceController {
private final DashboardFragment mFragment; private final DashboardFragment mFragment;
private EntityHeaderController mHeaderController; private EntityHeaderController mHeaderController;
@@ -51,7 +48,8 @@ public class ZenModeHeaderController extends AbstractZenModePreferenceController
@Override @Override
public void updateState(Preference preference) { public void updateState(Preference preference) {
if (getAZR() == null || mFragment == null) { ZenMode mode = getMode();
if (mode == null || mFragment == null) {
return; return;
} }
@@ -62,14 +60,12 @@ public class ZenModeHeaderController extends AbstractZenModePreferenceController
mFragment, mFragment,
pref.findViewById(R.id.entity_header)); pref.findViewById(R.id.entity_header));
} }
Drawable icon = null;
try { FutureUtil.whenDone(
icon = getMode().getIcon(mContext).get(200, TimeUnit.MILLISECONDS); mode.getIcon(IconLoader.getInstance(mContext)),
} catch (Exception e) { icon -> mHeaderController.setIcon(icon)
// no icon .setLabel(mode.getRule().getName())
} .done(false /* rebindActions */),
mHeaderController.setIcon(icon) mContext.getMainExecutor());
.setLabel(getAZR().getName())
.done(false /* rebindActions */);
} }
} }

View File

@@ -25,15 +25,11 @@ import com.android.settings.core.SubSettingLauncher;
import com.android.settings.notification.zen.ZenModeSettings; import com.android.settings.notification.zen.ZenModeSettings;
import com.android.settingslib.RestrictedPreference; 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 representing a single mode item on the modes aggregator page. Clicking on this
* preference leads to an individual mode's configuration page. * preference leads to an individual mode's configuration page.
*/ */
public class ZenModeListPreference extends RestrictedPreference { class ZenModeListPreference extends RestrictedPreference {
final Context mContext; final Context mContext;
ZenMode mZenMode; ZenMode mZenMode;
@@ -68,10 +64,10 @@ public class ZenModeListPreference extends RestrictedPreference {
mZenMode = zenMode; mZenMode = zenMode;
setTitle(mZenMode.getRule().getName()); setTitle(mZenMode.getRule().getName());
setSummary(mZenMode.getRule().getTriggerDescription()); setSummary(mZenMode.getRule().getTriggerDescription());
try {
setIcon(mZenMode.getIcon(mContext).get(200, TimeUnit.MILLISECONDS)); FutureUtil.whenDone(
} catch (Exception e) { mZenMode.getIcon(IconLoader.getInstance(mContext)),
// no icon icon -> setIcon(icon),
} mContext.getMainExecutor());
} }
} }

View File

@@ -41,7 +41,6 @@ import com.android.settings.R;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -104,7 +103,6 @@ class ZenModesBackend {
ZenMode getMode(String id) { ZenMode getMode(String id) {
ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig(); ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) { if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
// Regardless of its contents, non-null manualRule means that manual rule is active.
return getManualDndMode(currentConfig); return getManualDndMode(currentConfig);
} else { } else {
AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id); AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
@@ -177,8 +175,9 @@ class ZenModesBackend {
.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
.build(); .build();
// Regardless of its contents, non-null manualRule means that manual rule is active.
return ZenMode.manualDndMode(manualDndRule, return ZenMode.manualDndMode(manualDndRule,
config != null && config.manualRule != null); // isActive config != null && config.manualRule != null);
} }
private static boolean isRuleActive(String id, ZenModeConfig config) { 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.AlertDialogButton
import com.android.settingslib.spa.widget.dialog.AlertDialogPresenter import com.android.settingslib.spa.widget.dialog.AlertDialogPresenter
import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter 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 com.android.settingslib.spaprivileged.model.app.userId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
class AppForceStopButton( class AppForceStopButton(
private val packageInfoPresenter: PackageInfoPresenter, private val packageInfoPresenter: PackageInfoPresenter,
private val appForceStopRepository: AppForceStopRepository =
AppForceStopRepository(packageInfoPresenter),
) { ) {
private val context = packageInfoPresenter.context private val context = packageInfoPresenter.context
private val appButtonRepository = AppButtonRepository(context)
private val packageManager = context.packageManager private val packageManager = context.packageManager
@Composable @Composable
@@ -55,27 +51,11 @@ class AppForceStopButton(
return ActionButton( return ActionButton(
text = stringResource(R.string.force_stop), text = stringResource(R.string.force_stop),
imageVector = Icons.Outlined.Report, imageVector = Icons.Outlined.Report,
enabled = remember(app) { enabled = remember(app) { appForceStopRepository.canForceStopFlow() }
flow { .collectAsStateWithLifecycle(false).value,
emit(isForceStopButtonEnable(app))
}.flowOn(Dispatchers.Default)
}.collectAsStateWithLifecycle(false).value,
) { onForceStopButtonClicked(app, dialogPresenter) } ) { 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( private fun onForceStopButtonClicked(
app: ApplicationInfo, app: ApplicationInfo,
dialogPresenter: AlertDialogPresenter, 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 static org.mockito.Mockito.verify;
import android.content.Context; 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 android.provider.Settings;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@@ -39,7 +36,6 @@ import androidx.preference.SwitchPreference;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
@@ -49,8 +45,6 @@ import org.robolectric.shadows.ShadowContentResolver;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class MagnificationAlwaysOnPreferenceControllerTest { public class MagnificationAlwaysOnPreferenceControllerTest {
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String KEY_ALWAYS_ON = private static final String KEY_ALWAYS_ON =
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED; Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED;
@@ -99,8 +93,7 @@ public class MagnificationAlwaysOnPreferenceControllerTest {
} }
@Test @Test
@EnableFlags(Flags.FLAG_HIDE_MAGNIFICATION_ALWAYS_ON_TOGGLE_WHEN_WINDOW_MODE_ONLY) public void onResume_verifyRegisterCapabilityObserver() {
public void onResume_flagOn_verifyRegisterCapabilityObserver() {
mController.onResume(); mController.onResume();
assertThat(mShadowContentResolver.getContentObservers( assertThat(mShadowContentResolver.getContentObservers(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY))) Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY)))
@@ -108,8 +101,7 @@ public class MagnificationAlwaysOnPreferenceControllerTest {
} }
@Test @Test
@EnableFlags(Flags.FLAG_HIDE_MAGNIFICATION_ALWAYS_ON_TOGGLE_WHEN_WINDOW_MODE_ONLY) public void onPause_verifyUnregisterCapabilityObserver() {
public void onPause_flagOn_verifyUnregisterCapabilityObserver() {
mController.onResume(); mController.onResume();
mController.onPause(); mController.onPause();
assertThat(mShadowContentResolver.getContentObservers( assertThat(mShadowContentResolver.getContentObservers(
@@ -118,17 +110,7 @@ public class MagnificationAlwaysOnPreferenceControllerTest {
} }
@Test @Test
@DisableFlags(Flags.FLAG_HIDE_MAGNIFICATION_ALWAYS_ON_TOGGLE_WHEN_WINDOW_MODE_ONLY) public void updateState_windowModeOnly_preferenceBecomesUnavailable() {
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() {
MagnificationCapabilities.setCapabilities(mContext, MagnificationMode.WINDOW); MagnificationCapabilities.setCapabilities(mContext, MagnificationMode.WINDOW);
mController.updateState(mSwitchPreference); 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.content.Context
import android.telephony.AccessNetworkConstants import android.telephony.AccessNetworkConstants
import android.telephony.ims.ImsMmTelManager import android.telephony.ims.ImsMmTelManager
import android.telephony.ims.ImsReasonInfo
import android.telephony.ims.ImsRegistrationAttributes
import android.telephony.ims.ImsStateCallback import android.telephony.ims.ImsStateCallback
import android.telephony.ims.RegistrationManager.RegistrationCallback
import android.telephony.ims.feature.MmTelFeature import android.telephony.ims.feature.MmTelFeature
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spa.testutils.toListWithTimeout import com.android.settingslib.spa.testutils.toListWithTimeout
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer import java.util.function.Consumer
@@ -44,12 +48,17 @@ import org.mockito.kotlin.stub
class ImsMmTelRepositoryTest { class ImsMmTelRepositoryTest {
private val context: Context = ApplicationProvider.getApplicationContext() private val context: Context = ApplicationProvider.getApplicationContext()
private var registrationCallback: RegistrationCallback? = null
private var stateCallback: ImsStateCallback? = null private var stateCallback: ImsStateCallback? = null
private val mockImsMmTelManager = mock<ImsMmTelManager> { private val mockImsMmTelManager = mock<ImsMmTelManager> {
on { isVoWiFiSettingEnabled } doReturn true on { isVoWiFiSettingEnabled } doReturn true
on { getVoWiFiRoamingModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED on { getVoWiFiRoamingModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED
on { getVoWiFiModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_CELLULAR_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 { on { registerImsStateCallback(any(), any()) } doAnswer {
stateCallback = it.arguments[1] as ImsStateCallback stateCallback = it.arguments[1] as ImsStateCallback
stateCallback?.onAvailable() stateCallback?.onAvailable()
@@ -99,6 +108,25 @@ class ImsMmTelRepositoryTest {
assertThat(wiFiCallingMode).isEqualTo(ImsMmTelManager.WIFI_MODE_UNKNOWN) 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 @Test
fun imsReadyFlow_sendInitialValue() = runBlocking { fun imsReadyFlow_sendInitialValue() = runBlocking {
val flow = repository.imsReadyFlow() 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.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.util.trace
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R 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.framework.common.devicePolicyManager
import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.app.userId
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.spy import org.mockito.kotlin.spy
@@ -54,51 +55,30 @@ class AppForceStopButtonTest {
private val mockDevicePolicyManager = mock<DevicePolicyManager>() private val mockDevicePolicyManager = mock<DevicePolicyManager>()
private val mockUserManager = mock<UserManager> {
on { getUserRestrictionSources(any(), any()) } doReturn emptyList()
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) { private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { packageManager } doReturn mockPackageManager on { packageManager } doReturn mockPackageManager
on { devicePolicyManager } doReturn mockDevicePolicyManager on { devicePolicyManager } doReturn mockDevicePolicyManager
on { getSystemService(Context.DEVICE_POLICY_SERVICE) } doReturn mockDevicePolicyManager on { getSystemService(Context.DEVICE_POLICY_SERVICE) } doReturn mockDevicePolicyManager
on { getSystemService(Context.USER_SERVICE) } doReturn mockUserManager
} }
private val packageInfoPresenter = mock<PackageInfoPresenter> { private val packageInfoPresenter = mock<PackageInfoPresenter> {
on { context } doReturn context on { context } doReturn context
} }
private val appForceStopButton = AppForceStopButton(packageInfoPresenter) private val mockAppForceStopRepository = mock<AppForceStopRepository> {
on { canForceStopFlow() } doReturn flowOf(false)
@Test
fun getActionButton_isActiveAdmin_buttonDisabled() {
val app = createApp()
mockDevicePolicyManager.stub {
on { packageHasActiveAdmins(PACKAGE_NAME, app.userId) } doReturn true
} }
setForceStopButton(app) private val appForceStopButton = AppForceStopButton(
packageInfoPresenter = packageInfoPresenter,
composeTestRule.onNodeWithText(context.getString(R.string.force_stop)).assertIsNotEnabled() appForceStopRepository = mockAppForceStopRepository,
} )
@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()
}
@Test @Test
fun getActionButton_isStopped_buttonDisabled() { fun getActionButton_isStopped_buttonDisabled() {
val app = createApp { val app = createApp()
flags = ApplicationInfo.FLAG_STOPPED mockAppForceStopRepository.stub {
on { canForceStopFlow() } doReturn flowOf(false)
} }
setForceStopButton(app) setForceStopButton(app)
@@ -109,6 +89,9 @@ class AppForceStopButtonTest {
@Test @Test
fun getActionButton_regularApp_buttonEnabled() { fun getActionButton_regularApp_buttonEnabled() {
val app = createApp() val app = createApp()
mockAppForceStopRepository.stub {
on { canForceStopFlow() } doReturn flowOf(true)
}
setForceStopButton(app) 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 @Test
@Ignore
public void initialize_showImsRegistration_shouldNotRemoveImsRegistrationStateSetting() { public void initialize_showImsRegistration_shouldNotRemoveImsRegistrationStateSetting() {
mPersistableBundle.putBoolean( mPersistableBundle.putBoolean(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, true); CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, true);
@@ -429,7 +428,6 @@ public class SimStatusDialogControllerTest {
} }
@Test @Test
@Ignore
public void initialize_doNotShowImsRegistration_shouldRemoveImsRegistrationStateSetting() { public void initialize_doNotShowImsRegistration_shouldRemoveImsRegistrationStateSetting() {
mPersistableBundle.putBoolean( mPersistableBundle.putBoolean(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false); CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false);