chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/geo/PlatformNetworksManager.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.omnibox.geo;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.TransportInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Process;
import android.os.SystemClock;
import android.telephony.CellIdentityCdma;
import android.telephony.CellIdentityGsm;
import android.telephony.CellIdentityLte;
import android.telephony.CellIdentityWcdma;
import android.telephony.CellInfo;
import android.telephony.CellInfoCdma;
import android.telephony.CellInfoGsm;
import android.telephony.CellInfoLte;
import android.telephony.CellInfoWcdma;
import android.telephony.TelephonyManager;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.omnibox.geo.VisibleNetworks.VisibleCell;
import org.chromium.chrome.browser.omnibox.geo.VisibleNetworks.VisibleWifi;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/** Util methods for platform networking APIs. */
class PlatformNetworksManager {
    @VisibleForTesting static TimeProvider sTimeProvider = new TimeProvider();

    /**
     * Equivalent to WifiSsid.NONE which is hidden for some reason. This is returned by {@link
     * WifiManager} if it cannot get the ssid for the connected wifi access point.
     */
    static final String UNKNOWN_SSID = "<unknown ssid>";

    /**
     * Get the connected wifi, but do not use it (nullify it) if its BSSID is unknown.
     *
     * @param context The application context
     * @return The possibly null connected wifi
     */
    private static VisibleWifi getConnectedWifiIfKnown(Context context) {
        VisibleWifi connectedWifi = getConnectedWifi(context);
        if (connectedWifi != null && connectedWifi.bssid() == null) {
            return null;
        }
        return connectedWifi;
    }

    private static WifiInfo getWifiInfo(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // TODO(crbug.com/40750822): Look into taking a dependency on net/android and
            // extracting this logic there to a method that can be called from here.
            // On Android S+, need to use NetworkCapabilities to get the WifiInfo.
            ConnectivityManager connectivityManager =
                    (ConnectivityManager)
                            context.getApplicationContext()
                                    .getSystemService(Context.CONNECTIVITY_SERVICE);

            Network[] allNetworks = connectivityManager.getAllNetworks();
            for (Network network : allNetworks) {
                NetworkCapabilities networkCapabilities =
                        connectivityManager.getNetworkCapabilities(network);
                if (networkCapabilities != null
                        && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
                    TransportInfo transportInfo = networkCapabilities.getTransportInfo();
                    if (transportInfo != null && transportInfo instanceof WifiInfo) {
                        return (WifiInfo) transportInfo;
                    }
                }
            }
            return null;
        }

