Add Network Scan helper class

Add a helper class that builds the common interface and performs the
network scan for two different network scan APIs.

Bug: 115401728
Test: Builds & manual test
Change-Id: I48c743270a8adee9a18628f95a8724ea3a40e41d
diff --git a/src/com/android/phone/CellInfoUtil.java b/src/com/android/phone/CellInfoUtil.java
index 462cafe..8272029 100644
--- a/src/com/android/phone/CellInfoUtil.java
+++ b/src/com/android/phone/CellInfoUtil.java
@@ -127,6 +127,35 @@
         return oi;
     }
 
+    /**
+     * Creates a CellInfo object from OperatorInfo. GsmCellInfo is used here only because
+     * operatorInfo does not contain technology type while CellInfo is an abstract object that
+     * requires to specify technology type. It doesn't matter which CellInfo type to use here, since
+     * we only want to wrap the operator info and PLMN to a CellInfo object.
+     */
+    public static CellInfo convertOperatorInfoToCellInfo(OperatorInfo operatorInfo) {
+        String operatorNumeric = operatorInfo.getOperatorNumeric();
+        String mcc = null;
+        String mnc = null;
+        if (operatorNumeric != null && operatorNumeric.matches("^[0-9]{5,6}$")) {
+            mcc = operatorNumeric.substring(0, 3);
+            mnc = operatorNumeric.substring(3);
+        }
+        CellIdentityGsm cig = new CellIdentityGsm(
+                Integer.MAX_VALUE /* lac */,
+                Integer.MAX_VALUE /* cid */,
+                Integer.MAX_VALUE /* arfcn */,
+                Integer.MAX_VALUE /* bsic */,
+                mcc,
+                mnc,
+                operatorInfo.getOperatorAlphaLong(),
+                operatorInfo.getOperatorAlphaShort());
+
+        CellInfoGsm ci = new CellInfoGsm();
+        ci.setCellIdentity(cig);
+        return ci;
+    }
+
     /** Checks whether the network operator is forbidden. */
     public static boolean isForbidden(CellInfo cellInfo, List<String> forbiddenPlmns) {
         String plmn = CellInfoUtil.getOperatorInfoFromCellInfo(cellInfo).getOperatorNumeric();
diff --git a/src/com/android/phone/NetworkScanHelper.java b/src/com/android/phone/NetworkScanHelper.java
new file mode 100644
index 0000000..a21f547
--- /dev/null
+++ b/src/com/android/phone/NetworkScanHelper.java
@@ -0,0 +1,286 @@
+/*
+ * 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.phone;
+
+import android.annotation.IntDef;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.CellInfo;
+import android.telephony.NetworkScan;
+import android.telephony.NetworkScanRequest;
+import android.telephony.RadioAccessSpecifier;
+import android.telephony.TelephonyManager;
+import android.telephony.TelephonyScanManager;
+import android.util.Log;
+
+import com.android.internal.telephony.CellNetworkScanResult;
+
+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.SettableFuture;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+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";
+    private static final boolean DBG = true;
+
+    /**
+     * 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 android.telephony.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. */
+    private static final boolean INCREMENTAL_RESULTS = true;
+    private static final int SEARCH_PERIODICITY_SEC = 5;
+    private static final int MAX_SEARCH_TIME_SEC = 300;
+    private static final int INCREMENTAL_RESULTS_PERIODICITY_SEC = 3;
+
+    private static final NetworkScanRequest NETWORK_SCAN_REQUEST =
+            new NetworkScanRequest(
+                    NetworkScanRequest.SCAN_TYPE_ONE_SHOT,
+                    new RadioAccessSpecifier[]{
+                            // GSM
+                            new RadioAccessSpecifier(
+                                    AccessNetworkType.GERAN,
+                                    null /* bands */,
+                                    null /* channels */),
+                            // LTE
+                            new RadioAccessSpecifier(
+                                    AccessNetworkType.EUTRAN,
+                                    null /* bands */,
+                                    null /* channels */),
+                            // WCDMA
+                            new RadioAccessSpecifier(
+                                    AccessNetworkType.UTRAN,
+                                    null /* bands */,
+                                    null /* channels */)
+                    },
+                    SEARCH_PERIODICITY_SEC,
+                    MAX_SEARCH_TIME_SEC,
+                    INCREMENTAL_RESULTS,
+                    INCREMENTAL_RESULTS_PERIODICITY_SEC,
+                    null /* List of PLMN ids (MCC-MNC) */);
+
+    private final NetworkScanCallback mNetworkScanCallback;
+    private final TelephonyManager mTelephonyManager;
+    private final TelephonyScanManager.NetworkScanCallback mInternalNetworkScanCallback;
+    private final Executor mExecutor;
+
+    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;
+    }
+
+    /**
+     * 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) {
+                    int errCode = Integer.parseInt(t.getMessage());
+                    onError(errCode);
+                }
+            });
+            mExecutor.execute(new NetworkScanSyncTask(
+                    mTelephonyManager, (SettableFuture) mNetworkScanFuture));
+        } else if (type == NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS) {
+            if (DBG) Log.d(TAG, "start network scan async");
+            mNetworkScanRequester = mTelephonyManager.requestNetworkScan(
+                    NETWORK_SCAN_REQUEST,
+                    mExecutor,
+                    mInternalNetworkScanCallback);
+        }
+    }
+
+    /**
+     * 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();
+            mNetworkScanFuture = 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);
+    }
+
+    /**
+     * Converts the status code of {@link CellNetworkScanResult} to one of the
+     * {@link android.telephony.NetworkScan.ScanErrorCode}.
+     * @param errCode status code from {@link CellNetworkScanResult}.
+     *
+     * @return one of the scan error code from {@link android.telephony.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) {
+            if (DBG) Log.d(TAG, "async scan onResults() results = " + results);
+            NetworkScanHelper.this.onResults(results);
+        }
+
+        public void onComplete() {
+            if (DBG) Log.d(TAG, "async scan onComplete()");
+            NetworkScanHelper.this.onComplete();
+        }
+
+        public void onError(@NetworkScan.ScanErrorCode int errCode) {
+            if (DBG) 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() {
+            if (DBG) Log.d(TAG, "sync scan start");
+            CellNetworkScanResult result = mTelephonyManager.getAvailableNetworks();
+            if (result.getStatus() == CellNetworkScanResult.STATUS_SUCCESS) {
+                List<CellInfo> cellInfos = result.getOperators()
+                        .stream()
+                        .map(operatorInfo
+                                -> CellInfoUtil.convertOperatorInfoToCellInfo(operatorInfo))
+                        .collect(Collectors.toList());
+                if (DBG) Log.d(TAG, "sync scan complete");
+                mCallback.set(cellInfos);
+            } else {
+                mCallback.setException(new Throwable(
+                        Integer.toString(convertToScanErrorCode(result.getStatus()))));
+            }
+        }
+    }
+}