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

Commit 3e8e8ff8 authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Create VoNrRepository

Migrate to flow and not call setVoNrEnabled in main thread.

Fix: 339846642
Fix: 339542743
Test: manual - on Mobile Settings
Test: unit test
Change-Id: I27fe7cb2ddadc9a0aaa8c589af4703210b33612d
parent c479a308
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -295,11 +295,9 @@
            settings:controller=
                "com.android.settings.network.telephony.NullAlgorithmsPreferenceController"/>

        <SwitchPreferenceCompat
        <com.android.settings.spa.preference.ComposePreference
            android:key="nr_advanced_calling"
            android:title="@string/nr_advanced_calling_title"
            android:persistent="false"
            android:summary="@string/nr_advanced_calling_summary"
            settings:keywords="@string/keywords_nr_advanced_calling"
            settings:controller="com.android.settings.network.telephony.NrAdvancedCallingPreferenceController"/>
    </PreferenceCategory>
+0 −236
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.network.telephony;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.util.Log;

import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;

import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.util.ArrayUtils;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.utils.ThreadUtils;

/**
 * Preference controller for "Voice over NR".
 */
public class NrAdvancedCallingPreferenceController extends TelephonyTogglePreferenceController
        implements LifecycleObserver, OnStart, OnStop {

    private static final String TAG = "VoNrSettings";

    @VisibleForTesting
    Preference mPreference;
    private TelephonyManager mTelephonyManager;
    private PhoneCallStateTelephonyCallback mTelephonyCallback;
    private boolean mIsVonrEnabledFromCarrierConfig = false;
    private boolean mIsVonrVisibleFromCarrierConfig = false;
    private boolean mIsNrEnableFromCarrierConfig = false;
    private boolean mHas5gCapability = false;
    private boolean mIsVoNrEnabled = false;
    private Integer mCallState;

    private Handler mHandler = new Handler(Looper.getMainLooper());

    public NrAdvancedCallingPreferenceController(Context context, String key) {
        super(context, key);
        mTelephonyManager = context.getSystemService(TelephonyManager.class);
    }

    /**
     * Initial this PreferenceController.
     * @param subId The subscription Id.
     * @return This PreferenceController.
     */
    public NrAdvancedCallingPreferenceController init(int subId) {
        Log.d(TAG, "init: ");
        if (mTelephonyCallback == null) {
            mTelephonyCallback = new PhoneCallStateTelephonyCallback();
        }

        mSubId = subId;

        if (mTelephonyManager == null) {
            mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
        }
        if (SubscriptionManager.isValidSubscriptionId(subId)) {
            mTelephonyManager = mTelephonyManager.createForSubscriptionId(subId);
        }
        long supportedRadioBitmask = mTelephonyManager.getSupportedRadioAccessFamily();
        mHas5gCapability =
                (supportedRadioBitmask & TelephonyManager.NETWORK_TYPE_BITMASK_NR) > 0;

        PersistableBundle carrierConfig = getCarrierConfigForSubId(subId);
        if (carrierConfig == null) {
            return this;
        }
        mIsVonrEnabledFromCarrierConfig = carrierConfig.getBoolean(
            CarrierConfigManager.KEY_VONR_ENABLED_BOOL);

        mIsVonrVisibleFromCarrierConfig = carrierConfig.getBoolean(
                CarrierConfigManager.KEY_VONR_SETTING_VISIBILITY_BOOL);

        int[] nrAvailabilities = carrierConfig.getIntArray(
                CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY);
        mIsNrEnableFromCarrierConfig = !ArrayUtils.isEmpty(nrAvailabilities);

        updateVoNrState();

        Log.d(TAG, "mHas5gCapability: " + mHas5gCapability
                + ",mIsNrEnabledFromCarrierConfig: " + mIsNrEnableFromCarrierConfig
                + ",mIsVonrEnabledFromCarrierConfig: " + mIsVonrEnabledFromCarrierConfig
                + ",mIsVonrVisibleFromCarrierConfig: " + mIsVonrVisibleFromCarrierConfig);
        return this;
    }

    @Override
    public int getAvailabilityStatus(int subId) {
        init(subId);

        if (mHas5gCapability
                && mIsNrEnableFromCarrierConfig
                && mIsVonrEnabledFromCarrierConfig
                && mIsVonrVisibleFromCarrierConfig) {
            return AVAILABLE;
        }
        return CONDITIONALLY_UNAVAILABLE;
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);
        mPreference = screen.findPreference(getPreferenceKey());
    }

    @Override
    public void onStart() {
        if (mTelephonyCallback == null) {
            return;
        }
        mTelephonyCallback.register(mTelephonyManager);
    }

    @Override
    public void onStop() {
        if (mTelephonyCallback == null) {
            return;
        }
        mTelephonyCallback.unregister();
    }

    @Override
    public void updateState(Preference preference) {
        super.updateState(preference);
        if (preference == null) {
            return;
        }
        final TwoStatePreference switchPreference = (TwoStatePreference) preference;
        switchPreference.setEnabled(isUserControlAllowed());
    }

    @Override
    public boolean setChecked(boolean isChecked) {
        if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
            return false;
        }
        Log.d(TAG, "setChecked: " + isChecked);
        int result = mTelephonyManager.setVoNrEnabled(isChecked);
        if (result == TelephonyManager.ENABLE_VONR_SUCCESS) {
            return true;
        }
        Log.d(TAG, "Fail to set VoNR result= " + result + ". subId=" + mSubId);
        return false;
    }

    @Override
    public boolean isChecked() {
        return mIsVoNrEnabled;
    }

    @VisibleForTesting
    protected boolean isCallStateIdle() {
        return (mCallState != null) && (mCallState == TelephonyManager.CALL_STATE_IDLE);
    }

    private boolean isUserControlAllowed() {
        return isCallStateIdle();
    }

    private void updateVoNrState() {
        ThreadUtils.postOnBackgroundThread(() -> {
            boolean result = mTelephonyManager.isVoNrEnabled();
            if (result != mIsVoNrEnabled) {
                Log.i(TAG, "VoNr state : " + result);
                mIsVoNrEnabled = result;
                mHandler.post(() -> {
                    updateState(mPreference);
                });
            }
        });
    }

    private class PhoneCallStateTelephonyCallback extends TelephonyCallback implements
            TelephonyCallback.CallStateListener {

        private TelephonyManager mLocalTelephonyManager;

        @Override
        public void onCallStateChanged(int state) {
            mCallState = state;
            updateState(mPreference);
        }

        public void register(TelephonyManager telephonyManager) {
            mLocalTelephonyManager = telephonyManager;

            // assign current call state so that it helps to show correct preference state even
            // before first onCallStateChanged() by initial registration.
            if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
                try {
                    mCallState = mLocalTelephonyManager.getCallState();
                } catch (UnsupportedOperationException e) {
                    // Device doesn't support FEATURE_TELEPHONY_CALLING
                    mCallState = TelephonyManager.CALL_STATE_IDLE;
                }
            } else {
                mCallState = mLocalTelephonyManager.getCallState();
            }
            mLocalTelephonyManager.registerTelephonyCallback(
                    mContext.getMainExecutor(), mTelephonyCallback);
        }

        public void unregister() {
            mCallState = null;
            if (mLocalTelephonyManager != null) {
                mLocalTelephonyManager.unregisterTelephonyCallback(this);
            }
        }
    }
}
+76 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.network.telephony

