Snap for 12158986 from 23f6c8fe06 to 24Q4-release

Change-Id: I5615846b2aed5f77a1eb29914b5ae5ec32ae02ef
This commit is contained in:
Android Build Coastguard Worker
2024-07-30 23:21:41 +00:00
23 changed files with 1022 additions and 382 deletions

24
res/drawable/ic_close.xml Normal file
View File

@@ -0,0 +1,24 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dp"
android:height="24.0dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -48,7 +48,8 @@
app:layout_constrainedWidth="true"
app:layout_constraintHorizontal_bias="0"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Medium"
android:text="@string/zen_mode_start_time" />
android:text="@string/zen_mode_start_time"
android:importantForAccessibility="no" />
<!-- Start time display + setter -->
<TextView
@@ -85,7 +86,8 @@
app:layout_constrainedWidth="true"
app:layout_constraintHorizontal_bias="0"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Medium"
android:text="@string/zen_mode_end_time" />
android:text="@string/zen_mode_end_time"
android:importantForAccessibility="no" />
<!-- End time setter; right-aligned -->
<TextView

View File

@@ -0,0 +1,293 @@
/*
* 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.bluetooth.ui
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.DialogProperties
import com.android.settings.R
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.dialog.getDialogWidth
@Composable
fun MultiTogglePreferenceGroup(
preferenceModels: List<DeviceSettingModel.MultiTogglePreference>,
) {
var settingIdForPopUp by remember { mutableStateOf<Int?>(null) }
settingIdForPopUp?.let { id ->
preferenceModels.find { it.id == id }?.let { dialog(it) { settingIdForPopUp = null } }
}
Row(
modifier = Modifier.padding(SettingsDimension.itemPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
preferenceModels.forEach { preferenceModel ->
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Row {
Surface(
modifier = Modifier.height(64.dp),
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surface
) {
Button(
modifier =
Modifier.fillMaxSize().padding(8.dp).semantics {
role = Role.Switch
toggleableState =
if (preferenceModel.isActive) {
ToggleableState.On
} else {
ToggleableState.Off
}
contentDescription = preferenceModel.title
},
onClick = { settingIdForPopUp = preferenceModel.id },
shape = RoundedCornerShape(20.dp),
colors = getButtonColors(preferenceModel.isActive),
contentPadding = PaddingValues(0.dp)
) {
Icon(
preferenceModel.toggles[preferenceModel.state.selectedIndex]
.icon
.asImageBitmap(),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = LocalContentColor.current
)
}
}
}
Row { Text(text = preferenceModel.title, fontSize = 12.sp) }
}
}
}
}
@Composable
private fun getButtonColors(isActive: Boolean) =
if (isActive) {
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
)
} else {
ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun dialog(
multiTogglePreference: DeviceSettingModel.MultiTogglePreference,
onDismiss: () -> Unit
) {
BasicAlertDialog(
onDismissRequest = { onDismiss() },
modifier = Modifier.width(getDialogWidth()),
properties = DialogProperties(usePlatformDefaultWidth = false),
content = {
Card(
shape = RoundedCornerShape(28.dp),
modifier = Modifier.fillMaxWidth().height(192.dp),
content = {
Box {
Button(
onClick = { onDismiss() },
modifier = Modifier.padding(8.dp).align(Alignment.TopEnd).size(48.dp),
contentPadding = PaddingValues(12.dp),
colors =
ButtonDefaults.buttonColors(containerColor = Color.Transparent),
) {
Icon(
painterResource(id = R.drawable.ic_close),
null,
tint = MaterialTheme.colorScheme.inverseSurface
)
}
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) {
dialogContent(multiTogglePreference)
}
}
},
)
}
)
}
@Composable
private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiTogglePreference) {
Column {
Row(
modifier = Modifier.fillMaxWidth().height(24.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Text(text = multiTogglePreference.title, fontSize = 16.sp)
}
Spacer(modifier = Modifier.height(20.dp))
var selectedRect by remember { mutableStateOf<Rect?>(null) }
val offset =
selectedRect?.let { rect ->
animateFloatAsState(targetValue = rect.left, finishedListener = {}).value
}
Row(
modifier =
Modifier.fillMaxWidth()
.height(64.dp)
.background(
MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(28.dp)
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Box {
offset?.let { offset ->
with(LocalDensity.current) {
Box(
modifier =
Modifier.offset(offset.toDp(), 0.dp)
.height(selectedRect!!.height.toDp())
.width(selectedRect!!.width.toDp())
.background(
MaterialTheme.colorScheme.tertiaryContainer,
shape = RoundedCornerShape(20.dp)
)
)
}
}
Row {
for ((idx, toggle) in multiTogglePreference.toggles.withIndex()) {
val selected = idx == multiTogglePreference.state.selectedIndex
Column(
modifier =
Modifier.weight(1f)
.padding(horizontal = 8.dp)
.height(48.dp)
.background(
Color.Transparent,
shape = RoundedCornerShape(28.dp)
)
.onGloballyPositioned { layoutCoordinates ->
if (selected) {
selectedRect = layoutCoordinates.boundsInParent()
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(
onClick = {
multiTogglePreference.updateState(
DeviceSettingStateModel.MultiTogglePreferenceState(idx)
)
},
modifier = Modifier.fillMaxSize(),
colors =
ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = LocalContentColor.current
),
) {
Icon(
bitmap = toggle.icon.asImageBitmap(),
null,
modifier = Modifier.size(24.dp),
tint = LocalContentColor.current
)
}
}
}
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth().height(32.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
for (toggle in multiTogglePreference.toggles) {
Text(
text = toggle.label,
fontSize = 12.sp,
textAlign = TextAlign.Center,
modifier = Modifier.weight(1f).padding(horizontal = 8.dp)
)
}
}
}
}

View File

@@ -17,49 +17,37 @@
package com.android.settings.network.telephony
import android.content.Context
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.settings.R
import com.android.settings.flags.Flags
import com.android.settings.network.SubscriptionInfoListViewModel
import com.android.settings.network.SubscriptionUtil
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Preference controller for "Phone number"
*/
class MobileNetworkPhoneNumberPreferenceController(context: Context, key: String) :
TelephonyBasePreferenceController(context, key) {
/** Preference controller for "Phone number" */
class MobileNetworkPhoneNumberPreferenceController
@JvmOverloads
constructor(
context: Context,
key: String,
private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context),
) : TelephonyBasePreferenceController(context, key) {
private lateinit var lazyViewModel: Lazy<SubscriptionInfoListViewModel>
private lateinit var preference: Preference
private var phoneNumber = String()
fun init(fragment: Fragment, subId: Int) {
lazyViewModel = fragment.viewModels()
fun init(subId: Int) {
mSubId = subId
}
override fun getAvailabilityStatus(subId: Int): Int = when {
!Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE
SubscriptionManager.isValidSubscriptionId(subId)
&& SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE
else -> CONDITIONALLY_UNAVAILABLE
}
override fun getAvailabilityStatus(subId: Int): Int =
when {
!Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE
SubscriptionManager.isValidSubscriptionId(subId) &&
SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE
else -> CONDITIONALLY_UNAVAILABLE
}
override fun displayPreference(screen: PreferenceScreen) {
super.displayPreference(screen)
@@ -67,51 +55,10 @@ class MobileNetworkPhoneNumberPreferenceController(context: Context, key: String
}
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
if (!this::lazyViewModel.isInitialized) {
Log.e(
this.javaClass.simpleName,
"lateinit property lazyViewModel has not been initialized"
)
return
}
val viewModel by lazyViewModel
val coroutineScope = viewLifecycleOwner.lifecycleScope
viewModel.subscriptionInfoListFlow
.map { subscriptionInfoList ->
subscriptionInfoList
.firstOrNull { subInfo -> subInfo.subscriptionId == mSubId }
subscriptionRepository.phoneNumberFlow(mSubId).collectLatestWithLifecycle(
viewLifecycleOwner) { phoneNumber ->
preference.summary = phoneNumber ?: getStringUnknown()
}
.flowOn(Dispatchers.Default)
.collectLatestWithLifecycle(viewLifecycleOwner) {
it?.let {
coroutineScope.launch {
refreshData(it)
}
}
}
}
@VisibleForTesting
suspend fun refreshData(subscriptionInfo: SubscriptionInfo){
withContext(Dispatchers.Default) {
phoneNumber = getFormattedPhoneNumber(subscriptionInfo)
}
refreshUi()
}
private fun refreshUi(){
preference.summary = phoneNumber
}
private fun getFormattedPhoneNumber(subscriptionInfo: SubscriptionInfo?): String {
val phoneNumber = SubscriptionUtil.getBidiFormattedPhoneNumber(
mContext,
subscriptionInfo
)
return phoneNumber
?.let { return it.ifEmpty { getStringUnknown() } }
?: getStringUnknown()
}
private fun getStringUnknown(): String {

View File

@@ -257,7 +257,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme
use(NrDisabledInDsdsFooterPreferenceController.class).init(mSubId);
use(MobileNetworkSpnPreferenceController.class).init(this, mSubId);
use(MobileNetworkPhoneNumberPreferenceController.class).init(this, mSubId);
use(MobileNetworkPhoneNumberPreferenceController.class).init(mSubId);
use(MobileNetworkImeiPreferenceController.class).init(this, mSubId);
final MobileDataPreferenceController mobileDataPreferenceController =

View File

@@ -150,7 +150,7 @@ public class SatelliteSetting extends RestrictedDashboardFragment {
/* In case satellite is allowed by carrier's entitlement server, the page will show
the check icon with guidance that satellite is included in user's mobile plan */
preference.setTitle(R.string.title_have_satellite_plan);
icon = getResources().getDrawable(R.drawable.ic_check_circle_24px);
icon = getContext().getDrawable(R.drawable.ic_check_circle_24px);
} else {
/* Or, it will show the blocked icon with the guidance that satellite is not included
in user's mobile plan */

View File

@@ -24,13 +24,14 @@ import androidx.lifecycle.LifecycleOwner
import com.android.settings.network.SubscriptionUtil
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -52,7 +53,7 @@ class SubscriptionRepository(private val context: Context) {
/** Flow of whether the subscription enabled for the given [subId]. */
fun isSubscriptionEnabledFlow(subId: Int): Flow<Boolean> {
if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false)
return context.subscriptionsChangedFlow()
return subscriptionsChangedFlow()
.map { subscriptionManager.isSubscriptionEnabled(subId) }
.conflate()
.onEach { Log.d(TAG, "[$subId] isSubscriptionEnabledFlow: $it") }
@@ -87,12 +88,30 @@ class SubscriptionRepository(private val context: Context) {
}.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default)
/** Flow of active subscription ids. */
fun activeSubscriptionIdListFlow(): Flow<List<Int>> = context.subscriptionsChangedFlow()
.map { subscriptionManager.activeSubscriptionIdList.sorted() }
.distinctUntilChanged()
.conflate()
.onEach { Log.d(TAG, "activeSubscriptionIdList: $it") }
.flowOn(Dispatchers.Default)
fun activeSubscriptionIdListFlow(): Flow<List<Int>> =
subscriptionsChangedFlow()
.map { subscriptionManager.activeSubscriptionIdList.sorted() }
.distinctUntilChanged()
.conflate()
.onEach { Log.d(TAG, "activeSubscriptionIdList: $it") }
.flowOn(Dispatchers.Default)
fun activeSubscriptionInfoFlow(subId: Int): Flow<SubscriptionInfo?> =
subscriptionsChangedFlow()
.map { subscriptionManager.getActiveSubscriptionInfo(subId) }
.distinctUntilChanged()
.conflate()
.flowOn(Dispatchers.Default)
@OptIn(ExperimentalCoroutinesApi::class)
fun phoneNumberFlow(subId: Int): Flow<String?> =
activeSubscriptionInfoFlow(subId).flatMapLatest { subInfo ->
if (subInfo != null) {
context.phoneNumberFlow(subInfo)
} else {
flowOf(null)
}
}
}
val Context.subscriptionManager: SubscriptionManager?
@@ -100,9 +119,12 @@ val Context.subscriptionManager: SubscriptionManager?
fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!!
fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsChangedFlow().map {
SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo)
}.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default)
fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo): Flow<String?> =
subscriptionsChangedFlow()
.map { SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) }
.distinctUntilChanged()
.conflate()
.flowOn(Dispatchers.Default)
fun Context.subscriptionsChangedFlow(): Flow<Unit> =
SubscriptionRepository(this).subscriptionsChangedFlow()

