Merge "Fix restriction to configure Calls & SMS" into main

This commit is contained in:
Chaohui Wang
2023-11-16 03:32:37 +00:00
committed by Android (Google) Code Review
8 changed files with 399 additions and 659 deletions

View File

@@ -22,7 +22,6 @@ import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.SearchIndexableResource;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
@@ -31,19 +30,16 @@ import androidx.lifecycle.LifecycleOwner;
import com.android.settings.R;
import com.android.settings.SettingsDumpService;
import com.android.settings.Utils;
import com.android.settings.core.OnActivityResultListener;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.network.MobilePlanPreferenceController.MobilePlanPreferenceHost;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.wifi.WifiPrimarySwitchPreferenceController;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.search.SearchIndexable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@SearchIndexable
@@ -122,7 +118,6 @@ public class NetworkDashboardFragment extends DashboardFragment implements
controllers.add(internetPreferenceController);
}
controllers.add(privateDnsPreferenceController);
controllers.add(new NetworkProviderCallsSmsController(context, lifecycle, lifecycleOwner));
// Start SettingsDumpService after the MobileNetworkRepository is created.
Intent intent = new Intent(context, SettingsDumpService.class);

View File

@@ -1,258 +0,0 @@
/*
* Copyright (C) 2020 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;
import static androidx.lifecycle.Lifecycle.Event;
import android.content.Context;
import android.os.UserManager;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.Utils;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity;
import java.util.List;
public class NetworkProviderCallsSmsController extends AbstractPreferenceController implements
LifecycleObserver, MobileNetworkRepository.MobileNetworkCallback,
DefaultSubscriptionReceiver.DefaultSubscriptionListener {
private static final String TAG = "NetworkProviderCallsSmsController";
private static final String KEY = "calls_and_sms";
private static final String RTL_MARK = "\u200F";
private UserManager mUserManager;
private TelephonyManager mTelephonyManager;
private RestrictedPreference mPreference;
private boolean mIsRtlMode;
private LifecycleOwner mLifecycleOwner;
private MobileNetworkRepository mMobileNetworkRepository;
private List<SubscriptionInfoEntity> mSubInfoEntityList;
private int mDefaultVoiceSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private int mDefaultSmsSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private DefaultSubscriptionReceiver mDataSubscriptionChangedReceiver;
/**
* The summary text and click behavior of the "Calls & SMS" item on the
* Network & internet page.
*/
public NetworkProviderCallsSmsController(Context context, Lifecycle lifecycle,
LifecycleOwner lifecycleOwner) {
super(context);
mUserManager = context.getSystemService(UserManager.class);
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
mIsRtlMode = context.getResources().getConfiguration().getLayoutDirection()
== View.LAYOUT_DIRECTION_RTL;
mLifecycleOwner = lifecycleOwner;
mMobileNetworkRepository = MobileNetworkRepository.getInstance(context);
mDataSubscriptionChangedReceiver = new DefaultSubscriptionReceiver(context, this);
if (lifecycle != null) {
lifecycle.addObserver(this);
}
}
@OnLifecycleEvent(Event.ON_RESUME)
public void onResume() {
mMobileNetworkRepository.addRegister(mLifecycleOwner, this,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mMobileNetworkRepository.updateEntity();
mDataSubscriptionChangedReceiver.registerReceiver();
mDefaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
mDefaultSmsSubId = SubscriptionManager.getDefaultSmsSubscriptionId();
}
@OnLifecycleEvent(Event.ON_PAUSE)
public void onPause() {
mMobileNetworkRepository.removeRegister(this);
mDataSubscriptionChangedReceiver.unRegisterReceiver();
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
}
@Override
public CharSequence getSummary() {
List<SubscriptionInfoEntity> list = getSubscriptionInfoList();
if (list == null || list.isEmpty()) {
return setSummaryResId(R.string.calls_sms_no_sim);
} else {
final StringBuilder summary = new StringBuilder();
SubscriptionInfoEntity[] entityArray = list.toArray(
new SubscriptionInfoEntity[0]);
for (SubscriptionInfoEntity subInfo : entityArray) {
int subsSize = list.size();
int subId = Integer.parseInt(subInfo.subId);
final CharSequence displayName = subInfo.uniqueName;
// Set displayName as summary if there is only one valid SIM.
if (subsSize == 1
&& list.get(0).isValidSubscription
&& isInService(subId)) {
return displayName;
}
CharSequence status = getPreferredStatus(subInfo, subsSize, subId);
if (status.toString().isEmpty()) {
// If there are 2 or more SIMs and one of these has no preferred status,
// set only its displayName as summary.
summary.append(displayName);
} else {
summary.append(displayName)
.append(" (")
.append(status)
.append(")");
}
// Do not add ", " for the last subscription.
if (list.size() > 0 && !subInfo.equals(list.get(list.size() - 1))) {
summary.append(", ");
}
if (mIsRtlMode) {
summary.insert(0, RTL_MARK).insert(summary.length(), RTL_MARK);
}
}
return summary;
}
}
@VisibleForTesting
protected CharSequence getPreferredStatus(SubscriptionInfoEntity subInfo, int subsSize,
int subId) {
String status = "";
boolean isCallPreferred = subInfo.getSubId() == getDefaultVoiceSubscriptionId();
boolean isSmsPreferred = subInfo.getSubId() == getDefaultSmsSubscriptionId();
if (!subInfo.isValidSubscription || !isInService(subId)) {
status = setSummaryResId(subsSize > 1 ? R.string.calls_sms_unavailable :
R.string.calls_sms_temp_unavailable);
} else {
if (isCallPreferred && isSmsPreferred) {
status = setSummaryResId(R.string.calls_sms_preferred);
} else if (isCallPreferred) {
status = setSummaryResId(R.string.calls_sms_calls_preferred);
} else if (isSmsPreferred) {
status = setSummaryResId(R.string.calls_sms_sms_preferred);
}
}
return status;
}
private String setSummaryResId(int resId) {
return mContext.getResources().getString(resId);
}
@VisibleForTesting
protected List<SubscriptionInfoEntity> getSubscriptionInfoList() {
return mSubInfoEntityList;
}
private void update() {
if (mPreference == null || mPreference.isDisabledByAdmin()) {
return;
}
refreshSummary(mPreference);
mPreference.setOnPreferenceClickListener(null);
mPreference.setFragment(null);
if (mSubInfoEntityList == null || mSubInfoEntityList.isEmpty()) {
mPreference.setEnabled(false);
} else {
mPreference.setEnabled(true);
mPreference.setFragment(NetworkProviderCallsSmsFragment.class.getCanonicalName());
}
}
@Override
public boolean isAvailable() {
return SubscriptionUtil.isSimHardwareVisible(mContext) &&
mUserManager.isAdminUser();
}
@Override
public String getPreferenceKey() {
return KEY;
}
@Override
public void onAirplaneModeChanged(boolean airplaneModeEnabled) {
update();
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
if (preference == null) {
return;
}
refreshSummary(mPreference);
update();
}
@VisibleForTesting
protected boolean isInService(int subId) {
ServiceState serviceState =
mTelephonyManager.createForSubscriptionId(subId).getServiceState();
return Utils.isInService(serviceState);
}
@Override
public void onActiveSubInfoChanged(List<SubscriptionInfoEntity> activeSubInfoList) {
mSubInfoEntityList = activeSubInfoList;
update();
}
@VisibleForTesting
protected int getDefaultVoiceSubscriptionId() {
return mDefaultVoiceSubId;
}
@VisibleForTesting
protected int getDefaultSmsSubscriptionId() {
return mDefaultSmsSubId;
}
@Override
public void onDefaultVoiceChanged(int defaultVoiceSubId) {
mDefaultVoiceSubId = defaultVoiceSubId;
update();
}
@Override
public void onDefaultSmsChanged(int defaultSmsSubId) {
mDefaultSmsSubId = defaultSmsSubId;
update();
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright (C) 2023 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
import android.app.settings.SettingsEnums
import android.content.Context
import android.content.IntentFilter
import android.os.UserManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PermPhoneMsg
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settings.R
import com.android.settings.core.SubSettingLauncher
import com.android.settings.spa.preference.ComposePreferenceController
import com.android.settingslib.Utils
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.ui.SettingsIcon
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.framework.compose.placeholder
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.template.preference.RestrictedPreference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
/**
* The summary text and click behavior of the "Calls & SMS" item on the Network & internet page.
*/
open class NetworkProviderCallsSmsController @JvmOverloads constructor(
context: Context,
preferenceKey: String,
private val getDisplayName: (SubscriptionInfo) -> CharSequence = { subInfo ->
SubscriptionUtil.getUniqueSubscriptionDisplayName(subInfo, context)
},
private val isInService: (Int) -> Boolean = IsInServiceImpl(context)::isInService,
) : ComposePreferenceController(context, preferenceKey) {
override fun getAvailabilityStatus() = when {
!SubscriptionUtil.isSimHardwareVisible(mContext) -> UNSUPPORTED_ON_DEVICE
!mContext.userManager.isAdminUser -> DISABLED_FOR_USER
else -> AVAILABLE
}
@Composable
override fun Content() {
Column {
CallsAndSms()
HorizontalDivider()
}
}
@Composable
private fun CallsAndSms() {
val viewModel: SubscriptionInfoListViewModel = viewModel()
val subscriptionInfos by viewModel.subscriptionInfoListFlow.collectAsStateWithLifecycle()
val summary by remember { summaryFlow(viewModel.subscriptionInfoListFlow) }
.collectAsStateWithLifecycle(initialValue = placeholder())
RestrictedPreference(
model = object : PreferenceModel {
override val title = stringResource(R.string.calls_and_sms)
override val icon = @Composable { SettingsIcon(Icons.Outlined.PermPhoneMsg) }
override val summary = { summary }
override val enabled = { subscriptionInfos.isNotEmpty() }
override val onClick = {
SubSettingLauncher(mContext).apply {
setDestination(NetworkProviderCallsSmsFragment::class.qualifiedName)
setSourceMetricsCategory(SettingsEnums.SETTINGS_NETWORK_CATEGORY)
}.launch()
}
},
restrictions = Restrictions(keys = listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)),
)
}
private fun summaryFlow(subscriptionInfoListFlow: Flow<List<SubscriptionInfo>>) = combine(
subscriptionInfoListFlow,
mContext.defaultVoiceSubscriptionFlow(),
mContext.defaultSmsSubscriptionFlow(),
::getSummary,
).flowOn(Dispatchers.Default)
@VisibleForTesting
fun getSummary(
activeSubscriptionInfoList: List<SubscriptionInfo>,
defaultVoiceSubscriptionId: Int,
defaultSmsSubscriptionId: Int,
): String {
if (activeSubscriptionInfoList.isEmpty()) {
return mContext.getString(R.string.calls_sms_no_sim)
}
activeSubscriptionInfoList.singleOrNull()?.let {
// Set displayName as summary if there is only one valid SIM.
if (isInService(it.subscriptionId)) return it.displayName.toString()
}
return activeSubscriptionInfoList.joinToString { subInfo ->
val displayName = getDisplayName(subInfo)
val subId = subInfo.subscriptionId
val statusResId = getPreferredStatus(
subId = subId,
subsSize = activeSubscriptionInfoList.size,
isCallPreferred = subId == defaultVoiceSubscriptionId,
isSmsPreferred = subId == defaultSmsSubscriptionId,
)
if (statusResId == null) {
// If there are 2 or more SIMs and one of these has no preferred status,
// set only its displayName as summary.
displayName
} else {
"$displayName (${mContext.getString(statusResId)})"
}
}
}
private fun getPreferredStatus(
subId: Int,
subsSize: Int,
isCallPreferred: Boolean,
isSmsPreferred: Boolean,
): Int? = when {
!isInService(subId) -> {
if (subsSize > 1) {
R.string.calls_sms_unavailable
} else {
R.string.calls_sms_temp_unavailable
}
}
isCallPreferred && isSmsPreferred -> R.string.calls_sms_preferred
isCallPreferred -> R.string.calls_sms_calls_preferred
isSmsPreferred -> R.string.calls_sms_sms_preferred
else -> null
}
}
private fun Context.defaultVoiceSubscriptionFlow(): Flow<Int> =
merge(
flowOf(null), // kick an initial value
broadcastReceiverFlow(
IntentFilter(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)
),
).map { SubscriptionManager.getDefaultVoiceSubscriptionId() }
.conflate().flowOn(Dispatchers.Default)
private fun Context.defaultSmsSubscriptionFlow(): Flow<Int> =
merge(
flowOf(null), // kick an initial value
broadcastReceiverFlow(
IntentFilter(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED)
),
).map { SubscriptionManager.getDefaultSmsSubscriptionId() }
.conflate().flowOn(Dispatchers.Default)
private class IsInServiceImpl(context: Context) {
private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!!
fun isInService(subId: Int): Boolean {
if (!SubscriptionManager.isValidSubscriptionId(subId)) return false
val serviceState = telephonyManager.createForSubscriptionId(subId).serviceState
return Utils.isInService(serviceState)
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 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
import android.app.Application
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
class SubscriptionInfoListViewModel(application: Application) : AndroidViewModel(application) {
private val scope = viewModelScope + Dispatchers.Default
val subscriptionInfoListFlow = callbackFlow<List<SubscriptionInfo>> {
val subscriptionManager = application.getSystemService(SubscriptionManager::class.java)!!
val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() {
override fun onSubscriptionsChanged() {
trySend(subscriptionManager.activeSubscriptionInfoList ?: emptyList())
}
}
subscriptionManager.addOnSubscriptionsChangedListener(
Dispatchers.Default.asExecutor(),
listener,
)
awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) }
}.conflate().stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList())
}