        WifiManager wifiManager = getWifiManager(context);
        return wifiManager.getConnectionInfo();
    }

    static VisibleWifi getConnectedWifi(Context context) {
        if (hasLocationAndWifiPermission(context)) {
            return connectedWifiInfoToVisibleWifi(getWifiInfo(context));
        }
        if (hasLocationPermission(context)) {
            // Only location permission, so fallback to pre-marshmallow.
            return getConnectedWifiPreMarshmallow(context);
        }
        return VisibleWifi.NO_WIFI_INFO;
    }

    static VisibleWifi getConnectedWifiPreMarshmallow(Context context) {
        Intent intent =
                ContextUtils.registerProtectedBroadcastReceiver(
                        context.getApplicationContext(),
                        null,
                        new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION));
        if (intent != null) {
            WifiInfo wifiInfo = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
            return connectedWifiInfoToVisibleWifi(wifiInfo);
        }
        return VisibleWifi.NO_WIFI_INFO;
    }

    private static VisibleWifi connectedWifiInfoToVisibleWifi(@Nullable WifiInfo wifiInfo) {
        if (wifiInfo == null) {
            return VisibleWifi.NO_WIFI_INFO;
        }
        String ssid = wifiInfo.getSSID();
        if (ssid == null || UNKNOWN_SSID.equals(ssid)) {
            // No SSID.
            ssid = null;
        } else {
            // Remove double quotation if ssid has double quotation.
            if (ssid.startsWith("\"") && ssid.endsWith("\"") && ssid.length() > 2) {
                ssid = ssid.substring(1, ssid.length() - 1);
            }
        }
        String bssid = wifiInfo.getBSSID();
        // It's connected, so use current time.
        return VisibleWifi.create(ssid, bssid, null, sTimeProvider.getCurrentTime());
    }

    static Set<VisibleWifi> getAllVisibleWifis(Context context, WifiManager wifiManager) {
        if (!hasLocationAndWifiPermission(context)) {
            return Collections.emptySet();
        }
        Set<VisibleWifi> visibleWifis = new HashSet<>();
        // Do not trigger a scan, but use current visible networks from latest scan.
        List<ScanResult> scanResults = wifiManager.getScanResults();
        if (scanResults == null) {
            return visibleWifis;
        }
        long elapsedTime = sTimeProvider.getElapsedRealtime();
        long currentTime = sTimeProvider.getCurrentTime();
        for (int i = 0; i < scanResults.size(); i++) {
            ScanResult scanResult = scanResults.get(i);
            String bssid = scanResult.BSSID;
            if (bssid == null) continue;
            long ageMs = elapsedTime - TimeUnit.MICROSECONDS.toMillis(scanResult.timestamp);
            long wifiTimestamp = currentTime - ageMs;
            visibleWifis.add(
                    VisibleWifi.create(scanResult.SSID, bssid, scanResult.level, wifiTimestamp));
        }
        return visibleWifis;
    }

    static void getAllVisibleCells(
            Context context,
            TelephonyManager telephonyManager,
            Callback<Set<VisibleCell>> callback) {
        if (!hasLocationPermission(context) || telephonyManager == null) {
            callback.onResult(Collections.emptySet());
            return;
        }

        requestCellInfoUpdate(
                telephonyManager,
                (cellInfos) -> {
                    PostTask.postTask(
                            TaskTraits.UI_DEFAULT,
                            () -> callback.onResult(getAllVisibleCellsFromCellInfo(cellInfos)));
                });
    }

    private static Set<VisibleCell> getAllVisibleCellsFromCellInfo(List<CellInfo> cellInfos) {
        Set<VisibleCell> visibleCells = new HashSet<>();
        if (cellInfos == null) {
            return visibleCells;
        }

        long elapsedTime = sTimeProvider.getElapsedRealtime();
        long currentTime = sTimeProvider.getCurrentTime();
        for (int i = 0; i < cellInfos.size(); i++) {
            CellInfo cellInfo = cellInfos.get(i);
            VisibleCell visibleCell = getVisibleCell(cellInfo, elapsedTime, currentTime);
            if (visibleCell.radioType() != VisibleCell.RadioType.UNKNOWN) {
                visibleCells.add(visibleCell);
            }
        }
        return visibleCells;
    }

    /**
     * Get the connected cell network, but do not use it (nullify it) if its radio type is unknown.
     *
     * @param context The application context
     * @param telephonyManager Provides access to cell information on the device
     * @return The possibly null connected cell
     */
    private static VisibleCell getConnectedCellIfKnown(
            Context context, TelephonyManager telephonyManager) {
        VisibleCell connectedCell = getConnectedCell(context, telephonyManager);
        if (connectedCell != null
                && (connectedCell.radioType() == VisibleCell.RadioType.UNKNOWN
                        || connectedCell.radioType()
                                == VisibleCell.RadioType.UNKNOWN_MISSING_LOCATION_PERMISSION)) {
            return null;
        }
        return connectedCell;
    }

    static VisibleCell getConnectedCell(Context context, TelephonyManager telephonyManager) {
        if (!hasLocationPermission(context)) {
            return VisibleCell.UNKNOWN_MISSING_LOCATION_PERMISSION_VISIBLE_CELL;
        }
        CellInfo cellInfo = getActiveCellInfo(telephonyManager);
        return getVisibleCell(
                cellInfo, sTimeProvider.getElapsedRealtime(), sTimeProvider.getCurrentTime());
    }

    private static VisibleCell getVisibleCell(
            @Nullable CellInfo cellInfo, long elapsedTime, long currentTime) {
        if (cellInfo == null) {
            return VisibleCell.UNKNOWN_VISIBLE_CELL;
        }
        long cellInfoAge = elapsedTime - TimeUnit.NANOSECONDS.toMillis(cellInfo.getTimeStamp());
        long cellTimestamp = currentTime - cellInfoAge;
        if (cellInfo instanceof CellInfoCdma) {
            CellIdentityCdma cellIdentityCdma = ((CellInfoCdma) cellInfo).getCellIdentity();
            return VisibleCell.builder(VisibleCell.RadioType.CDMA)
                    .setCellId(cellIdentityCdma.getBasestationId())
                    .setLocationAreaCode(cellIdentityCdma.getNetworkId())
                    .setMobileNetworkCode(cellIdentityCdma.getSystemId())
                    .setTimestamp(cellTimestamp)
                    .build();
        }
        if (cellInfo instanceof CellInfoGsm) {
            CellIdentityGsm cellIdentityGsm = ((CellInfoGsm) cellInfo).getCellIdentity();
            return VisibleCell.builder(VisibleCell.RadioType.GSM)
                    .setCellId(cellIdentityGsm.getCid())
                    .setLocationAreaCode(cellIdentityGsm.getLac())
                    .setMobileCountryCode(cellIdentityGsm.getMcc())
                    .setMobileNetworkCode(cellIdentityGsm.getMnc())
                    .setTimestamp(cellTimestamp)
                    .build();
        }
        if (cellInfo instanceof CellInfoLte) {
            CellIdentityLte cellIdLte = ((CellInfoLte) cellInfo).getCellIdentity();
            return VisibleCell.builder(VisibleCell.RadioType.LTE)
                    .setCellId(cellIdLte.getCi())
                    .setMobileCountryCode(cellIdLte.getMcc())
                    .setMobileNetworkCode(cellIdLte.getMnc())
                    .setPhysicalCellId(cellIdLte.getPci())
                    .setTrackingAreaCode(cellIdLte.getTac())
                    .setTimestamp(cellTimestamp)
                    .build();
        }
        if (cellInfo instanceof CellInfoWcdma) {
            CellIdentityWcdma cellIdentityWcdma = ((CellInfoWcdma) cellInfo).getCellIdentity();
            return VisibleCell.builder(VisibleCell.RadioType.WCDMA)
                    .setCellId(cellIdentityWcdma.getCid())
                    .setLocationAreaCode(cellIdentityWcdma.getLac())
                    .setMobileCountryCode(cellIdentityWcdma.getMcc())
                    .setMobileNetworkCode(cellIdentityWcdma.getMnc())
                    .setPrimaryScramblingCode(cellIdentityWcdma.getPsc())
                    .setTimestamp(cellTimestamp)
                    .build();
        }
        return VisibleCell.UNKNOWN_VISIBLE_CELL;
    }

    /**
     * Returns a CellInfo object representing the currently registered base stations, containing its
     * identity fields and signal strength. Null if no base station is active.
     */
    private static @Nullable CellInfo getActiveCellInfo(TelephonyManager telephonyManager) {
        int numRegisteredCellInfo = 0;
        List<CellInfo> cellInfos = telephonyManager.getAllCellInfo();

        if (cellInfos == null) {
            return null;
        }
        CellInfo result = null;

        for (int i = 0; i < cellInfos.size(); i++) {
            CellInfo cellInfo = cellInfos.get(i);
            if (cellInfo.isRegistered()) {
                numRegisteredCellInfo++;
                if (numRegisteredCellInfo > 1) {
                    return null;
                }
                result = cellInfo;
            }
        }
        // Only found one registered cellinfo, so we know which base station was used to measure
        // network quality
        return result;
    }

    /**
     * Computes the connected networks.
     *
     * <p>Only includes network connections that are active or in the process of being set up.
     *
     * @param context The application context
     */
    static VisibleNetworks computeConnectedNetworks(Context context) {
        TelephonyManager telephonyManager = getTelephonyManager(context);

        VisibleWifi connectedWifi = getConnectedWifiIfKnown(context);
        VisibleCell connectedCell = getConnectedCellIfKnown(context, telephonyManager);

        return VisibleNetworks.create(connectedWifi, connectedCell, null, null);
    }

    /**
     * Computes all visible networks.
     *
     * <p>Along with connected networks, also includes all networks found in the most recent {@link
     * WifiManager} scan, and triggers an update to get refreshed {@link TelephonyManager} {@link
     * CellInfo} data. The {@link CellInfo} includes all available cell information from all radios
     * on the device including the camped/registered, serving, and neighboring cells. This update
     * can degrade latency which is why it is performed asynchronously.
     *
     * @param context The application context
     * @param callback The callback to invoke with the results of this computation
     */
    static void computeVisibleNetworks(Context context, Callback<VisibleNetworks> callback) {
        TelephonyManager telephonyManager = getTelephonyManager(context);

        VisibleWifi connectedWifi = getConnectedWifiIfKnown(context);
        VisibleCell connectedCell = getConnectedCellIfKnown(context, telephonyManager);

        Set<VisibleWifi> allVisibleWifis = getAllVisibleWifis(context, getWifiManager(context));

        getAllVisibleCells(
                context,
                telephonyManager,
                (allVisibleCells) -> {
                    callback.onResult(
                            VisibleNetworks.create(
                                    connectedWifi,
                                    connectedCell,
                                    allVisibleWifis,
                                    allVisibleCells));
                });
    }

    private static TelephonyManager getTelephonyManager(Context context) {
        return (TelephonyManager)
                context.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
    }

    private static WifiManager getWifiManager(Context context) {
        Context applicationContext = context.getApplicationContext();
        return (WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
    }

    private static boolean hasPermission(Context context, String permission) {
        return ApiCompatibilityUtils.checkPermission(
                        context, permission, Process.myPid(), Process.myUid())
                == PackageManager.PERMISSION_GRANTED;
    }

    private static boolean hasLocationPermission(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION);
        }

        return hasPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
                || hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION);
    }

    private static boolean hasLocationAndWifiPermission(Context context) {
        return hasLocationPermission(context)
                && hasPermission(context, Manifest.permission.ACCESS_WIFI_STATE);
    }

    private static void requestCellInfoUpdate(
            TelephonyManager telephonyManager, Callback<List<CellInfo>> callback) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            try {
                telephonyManager.requestCellInfoUpdate(
                        AsyncTask.THREAD_POOL_EXECUTOR,
                        new TelephonyManager.CellInfoCallback() {
                            @Override
                            @SuppressLint("Override")
                            public void onCellInfo(List<CellInfo> cellInfos) {
                                callback.onResult(cellInfos);
                            }
                        });
            } catch (IllegalStateException e) {
                // TelephonyManager#requestCellInfoUpdate() throws IllegalStateException when
                // Telephony is unavailable. It doesn't make sense to pass the call to the
                // TelephonyManager#getAllCellInfo(), because given the same exact conditions it
                // will return null, too.
                callback.onResult(null);
            }
            return;
        }
        callback.onResult(telephonyManager.getAllCellInfo());
    }

    /** Wrapper around static time providers that allows us to mock the implementation in tests. */
    static class TimeProvider {
        /** Get current time in milliseconds. */
        long getCurrentTime() {
            return System.currentTimeMillis();
        }

        /** Get elapsed real time in milliseconds. */
        long getElapsedRealtime() {
            return SystemClock.elapsedRealtime();
        }
    }
}