View File

@@ -25,8 +25,8 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.notification.modes.ZenMode;
@@ -92,29 +92,14 @@ abstract class AbstractZenModePreferenceController extends AbstractPreferenceCon
return true;
}
// Called by parent Fragment onAttach, for any methods (such as isAvailable()) that need
// zen mode info before onStart. Most callers should use updateZenMode instead, which will
// do any further necessary propagation.
protected final void setZenMode(@NonNull ZenMode zenMode) {
/**
* Assigns the {@link ZenMode} of this controller, so that it can be used later from
* {@link #isAvailable()} and {@link #updateState(Preference)}.
*/
final void setZenMode(@NonNull ZenMode zenMode) {
mZenMode = zenMode;
}
// Called by the parent Fragment onStart, which means it will happen before resume.
public void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) {
mZenMode = zenMode;
updateState(preference);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
if (mZenMode != null) {
displayPreference(screen, mZenMode);
}
}
public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {}
@Override
public final void updateState(Preference preference) {
super.updateState(preference);
@@ -167,4 +152,20 @@ abstract class AbstractZenModePreferenceController extends AbstractPreferenceCon
return mode;
});
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
@Nullable
ZenMode getZenMode() {
return mZenMode;
}
/**
* Convenience method for tests. Assigns the {@link ZenMode} of this controller, and calls
* {@link #updateState(Preference)} immediately.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
final void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) {
mZenMode = zenMode;
updateState(preference);
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.notification.modes;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
@@ -49,12 +50,12 @@ public class ManualDurationPreferenceController extends AbstractZenModePreferenc
return zenMode.isManualDnd();
}
// Called by parent fragment onAttach().
// Called by parent fragment onStart().
void registerSettingsObserver() {
mSettingsObserver.register();
}
// Called by parent fragment onDetach().
// Called by parent fragment onStop().
void unregisterSettingsObserver() {
mSettingsObserver.unregister();
}
@@ -69,7 +70,7 @@ public class ManualDurationPreferenceController extends AbstractZenModePreferenc
}
@Override
public void updateState(Preference preference, ZenMode unusedZenMode) {
public void updateState(Preference preference, @NonNull ZenMode unusedZenMode) {
// This controller is a link between a Settings value (ZEN_DURATION) and the manual DND
// mode. The status of the zen mode object itself doesn't affect the preference
// value, as that comes from settings; that value from settings will determine the

View File

@@ -21,14 +21,11 @@ import static com.google.common.base.Preconditions.checkState;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
@@ -39,7 +36,6 @@ import com.android.settingslib.notification.modes.ZenModesBackend;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.List;
/**
@@ -79,7 +75,11 @@ public abstract class ZenModeEditNameIconFragmentBase extends DashboardFragment
? icicle.getParcelable(MODE_KEY, ZenMode.class)
: onCreateInstantiateZenMode();
if (mZenMode == null) {
if (mZenMode != null) {
for (var controller : getZenPreferenceControllers()) {
controller.setZenMode(mZenMode);
}
} else {
finish();
}
}
@@ -110,58 +110,32 @@ public abstract class ZenModeEditNameIconFragmentBase extends DashboardFragment
);
}
private Iterable<AbstractZenModePreferenceController> getZenPreferenceControllers() {
return getPreferenceControllers().stream()
.flatMap(List::stream)
.filter(AbstractZenModePreferenceController.class::isInstance)
.map(AbstractZenModePreferenceController.class::cast)
.toList();
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
@Nullable
ZenMode getZenMode() {
return mZenMode;
}
@Override
public void onStart() {
super.onStart();
updateControllers();
}
@VisibleForTesting
final void setModeName(String name) {
checkNotNull(mZenMode).getRule().setName(Strings.nullToEmpty(name));
updateControllers(); // Updates confirmation button.
forceUpdatePreferences(); // Updates confirmation button.
}
@VisibleForTesting
final void setModeIcon(@DrawableRes int iconResId) {
checkNotNull(mZenMode).getRule().setIconResId(iconResId);
updateControllers(); // Updates icon at the top.
forceUpdatePreferences(); // Updates icon at the top.
}
protected void updateControllers() {
PreferenceScreen screen = getPreferenceScreen();
Collection<List<AbstractPreferenceController>> controllers = getPreferenceControllers();
if (mZenMode == null || screen == null || controllers == null) {
return;
}
for (List<AbstractPreferenceController> list : controllers) {
for (AbstractPreferenceController controller : list) {
try {
final String key = controller.getPreferenceKey();
final Preference preference = screen.findPreference(key);
if (preference != null) {
AbstractZenModePreferenceController zenController =
(AbstractZenModePreferenceController) controller;
zenController.updateZenMode(preference, mZenMode);
} else {
Log.d(getLogTag(),
String.format("Cannot find preference with key %s in Controller %s",
key, controller.getClass().getSimpleName()));
}
controller.displayPreference(screen);
} catch (ClassCastException e) {
// Skip any controllers that aren't AbstractZenModePreferenceController.
Log.d(getLogTag(), "Could not cast: " + controller.getClass().getSimpleName());
}
}
}
}
@VisibleForTesting
final void saveMode() {

View File

@@ -79,14 +79,6 @@ public class ZenModeFragment extends ZenModeFragmentBase {
return prefControllers;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
// allow duration preference controller to listen for settings changes
use(ManualDurationPreferenceController.class).registerSettingsObserver();
}
@Override
public void onStart() {
super.onStart();
@@ -99,6 +91,9 @@ public class ZenModeFragment extends ZenModeFragmentBase {
mModeMenuProvider = new ModeMenuProvider(mode);
activity.addMenuProvider(mModeMenuProvider);
}
// allow duration preference controller to listen for settings changes
use(ManualDurationPreferenceController.class).registerSettingsObserver();
}
@Override
@@ -106,13 +101,8 @@ public class ZenModeFragment extends ZenModeFragmentBase {
if (getActivity() != null) {
getActivity().removeMenuProvider(mModeMenuProvider);
}
super.onStop();
}
@Override
public void onDetach() {
use(ManualDurationPreferenceController.class).unregisterSettingsObserver();
super.onDetach();
super.onStop();
}
@Override
@@ -122,13 +112,13 @@ public class ZenModeFragment extends ZenModeFragmentBase {
}
@Override
protected void updateZenModeState() {
protected void onUpdatedZenModeState() {
// Because this fragment may be asked to finish by the delete menu but not be done doing
// so yet, ignore any attempts to update info in that case.
if (getActivity() != null && getActivity().isFinishing()) {
return;
}
super.updateZenModeState();
super.onUpdatedZenModeState();
}
private class ModeMenuProvider implements MenuProvider {

View File

@@ -18,24 +18,18 @@ package com.android.settings.notification.modes;
import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.notification.modes.ZenMode;
import com.google.common.base.Preconditions;
import java.util.List;
import java.util.function.Consumer;
/**
* Base class for Settings pages used to configure individual modes.
@@ -43,13 +37,27 @@ import java.util.function.Consumer;
abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
static final String TAG = "ZenModeSettings";
@Nullable // only until reloadMode() is called
private ZenMode mZenMode;
@Nullable private ZenMode mZenMode;
@Nullable private ZenMode mModeOnLastControllerUpdate;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
public void onCreate(Bundle icicle) {
mZenMode = loadModeFromArguments();
if (mZenMode != null) {
// Propagate mode info through to controllers. Must be done before super.onCreate(),
// because that one calls AbstractPreferenceController.isAvailable().
for (var controller : getZenPreferenceControllers()) {
controller.setZenMode(mZenMode);
}
} else {
toastAndFinish();
}
super.onCreate(icicle);
}
@Nullable
private ZenMode loadModeFromArguments() {
String id = null;
if (getActivity() != null && getActivity().getIntent() != null) {
id = getActivity().getIntent().getStringExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID);
@@ -60,93 +68,65 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
}
if (id == null) {
Log.d(TAG, "No id provided");
toastAndFinish();
return;
return null;
}
if (!reloadMode(id)) {
Log.d(TAG, "Mode id " + id + " not found");
toastAndFinish();
return;
}
if (mZenMode != null) {
// Propagate mode info through to controllers.
for (List<AbstractPreferenceController> list : getPreferenceControllers()) {
try {
for (AbstractPreferenceController controller : list) {
// mZenMode guaranteed non-null from reloadMode() above
((AbstractZenModePreferenceController) controller).setZenMode(mZenMode);
}
} catch (ClassCastException e) {
// ignore controllers that aren't AbstractZenModePreferenceController
}
}
ZenMode mode = mBackend.getMode(id);
if (mode == null) {
Log.d(TAG, "Mode with id " + id + " not found");
return null;
}
return mode;
}
/**
* Refresh stored ZenMode data.
* @param id the mode ID
* @return whether we successfully got mode data from the backend.
*/
private boolean reloadMode(String id) {
mZenMode = mBackend.getMode(id);
if (mZenMode == null) {
return false;
}
return true;
private Iterable<AbstractZenModePreferenceController> getZenPreferenceControllers() {
return getPreferenceControllers().stream()
.flatMap(List::stream)
.filter(AbstractZenModePreferenceController.class::isInstance)
.map(AbstractZenModePreferenceController.class::cast)
.toList();
}
/**
* Refresh ZenMode data any time the system's zen mode state changes (either the zen mode value
* itself, or the config), and also (once updated) update the info for all controllers.
*/
@Override
protected void updateZenModeState() {
protected void onUpdatedZenModeState() {
if (mZenMode == null) {
// This shouldn't happen, but guard against it in case
Log.wtf(TAG, "mZenMode is null in onUpdatedZenModeState");
toastAndFinish();
return;
}
String id = mZenMode.getId();
if (!reloadMode(id)) {
ZenMode mode = mBackend.getMode(id);
if (mode == null) {
Log.d(TAG, "Mode id=" + id + " not found");
toastAndFinish();
return;
}
updateControllers();
mZenMode = mode;
maybeUpdateControllersState(mode);
}
private void updateControllers() {
if (getPreferenceControllers() == null || mZenMode == null) {
return;
/**
* Updates all {@link AbstractZenModePreferenceController} based on the loaded mode info.
* For each controller, {@link AbstractZenModePreferenceController#setZenMode} will be called.
* Then, {@link AbstractZenModePreferenceController#updateState} will be called as well, unless
* we determine it's not necessary (for example, if we know that {@code DashboardFragment} will
* do it soon).
*/
private void maybeUpdateControllersState(@NonNull ZenMode zenMode) {
boolean needsFullUpdate =
getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)
&& (mModeOnLastControllerUpdate == null
|| !mModeOnLastControllerUpdate.equals(zenMode));
mModeOnLastControllerUpdate = zenMode.copy();
for (var controller : getZenPreferenceControllers()) {
controller.setZenMode(zenMode);
}
final PreferenceScreen screen = getPreferenceScreen();
if (screen == null) {
Log.d(TAG, "PreferenceScreen not found");
return;
}
for (List<AbstractPreferenceController> list : getPreferenceControllers()) {
for (AbstractPreferenceController controller : list) {
try {
// Find preference associated with controller
final String key = controller.getPreferenceKey();
final Preference preference = screen.findPreference(key);
if (preference != null) {
AbstractZenModePreferenceController zenController =
(AbstractZenModePreferenceController) controller;
zenController.updateZenMode(preference, mZenMode);
} else {
Log.d(TAG,
String.format("Cannot find preference with key %s in Controller %s",
key, controller.getClass().getSimpleName()));
}
controller.displayPreference(screen);
} catch (ClassCastException e) {
// Skip any controllers that aren't AbstractZenModePreferenceController.
Log.d(TAG, "Could not cast: " + controller.getClass().getSimpleName());
}
}
if (needsFullUpdate) {
forceUpdatePreferences();
}
}
@@ -163,16 +143,4 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
public ZenMode getMode() {
return mZenMode;
}
protected final boolean saveMode(Consumer<ZenMode> updater) {
Preconditions.checkState(mBackend != null);
ZenMode mode = mZenMode;
if (mode == null) {
Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")");
return false;
}
updater.accept(mode);
mBackend.updateMode(mode);
return true;
}
}

View File

@@ -67,12 +67,18 @@ class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceCo
LayoutPreference layoutPref = (LayoutPreference) preference;
TextView start = layoutPref.findViewById(R.id.start_time);
start.setText(timeString(mSchedule.startHour, mSchedule.startMinute));
String startTimeString = timeString(mSchedule.startHour, mSchedule.startMinute);
start.setText(startTimeString);
start.setContentDescription(
mContext.getString(R.string.zen_mode_start_time) + "\n" + startTimeString);
start.setOnClickListener(
timePickerLauncher(mSchedule.startHour, mSchedule.startMinute, mStartSetter));
TextView end = layoutPref.findViewById(R.id.end_time);
end.setText(timeString(mSchedule.endHour, mSchedule.endMinute));
String endTimeString = timeString(mSchedule.endHour, mSchedule.endMinute);
end.setText(endTimeString);
end.setContentDescription(
mContext.getString(R.string.zen_mode_end_time) + "\n" + endTimeString);
end.setOnClickListener(
timePickerLauncher(mSchedule.endHour, mSchedule.endMinute, mEndSetter));
@@ -198,7 +204,10 @@ class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceCo
// day label.
dayToggle.setTextOn(mShortDayFormat.format(c.getTime()));
dayToggle.setTextOff(mShortDayFormat.format(c.getTime()));
dayToggle.setContentDescription(mLongDayFormat.format(c.getTime()));
String state = dayEnabled
? mContext.getString(com.android.internal.R.string.capital_on)
: mContext.getString(com.android.internal.R.string.capital_off);
dayToggle.setStateDescription(mLongDayFormat.format(c.getTime()) + ", " + state);
dayToggle.setChecked(dayEnabled);
dayToggle.setOnCheckedChangeListener((buttonView, isChecked) -> {

View File

@@ -37,7 +37,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settingslib.PrimarySwitchPreference;
@@ -77,13 +76,6 @@ class ZenModeTriggerUpdatePreferenceController extends AbstractZenModePreference
return !zenMode.isCustomManual() && !zenMode.isManualDnd();
}
@Override
public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {
// Preload approved components, but only for the package that owns the rule (since it's the
// only package that can have a valid configurationActivity).
mServiceListing.loadApprovedComponents(zenMode.getRule().getPackageName());
}
@Override
void updateState(Preference preference, @NonNull ZenMode zenMode) {
if (!isAvailable(zenMode)) {
@@ -137,6 +129,7 @@ class ZenModeTriggerUpdatePreferenceController extends AbstractZenModePreference
@SuppressLint("SwitchIntDef")
private void setUpForAppTrigger(Preference preference, ZenMode mode) {
// App-owned mode may have triggerDescription, configurationActivity, or both/neither.
mServiceListing.loadApprovedComponents(mode.getRule().getPackageName());
Intent configurationIntent =
mConfigurationActivityHelper.getConfigurationActivityIntentForMode(
mode, mServiceListing::findService);

View File

@@ -16,14 +16,11 @@
package com.android.settings.notification.modes;
import static com.google.common.base.Preconditions.checkNotNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.UserManager;
import android.provider.Settings.Global;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -38,17 +35,10 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment {
protected static final String TAG = "ZenModesSettings";
protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Handler mHandler = new Handler();
private final SettingsObserver mSettingsObserver = new SettingsObserver();
protected Context mContext;
protected ZenModesBackend mBackend;
protected ZenHelperBackend mHelperBackend;
// Individual pages must implement this method based on what they should do when
// the device's zen mode state changes.
protected abstract void updateZenModeState();
private ZenSettingsObserver mSettingsObserver;
ZenModesFragmentBase() {
super(UserManager.DISALLOW_ADJUST_VOLUME);
@@ -69,8 +59,8 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment {
mContext = context;
mBackend = ZenModesBackend.getInstance(context);
mHelperBackend = ZenHelperBackend.getInstance(context);
mSettingsObserver = new ZenSettingsObserver(context, this::onUpdatedZenModeState);
super.onAttach(context);
mSettingsObserver.register();
}
@Override
@@ -83,45 +73,20 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment {
finish();
}
}
onUpdatedZenModeState(); // Maybe, while we weren't observing.
checkNotNull(mSettingsObserver).register();
}
/**
* Called by this fragment when we know or suspect that Zen Modes data or state has changed.
* Individual pages must implement this method to refresh whatever they're displaying.
*/
protected abstract void onUpdatedZenModeState();
@Override
public void onResume() {
super.onResume();
updateZenModeState();
}
@Override
public void onDetach() {
super.onDetach();
mSettingsObserver.unregister();
}
private final class SettingsObserver extends ContentObserver {
private static final Uri ZEN_MODE_URI = Global.getUriFor(Global.ZEN_MODE);
private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Global.getUriFor(
Global.ZEN_MODE_CONFIG_ETAG);
private SettingsObserver() {
super(mHandler);
}
public void register() {
getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);
getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false, this);
}
public void unregister() {
getContentResolver().unregisterContentObserver(this);
}
@Override
public void onChange(boolean selfChange, @Nullable Uri uri) {
super.onChange(selfChange, uri);
// Shouldn't have any other URIs trigger this method, but check just in case.
if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) {
updateZenModeState();
}
}
public void onStop() {
checkNotNull(mSettingsObserver).unregister();
super.onStop();
}
}

View File

@@ -65,7 +65,7 @@ public class ZenModesListFragment extends ZenModesFragmentBase {
}
@Override
protected void updateZenModeState() {
protected void onUpdatedZenModeState() {
// TODO: b/322373473 -- update any overall description of modes state here if necessary.
// Note the preferences linking to individual rules do not need to be updated, as
// updateState() is called on all preference controllers whenever the page is resumed.

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.notification.modes;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.provider.Settings;
import androidx.annotation.Nullable;
class ZenSettingsObserver extends ContentObserver {
private static final Uri ZEN_MODE_URI = Settings.Global.getUriFor(Settings.Global.ZEN_MODE);
private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Settings.Global.getUriFor(
Settings.Global.ZEN_MODE_CONFIG_ETAG);
private final Context mContext;
@Nullable private Runnable mCallback;
ZenSettingsObserver(Context context) {
this(context, null);
}
ZenSettingsObserver(Context context, @Nullable Runnable callback) {
super(context.getMainExecutor(), 0);
mContext = context;
setOnChangeListener(callback);
}
void register() {
mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);
mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false,
this);
}
void unregister() {
mContext.getContentResolver().unregisterContentObserver(this);
}
void setOnChangeListener(@Nullable Runnable callback) {
mCallback = callback;
}
@Override
public void onChange(boolean selfChange, @Nullable Uri uri) {
super.onChange(selfChange, uri);
// Shouldn't have any other URIs trigger this method, but check just in case.
if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) {
if (mCallback != null) {
mCallback.run();
}
}
}
}

View File

@@ -50,14 +50,20 @@ class WifiStatusRepository(
var wifiStatusTracker: WifiStatusTracker? = null
wifiStatusTracker = wifiStatusTrackerFactory { wifiStatusTracker?.let(::trySend) }
// Fetches initial state first, before set listening to true, otherwise could cause
// race condition.
wifiStatusTracker.fetchInitialState()
trySend(wifiStatusTracker)
context
.broadcastReceiverFlow(INTENT_FILTER)
.onEach { intent -> wifiStatusTracker.handleBroadcast(intent) }
.onEach { intent ->
wifiStatusTracker.handleBroadcast(intent)
trySend(wifiStatusTracker)
}
.launchIn(this)
wifiStatusTracker.setListening(true)
wifiStatusTracker.fetchInitialState()
trySend(wifiStatusTracker)
awaitClose { wifiStatusTracker.setListening(false) }
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference android:key="pref_id" />
<Preference android:key="pref_name" />
<Preference android:key="pref_enabled" />
</PreferenceScreen>

View File

@@ -75,7 +75,8 @@ public class BatteryInfoTest {
private static final String STATUS_CHARGING_TIME = "50% - 0 min left until full";
private static final String STATUS_NOT_CHARGING = "Not charging";
private static final String STATUS_CHARGING_FUTURE_BYPASS = "50% - Charging";
private static final String STATUS_CHARGING_PAUSED = "50% - Charging optimized";
private static final String STATUS_CHARGING_PAUSED =
"50% - Charging on hold to protect battery";
private static final long REMAINING_TIME_NULL = -1;
private static final long REMAINING_TIME = 2;
// Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml

View File

@@ -0,0 +1,364 @@
/*
* 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.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
import static com.android.settings.notification.modes.CharSequenceTruth.assertThat;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.app.Flags;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.testing.FragmentScenario;
import androidx.lifecycle.Lifecycle.State;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.notification.modes.TestModeBuilder;
import com.android.settingslib.notification.modes.ZenMode;
import com.android.settingslib.notification.modes.ZenModesBackend;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowContentResolver;
import org.robolectric.shadows.ShadowLooper;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public class ZenModeFragmentBaseTest {
private static final Uri SETTINGS_URI = Settings.Global.getUriFor(
Settings.Global.ZEN_MODE_CONFIG_ETAG);
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Mock ZenModesBackend mBackend;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void fragment_noArguments_finishes() {
when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE);
FragmentScenario<TestableFragment> scenario = createScenario(null);
scenario.moveToState(State.RESUMED).onFragment(fragment -> {
assertThat(fragment.requireActivity().isFinishing()).isTrue();
});
scenario.close();
}
@Test
public void fragment_modeDoesNotExist_finishes() {
when(mBackend.getMode(any())).thenReturn(null);
FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
scenario.moveToState(State.RESUMED).onFragment(fragment -> {
assertThat(fragment.requireActivity().isFinishing()).isTrue();
});
scenario.close();
}
@Test
public void fragment_validMode_updatesControllersOnce() {
ZenMode mode = new TestModeBuilder().setId("mode_id").build();
when(mBackend.getMode("mode_id")).thenReturn(mode);
FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
scenario.moveToState(State.CREATED).onFragment(fragment -> {
assertThat(fragment.mShowsId.getZenMode()).isEqualTo(mode);
assertThat(fragment.mShowsId.isAvailable()).isTrue();
assertThat(fragment.mAvailableIfEnabled.getZenMode()).isEqualTo(mode);
assertThat(fragment.mAvailableIfEnabled.isAvailable()).isTrue();
verify(fragment.mShowsId, never()).updateState(any(), any());
verify(fragment.mAvailableIfEnabled, never()).updateState(any(), any());
});
scenario.moveToState(State.RESUMED).onFragment(fragment -> {
Preference preferenceOne = fragment.requirePreference("pref_id");
assertThat(preferenceOne.getSummary()).isEqualTo("Id is mode_id");
verify(fragment.mShowsId).updateState(any(), eq(mode));
verify(fragment.mAvailableIfEnabled).updateState(any(), eq(mode));
});
scenario.close();
}
@Test
public void fragment_onStartToOnStop_hasRegisteredContentObserver() {
when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE);
FragmentScenario<TestableFragment> scenario = createScenario("id");
scenario.moveToState(State.CREATED).onFragment(fragment ->
assertThat(getSettingsContentObservers(fragment)).isEmpty());
scenario.moveToState(State.STARTED).onFragment(fragment ->
assertThat(getSettingsContentObservers(fragment)).hasSize(1));
scenario.moveToState(State.RESUMED).onFragment(fragment ->
assertThat(getSettingsContentObservers(fragment)).hasSize(1));
scenario.moveToState(State.STARTED).onFragment(fragment ->
assertThat(getSettingsContentObservers(fragment)).hasSize(1));
scenario.moveToState(State.CREATED).onFragment(fragment ->
assertThat(getSettingsContentObservers(fragment)).isEmpty());
scenario.close();
}
@Test
public void fragment_onModeUpdatedWithDifferences_updatesControllers() {
ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
when(mBackend.getMode("id")).thenReturn(originalMode);
FragmentScenario<TestableFragment> scenario = createScenario("id");
scenario.moveToState(State.RESUMED).onFragment(fragment -> {
Preference preference = fragment.requirePreference("pref_name");
assertThat(preference.getSummary()).isEqualTo("Original");
verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
// Now, we get a message saying something changed.
ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build();
when(mBackend.getMode("id")).thenReturn(updatedMode);
getSettingsContentObservers(fragment).stream().findFirst().get()
.dispatchChange(false, SETTINGS_URI);
ShadowLooper.idleMainLooper();
// The screen was updated, and only updated once.
assertThat(preference.getSummary()).isEqualTo("Updated");
verify(fragment.mShowsName, times(1)).updateState(any(), eq(updatedMode));
});
scenario.close();
}
@Test
public void fragment_onModeUpdatedWithoutDifferences_setsModeInControllersButNothingElse() {
ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
when(mBackend.getMode("id")).thenReturn(originalMode);
FragmentScenario<TestableFragment> scenario = createScenario("id");
scenario.moveToState(State.RESUMED).onFragment(fragment -> {
Preference preference = fragment.requirePreference("pref_name");
assertThat(preference.getSummary()).isEqualTo("Original");
verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
// Now, we get a message saying something changed, but it was for a different mode.
ZenMode notUpdatedMode = new TestModeBuilder(originalMode).build();
when(mBackend.getMode("id")).thenReturn(notUpdatedMode);
getSettingsContentObservers(fragment).stream().findFirst().get()
.dispatchChange(false, SETTINGS_URI);
ShadowLooper.idleMainLooper();
// The mode instance was updated, but updateState() was not called.
assertThat(preference.getSummary()).isEqualTo("Original");
assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(notUpdatedMode);
verify(fragment.mShowsName, never()).updateState(any(), same(notUpdatedMode));
});
scenario.close();
}
@Test
public void fragment_onFragmentRestart_reloadsMode() {
ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
when(mBackend.getMode("id")).thenReturn(originalMode);
FragmentScenario<TestableFragment> scenario = createScenario("id");
scenario.moveToState(State.RESUMED).onFragment(fragment -> {
Preference preference = fragment.requirePreference("pref_name");
assertThat(preference.getSummary()).isEqualTo("Original");
verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
});
ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build();
when(mBackend.getMode("id")).thenReturn(updatedMode);
scenario.moveToState(State.CREATED).moveToState(State.RESUMED).onFragment(fragment -> {
Preference preference = fragment.requirePreference("pref_name");
assertThat(preference.getSummary()).isEqualTo("Updated");
assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(updatedMode);
});
scenario.close();
}
@Test
public void fragment_onModeDeleted_finishes() {
ZenMode originalMode = new TestModeBuilder().setId("mode_id").build();
when(mBackend.getMode("mode_id")).thenReturn(originalMode);
FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
scenario.moveToState(State.RESUMED).onFragment(fragment -> {
assertThat(fragment.requireActivity().isFinishing()).isFalse();
// Now it's no longer there...
when(mBackend.getMode(any())).thenReturn(null);
getSettingsContentObservers(fragment).stream().findFirst().get()
.dispatchChange(false, SETTINGS_URI);
ShadowLooper.idleMainLooper();
assertThat(fragment.requireActivity().isFinishing()).isTrue();
});
scenario.close();
}
private FragmentScenario<TestableFragment> createScenario(@Nullable String modeId) {
Bundle fragmentArgs = null;
if (modeId != null) {
fragmentArgs = new Bundle();
fragmentArgs.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId);
}
FragmentScenario<TestableFragment> scenario = FragmentScenario.launch(
TestableFragment.class, fragmentArgs, 0, State.INITIALIZED);
scenario.onFragment(fragment -> {
fragment.setBackend(mBackend); // Before onCreate().
});
return scenario;
}
public static class TestableFragment extends ZenModeFragmentBase {
private ShowsIdPreferenceController mShowsId;
private ShowsNamePreferenceController mShowsName;
private AvailableIfEnabledPreferenceController mAvailableIfEnabled;
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
mShowsId = spy(new ShowsIdPreferenceController(context, "pref_id"));
mShowsName = spy(new ShowsNamePreferenceController(context, "pref_name"));
mAvailableIfEnabled = spy(
new AvailableIfEnabledPreferenceController(context, "pref_enabled"));
return ImmutableList.of(mShowsId, mShowsName, mAvailableIfEnabled);
}
@NonNull
Preference requirePreference(String key) {
Preference preference = getPreferenceScreen().findPreference(key);
checkNotNull(preference, "Didn't find preference with key " + key);
return preference;
}
ShadowContentResolver getShadowContentResolver() {
return shadowOf(requireActivity().getContentResolver());
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.modes_fake_settings;
}
@Override
public int getMetricsCategory() {
return 0;
}
}
private static class ShowsIdPreferenceController extends AbstractZenModePreferenceController {
ShowsIdPreferenceController(@NonNull Context context, @NonNull String key) {
super(context, key);
}
@Override
void updateState(Preference preference, @NonNull ZenMode zenMode) {
preference.setSummary("Id is " + zenMode.getId());
}
}
private static class ShowsNamePreferenceController extends AbstractZenModePreferenceController {
ShowsNamePreferenceController(@NonNull Context context, @NonNull String key) {
super(context, key);
}
@Override
void updateState(Preference preference, @NonNull ZenMode zenMode) {
preference.setSummary(zenMode.getName());
}
}
private static class AvailableIfEnabledPreferenceController extends
AbstractZenModePreferenceController {
AvailableIfEnabledPreferenceController(@NonNull Context context, @NonNull String key) {
super(context, key);
}
@Override
public boolean isAvailable(@NonNull ZenMode zenMode) {
return zenMode.isEnabled();
}
@Override
void updateState(Preference preference, @NonNull ZenMode zenMode) {
preference.setSummary("Enabled is " + zenMode.isEnabled());
}
}
private ImmutableList<ContentObserver> getSettingsContentObservers(Fragment fragment) {
return ImmutableList.copyOf(
shadowOf(fragment.requireActivity().getContentResolver())
.getContentObservers(SETTINGS_URI));
}
}

View File

@@ -17,8 +17,7 @@
package com.android.settings.network.telephony
import android.content.Context
import android.telephony.SubscriptionInfo
import androidx.fragment.app.Fragment
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
@@ -26,17 +25,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.settings.R
import com.android.settings.core.BasePreferenceController
import com.android.settings.network.SubscriptionInfoListViewModel
import com.android.settings.network.SubscriptionUtil
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoSession
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
@@ -44,29 +45,25 @@ import org.mockito.quality.Strictness
class MobileNetworkPhoneNumberPreferenceControllerTest {
private lateinit var mockSession: MockitoSession
private val mockViewModels = mock<Lazy<SubscriptionInfoListViewModel>>()
private val mockFragment = mock<Fragment>{
val viewmodel = mockViewModels
}
private var mockPhoneNumber = String()
private val context: Context = ApplicationProvider.getApplicationContext()
private val controller = MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY)
private val mockSubscriptionRepository = mock<SubscriptionRepository>()
private val controller =
MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY, mockSubscriptionRepository)
private val preference = Preference(context).apply { key = TEST_KEY }
private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
@Before
fun setUp() {
mockSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.mockStatic(SubscriptionUtil::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
mockSession =
ExtendedMockito.mockitoSession()
.mockStatic(SubscriptionUtil::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
preferenceScreen.addPreference(preference)
controller.init(SUB_ID)
controller.displayPreference(preferenceScreen)
whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber)
}
@After
@@ -75,41 +72,29 @@ class MobileNetworkPhoneNumberPreferenceControllerTest {
}
@Test
fun refreshData_getEmptyPhoneNumber_preferenceIsNotVisible() = runBlocking {
fun onViewCreated_cannotGetPhoneNumber_displayUnknown() = runBlocking {
whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn(
listOf(
SUB_INFO_1,
SUB_INFO_2
)
)
var mockSubId = 2
controller.init(mockFragment, mockSubId)
mockPhoneNumber = String()
mockSubscriptionRepository.stub {
on { phoneNumberFlow(SUB_ID) } doReturn flowOf(null)
}
controller.refreshData(SUB_INFO_2)
controller.onViewCreated(TestLifecycleOwner())
delay(100)
assertThat(preference.summary).isEqualTo(
context.getString(R.string.device_info_default))
assertThat(preference.summary).isEqualTo(context.getString(R.string.device_info_default))
}
@Test
fun refreshData_getPhoneNumber_preferenceSummaryIsExpected() = runBlocking {
fun onViewCreated_canGetPhoneNumber_displayPhoneNumber() = runBlocking {
whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn(
listOf(
SUB_INFO_1,
SUB_INFO_2
)
)
var mockSubId = 2
controller.init(mockFragment, mockSubId)
mockPhoneNumber = "test phone number"
whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber)
mockSubscriptionRepository.stub {
on { phoneNumberFlow(SUB_ID) } doReturn flowOf(PHONE_NUMBER)
}
controller.refreshData(SUB_INFO_2)
controller.onViewCreated(TestLifecycleOwner())
delay(100)
assertThat(preference.summary).isEqualTo(mockPhoneNumber)
assertThat(preference.summary).isEqualTo(PHONE_NUMBER)
}
@Test
@@ -123,18 +108,7 @@ class MobileNetworkPhoneNumberPreferenceControllerTest {
private companion object {
const val TEST_KEY = "test_key"
const val DISPLAY_NAME_1 = "Sub 1"
const val DISPLAY_NAME_2 = "Sub 2"
val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(1)
setDisplayName(DISPLAY_NAME_1)
}.build()
val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(2)
setDisplayName(DISPLAY_NAME_2)
}.build()
const val SUB_ID = 10
const val PHONE_NUMBER = "1234567890"
}
}

View File

@@ -204,6 +204,22 @@ class SubscriptionRepositoryTest {
assertThat(phoneNumber).isEqualTo(NUMBER_1)
}
@Test
fun phoneNumberFlow_withSubId() = runBlocking {
val subInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_IN_SLOT_1)
setMcc(MCC)
}.build()
mockSubscriptionManager.stub {
on { getActiveSubscriptionInfo(SUB_ID_IN_SLOT_1) } doReturn subInfo
on { getPhoneNumber(SUB_ID_IN_SLOT_1) } doReturn NUMBER_1
}
val phoneNumber = repository.phoneNumberFlow(SUB_ID_IN_SLOT_1).firstWithTimeoutOrNull()
assertThat(phoneNumber).isEqualTo(NUMBER_1)
}
private companion object {
const val SIM_SLOT_INDEX_0 = 0
const val SUB_ID_IN_SLOT_0 = 2