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

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

Merge changes If41d957f,I5c49d195 into 24D1-dev

* changes:
  Fix empty network scan result
  Create NetworkScanRepository
parents 365cc18c 99b09df4
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -82,7 +82,7 @@ object CellInfoUtil {
     */
    @JvmStatic
    fun cellInfoListToString(cellInfos: List<CellInfo>): String =
        cellInfos.joinToString { cellInfo -> cellInfo.readableString() }
        cellInfos.joinToString(System.lineSeparator()) { cellInfo -> cellInfo.readableString() }

    /**
     * Convert [CellInfo] to a readable string without sensitive info.
+0 −346
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.annotation.IntDef;
import android.content.Context;
import android.telephony.AccessNetworkConstants.AccessNetworkType;
import android.telephony.CellInfo;
import android.telephony.NetworkScan;
import android.telephony.NetworkScanRequest;
import android.telephony.PhoneCapability;
import android.telephony.RadioAccessSpecifier;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyScanManager;
import android.util.Log;

import androidx.annotation.VisibleForTesting;

import com.android.internal.telephony.CellNetworkScanResult;

import com.android.settings.R;

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

/**
 * A helper class that builds the common interface and performs the network scan for two different
 * network scan APIs.
 */
public class NetworkScanHelper {
    public static final String TAG = "NetworkScanHelper";

    /**
     * Callbacks interface to inform the network scan results.
     */
    public interface NetworkScanCallback {
        /**
         * Called when the results is returned from {@link TelephonyManager}. This method will be
         * called at least one time if there is no error occurred during the network scan.
         *
         * <p> This method can be called multiple times in one network scan, until
         * {@link #onComplete()} or {@link #onError(int)} is called.
         *
         * @param results
         */
        void onResults(List<CellInfo> results);

        /**
         * Called when the current network scan process is finished. No more
         * {@link #onResults(List)} will be called for the current network scan after this method is
         * called.
         */
        void onComplete();

        /**
         * Called when an error occurred during the network scan process.
         *
         * <p> There is no more result returned from {@link TelephonyManager} if an error occurred.
         *
         * <p> {@link #onComplete()} will not be called if an error occurred.
         *
         * @see {@link NetworkScan.ScanErrorCode}
         */
        void onError(int errorCode);
    }

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS, NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS})
    public @interface NetworkQueryType {}

    /**
     * Performs the network scan using {@link TelephonyManager#getAvailableNetworks()}. The network
     * scan results won't be returned to the caller until the network scan is completed.
     *
     * <p> This is typically used when the modem doesn't support the new network scan api
     * {@link TelephonyManager#requestNetworkScan(
     * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)}.
     */
    public static final int NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS = 1;

    /**
     * Performs the network scan using {@link TelephonyManager#requestNetworkScan(
     * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)} The network scan
     * results will be returned to the caller periodically in a small time window until the network
     * scan is completed. The complete results should be returned in the last called of
     * {@link NetworkScanCallback#onResults(List)}.
     *
     * <p> This is recommended to be used if modem supports the new network scan api
     * {@link TelephonyManager#requestNetworkScan(
     * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)}
     */
    public static final int NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS = 2;

    /** The constants below are used in the async network scan. */
    @VisibleForTesting
    static final boolean INCREMENTAL_RESULTS = true;
    @VisibleForTesting
    static final int SEARCH_PERIODICITY_SEC = 5;
    @VisibleForTesting
    static final int MAX_SEARCH_TIME_SEC = 300;
    @VisibleForTesting
    static final int INCREMENTAL_RESULTS_PERIODICITY_SEC = 3;

    private final NetworkScanCallback mNetworkScanCallback;
    private final TelephonyManager mTelephonyManager;
    private final TelephonyScanManager.NetworkScanCallback mInternalNetworkScanCallback;
    private final Executor mExecutor;

    private int mMaxSearchTimeSec = MAX_SEARCH_TIME_SEC;
    private NetworkScan mNetworkScanRequester;

    /** Callbacks for sync network scan */
    private ListenableFuture<List<CellInfo>> mNetworkScanFuture;

    public NetworkScanHelper(TelephonyManager tm, NetworkScanCallback callback, Executor executor) {
        mTelephonyManager = tm;
        mNetworkScanCallback = callback;
        mInternalNetworkScanCallback = new NetworkScanCallbackImpl();
        mExecutor = executor;
    }

    public NetworkScanHelper(Context context, TelephonyManager tm, NetworkScanCallback callback,
            Executor executor) {
        this(tm, callback, executor);
        mMaxSearchTimeSec = context.getResources().getInteger(
                R.integer.config_network_scan_helper_max_search_time_sec);
    }

    @VisibleForTesting
    NetworkScanRequest createNetworkScanForPreferredAccessNetworks() {
        long networkTypeBitmap3gpp = mTelephonyManager.getPreferredNetworkTypeBitmask()
                & TelephonyManager.NETWORK_STANDARDS_FAMILY_BITMASK_3GPP;

        List<RadioAccessSpecifier> radioAccessSpecifiers = new ArrayList<>();
        // If the allowed network types are unknown or if they are of the right class, scan for
        // them; otherwise, skip them to save scan time and prevent users from being shown networks
        // that they can't connect to.
        if (networkTypeBitmap3gpp == 0
                || (networkTypeBitmap3gpp & TelephonyManager.NETWORK_CLASS_BITMASK_2G) != 0) {
            radioAccessSpecifiers.add(
                    new RadioAccessSpecifier(AccessNetworkType.GERAN, null, null));
        }
        if (networkTypeBitmap3gpp == 0
                || (networkTypeBitmap3gpp & TelephonyManager.NETWORK_CLASS_BITMASK_3G) != 0) {
            radioAccessSpecifiers.add(
                    new RadioAccessSpecifier(AccessNetworkType.UTRAN, null, null));
        }
        if (networkTypeBitmap3gpp == 0
                || (networkTypeBitmap3gpp & TelephonyManager.NETWORK_CLASS_BITMASK_4G) != 0) {
            radioAccessSpecifiers.add(
                    new RadioAccessSpecifier(AccessNetworkType.EUTRAN, null, null));
        }
        // If a device supports 5G stand-alone then the code below should be re-enabled; however
        // a device supporting only non-standalone mode cannot perform PLMN selection and camp on
        // a 5G network, which means that it shouldn't scan for 5G at the expense of battery as
        // part of the manual network selection process.
        //
        if (networkTypeBitmap3gpp == 0
                || (hasNrSaCapability()
                && (networkTypeBitmap3gpp & TelephonyManager.NETWORK_CLASS_BITMASK_5G) != 0)) {
            radioAccessSpecifiers.add(
                    new RadioAccessSpecifier(AccessNetworkType.NGRAN, null, null));
            Log.d(TAG, "radioAccessSpecifiers add NGRAN.");
        }

        return new NetworkScanRequest(
                NetworkScanRequest.SCAN_TYPE_ONE_SHOT,
                radioAccessSpecifiers.toArray(
                        new RadioAccessSpecifier[radioAccessSpecifiers.size()]),
                SEARCH_PERIODICITY_SEC,
                mMaxSearchTimeSec,
                INCREMENTAL_RESULTS,
                INCREMENTAL_RESULTS_PERIODICITY_SEC,
                null /* List of PLMN ids (MCC-MNC) */);
    }

    /**
     * Performs a network scan for the given type {@code type}.
     * {@link #NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS} is recommended if modem supports
     * {@link TelephonyManager#requestNetworkScan(
     * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)}.
     *
     * @param type used to tell which network scan API should be used.
     */
    public void startNetworkScan(@NetworkQueryType int type) {
        if (type == NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS) {
            mNetworkScanFuture = SettableFuture.create();
            Futures.addCallback(mNetworkScanFuture, new FutureCallback<List<CellInfo>>() {
                @Override
                public void onSuccess(List<CellInfo> result) {
                    onResults(result);
                    onComplete();
                }

                @Override
                public void onFailure(Throwable t) {
                    if (t instanceof CancellationException) {
                        return;
                    }
                    int errCode = Integer.parseInt(t.getMessage());
                    onError(errCode);
                }
            }, MoreExecutors.directExecutor());
            mExecutor.execute(new NetworkScanSyncTask(
                    mTelephonyManager, (SettableFuture) mNetworkScanFuture));
        } else if (type == NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS) {
            if (mNetworkScanRequester != null) {
                return;
            }
            mNetworkScanRequester = mTelephonyManager.requestNetworkScan(
                    createNetworkScanForPreferredAccessNetworks(),
                    mExecutor,
                    mInternalNetworkScanCallback);
            if (mNetworkScanRequester == null) {
                onError(NetworkScan.ERROR_RADIO_INTERFACE_ERROR);
            }
        }
    }

    /**
     * The network scan of type {@link #NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS} can't be stopped,
     * however, the result of the current network scan won't be returned to the callback after
     * calling this method.
     */
    public void stopNetworkQuery() {
        if (mNetworkScanRequester != null) {
            mNetworkScanRequester.stopScan();
            mNetworkScanRequester = null;
        }

        if (mNetworkScanFuture != null) {
            mNetworkScanFuture.cancel(true /* mayInterruptIfRunning */);
            mNetworkScanFuture = null;
        }
    }

    private void onResults(List<CellInfo> cellInfos) {
        mNetworkScanCallback.onResults(cellInfos);
    }

    private void onComplete() {
        mNetworkScanCallback.onComplete();
    }

    private void onError(int errCode) {
        mNetworkScanCallback.onError(errCode);
    }

    private boolean hasNrSaCapability() {
        return Arrays.stream(
                mTelephonyManager.getPhoneCapability().getDeviceNrCapabilities())
                .anyMatch(i -> i == PhoneCapability.DEVICE_NR_CAPABILITY_SA);
    }

    /**
     * Converts the status code of {@link CellNetworkScanResult} to one of the
     * {@link NetworkScan.ScanErrorCode}.
     * @param errCode status code from {@link CellNetworkScanResult}.
     *
     * @return one of the scan error code from {@link NetworkScan.ScanErrorCode}.
     */
    private static int convertToScanErrorCode(int errCode) {
        switch (errCode) {
            case CellNetworkScanResult.STATUS_RADIO_NOT_AVAILABLE:
                return NetworkScan.ERROR_RADIO_INTERFACE_ERROR;
            case CellNetworkScanResult.STATUS_RADIO_GENERIC_FAILURE:
            default:
                return NetworkScan.ERROR_MODEM_ERROR;
        }
    }

    private final class NetworkScanCallbackImpl extends TelephonyScanManager.NetworkScanCallback {
        public void onResults(List<CellInfo> results) {
            Log.d(TAG, "Async scan onResults() results = "
                    + CellInfoUtil.cellInfoListToString(results));
            NetworkScanHelper.this.onResults(results);
        }

        public void onComplete() {
            Log.d(TAG, "async scan onComplete()");
            NetworkScanHelper.this.onComplete();
        }

        public void onError(@NetworkScan.ScanErrorCode int errCode) {
            Log.d(TAG, "async scan onError() errorCode = " + errCode);
            NetworkScanHelper.this.onError(errCode);
        }
    }

    private static final class NetworkScanSyncTask implements Runnable {
        private final SettableFuture<List<CellInfo>> mCallback;
        private final TelephonyManager mTelephonyManager;

        NetworkScanSyncTask(
                TelephonyManager telephonyManager, SettableFuture<List<CellInfo>> callback) {
            mTelephonyManager = telephonyManager;
            mCallback = callback;
        }

        @Override
        public void run() {
            final CellNetworkScanResult result = mTelephonyManager.getAvailableNetworks();
            if (result.getStatus() == CellNetworkScanResult.STATUS_SUCCESS) {
                final List<CellInfo> cellInfos = result.getOperators()
                        .stream()
                        .map(operatorInfo
                                -> CellInfoUtil.convertOperatorInfoToCellInfo(operatorInfo))
                        .collect(Collectors.toList());
                Log.d(TAG, "Sync network scan completed, cellInfos = "
                        + CellInfoUtil.cellInfoListToString(cellInfos));
                mCallback.set(cellInfos);
            } else {
                final Throwable error = new Throwable(
                        Integer.toString(convertToScanErrorCode(result.getStatus())));
                mCallback.setException(error);
                Log.d(TAG, "Sync network scan error, ex = " + error);
            }
        }
    }
}
+36 −237

