Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1a2bf18e authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

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

parents 97cf66ef 9e0bd186
Loading
Loading
Loading
Loading

res/drawable/ic_calls_sms.xml

deleted100644 → 0
+0 −29
Original line number Diff line number Diff line
<!--
    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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24"
    android:tint="?android:attr/colorControlNormal"
    >

    <path
        android:pathData="M 0 0 H 24 V 24 H 0 V 0 Z" />
    <path
        android:fillColor="#FF000000"
        android:pathData="M20.17,14.85l-3.26-0.65c-0.33-0.07-0.67,0.04-0.9,0.27l-2.62,2.62c-2.75-1.49-5.01-3.75-6.5-6.5l2.62-2.62 c0.24-0.24,0.34-0.58,0.27-0.9L9.13,3.82c-0.09-0.47-0.5-0.8-0.98-0.8H4c-0.56,0-1.03,0.47-1,1.03c0.17,2.91,1.04,5.63,2.43,8.01 c1.57,2.69,3.81,4.93,6.5,6.5c2.38,1.39,5.1,2.26,8.01,2.43c0.56,0.03,1.03-0.44,1.03-1v-4.15C20.97,15.36,20.64,14.95,20.17,14.85 L20.17,14.85z M12,3v10l3-3h6V3H12z M19,8h-5V5h5V8z" />
</vector>
+2 −7
Original line number Diff line number Diff line
@@ -31,16 +31,11 @@
        settings:keywords="@string/keywords_internet"
        settings:useAdminDisabledSummary="true" />

    <com.android.settingslib.RestrictedPreference
    <com.android.settings.spa.preference.ComposePreference
        android:key="calls_and_sms"
        android:title="@string/calls_and_sms"
        android:icon="@drawable/ic_calls_sms"
        android:order="-20"
        android:summary="@string/summary_placeholder"
        settings:isPreferenceVisible="@bool/config_show_sim_info"
        settings:allowDividerBelow="true"
        settings:keywords="@string/calls_and_sms"
        settings:useAdminDisabledSummary="true" />
        settings:controller="com.android.settings.network.NetworkProviderCallsSmsController" />

    <com.android.settingslib.RestrictedPreference
        android:key="mobile_network_list"
+0 −5
Original line number Diff line number Diff line
@@ -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);
+0 −258
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+196 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
Loading