import android.content.Context
import android.telephony.SubscriptionManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.spa.preference.ComposePreferenceController
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch

/**
 * Preference controller for "Voice over NR".
 */
class NrAdvancedCallingPreferenceController @JvmOverloads constructor(
    context: Context,
    key: String,
    private val callStateRepository : CallStateRepository = CallStateRepository(context),
) : ComposePreferenceController(context, key) {
    private var subId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
    private var repository: VoNrRepository? = null

    /** Initial this PreferenceController. */
    @JvmOverloads
    fun init(subId: Int, repository: VoNrRepository = VoNrRepository(mContext, subId)) {
        this.subId = subId
        this.repository = repository
    }

    override fun getAvailabilityStatus() =
        if (repository?.isVoNrAvailable() == true) AVAILABLE else CONDITIONALLY_UNAVAILABLE

    @Composable
    override fun Content() {
        val summary = stringResource(R.string.nr_advanced_calling_summary)
        val isInCall by remember { callStateRepository.isInCallFlow() }
            .collectAsStateWithLifecycle(initialValue = false)
        val isEnabled by remember {
            repository?.isVoNrEnabledFlow() ?: flowOf(false)
        }.collectAsStateWithLifecycle(initialValue = false)
        val coroutineScope = rememberCoroutineScope()
        SwitchPreference(object : SwitchPreferenceModel {
            override val title = stringResource(R.string.nr_advanced_calling_title)
            override val summary = { summary }
            override val changeable = { !isInCall }
            override val checked = { isEnabled }
            override val onCheckedChange: (Boolean) -> Unit = { newChecked ->
                coroutineScope.launch {
                    repository?.setVoNrEnabled(newChecked)
                }
            }
        })
    }
}
+72 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.network.telephony

import android.content.Context
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext

class VoNrRepository(private val context: Context, private val subId: Int) {
    private val telephonyManager = context.telephonyManager(subId)
    private val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java)!!

    fun isVoNrAvailable(): Boolean {
        if (!SubscriptionManager.isValidSubscriptionId(subId) || !has5gCapability()) return false
        val carrierConfig = carrierConfigManager.safeGetConfig(
            keys = listOf(
                CarrierConfigManager.KEY_VONR_ENABLED_BOOL,
                CarrierConfigManager.KEY_VONR_SETTING_VISIBILITY_BOOL,
                CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY,
            ),
            subId = subId,
        )
        return carrierConfig.getBoolean(CarrierConfigManager.KEY_VONR_ENABLED_BOOL) &&
            carrierConfig.getBoolean(CarrierConfigManager.KEY_VONR_SETTING_VISIBILITY_BOOL) &&
            (carrierConfig.getIntArray(CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY)
                ?.isNotEmpty() ?: false)
    }

    private fun has5gCapability() =
        ((telephonyManager.supportedRadioAccessFamily and
            TelephonyManager.NETWORK_TYPE_BITMASK_NR) > 0)
            .also { Log.d(TAG, "[$subId] has5gCapability: $it") }

    fun isVoNrEnabledFlow(): Flow<Boolean> = context.subscriptionsChangedFlow()
        .map { telephonyManager.isVoNrEnabled }
        .conflate()
        .onEach { Log.d(TAG, "[$subId] isVoNrEnabled: $it") }
        .flowOn(Dispatchers.Default)

    suspend fun setVoNrEnabled(enabled: Boolean) = withContext(Dispatchers.Default) {
        if (!SubscriptionManager.isValidSubscriptionId(subId)) return@withContext
        val result = telephonyManager.setVoNrEnabled(enabled)
        Log.d(TAG, "[$subId] setVoNrEnabled: $enabled, result: $result")
    }

    private companion object {
        private const val TAG = "VoNrRepository"
    }
}
+135 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.network.telephony

import android.content.Context
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify

@RunWith(AndroidJUnit4::class)
class NrAdvancedCallingPreferenceControllerTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}

    private val callStateRepository = mock<CallStateRepository> {
        on { isInCallFlow() } doReturn flowOf(false)
    }

    private val voNrRepository = mock<VoNrRepository>()

    private val controller = NrAdvancedCallingPreferenceController(
        context = context,
        key = TEST_KEY,
        callStateRepository = callStateRepository,
    ).apply { init(SUB_ID, voNrRepository) }

    @Test
    fun isChecked_voNrEnabled_on() {
        voNrRepository.stub {
            on { isVoNrEnabledFlow() } doReturn flowOf(true)
        }

        composeTestRule.setContent {
            controller.Content()
        }

        composeTestRule.onNodeWithText(context.getString(R.string.nr_advanced_calling_title))
            .assertIsOn()
    }

    @Test
    fun isChecked_voNrDisabled_off() {
        voNrRepository.stub {
            on { isVoNrEnabledFlow() } doReturn flowOf(false)
        }

        composeTestRule.setContent {
            controller.Content()
        }

        composeTestRule.onNodeWithText(context.getString(R.string.nr_advanced_calling_title))
            .assertIsOff()
    }

    @Test
    fun isEnabled_notInCall_enabled() {
        callStateRepository.stub {
            on { isInCallFlow() } doReturn flowOf(false)
        }

        composeTestRule.setContent {
            controller.Content()
        }

        composeTestRule.onNodeWithText(context.getString(R.string.nr_advanced_calling_title))
            .assertIsEnabled()
    }

    @Test
    fun isEnabled_inCall_notEnabled() {
        callStateRepository.stub {
            on { isInCallFlow() } doReturn flowOf(true)
        }

        composeTestRule.setContent {
            controller.Content()
        }

        composeTestRule.onNodeWithText(context.getString(R.string.nr_advanced_calling_title))
            .assertIsNotEnabled()
    }

    @Test
    fun onClick_setVoNrEnabled(): Unit = runBlocking {
        voNrRepository.stub {
            on { isVoNrEnabledFlow() } doReturn flowOf(false)
        }

        composeTestRule.setContent {
            controller.Content()
        }
        composeTestRule.onRoot().performClick()

        verify(voNrRepository).setVoNrEnabled(true)
    }

    private companion object {
        const val TEST_KEY = "test_key"
        const val SUB_ID = 2
    }
}
Loading