File changed.

Preview size limit exceeded, changes collapsed.

+177 −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.scan

import android.content.Context
import android.telephony.AccessNetworkConstants.AccessNetworkType
import android.telephony.CellInfo
import android.telephony.NetworkScanRequest
import android.telephony.PhoneCapability
import android.telephony.RadioAccessSpecifier
import android.telephony.TelephonyManager
import android.telephony.TelephonyScanManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleOwner
import com.android.settings.network.telephony.CellInfoUtil.getNetworkTitle
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers
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.flowOn
import kotlinx.coroutines.flow.onEach

class NetworkScanRepository(context: Context, subId: Int) {
    enum class NetworkScanState {
        ACTIVE, COMPLETE, ERROR
    }

    data class NetworkScanResult(
        val state: NetworkScanState,
        val cellInfos: List<CellInfo>,
    )

    private val telephonyManager =
        context.getSystemService(TelephonyManager::class.java)!!.createForSubscriptionId(subId)

    /** TODO: Move this to UI layer, when UI layer migrated to Kotlin. */
    fun launchNetworkScan(lifecycleOwner: LifecycleOwner, onResult: (NetworkScanResult) -> Unit) {
        networkScanFlow().collectLatestWithLifecycle(lifecycleOwner, action = onResult)
    }

    data class CellInfoScanKey(
        val title: String?,
        val className: String,
        val isRegistered: Boolean,
    ) {
        constructor(cellInfo: CellInfo) : this(
            title = cellInfo.cellIdentity.getNetworkTitle(),
            className = cellInfo.javaClass.name,
            isRegistered = cellInfo.isRegistered,
        )
    }

    fun networkScanFlow(): Flow<NetworkScanResult> = callbackFlow {
        var state = NetworkScanState.ACTIVE
        var cellInfos: List<CellInfo> = emptyList()

        val callback = object : TelephonyScanManager.NetworkScanCallback() {
            override fun onResults(results: List<CellInfo>) {
                cellInfos = results.distinctBy { CellInfoScanKey(it) }
                sendResult()
            }

            override fun onComplete() {
                state = NetworkScanState.COMPLETE
                sendResult()
                // Don't call close() here since onComplete() could happens before onResults()
            }

            override fun onError(error: Int) {
                state = NetworkScanState.ERROR
                sendResult()
                close()
            }

            private fun sendResult() {
                trySend(NetworkScanResult(state, cellInfos))
            }
        }

        val networkScan = telephonyManager.requestNetworkScan(
            createNetworkScan(),
            Dispatchers.Default.asExecutor(),
            callback,
        )

        awaitClose { networkScan.stopScan() }
    }.conflate().onEach { Log.d(TAG, "networkScanFlow: $it") }.flowOn(Dispatchers.Default)

    /** Create network scan for allowed network types. */
    private fun createNetworkScan(): NetworkScanRequest {
        val allowedNetworkTypes = getAllowedNetworkTypes()
        Log.d(TAG, "createNetworkScan: allowedNetworkTypes = $allowedNetworkTypes")
        val radioAccessSpecifiers = allowedNetworkTypes
            .map { RadioAccessSpecifier(it, null, null) }
            .toTypedArray()
        return NetworkScanRequest(
            NetworkScanRequest.SCAN_TYPE_ONE_SHOT,
            radioAccessSpecifiers,
            NetworkScanRequest.MIN_SEARCH_PERIODICITY_SEC, // one shot, not used
            MAX_SEARCH_TIME_SEC,
            true,
            INCREMENTAL_RESULTS_PERIODICITY_SEC,
            null,
        )
    }

    private fun getAllowedNetworkTypes(): List<Int> {
        val networkTypeBitmap3gpp: Long =
            telephonyManager.getAllowedNetworkTypesBitmask() and
                TelephonyManager.NETWORK_STANDARDS_FAMILY_BITMASK_3GPP
        return buildList {
            // If the allowed network types are unknown or if they are of the right class, scan for
            // them; otherwise, skip them to save scan time and prevent users from being shown
            // networks that they can't connect to.
            if (networkTypeBitmap3gpp == 0L
                || networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_2G != 0L
            ) {
                add(AccessNetworkType.GERAN)
            }
            if (networkTypeBitmap3gpp == 0L
                || networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_3G != 0L
            ) {
                add(AccessNetworkType.UTRAN)
            }
            if (networkTypeBitmap3gpp == 0L
                || networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_4G != 0L
            ) {
                add(AccessNetworkType.EUTRAN)
            }
            // If a device supports 5G stand-alone then the code below should be re-enabled; however
            // a device supporting only non-standalone mode cannot perform PLMN selection and camp
            // on a 5G network, which means that it shouldn't scan for 5G at the expense of battery
            // as part of the manual network selection process.
            //
            if (networkTypeBitmap3gpp == 0L
                || (networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_5G != 0L &&
                    hasNrSaCapability())
            ) {
                add(AccessNetworkType.NGRAN)
                Log.d(TAG, "radioAccessSpecifiers add NGRAN.")
            }
        }
    }

    private fun hasNrSaCapability(): Boolean {
        val phoneCapability = telephonyManager.getPhoneCapability()
        return PhoneCapability.DEVICE_NR_CAPABILITY_SA in phoneCapability.deviceNrCapabilities
    }

    companion object {
        private const val TAG = "NetworkScanRepository"

        @VisibleForTesting
        val MAX_SEARCH_TIME_SEC = 300

        @VisibleForTesting
        val INCREMENTAL_RESULTS_PERIODICITY_SEC = 3
    }
}
+279 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading