content: /*
   * Copyright (C) 2008 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.server.wifi.scanner;
  
  import static android.content.pm.PackageManager.PERMISSION_DENIED;
  import static android.content.pm.PackageManager.PERMISSION_GRANTED;
  
  import android.Manifest;
  import android.app.AlarmManager;
  import android.content.Context;
  import android.net.wifi.IWifiScanner;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiScanner;
  import android.net.wifi.WifiScanner.ChannelSpec;
  import android.net.wifi.WifiScanner.PnoSettings;
  import android.net.wifi.WifiScanner.ScanData;
  import android.net.wifi.WifiScanner.ScanSettings;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Messenger;
  import android.os.RemoteException;
  import android.os.WorkSource;
  import android.util.ArrayMap;
  import android.util.LocalLog;
  import android.util.Log;
  import android.util.Pair;
  import android.util.StatsLog;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.app.IBatteryStats;
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.util.AsyncChannel;
  import com.android.internal.util.Protocol;
  import com.android.internal.util.State;
  import com.android.internal.util.StateMachine;
  import com.android.server.wifi.ClientModeImpl;
  import com.android.server.wifi.Clock;
  import com.android.server.wifi.FrameworkFacade;
  import com.android.server.wifi.WifiInjector;
  import com.android.server.wifi.WifiLog;
  import com.android.server.wifi.WifiMetrics;
  import com.android.server.wifi.WifiNative;
  import com.android.server.wifi.nano.WifiMetricsProto;
  import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;
  import com.android.server.wifi.util.ScanResultUtil;
  import com.android.server.wifi.util.WifiHandler;
  import com.android.server.wifi.util.WifiPermissionsUtil;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Iterator;
  import java.util.List;
  
  public class WifiScanningServiceImpl extends IWifiScanner.Stub {
  
      private static final String TAG = WifiScanningService.TAG;
      private static final boolean DBG = false;
  
      private static final int UNKNOWN_PID = -1;
  
      private final LocalLog mLocalLog = new LocalLog(512);
  
      private WifiLog mLog;
  
      private void localLog(String message) {
          mLocalLog.log(message);
      }
  
      private void logw(String message) {
          Log.w(TAG, message);
          mLocalLog.log(message);
      }
  
      private void loge(String message) {
          Log.e(TAG, message);
          mLocalLog.log(message);
      }
  
      private WifiScannerImpl mScannerImpl;
  
      @Override
      public Messenger getMessenger() {
          if (mClientHandler != null) {
              mLog.trace("getMessenger() uid=%").c(Binder.getCallingUid()).flush();
              return new Messenger(mClientHandler);
          }
          loge("WifiScanningServiceImpl trying to get messenger w/o initialization");
          return null;
      }
  
      @Override
      public Bundle getAvailableChannels(int band) {
          mChannelHelper.updateChannels();
          ChannelSpec[] channelSpecs = mChannelHelper.getAvailableScanChannels(band);
          ArrayList<Integer> list = new ArrayList<Integer>(channelSpecs.length);
          for (ChannelSpec channelSpec : channelSpecs) {
              list.add(channelSpec.frequency);
          }
          Bundle b = new Bundle();
          b.putIntegerArrayList(WifiScanner.GET_AVAILABLE_CHANNELS_EXTRA, list);
          mLog.trace("getAvailableChannels uid=%").c(Binder.getCallingUid()).flush();
          return b;
      }
  
      private void enforceNetworkStack(int uid) {
          mContext.enforcePermission(
                  Manifest.permission.NETWORK_STACK,
                  UNKNOWN_PID, uid,
                  "NetworkStack");
      }
  
      // Helper method to check if the incoming message is for a privileged request.
      private boolean isPrivilegedMessage(int msgWhat) {
          return (msgWhat == WifiScanner.CMD_ENABLE
                  || msgWhat == WifiScanner.CMD_DISABLE
                  || msgWhat == WifiScanner.CMD_START_PNO_SCAN
                  || msgWhat == WifiScanner.CMD_STOP_PNO_SCAN
                  || msgWhat == WifiScanner.CMD_REGISTER_SCAN_LISTENER);
      }
  
      // For non-privileged requests, retrieve the bundled package name for app-op & permission
      // checks.
      private String getPackageName(Message msg) {
          if (!(msg.obj instanceof Bundle)) {
              return null;
          }
          Bundle bundle = (Bundle) msg.obj;
          return bundle.getString(WifiScanner.REQUEST_PACKAGE_NAME_KEY);
      }
  
      // Check if we should ignore location settings if this is a single scan request.
      private boolean shouldIgnoreLocationSettingsForSingleScan(Message msg) {
          if (msg.what != WifiScanner.CMD_START_SINGLE_SCAN) return false;
          if (!(msg.obj instanceof Bundle)) return false;
          Bundle bundle = (Bundle) msg.obj;
          ScanSettings scanSettings = bundle.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
          return scanSettings.ignoreLocationSettings;
      }
  
      // Check if we should hide this request from app-ops if this is a single scan request.
      private boolean shouldHideFromAppsForSingleScan(Message msg) {
          if (msg.what != WifiScanner.CMD_START_SINGLE_SCAN) return false;
          if (!(msg.obj instanceof Bundle)) return false;
          Bundle bundle = (Bundle) msg.obj;
          ScanSettings scanSettings = bundle.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
          return scanSettings.hideFromAppOps;
      }
  
      /**
       * Enforce the necessary client permissions for WifiScanner.
       * If the client has NETWORK_STACK permission, then it can "always" send "any" request.
       * If the client has only LOCATION_HARDWARE permission, then it can
       *    a) Only make scan related requests when location is turned on.
       *    b) Can never make one of the privileged requests.
       *
       * @param uid Uid of the client.
       * @param msg {@link Message} of the incoming request.
       * @throws {@link SecurityException} if the client does not have the necessary permissions.
       */
      private void enforcePermission(int uid, Message msg) throws SecurityException {
          try {
              /** Wifi stack issued requests.*/
              enforceNetworkStack(uid);
          } catch (SecurityException e) {
              /** System-app issued requests. */
              if (isPrivilegedMessage(msg.what)) {
                  // Privileged message, only requests from clients with NETWORK_STACK allowed!
                  throw e;
              }
              mWifiPermissionsUtil.enforceCanAccessScanResultsForWifiScanner(
                      getPackageName(msg), uid, shouldIgnoreLocationSettingsForSingleScan(msg),
                      shouldHideFromAppsForSingleScan(msg));
          }
      }
  
      private class ClientHandler extends WifiHandler {
  
          ClientHandler(String tag, Looper looper) {
              super(tag, looper);
          }
  
          @Override
          public void handleMessage(Message msg) {
              super.handleMessage(msg);
              switch (msg.what) {
                  case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {
                      if (msg.replyTo == null) {
                          logw("msg.replyTo is null");
                          return;
                      }
                      ExternalClientInfo client = (ExternalClientInfo) mClients.get(msg.replyTo);
                      if (client != null) {
                          logw("duplicate client connection: " + msg.sendingUid + ", messenger="
                                  + msg.replyTo);
                          client.mChannel.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
                                  AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED);
                          return;
                      }
  
                      AsyncChannel ac = mFrameworkFacade.makeWifiAsyncChannel(TAG);
                      ac.connected(mContext, this, msg.replyTo);
  
                      client = new ExternalClientInfo(msg.sendingUid, msg.replyTo, ac);
                      client.register();
  
                      ac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
                              AsyncChannel.STATUS_SUCCESSFUL);
                      localLog("client connected: " + client);
                      return;
                  }
                  case AsyncChannel.CMD_CHANNEL_DISCONNECT: {
                      ExternalClientInfo client = (ExternalClientInfo) mClients.get(msg.replyTo);
                      if (client != null) {
                          client.mChannel.disconnect();
                      }
                      return;
                  }
                  case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {
                      ExternalClientInfo client = (ExternalClientInfo) mClients.get(msg.replyTo);
                      if (client != null && msg.arg1 != AsyncChannel.STATUS_SEND_UNSUCCESSFUL
                              && msg.arg1
                              != AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED) {
                          localLog("client disconnected: " + client + ", reason: " + msg.arg1);
                          client.cleanup();
                      }
                      return;
                  }
              }
  
              try {
                  enforcePermission(msg.sendingUid, msg);
              } catch (SecurityException e) {
                  localLog("failed to authorize app: " + e);
                  replyFailed(msg, WifiScanner.REASON_NOT_AUTHORIZED, "Not authorized");
                  return;
              }
  
              // Since the CMD_GET_SCAN_RESULTS and CMD_GET_SINGLE_SCAN_RESULTS messages are
              // sent from WifiScanner using |sendMessageSynchronously|, handle separately since
              // the |msg.replyTo| field does not actually correspond to the Messenger that is
              // registered for that client.
              if (msg.what == WifiScanner.CMD_GET_SCAN_RESULTS) {
                  mBackgroundScanStateMachine.sendMessage(Message.obtain(msg));
                  return;
              }
              if (msg.what == WifiScanner.CMD_GET_SINGLE_SCAN_RESULTS) {
                  mSingleScanStateMachine.sendMessage(Message.obtain(msg));
                  return;
              }
  
              ClientInfo ci = mClients.get(msg.replyTo);
              if (ci == null) {
                  loge("Could not find client info for message " + msg.replyTo + ", msg=" + msg);
                  replyFailed(msg, WifiScanner.REASON_INVALID_LISTENER, "Could not find listener");
                  return;
              }
  
              switch (msg.what) {
                  case WifiScanner.CMD_ENABLE:
                      mBackgroundScanStateMachine.sendMessage(CMD_DRIVER_LOADED);
                      mSingleScanStateMachine.sendMessage(CMD_DRIVER_LOADED);
                      mPnoScanStateMachine.sendMessage(CMD_DRIVER_LOADED);
                      break;
                  case WifiScanner.CMD_DISABLE:
                      mBackgroundScanStateMachine.sendMessage(CMD_DRIVER_UNLOADED);
                      mSingleScanStateMachine.sendMessage(CMD_DRIVER_UNLOADED);
                      mPnoScanStateMachine.sendMessage(CMD_DRIVER_UNLOADED);
                      break;
                  case WifiScanner.CMD_START_BACKGROUND_SCAN:
                  case WifiScanner.CMD_STOP_BACKGROUND_SCAN:
                      mBackgroundScanStateMachine.sendMessage(Message.obtain(msg));
                      break;
                  case WifiScanner.CMD_START_PNO_SCAN:
                  case WifiScanner.CMD_STOP_PNO_SCAN:
                      mPnoScanStateMachine.sendMessage(Message.obtain(msg));
                      break;
                  case WifiScanner.CMD_START_SINGLE_SCAN:
                  case WifiScanner.CMD_STOP_SINGLE_SCAN:
                      mSingleScanStateMachine.sendMessage(Message.obtain(msg));
                      break;
                  case WifiScanner.CMD_REGISTER_SCAN_LISTENER:
                      logScanRequest("registerScanListener", ci, msg.arg2, null, null, null);
                      mSingleScanListeners.addRequest(ci, msg.arg2, null, null);
                      replySucceeded(msg);
                      break;
                  case WifiScanner.CMD_DEREGISTER_SCAN_LISTENER:
                      logScanRequest("deregisterScanListener", ci, msg.arg2, null, null, null);
                      mSingleScanListeners.removeRequest(ci, msg.arg2);
                      break;
                  default:
                      replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "Invalid request");
                      break;
              }
          }
      }
  
      private static final int BASE = Protocol.BASE_WIFI_SCANNER_SERVICE;
  
      private static final int CMD_SCAN_RESULTS_AVAILABLE              = BASE + 0;
      private static final int CMD_FULL_SCAN_RESULTS                   = BASE + 1;
      private static final int CMD_DRIVER_LOADED                       = BASE + 6;
      private static final int CMD_DRIVER_UNLOADED                     = BASE + 7;
      private static final int CMD_SCAN_PAUSED                         = BASE + 8;
      private static final int CMD_SCAN_RESTARTED                      = BASE + 9;
      private static final int CMD_SCAN_FAILED                         = BASE + 10;
      private static final int CMD_PNO_NETWORK_FOUND                   = BASE + 11;
      private static final int CMD_PNO_SCAN_FAILED                     = BASE + 12;
  
      private final Context mContext;
      private final Looper mLooper;
      private final WifiScannerImpl.WifiScannerImplFactory mScannerImplFactory;
      private final ArrayMap<Messenger, ClientInfo> mClients;
  
      private final RequestList<Void> mSingleScanListeners = new RequestList<>();
  
      private ChannelHelper mChannelHelper;
      private BackgroundScanScheduler mBackgroundScheduler;
      private WifiNative.ScanSettings mPreviousSchedule;
  
      private WifiBackgroundScanStateMachine mBackgroundScanStateMachine;
      private WifiSingleScanStateMachine mSingleScanStateMachine;
      private WifiPnoScanStateMachine mPnoScanStateMachine;
      private ClientHandler mClientHandler;
      // This is retrieved lazily because location service is started after wifi scanner.
      private final IBatteryStats mBatteryStats;
      private final AlarmManager mAlarmManager;
      private final WifiMetrics mWifiMetrics;
      private final Clock mClock;
      private final FrameworkFacade mFrameworkFacade;
      private final WifiPermissionsUtil mWifiPermissionsUtil;
  
      WifiScanningServiceImpl(Context context, Looper looper,
              WifiScannerImpl.WifiScannerImplFactory scannerImplFactory, IBatteryStats batteryStats,
              WifiInjector wifiInjector) {
          mContext = context;
          mLooper = looper;
          mScannerImplFactory = scannerImplFactory;
          mBatteryStats = batteryStats;
          mClients = new ArrayMap<>();
          mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
          mWifiMetrics = wifiInjector.getWifiMetrics();
          mClock = wifiInjector.getClock();
          mLog = wifiInjector.makeLog(TAG);
          mFrameworkFacade = wifiInjector.getFrameworkFacade();
          mWifiPermissionsUtil = wifiInjector.getWifiPermissionsUtil();
          mPreviousSchedule = null;
      }
  
      public void startService() {
          mBackgroundScanStateMachine = new WifiBackgroundScanStateMachine(mLooper);
          mSingleScanStateMachine = new WifiSingleScanStateMachine(mLooper);
          mPnoScanStateMachine = new WifiPnoScanStateMachine(mLooper);
  
          mBackgroundScanStateMachine.start();
          mSingleScanStateMachine.start();
          mPnoScanStateMachine.start();
  
          // Create client handler only after StateMachines are ready.
          mClientHandler = new ClientHandler(TAG, mLooper);
      }
  
      /**
       * Provide a way for unit tests to set valid log object in the WifiHandler
       * @param log WifiLog object to assign to the clientHandler
       */
      @VisibleForTesting
      public void setWifiHandlerLogForTest(WifiLog log) {
          mClientHandler.setWifiLog(log);
      }
  
      private WorkSource computeWorkSource(ClientInfo ci, WorkSource requestedWorkSource) {
          if (requestedWorkSource != null) {
              requestedWorkSource.clearNames();
  
              if (!requestedWorkSource.isEmpty()) {
                  return requestedWorkSource;
              }
          }
  
          if (ci.getUid() > 0) {
              return new WorkSource(ci.getUid());
          }
  
          // We can't construct a sensible WorkSource because the one supplied to us was empty and
          // we don't have a valid UID for the given client.
          loge("Unable to compute workSource for client: " + ci + ", requested: "
                  + requestedWorkSource);
          return new WorkSource();
      }
  
      private class RequestInfo<T> {
          final ClientInfo clientInfo;
          final int handlerId;
          final WorkSource workSource;
          final T settings;
  
          RequestInfo(ClientInfo clientInfo, int handlerId, WorkSource requestedWorkSource,
                  T settings) {
              this.clientInfo = clientInfo;
              this.handlerId = handlerId;
              this.settings = settings;
              this.workSource = computeWorkSource(clientInfo, requestedWorkSource);
          }
  
          void reportEvent(int what, int arg1, Object obj) {
              clientInfo.reportEvent(what, arg1, handlerId, obj);
          }
      }
  
      private class RequestList<T> extends ArrayList<RequestInfo<T>> {
          void addRequest(ClientInfo ci, int handler, WorkSource reqworkSource, T settings) {
              add(new RequestInfo<T>(ci, handler, reqworkSource, settings));
          }
  
          T removeRequest(ClientInfo ci, int handlerId) {
              T removed = null;
              Iterator<RequestInfo<T>> iter = iterator();
              while (iter.hasNext()) {
                  RequestInfo<T> entry = iter.next();
                  if (entry.clientInfo == ci && entry.handlerId == handlerId) {
                      removed = entry.settings;
                      iter.remove();
                  }
              }
              return removed;
          }
  
          Collection<T> getAllSettings() {
              ArrayList<T> settingsList = new ArrayList<>();
              Iterator<RequestInfo<T>> iter = iterator();
              while (iter.hasNext()) {
                  RequestInfo<T> entry = iter.next();
                  settingsList.add(entry.settings);
              }
              return settingsList;
          }
  
          Collection<T> getAllSettingsForClient(ClientInfo ci) {
              ArrayList<T> settingsList = new ArrayList<>();
              Iterator<RequestInfo<T>> iter = iterator();
              while (iter.hasNext()) {
                  RequestInfo<T> entry = iter.next();
                  if (entry.clientInfo == ci) {
                      settingsList.add(entry.settings);
                  }
              }
              return settingsList;
          }
  
          void removeAllForClient(ClientInfo ci) {
              Iterator<RequestInfo<T>> iter = iterator();
              while (iter.hasNext()) {
                  RequestInfo<T> entry = iter.next();
                  if (entry.clientInfo == ci) {
                      iter.remove();
                  }
              }
          }
  
          WorkSource createMergedWorkSource() {
              WorkSource mergedSource = new WorkSource();
              for (RequestInfo<T> entry : this) {
                  mergedSource.add(entry.workSource);
              }
              return mergedSource;
          }
      }
  
      /**
       * State machine that holds the state of single scans. Scans should only be active in the
       * ScanningState. The pending scans and active scans maps are swapped when entering
       * ScanningState. Any requests queued while scanning will be placed in the pending queue and
       * executed after transitioning back to IdleState.
       */
      class WifiSingleScanStateMachine extends StateMachine implements WifiNative.ScanEventHandler {
          /**
           * Maximum age of results that we return from our cache via
           * {@link WifiScanner#getScanResults()}.
           * This is currently set to 3 minutes to restore parity with the wpa_supplicant's scan
           * result cache expiration policy. (See b/62253332 for details)
           */
          @VisibleForTesting
          public static final int CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS = 180 * 1000;
  
          private final DefaultState mDefaultState = new DefaultState();
          private final DriverStartedState mDriverStartedState = new DriverStartedState();
          private final IdleState  mIdleState  = new IdleState();
          private final ScanningState  mScanningState  = new ScanningState();
  
          private WifiNative.ScanSettings mActiveScanSettings = null;
          private RequestList<ScanSettings> mActiveScans = new RequestList<>();
          private RequestList<ScanSettings> mPendingScans = new RequestList<>();
  
          // Scan results cached from the last full single scan request.
          private final List<ScanResult> mCachedScanResults = new ArrayList<>();
  
          WifiSingleScanStateMachine(Looper looper) {
              super("WifiSingleScanStateMachine", looper);
  
              setLogRecSize(128);
              setLogOnlyTransitions(false);
  
              // CHECKSTYLE:OFF IndentationCheck
              addState(mDefaultState);
                  addState(mDriverStartedState, mDefaultState);
                      addState(mIdleState, mDriverStartedState);
                      addState(mScanningState, mDriverStartedState);
              // CHECKSTYLE:ON IndentationCheck
  
              setInitialState(mDefaultState);
          }
  
          /**
           * Called to indicate a change in state for the current scan.
           * Will dispatch a coresponding event to the state machine
           */
          @Override
          public void onScanStatus(int event) {
              if (DBG) localLog("onScanStatus event received, event=" + event);
              switch(event) {
                  case WifiNative.WIFI_SCAN_RESULTS_AVAILABLE:
                  case WifiNative.WIFI_SCAN_THRESHOLD_NUM_SCANS:
                  case WifiNative.WIFI_SCAN_THRESHOLD_PERCENT:
                      sendMessage(CMD_SCAN_RESULTS_AVAILABLE);
                      break;
                  case WifiNative.WIFI_SCAN_FAILED:
                      sendMessage(CMD_SCAN_FAILED);
                      break;
                  default:
                      Log.e(TAG, "Unknown scan status event: " + event);
                      break;
              }
          }
  
          /**
           * Called for each full scan result if requested
           */
          @Override
          public void onFullScanResult(ScanResult fullScanResult, int bucketsScanned) {
              if (DBG) localLog("onFullScanResult received");
              sendMessage(CMD_FULL_SCAN_RESULTS, 0, bucketsScanned, fullScanResult);
          }
  
          @Override
          public void onScanPaused(ScanData[] scanData) {
              // should not happen for single scan
              Log.e(TAG, "Got scan paused for single scan");
          }
  
          @Override
          public void onScanRestarted() {
              // should not happen for single scan
              Log.e(TAG, "Got scan restarted for single scan");
          }
  
          class DefaultState extends State {
              @Override
              public void enter() {
                  mActiveScans.clear();
                  mPendingScans.clear();
              }
              @Override
              public boolean processMessage(Message msg) {
                  switch (msg.what) {
                      case CMD_DRIVER_LOADED:
                          if (mScannerImpl == null) {
                              loge("Failed to start single scan state machine because scanner impl"
                                      + " is null");
                              return HANDLED;
                          }
                          transitionTo(mIdleState);
                          return HANDLED;
                      case CMD_DRIVER_UNLOADED:
                          transitionTo(mDefaultState);
                          return HANDLED;
                      case WifiScanner.CMD_START_SINGLE_SCAN:
                      case WifiScanner.CMD_STOP_SINGLE_SCAN:
                          replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "not available");
                          return HANDLED;
                      case CMD_SCAN_RESULTS_AVAILABLE:
                          if (DBG) localLog("ignored scan results available event");
                          return HANDLED;
                      case CMD_FULL_SCAN_RESULTS:
                          if (DBG) localLog("ignored full scan result event");
                          return HANDLED;
                      case WifiScanner.CMD_GET_SINGLE_SCAN_RESULTS:
                          msg.obj = new WifiScanner.ParcelableScanResults(
                              filterCachedScanResultsByAge());
                          replySucceeded(msg);
                          return HANDLED;
                      default:
                          return NOT_HANDLED;
                  }
              }
  
              /**
               * Filter out  any scan results that are older than
               * {@link #CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS}.
               *
               * @return Filtered list of scan results.
               */
              private ScanResult[] filterCachedScanResultsByAge() {
                  // Using ScanResult.timestamp here to ensure that we use the same fields as
                  // WificondScannerImpl for filtering stale results.
                  long currentTimeInMillis = mClock.getElapsedSinceBootMillis();
                  return mCachedScanResults.stream()
                          .filter(scanResult
                                  -> ((currentTimeInMillis - (scanResult.timestamp / 1000))
                                          < CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS))
                          .toArray(ScanResult[]::new);
              }
          }
  
          /**
           * State representing when the driver is running. This state is not meant to be transitioned
           * directly, but is instead intended as a parent state of ScanningState and IdleState
           * to hold common functionality and handle cleaning up scans when the driver is shut down.
           */
          class DriverStartedState extends State {
              @Override
              public void exit() {
                  // clear scan results when scan mode is not active
                  mCachedScanResults.clear();
  
                  mWifiMetrics.incrementScanReturnEntry(
                          WifiMetricsProto.WifiLog.SCAN_FAILURE_INTERRUPTED,
                          mPendingScans.size());
                  sendOpFailedToAllAndClear(mPendingScans, WifiScanner.REASON_UNSPECIFIED,
                          "Scan was interrupted");
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  ClientInfo ci = mClients.get(msg.replyTo);
  
                  switch (msg.what) {
                      case CMD_DRIVER_LOADED:
                          // Ignore if we're already in driver loaded state.
                          return HANDLED;
                      case WifiScanner.CMD_START_SINGLE_SCAN:
                          int handler = msg.arg2;
                          Bundle scanParams = (Bundle) msg.obj;
                          if (scanParams == null) {
                              logCallback("singleScanInvalidRequest",  ci, handler, "null params");
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null");
                              return HANDLED;
                          }
                          scanParams.setDefusable(true);
                          ScanSettings scanSettings =
                                  scanParams.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
                          WorkSource workSource =
                                  scanParams.getParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY);
                          if (validateScanRequest(ci, handler, scanSettings)) {
                              mWifiMetrics.incrementOneshotScanCount();
                              if (scanSettings.band == WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY
                                      || scanSettings.band == WifiScanner.WIFI_BAND_5_GHZ_WITH_DFS
                                      || scanSettings.band == WifiScanner.WIFI_BAND_BOTH_WITH_DFS) {
                                  mWifiMetrics.incrementOneshotScanWithDfsCount();
                              }
                              logScanRequest("addSingleScanRequest", ci, handler, workSource,
                                      scanSettings, null);
                              replySucceeded(msg);
  
                              // If there is an active scan that will fulfill the scan request then
                              // mark this request as an active scan, otherwise mark it pending.
                              // If were not currently scanning then try to start a scan. Otherwise
                              // this scan will be scheduled when transitioning back to IdleState
                              // after finishing the current scan.
                              if (getCurrentState() == mScanningState) {
                                  if (activeScanSatisfies(scanSettings)) {
                                      mActiveScans.addRequest(ci, handler, workSource, scanSettings);
                                  } else {
                                      mPendingScans.addRequest(ci, handler, workSource, scanSettings);
                                  }
                              } else {
                                  mPendingScans.addRequest(ci, handler, workSource, scanSettings);
                                  tryToStartNewScan();
                              }
                          } else {
                              logCallback("singleScanInvalidRequest",  ci, handler, "bad request");
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request");
                              mWifiMetrics.incrementScanReturnEntry(
                                      WifiMetricsProto.WifiLog.SCAN_FAILURE_INVALID_CONFIGURATION, 1);
                          }
                          return HANDLED;
                      case WifiScanner.CMD_STOP_SINGLE_SCAN:
                          removeSingleScanRequest(ci, msg.arg2);
                          return HANDLED;
                      default:
                          return NOT_HANDLED;
                  }
              }
          }
  
          class IdleState extends State {
              @Override
              public void enter() {
                  tryToStartNewScan();
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  return NOT_HANDLED;
              }
          }
  
          class ScanningState extends State {
              private WorkSource mScanWorkSource;
  
              @Override
              public void enter() {
                  mScanWorkSource = mActiveScans.createMergedWorkSource();
                  try {
                      mBatteryStats.noteWifiScanStartedFromSource(mScanWorkSource);
                  } catch (RemoteException e) {
                      loge(e.toString());
                  }
                  StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED, mScanWorkSource,
                          StatsLog.WIFI_SCAN_STATE_CHANGED__STATE__ON);
              }
  
              @Override
              public void exit() {
                  mActiveScanSettings = null;
                  try {
                      mBatteryStats.noteWifiScanStoppedFromSource(mScanWorkSource);
                  } catch (RemoteException e) {
                      loge(e.toString());
                  }
                  StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED, mScanWorkSource,
                          StatsLog.WIFI_SCAN_STATE_CHANGED__STATE__OFF);
  
                  // if any scans are still active (never got results available then indicate failure)
                  mWifiMetrics.incrementScanReturnEntry(
                                  WifiMetricsProto.WifiLog.SCAN_UNKNOWN,
                                  mActiveScans.size());
                  sendOpFailedToAllAndClear(mActiveScans, WifiScanner.REASON_UNSPECIFIED,
                          "Scan was interrupted");
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  switch (msg.what) {
                      case CMD_SCAN_RESULTS_AVAILABLE:
                          mWifiMetrics.incrementScanReturnEntry(
                                  WifiMetricsProto.WifiLog.SCAN_SUCCESS,
                                  mActiveScans.size());
                          reportScanResults(mScannerImpl.getLatestSingleScanResults());
                          mActiveScans.clear();
                          transitionTo(mIdleState);
                          return HANDLED;
                      case CMD_FULL_SCAN_RESULTS:
                          reportFullScanResult((ScanResult) msg.obj, /* bucketsScanned */ msg.arg2);
                          return HANDLED;
                      case CMD_SCAN_FAILED:
                          mWifiMetrics.incrementScanReturnEntry(
                                  WifiMetricsProto.WifiLog.SCAN_UNKNOWN, mActiveScans.size());
                          sendOpFailedToAllAndClear(mActiveScans, WifiScanner.REASON_UNSPECIFIED,
                                  "Scan failed");
                          transitionTo(mIdleState);
                          return HANDLED;
                      default:
                          return NOT_HANDLED;
                  }
              }
          }
  
          boolean validateScanType(int type) {
              return (type == WifiScanner.TYPE_LOW_LATENCY || type == WifiScanner.TYPE_LOW_POWER
                      || type == WifiScanner.TYPE_HIGH_ACCURACY);
          }
  
          boolean validateScanRequest(ClientInfo ci, int handler, ScanSettings settings) {
              if (ci == null) {
                  Log.d(TAG, "Failing single scan request ClientInfo not found " + handler);
                  return false;
              }
              if (settings.band == WifiScanner.WIFI_BAND_UNSPECIFIED) {
                  if (settings.channels == null || settings.channels.length == 0) {
                      Log.d(TAG, "Failing single scan because channel list was empty");
                      return false;
                  }
              }
              if (!validateScanType(settings.type)) {
                  Log.e(TAG, "Invalid scan type " + settings.type);
                  return false;
              }
              if (mContext.checkPermission(
                      Manifest.permission.NETWORK_STACK, UNKNOWN_PID, ci.getUid())
                      == PERMISSION_DENIED) {
                  if (!ArrayUtils.isEmpty(settings.hiddenNetworks)) {
                      Log.e(TAG, "Failing single scan because app " + ci.getUid()
                              + " does not have permission to set hidden networks");
                      return false;
                  }
                  if (settings.type != WifiScanner.TYPE_LOW_LATENCY) {
                      Log.e(TAG, "Failing single scan because app " + ci.getUid()
                              + " does not have permission to set type");
                      return false;
                  }
              }
              return true;
          }
  
          int getNativeScanType(int type) {
              switch(type) {
                  case WifiScanner.TYPE_LOW_LATENCY:
                      return WifiNative.SCAN_TYPE_LOW_LATENCY;
                  case WifiScanner.TYPE_LOW_POWER:
                      return WifiNative.SCAN_TYPE_LOW_POWER;
                  case WifiScanner.TYPE_HIGH_ACCURACY:
                      return WifiNative.SCAN_TYPE_HIGH_ACCURACY;
                  default:
                      // This should never happen becuase we've validated the incoming type in
                      // |validateScanType|.
                      throw new IllegalArgumentException("Invalid scan type " + type);
              }
          }
  
          // We can coalesce a LOW_POWER/LOW_LATENCY scan request into an ongoing HIGH_ACCURACY
          // scan request. But, we can't coalesce a HIGH_ACCURACY scan request into an ongoing
          // LOW_POWER/LOW_LATENCY scan request.
          boolean activeScanTypeSatisfies(int requestScanType) {
              switch(mActiveScanSettings.scanType) {
                  case WifiNative.SCAN_TYPE_LOW_LATENCY:
                  case WifiNative.SCAN_TYPE_LOW_POWER:
                      return requestScanType != WifiNative.SCAN_TYPE_HIGH_ACCURACY;
                  case WifiNative.SCAN_TYPE_HIGH_ACCURACY:
                      return true;
                  default:
                      // This should never happen becuase we've validated the incoming type in
                      // |validateScanType|.
                      throw new IllegalArgumentException("Invalid scan type "
                          + mActiveScanSettings.scanType);
              }
          }
  
          // If there is a HIGH_ACCURACY scan request among the requests being merged, the merged
          // scan type should be HIGH_ACCURACY.
          int mergeScanTypes(int existingScanType, int newScanType) {
              switch(existingScanType) {
                  case WifiNative.SCAN_TYPE_LOW_LATENCY:
                  case WifiNative.SCAN_TYPE_LOW_POWER:
                      return newScanType;
                  case WifiNative.SCAN_TYPE_HIGH_ACCURACY:
                      return existingScanType;
                  default:
                      // This should never happen becuase we've validated the incoming type in
                      // |validateScanType|.
                      throw new IllegalArgumentException("Invalid scan type " + existingScanType);
              }
          }
  
          boolean activeScanSatisfies(ScanSettings settings) {
              if (mActiveScanSettings == null) {
                  return false;
              }
  
              if (!activeScanTypeSatisfies(getNativeScanType(settings.type))) {
                  return false;
              }
  
              // there is always one bucket for a single scan
              WifiNative.BucketSettings activeBucket = mActiveScanSettings.buckets[0];
  
              // validate that all requested channels are being scanned
              ChannelCollection activeChannels = mChannelHelper.createChannelCollection();
              activeChannels.addChannels(activeBucket);
              if (!activeChannels.containsSettings(settings)) {
                  return false;
              }
  
              // if the request is for a full scan, but there is no ongoing full scan
              if ((settings.reportEvents & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0
                      && (activeBucket.report_events & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT)
                      == 0) {
                  return false;
              }
  
              if (!ArrayUtils.isEmpty(settings.hiddenNetworks)) {
                  if (ArrayUtils.isEmpty(mActiveScanSettings.hiddenNetworks)) {
                      return false;
                  }
                  List<WifiNative.HiddenNetwork> activeHiddenNetworks = new ArrayList<>();
                  for (WifiNative.HiddenNetwork hiddenNetwork : mActiveScanSettings.hiddenNetworks) {
                      activeHiddenNetworks.add(hiddenNetwork);
                  }
                  for (ScanSettings.HiddenNetwork hiddenNetwork : settings.hiddenNetworks) {
                      WifiNative.HiddenNetwork nativeHiddenNetwork = new WifiNative.HiddenNetwork();
                      nativeHiddenNetwork.ssid = hiddenNetwork.ssid;
                      if (!activeHiddenNetworks.contains(nativeHiddenNetwork)) {
                          return false;
                      }
                  }
              }
  
              return true;
          }
  
          void removeSingleScanRequest(ClientInfo ci, int handler) {
              if (ci != null) {
                  logScanRequest("removeSingleScanRequest", ci, handler, null, null, null);
                  mPendingScans.removeRequest(ci, handler);
                  mActiveScans.removeRequest(ci, handler);
              }
          }
  
          void removeSingleScanRequests(ClientInfo ci) {
              if (ci != null) {
                  logScanRequest("removeSingleScanRequests", ci, -1, null, null, null);
                  mPendingScans.removeAllForClient(ci);
                  mActiveScans.removeAllForClient(ci);
              }
          }
  
          void tryToStartNewScan() {
              if (mPendingScans.size() == 0) { // no pending requests
                  return;
              }
              mChannelHelper.updateChannels();
              // TODO move merging logic to a scheduler
              WifiNative.ScanSettings settings = new WifiNative.ScanSettings();
              settings.num_buckets = 1;
              WifiNative.BucketSettings bucketSettings = new WifiNative.BucketSettings();
              bucketSettings.bucket = 0;
              bucketSettings.period_ms = 0;
              bucketSettings.report_events = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
  
              ChannelCollection channels = mChannelHelper.createChannelCollection();
              List<WifiNative.HiddenNetwork> hiddenNetworkList = new ArrayList<>();
              for (RequestInfo<ScanSettings> entry : mPendingScans) {
                  settings.scanType =
                      mergeScanTypes(settings.scanType, getNativeScanType(entry.settings.type));
                  channels.addChannels(entry.settings);
                  if (entry.settings.hiddenNetworks != null) {
                      for (int i = 0; i < entry.settings.hiddenNetworks.length; i++) {
                          WifiNative.HiddenNetwork hiddenNetwork = new WifiNative.HiddenNetwork();
                          hiddenNetwork.ssid = entry.settings.hiddenNetworks[i].ssid;
                          hiddenNetworkList.add(hiddenNetwork);
                      }
                  }
                  if ((entry.settings.reportEvents & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT)
                          != 0) {
                      bucketSettings.report_events |= WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
                  }
              }
              if (hiddenNetworkList.size() > 0) {
                  settings.hiddenNetworks = new WifiNative.HiddenNetwork[hiddenNetworkList.size()];
                  int numHiddenNetworks = 0;
                  for (WifiNative.HiddenNetwork hiddenNetwork : hiddenNetworkList) {
                      settings.hiddenNetworks[numHiddenNetworks++] = hiddenNetwork;
                  }
              }
  
              channels.fillBucketSettings(bucketSettings, Integer.MAX_VALUE);
  
              settings.buckets = new WifiNative.BucketSettings[] {bucketSettings};
              if (mScannerImpl.startSingleScan(settings, this)) {
                  // store the active scan settings
                  mActiveScanSettings = settings;
                  // swap pending and active scan requests
                  RequestList<ScanSettings> tmp = mActiveScans;
                  mActiveScans = mPendingScans;
                  mPendingScans = tmp;
                  // make sure that the pending list is clear
                  mPendingScans.clear();
                  transitionTo(mScanningState);
              } else {
                  mWifiMetrics.incrementScanReturnEntry(
                          WifiMetricsProto.WifiLog.SCAN_UNKNOWN, mPendingScans.size());
                  // notify and cancel failed scans
                  sendOpFailedToAllAndClear(mPendingScans, WifiScanner.REASON_UNSPECIFIED,
                          "Failed to start single scan");
              }
          }
  
          void sendOpFailedToAllAndClear(RequestList<?> clientHandlers, int reason,
                  String description) {
              for (RequestInfo<?> entry : clientHandlers) {
                  logCallback("singleScanFailed",  entry.clientInfo, entry.handlerId,
                          "reason=" + reason + ", " + description);
                  entry.reportEvent(WifiScanner.CMD_OP_FAILED, 0,
                          new WifiScanner.OperationResult(reason, description));
              }
              clientHandlers.clear();
          }
  
          void reportFullScanResult(ScanResult result, int bucketsScanned) {
              for (RequestInfo<ScanSettings> entry : mActiveScans) {
                  if (ScanScheduleUtil.shouldReportFullScanResultForSettings(mChannelHelper,
                                  result, bucketsScanned, entry.settings, -1)) {
                      entry.reportEvent(WifiScanner.CMD_FULL_SCAN_RESULT, 0, result);
                  }
              }
  
              for (RequestInfo<Void> entry : mSingleScanListeners) {
                  entry.reportEvent(WifiScanner.CMD_FULL_SCAN_RESULT, 0, result);
              }
          }
  
          void reportScanResults(ScanData results) {
              if (results != null && results.getResults() != null) {
                  if (results.getResults().length > 0) {
                      mWifiMetrics.incrementNonEmptyScanResultCount();
                  } else {
                      mWifiMetrics.incrementEmptyScanResultCount();
                  }
              }
              ScanData[] allResults = new ScanData[] {results};
              for (RequestInfo<ScanSettings> entry : mActiveScans) {
                  ScanData[] resultsToDeliver = ScanScheduleUtil.filterResultsForSettings(
                          mChannelHelper, allResults, entry.settings, -1);
                  WifiScanner.ParcelableScanData parcelableResultsToDeliver =
                          new WifiScanner.ParcelableScanData(resultsToDeliver);
                  logCallback("singleScanResults",  entry.clientInfo, entry.handlerId,
                          describeForLog(resultsToDeliver));
                  entry.reportEvent(WifiScanner.CMD_SCAN_RESULT, 0, parcelableResultsToDeliver);
                  // make sure the handler is removed
                  entry.reportEvent(WifiScanner.CMD_SINGLE_SCAN_COMPLETED, 0, null);
              }
  
              WifiScanner.ParcelableScanData parcelableAllResults =
                      new WifiScanner.ParcelableScanData(allResults);
              for (RequestInfo<Void> entry : mSingleScanListeners) {
                  logCallback("singleScanResults",  entry.clientInfo, entry.handlerId,
                          describeForLog(allResults));
                  entry.reportEvent(WifiScanner.CMD_SCAN_RESULT, 0, parcelableAllResults);
              }
  
              // Cache full band (with DFS or not) scan results.
              if (results.getBandScanned() == WifiScanner.WIFI_BAND_BOTH_WITH_DFS
                      || results.getBandScanned() == WifiScanner.WIFI_BAND_BOTH) {
                  mCachedScanResults.clear();
                  mCachedScanResults.addAll(Arrays.asList(results.getResults()));
              }
          }
  
          List<ScanResult> getCachedScanResultsAsList() {
              return mCachedScanResults;
          }
      }
  
      class WifiBackgroundScanStateMachine extends StateMachine
              implements WifiNative.ScanEventHandler {
  
          private final DefaultState mDefaultState = new DefaultState();
          private final StartedState mStartedState = new StartedState();
          private final PausedState  mPausedState  = new PausedState();
  
          private final RequestList<ScanSettings> mActiveBackgroundScans = new RequestList<>();
  
          WifiBackgroundScanStateMachine(Looper looper) {
              super("WifiBackgroundScanStateMachine", looper);
  
              setLogRecSize(512);
              setLogOnlyTransitions(false);
  
              // CHECKSTYLE:OFF IndentationCheck
              addState(mDefaultState);
                  addState(mStartedState, mDefaultState);
                  addState(mPausedState, mDefaultState);
              // CHECKSTYLE:ON IndentationCheck
  
              setInitialState(mDefaultState);
          }
  
          public Collection<ScanSettings> getBackgroundScanSettings(ClientInfo ci) {
              return mActiveBackgroundScans.getAllSettingsForClient(ci);
          }
  
          public void removeBackgroundScanSettings(ClientInfo ci) {
              mActiveBackgroundScans.removeAllForClient(ci);
              updateSchedule();
          }
  
          @Override
          public void onScanStatus(int event) {
              if (DBG) localLog("onScanStatus event received, event=" + event);
              switch(event) {
                  case WifiNative.WIFI_SCAN_RESULTS_AVAILABLE:
                  case WifiNative.WIFI_SCAN_THRESHOLD_NUM_SCANS:
                  case WifiNative.WIFI_SCAN_THRESHOLD_PERCENT:
                      sendMessage(CMD_SCAN_RESULTS_AVAILABLE);
                      break;
                  case WifiNative.WIFI_SCAN_FAILED:
                      sendMessage(CMD_SCAN_FAILED);
                      break;
                  default:
                      Log.e(TAG, "Unknown scan status event: " + event);
                      break;
              }
          }
  
          @Override
          public void onFullScanResult(ScanResult fullScanResult, int bucketsScanned) {
              if (DBG) localLog("onFullScanResult received");
              sendMessage(CMD_FULL_SCAN_RESULTS, 0, bucketsScanned, fullScanResult);
          }
  
          @Override
          public void onScanPaused(ScanData scanData[]) {
              if (DBG) localLog("onScanPaused received");
              sendMessage(CMD_SCAN_PAUSED, scanData);
          }
  
          @Override
          public void onScanRestarted() {
              if (DBG) localLog("onScanRestarted received");
              sendMessage(CMD_SCAN_RESTARTED);
          }
  
          class DefaultState extends State {
              @Override
              public void enter() {
                  if (DBG) localLog("DefaultState");
                  mActiveBackgroundScans.clear();
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  switch (msg.what) {
                      case CMD_DRIVER_LOADED:
                          // TODO this should be moved to a common location since it is used outside
                          // of this state machine. It is ok right now because the driver loaded event
                          // is sent to this state machine first.
                          mScannerImpl = mScannerImplFactory.create(mContext, mLooper, mClock);
                          if (mScannerImpl == null) {
                              loge("Failed to start bgscan scan state machine because scanner impl"
                                      + " is null");
                              return HANDLED;
                          }
                          mChannelHelper = mScannerImpl.getChannelHelper();
  
                          mBackgroundScheduler = new BackgroundScanScheduler(mChannelHelper);
  
                          WifiNative.ScanCapabilities capabilities =
                                  new WifiNative.ScanCapabilities();
                          if (!mScannerImpl.getScanCapabilities(capabilities)) {
                              loge("could not get scan capabilities");
                              return HANDLED;
                          }
                          if (capabilities.max_scan_buckets <= 0) {
                              loge("invalid max buckets in scan capabilities "
                                      + capabilities.max_scan_buckets);
                              return HANDLED;
                          }
                          mBackgroundScheduler.setMaxBuckets(capabilities.max_scan_buckets);
                          mBackgroundScheduler.setMaxApPerScan(capabilities.max_ap_cache_per_scan);
  
                          Log.i(TAG, "wifi driver loaded with scan capabilities: "
                                  + "max buckets=" + capabilities.max_scan_buckets);
  
                          transitionTo(mStartedState);
                          return HANDLED;
                      case CMD_DRIVER_UNLOADED:
                          Log.i(TAG, "wifi driver unloaded");
                          transitionTo(mDefaultState);
                          break;
                      case WifiScanner.CMD_START_BACKGROUND_SCAN:
                      case WifiScanner.CMD_STOP_BACKGROUND_SCAN:
                      case WifiScanner.CMD_START_SINGLE_SCAN:
                      case WifiScanner.CMD_STOP_SINGLE_SCAN:
                      case WifiScanner.CMD_GET_SCAN_RESULTS:
                          replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "not available");
                          break;
  
                      case CMD_SCAN_RESULTS_AVAILABLE:
                          if (DBG) localLog("ignored scan results available event");
                          break;
  
                      case CMD_FULL_SCAN_RESULTS:
                          if (DBG) localLog("ignored full scan result event");
                          break;
  
                      default:
                          break;
                  }
  
                  return HANDLED;
              }
          }
  
          class StartedState extends State {
  
              @Override
              public void enter() {
                  if (DBG) localLog("StartedState");
              }
  
              @Override
              public void exit() {
                  sendBackgroundScanFailedToAllAndClear(
                          WifiScanner.REASON_UNSPECIFIED, "Scan was interrupted");
                  if (mScannerImpl != null) {
                      mScannerImpl.cleanup();
                  }
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  ClientInfo ci = mClients.get(msg.replyTo);
  
                  switch (msg.what) {
                      case CMD_DRIVER_LOADED:
                          Log.e(TAG, "wifi driver loaded received while already loaded");
                          // Ignore if we're already in driver loaded state.
                          return HANDLED;
                      case CMD_DRIVER_UNLOADED:
                          return NOT_HANDLED;
                      case WifiScanner.CMD_START_BACKGROUND_SCAN: {
                          mWifiMetrics.incrementBackgroundScanCount();
                          Bundle scanParams = (Bundle) msg.obj;
                          if (scanParams == null) {
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null");
                              return HANDLED;
                          }
                          scanParams.setDefusable(true);
                          ScanSettings scanSettings =
                                  scanParams.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
                          WorkSource workSource =
                                  scanParams.getParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY);
                          if (addBackgroundScanRequest(ci, msg.arg2, scanSettings, workSource)) {
                              replySucceeded(msg);
                          } else {
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request");
                          }
                          break;
                      }
                      case WifiScanner.CMD_STOP_BACKGROUND_SCAN:
                          removeBackgroundScanRequest(ci, msg.arg2);
                          break;
                      case WifiScanner.CMD_GET_SCAN_RESULTS:
                          reportScanResults(mScannerImpl.getLatestBatchedScanResults(true));
                          replySucceeded(msg);
                          break;
                      case CMD_SCAN_RESULTS_AVAILABLE:
                          reportScanResults(mScannerImpl.getLatestBatchedScanResults(true));
                          break;
                      case CMD_FULL_SCAN_RESULTS:
                          reportFullScanResult((ScanResult) msg.obj, /* bucketsScanned */ msg.arg2);
                          break;
                      case CMD_SCAN_PAUSED:
                          reportScanResults((ScanData[]) msg.obj);
                          transitionTo(mPausedState);
                          break;
                      case CMD_SCAN_FAILED:
                          Log.e(TAG, "WifiScanner background scan gave CMD_SCAN_FAILED");
                          sendBackgroundScanFailedToAllAndClear(
                                  WifiScanner.REASON_UNSPECIFIED, "Background Scan failed");
                          break;
                      default:
                          return NOT_HANDLED;
                  }
  
                  return HANDLED;
              }
          }
  
          class PausedState extends State {
              @Override
              public void enter() {
                  if (DBG) localLog("PausedState");
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  switch (msg.what) {
                      case CMD_SCAN_RESTARTED:
                          transitionTo(mStartedState);
                          break;
                      default:
                          deferMessage(msg);
                          break;
                  }
                  return HANDLED;
              }
          }
  
          private boolean addBackgroundScanRequest(ClientInfo ci, int handler,
                  ScanSettings settings, WorkSource workSource) {
              // sanity check the input
              if (ci == null) {
                  Log.d(TAG, "Failing scan request ClientInfo not found " + handler);
                  return false;
              }
              if (settings.periodInMs < WifiScanner.MIN_SCAN_PERIOD_MS) {
                  loge("Failing scan request because periodInMs is " + settings.periodInMs
                          + ", min scan period is: " + WifiScanner.MIN_SCAN_PERIOD_MS);
                  return false;
              }
  
              if (settings.band == WifiScanner.WIFI_BAND_UNSPECIFIED && settings.channels == null) {
                  loge("Channels was null with unspecified band");
                  return false;
              }
  
              if (settings.band == WifiScanner.WIFI_BAND_UNSPECIFIED
                      && settings.channels.length == 0) {
                  loge("No channels specified");
                  return false;
              }
  
              int minSupportedPeriodMs = mChannelHelper.estimateScanDuration(settings);
              if (settings.periodInMs < minSupportedPeriodMs) {
                  loge("Failing scan request because minSupportedPeriodMs is "
                          + minSupportedPeriodMs + " but the request wants " + settings.periodInMs);
                  return false;
              }
  
              // check truncated binary exponential back off scan settings
              if (settings.maxPeriodInMs != 0 && settings.maxPeriodInMs != settings.periodInMs) {
                  if (settings.maxPeriodInMs < settings.periodInMs) {
                      loge("Failing scan request because maxPeriodInMs is " + settings.maxPeriodInMs
                              + " but less than periodInMs " + settings.periodInMs);
                      return false;
                  }
                  if (settings.maxPeriodInMs > WifiScanner.MAX_SCAN_PERIOD_MS) {
                      loge("Failing scan request because maxSupportedPeriodMs is "
                              + WifiScanner.MAX_SCAN_PERIOD_MS + " but the request wants "
                              + settings.maxPeriodInMs);
                      return false;
                  }
                  if (settings.stepCount < 1) {
                      loge("Failing scan request because stepCount is " + settings.stepCount
                              + " which is less than 1");
                      return false;
                  }
              }
  
              logScanRequest("addBackgroundScanRequest", ci, handler, null, settings, null);
              mActiveBackgroundScans.addRequest(ci, handler, workSource, settings);
  
              if (updateSchedule()) {
                  return true;
              } else {
                  mActiveBackgroundScans.removeRequest(ci, handler);
                  localLog("Failing scan request because failed to reset scan");
                  return false;
              }
          }
  
          private boolean updateSchedule() {
              if (mChannelHelper == null || mBackgroundScheduler == null || mScannerImpl == null) {
                  loge("Failed to update schedule because WifiScanningService is not initialized");
                  return false;
              }
              mChannelHelper.updateChannels();
              Collection<ScanSettings> settings = mActiveBackgroundScans.getAllSettings();
  
              mBackgroundScheduler.updateSchedule(settings);
              WifiNative.ScanSettings schedule = mBackgroundScheduler.getSchedule();
  
              if (ScanScheduleUtil.scheduleEquals(mPreviousSchedule, schedule)) {
                  if (DBG) Log.d(TAG, "schedule updated with no change");
                  return true;
              }
  
              mPreviousSchedule = schedule;
  
              if (schedule.num_buckets == 0) {
                  mScannerImpl.stopBatchedScan();
                  if (DBG) Log.d(TAG, "scan stopped");
                  return true;
              } else {
                  localLog("starting scan: "
                          + "base period=" + schedule.base_period_ms
                          + ", max ap per scan=" + schedule.max_ap_per_scan
                          + ", batched scans=" + schedule.report_threshold_num_scans);
                  for (int b = 0; b < schedule.num_buckets; b++) {
                      WifiNative.BucketSettings bucket = schedule.buckets[b];
                      localLog("bucket " + bucket.bucket + " (" + bucket.period_ms + "ms)"
                              + "[" + bucket.report_events + "]: "
                              + ChannelHelper.toString(bucket));
                  }
  
                  if (mScannerImpl.startBatchedScan(schedule, this)) {
                      if (DBG) {
                          Log.d(TAG, "scan restarted with " + schedule.num_buckets
                                  + " bucket(s) and base period: " + schedule.base_period_ms);
                      }
                      return true;
                  } else {
                      mPreviousSchedule = null;
                      loge("error starting scan: "
                              + "base period=" + schedule.base_period_ms
                              + ", max ap per scan=" + schedule.max_ap_per_scan
                              + ", batched scans=" + schedule.report_threshold_num_scans);
                      for (int b = 0; b < schedule.num_buckets; b++) {
                          WifiNative.BucketSettings bucket = schedule.buckets[b];
                          loge("bucket " + bucket.bucket + " (" + bucket.period_ms + "ms)"
                                  + "[" + bucket.report_events + "]: "
                                  + ChannelHelper.toString(bucket));
                      }
                      return false;
                  }
              }
          }
  
          private void removeBackgroundScanRequest(ClientInfo ci, int handler) {
              if (ci != null) {
                  ScanSettings settings = mActiveBackgroundScans.removeRequest(ci, handler);
                  logScanRequest("removeBackgroundScanRequest", ci, handler, null, settings, null);
                  updateSchedule();
              }
          }
  
          private void reportFullScanResult(ScanResult result, int bucketsScanned) {
              for (RequestInfo<ScanSettings> entry : mActiveBackgroundScans) {
                  ClientInfo ci = entry.clientInfo;
                  int handler = entry.handlerId;
                  ScanSettings settings = entry.settings;
                  if (mBackgroundScheduler.shouldReportFullScanResultForSettings(
                                  result, bucketsScanned, settings)) {
                      ScanResult newResult = new ScanResult(result);
                      if (result.informationElements != null) {
                          newResult.informationElements = result.informationElements.clone();
                      }
                      else {
                          newResult.informationElements = null;
                      }
                      ci.reportEvent(WifiScanner.CMD_FULL_SCAN_RESULT, 0, handler, newResult);
                  }
              }
          }
  
          private void reportScanResults(ScanData[] results) {
              if (results == null) {
                  Log.d(TAG,"The results is null, nothing to report.");
                  return;
              }
              for (ScanData result : results) {
                  if (result != null && result.getResults() != null) {
                      if (result.getResults().length > 0) {
                          mWifiMetrics.incrementNonEmptyScanResultCount();
                      } else {
                          mWifiMetrics.incrementEmptyScanResultCount();
                      }
                  }
              }
              for (RequestInfo<ScanSettings> entry : mActiveBackgroundScans) {
                  ClientInfo ci = entry.clientInfo;
                  int handler = entry.handlerId;
                  ScanSettings settings = entry.settings;
                  ScanData[] resultsToDeliver =
                          mBackgroundScheduler.filterResultsForSettings(results, settings);
                  if (resultsToDeliver != null) {
                      logCallback("backgroundScanResults", ci, handler,
                              describeForLog(resultsToDeliver));
                      WifiScanner.ParcelableScanData parcelableScanData =
                              new WifiScanner.ParcelableScanData(resultsToDeliver);
                      ci.reportEvent(WifiScanner.CMD_SCAN_RESULT, 0, handler, parcelableScanData);
                  }
              }
          }
  
          private void sendBackgroundScanFailedToAllAndClear(int reason, String description) {
              for (RequestInfo<ScanSettings> entry : mActiveBackgroundScans) {
                  ClientInfo ci = entry.clientInfo;
                  int handler = entry.handlerId;
                  ci.reportEvent(WifiScanner.CMD_OP_FAILED, 0, handler,
                          new WifiScanner.OperationResult(reason, description));
              }
              mActiveBackgroundScans.clear();
          }
      }
  
      /**
       * PNO scan state machine has 5 states:
       * -Default State
       *   -Started State
       *     -Hw Pno Scan state
       *       -Single Scan state
       *
       * These are the main state transitions:
       * 1. Start at |Default State|
       * 2. Move to |Started State| when we get the |WIFI_SCAN_AVAILABLE| broadcast from WifiManager.
       * 3. When a new PNO scan request comes in:
       *   a.1. Switch to |Hw Pno Scan state| when the device supports HW PNO
       *        (This could either be HAL based ePNO or wificond based PNO).
       *   a.2. In |Hw Pno Scan state| when PNO scan results are received, check if the result
       *        contains IE (information elements). If yes, send the results to the client, else
       *        switch to |Single Scan state| and send the result to the client when the scan result
       *        is obtained.
       *
       * Note: PNO scans only work for a single client today. We don't have support in HW to support
       * multiple requests at the same time, so will need non-trivial changes to support (if at all
       * possible) in WifiScanningService.
       */
      class WifiPnoScanStateMachine extends StateMachine implements WifiNative.PnoEventHandler {
  
          private final DefaultState mDefaultState = new DefaultState();
          private final StartedState mStartedState = new StartedState();
          private final HwPnoScanState mHwPnoScanState = new HwPnoScanState();
          private final SingleScanState mSingleScanState = new SingleScanState();
          private InternalClientInfo mInternalClientInfo;
  
          private final RequestList<Pair<PnoSettings, ScanSettings>> mActivePnoScans =
                  new RequestList<>();
  
          WifiPnoScanStateMachine(Looper looper) {
              super("WifiPnoScanStateMachine", looper);
  
              setLogRecSize(256);
              setLogOnlyTransitions(false);
  
              // CHECKSTYLE:OFF IndentationCheck
              addState(mDefaultState);
                  addState(mStartedState, mDefaultState);
                      addState(mHwPnoScanState, mStartedState);
                          addState(mSingleScanState, mHwPnoScanState);
              // CHECKSTYLE:ON IndentationCheck
  
              setInitialState(mDefaultState);
          }
  
          public void removePnoSettings(ClientInfo ci) {
              mActivePnoScans.removeAllForClient(ci);
              transitionTo(mStartedState);
          }
  
          @Override
          public void onPnoNetworkFound(ScanResult[] results) {
              if (DBG) localLog("onWifiPnoNetworkFound event received");
              sendMessage(CMD_PNO_NETWORK_FOUND, 0, 0, results);
          }
  
          @Override
          public void onPnoScanFailed() {
              if (DBG) localLog("onWifiPnoScanFailed event received");
              sendMessage(CMD_PNO_SCAN_FAILED, 0, 0, null);
          }
  
          class DefaultState extends State {
              @Override
              public void enter() {
                  if (DBG) localLog("DefaultState");
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  switch (msg.what) {
                      case CMD_DRIVER_LOADED:
                          if (mScannerImpl == null) {
                              loge("Failed to start pno scan state machine because scanner impl"
                                      + " is null");
                              return HANDLED;
                          }
                          transitionTo(mStartedState);
                          break;
                      case CMD_DRIVER_UNLOADED:
                          transitionTo(mDefaultState);
                          break;
                      case WifiScanner.CMD_START_PNO_SCAN:
                      case WifiScanner.CMD_STOP_PNO_SCAN:
                          replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "not available");
                          break;
                      case CMD_PNO_NETWORK_FOUND:
                      case CMD_PNO_SCAN_FAILED:
                      case WifiScanner.CMD_SCAN_RESULT:
                      case WifiScanner.CMD_OP_FAILED:
                          loge("Unexpected message " + msg.what);
                          break;
                      default:
                          return NOT_HANDLED;
                  }
                  return HANDLED;
              }
          }
  
          class StartedState extends State {
              @Override
              public void enter() {
                  if (DBG) localLog("StartedState");
              }
  
              @Override
              public void exit() {
                  sendPnoScanFailedToAllAndClear(
                          WifiScanner.REASON_UNSPECIFIED, "Scan was interrupted");
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  ClientInfo ci = mClients.get(msg.replyTo);
                  switch (msg.what) {
                      case CMD_DRIVER_LOADED:
                          // Ignore if we're already in driver loaded state.
                          return HANDLED;
                      case WifiScanner.CMD_START_PNO_SCAN:
                          Bundle pnoParams = (Bundle) msg.obj;
                          if (pnoParams == null) {
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null");
                              return HANDLED;
                          }
                          pnoParams.setDefusable(true);
                          PnoSettings pnoSettings =
                                  pnoParams.getParcelable(WifiScanner.PNO_PARAMS_PNO_SETTINGS_KEY);
                          if (mScannerImpl.isHwPnoSupported(pnoSettings.isConnected)) {
                              deferMessage(msg);
                              transitionTo(mHwPnoScanState);
                          } else {
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "not supported");
                          }
                          break;
                      case WifiScanner.CMD_STOP_PNO_SCAN:
                          replyFailed(msg, WifiScanner.REASON_UNSPECIFIED, "no scan running");
                          break;
                      default:
                          return NOT_HANDLED;
                  }
                  return HANDLED;
              }
          }
  
          class HwPnoScanState extends State {
              @Override
              public void enter() {
                  if (DBG) localLog("HwPnoScanState");
              }
  
              @Override
              public void exit() {
                  // Reset PNO scan in ScannerImpl before we exit.
                  mScannerImpl.resetHwPnoList();
                  removeInternalClient();
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  ClientInfo ci = mClients.get(msg.replyTo);
                  switch (msg.what) {
                      case WifiScanner.CMD_START_PNO_SCAN:
                          Bundle pnoParams = (Bundle) msg.obj;
                          if (pnoParams == null) {
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null");
                              return HANDLED;
                          }
                          pnoParams.setDefusable(true);
                          PnoSettings pnoSettings =
                                  pnoParams.getParcelable(WifiScanner.PNO_PARAMS_PNO_SETTINGS_KEY);
                          ScanSettings scanSettings =
                                  pnoParams.getParcelable(WifiScanner.PNO_PARAMS_SCAN_SETTINGS_KEY);
                          if (addHwPnoScanRequest(ci, msg.arg2, scanSettings, pnoSettings)) {
                              replySucceeded(msg);
                          } else {
                              replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request");
                              transitionTo(mStartedState);
                          }
                          break;
                      case WifiScanner.CMD_STOP_PNO_SCAN:
                          removeHwPnoScanRequest(ci, msg.arg2);
                          transitionTo(mStartedState);
                          break;
                      case CMD_PNO_NETWORK_FOUND:
                          ScanResult[] scanResults = ((ScanResult[]) msg.obj);
                          if (isSingleScanNeeded(scanResults)) {
                              ScanSettings activeScanSettings = getScanSettings();
                              if (activeScanSettings == null) {
                                  sendPnoScanFailedToAllAndClear(
                                          WifiScanner.REASON_UNSPECIFIED,
                                          "couldn't retrieve setting");
                                  transitionTo(mStartedState);
                              } else {
                                  addSingleScanRequest(activeScanSettings);
                                  transitionTo(mSingleScanState);
                              }
                          } else {
                              reportPnoNetworkFound((ScanResult[]) msg.obj);
                          }
                          break;
                      case CMD_PNO_SCAN_FAILED:
                          sendPnoScanFailedToAllAndClear(
                                  WifiScanner.REASON_UNSPECIFIED, "pno scan failed");
                          transitionTo(mStartedState);
                          break;
                      default:
                          return NOT_HANDLED;
                  }
                  return HANDLED;
              }
          }
  
          class SingleScanState extends State {
              @Override
              public void enter() {
                  if (DBG) localLog("SingleScanState");
              }
  
              @Override
              public boolean processMessage(Message msg) {
                  ClientInfo ci = mClients.get(msg.replyTo);
                  switch (msg.what) {
                      case WifiScanner.CMD_SCAN_RESULT:
                          WifiScanner.ParcelableScanData parcelableScanData =
                                  (WifiScanner.ParcelableScanData) msg.obj;
                          ScanData[] scanDatas = parcelableScanData.getResults();
                          ScanData lastScanData = scanDatas[scanDatas.length - 1];
                          reportPnoNetworkFound(lastScanData.getResults());
                          transitionTo(mHwPnoScanState);
                          break;
                      case WifiScanner.CMD_OP_FAILED:
                          sendPnoScanFailedToAllAndClear(
                                  WifiScanner.REASON_UNSPECIFIED, "single scan failed");
                          transitionTo(mStartedState);
                          break;
                      default:
                          return NOT_HANDLED;
                  }
                  return HANDLED;
              }
          }
  
          private WifiNative.PnoSettings convertSettingsToPnoNative(ScanSettings scanSettings,
                                                                    PnoSettings pnoSettings) {
              WifiNative.PnoSettings nativePnoSetting = new WifiNative.PnoSettings();
              nativePnoSetting.periodInMs = scanSettings.periodInMs;
              nativePnoSetting.min5GHzRssi = pnoSettings.min5GHzRssi;
              nativePnoSetting.min24GHzRssi = pnoSettings.min24GHzRssi;
              nativePnoSetting.initialScoreMax = pnoSettings.initialScoreMax;
              nativePnoSetting.currentConnectionBonus = pnoSettings.currentConnectionBonus;
              nativePnoSetting.sameNetworkBonus = pnoSettings.sameNetworkBonus;
              nativePnoSetting.secureBonus = pnoSettings.secureBonus;
              nativePnoSetting.band5GHzBonus = pnoSettings.band5GHzBonus;
              nativePnoSetting.isConnected = pnoSettings.isConnected;
              nativePnoSetting.networkList =
                      new WifiNative.PnoNetwork[pnoSettings.networkList.length];
              for (int i = 0; i < pnoSettings.networkList.length; i++) {
                  nativePnoSetting.networkList[i] = new WifiNative.PnoNetwork();
                  nativePnoSetting.networkList[i].ssid = pnoSettings.networkList[i].ssid;
                  nativePnoSetting.networkList[i].flags = pnoSettings.networkList[i].flags;
                  nativePnoSetting.networkList[i].auth_bit_field =
                          pnoSettings.networkList[i].authBitField;
                  nativePnoSetting.networkList[i].frequencies =
                          pnoSettings.networkList[i].frequencies;
              }
              return nativePnoSetting;
          }
  
          // Retrieve the only active scan settings.
          private ScanSettings getScanSettings() {
              for (Pair<PnoSettings, ScanSettings> settingsPair : mActivePnoScans.getAllSettings()) {
                  return settingsPair.second;
              }
              return null;
          }
  
          private void removeInternalClient() {
              if (mInternalClientInfo != null) {
                  mInternalClientInfo.cleanup();
                  mInternalClientInfo = null;
              } else {
                  Log.w(TAG, "No Internal client for PNO");
              }
          }
  
          private void addInternalClient(ClientInfo ci) {
              if (mInternalClientInfo == null) {
                  mInternalClientInfo =
                          new InternalClientInfo(ci.getUid(), new Messenger(this.getHandler()));
                  mInternalClientInfo.register();
              } else {
                  Log.w(TAG, "Internal client for PNO already exists");
              }
          }
  
          private void addPnoScanRequest(ClientInfo ci, int handler, ScanSettings scanSettings,
                  PnoSettings pnoSettings) {
              mActivePnoScans.addRequest(ci, handler, ClientModeImpl.WIFI_WORK_SOURCE,
                      Pair.create(pnoSettings, scanSettings));
              addInternalClient(ci);
          }
  
          private Pair<PnoSettings, ScanSettings> removePnoScanRequest(ClientInfo ci, int handler) {
              Pair<PnoSettings, ScanSettings> settings = mActivePnoScans.removeRequest(ci, handler);
              return settings;
          }
  
          private boolean addHwPnoScanRequest(ClientInfo ci, int handler, ScanSettings scanSettings,
                  PnoSettings pnoSettings) {
              if (ci == null) {
                  Log.d(TAG, "Failing scan request ClientInfo not found " + handler);
                  return false;
              }
              if (!mActivePnoScans.isEmpty()) {
                  loge("Failing scan request because there is already an active scan");
                  return false;
              }
              WifiNative.PnoSettings nativePnoSettings =
                      convertSettingsToPnoNative(scanSettings, pnoSettings);
              if (!mScannerImpl.setHwPnoList(nativePnoSettings, mPnoScanStateMachine)) {
                  return false;
              }
              logScanRequest("addHwPnoScanRequest", ci, handler, null, scanSettings, pnoSettings);
              addPnoScanRequest(ci, handler, scanSettings, pnoSettings);
  
              return true;
          }
  
          private void removeHwPnoScanRequest(ClientInfo ci, int handler) {
              if (ci != null) {
                  Pair<PnoSettings, ScanSettings> settings = removePnoScanRequest(ci, handler);
                  logScanRequest("removeHwPnoScanRequest", ci, handler, null,
                          settings.second, settings.first);
              }
          }
  
          private void reportPnoNetworkFound(ScanResult[] results) {
              WifiScanner.ParcelableScanResults parcelableScanResults =
                      new WifiScanner.ParcelableScanResults(results);
              for (RequestInfo<Pair<PnoSettings, ScanSettings>> entry : mActivePnoScans) {
                  ClientInfo ci = entry.clientInfo;
                  int handler = entry.handlerId;
                  logCallback("pnoNetworkFound", ci, handler, describeForLog(results));
                  ci.reportEvent(
                          WifiScanner.CMD_PNO_NETWORK_FOUND, 0, handler, parcelableScanResults);
              }
          }
  
          private void sendPnoScanFailedToAllAndClear(int reason, String description) {
              for (RequestInfo<Pair<PnoSettings, ScanSettings>> entry : mActivePnoScans) {
                  ClientInfo ci = entry.clientInfo;
                  int handler = entry.handlerId;
                  ci.reportEvent(WifiScanner.CMD_OP_FAILED, 0, handler,
                          new WifiScanner.OperationResult(reason, description));
              }
              mActivePnoScans.clear();
          }
  
          private void addSingleScanRequest(ScanSettings settings) {
              if (DBG) localLog("Starting single scan");
              if (mInternalClientInfo != null) {
                  mInternalClientInfo.sendRequestToClientHandler(
                          WifiScanner.CMD_START_SINGLE_SCAN, settings,
                          ClientModeImpl.WIFI_WORK_SOURCE);
              }
          }
  
          /**
           * Checks if IE are present in scan data, if no single scan is needed to report event to
           * client
           */
          private boolean isSingleScanNeeded(ScanResult[] scanResults) {
              for (ScanResult scanResult : scanResults) {
                  if (scanResult.informationElements != null
                          && scanResult.informationElements.length > 0) {
                      return false;
                  }
              }
              return true;
          }
      }
  
      private abstract class ClientInfo {
          private final int mUid;
          private final WorkSource mWorkSource;
          private boolean mScanWorkReported = false;
          protected final Messenger mMessenger;
  
          ClientInfo(int uid, Messenger messenger) {
              mUid = uid;
              mMessenger = messenger;
              mWorkSource = new WorkSource(uid);
          }
  
          /**
           * Register this client to main client map.
           */
          public void register() {
              mClients.put(mMessenger, this);
          }
  
          /**
           * Unregister this client from main client map.
           */
          private void unregister() {
              mClients.remove(mMessenger);
          }
  
          public void cleanup() {
              mSingleScanListeners.removeAllForClient(this);
              mSingleScanStateMachine.removeSingleScanRequests(this);
              mBackgroundScanStateMachine.removeBackgroundScanSettings(this);
              unregister();
              localLog("Successfully stopped all requests for client " + this);
          }
  
          public int getUid() {
              return mUid;
          }
  
          public void reportEvent(int what, int arg1, int arg2) {
              reportEvent(what, arg1, arg2, null);
          }
  
          // This has to be implemented by subclasses to report events back to clients.
          public abstract void reportEvent(int what, int arg1, int arg2, Object obj);
  
          // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ?
          private void reportBatchedScanStart() {
              if (mUid == 0)
                  return;
  
              int csph = getCsph();
  
              try {
                  mBatteryStats.noteWifiBatchedScanStartedFromSource(mWorkSource, csph);
              } catch (RemoteException e) {
                  logw("failed to report scan work: " + e.toString());
              }
          }
  
          // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ?
          private void reportBatchedScanStop() {
              if (mUid == 0)
                  return;
  
              try {
                  mBatteryStats.noteWifiBatchedScanStoppedFromSource(mWorkSource);
              } catch (RemoteException e) {
                  logw("failed to cleanup scan work: " + e.toString());
              }
          }
  
          // TODO migrate batterystats to accept scan duration per hour instead of csph
          private int getCsph() {
              int totalScanDurationPerHour = 0;
              Collection<ScanSettings> settingsList =
                      mBackgroundScanStateMachine.getBackgroundScanSettings(this);
              for (ScanSettings settings : settingsList) {
                  int scanDurationMs = mChannelHelper.estimateScanDuration(settings);
                  int scans_per_Hour = settings.periodInMs == 0 ? 1 : (3600 * 1000) /
                          settings.periodInMs;
                  totalScanDurationPerHour += scanDurationMs * scans_per_Hour;
              }
  
              return totalScanDurationPerHour / ChannelHelper.SCAN_PERIOD_PER_CHANNEL_MS;
          }
  
          // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ?
          private void reportScanWorkUpdate() {
              if (mScanWorkReported) {
                  reportBatchedScanStop();
                  mScanWorkReported = false;
              }
              if (mBackgroundScanStateMachine.getBackgroundScanSettings(this).isEmpty()) {
                  reportBatchedScanStart();
                  mScanWorkReported = true;
              }
          }
  
          @Override
          public String toString() {
              return "ClientInfo[uid=" + mUid + "," + mMessenger + "]";
          }
      }
  
      /**
       * This class is used to represent external clients to the WifiScanning Service.
       */
      private class ExternalClientInfo extends ClientInfo {
          private final AsyncChannel mChannel;
          /**
           * Indicates if the client is still connected
           * If the client is no longer connected then messages to it will be silently dropped
           */
          private boolean mDisconnected = false;
  
          ExternalClientInfo(int uid, Messenger messenger, AsyncChannel c) {
              super(uid, messenger);
              mChannel = c;
              if (DBG) localLog("New client, channel: " + c);
          }
  
          @Override
          public void reportEvent(int what, int arg1, int arg2, Object obj) {
              if (!mDisconnected) {
                  mChannel.sendMessage(what, arg1, arg2, obj);
              }
          }
  
          @Override
          public void cleanup() {
              mDisconnected = true;
              mPnoScanStateMachine.removePnoSettings(this);
              super.cleanup();
          }
      }
  
      /**
       * This class is used to represent internal clients to the WifiScanning Service. This is needed
       * for communicating between State Machines.
       * This leaves the onReportEvent method unimplemented, so that the clients have the freedom
       * to handle the events as they need.
       */
      private class InternalClientInfo extends ClientInfo {
          private static final int INTERNAL_CLIENT_HANDLER = 0;
  
          /**
           * The UID here is used to proxy the original external requester UID.
           */
          InternalClientInfo(int requesterUid, Messenger messenger) {
              super(requesterUid, messenger);
          }
  
          @Override
          public void reportEvent(int what, int arg1, int arg2, Object obj) {
              Message message = Message.obtain();
              message.what = what;
              message.arg1 = arg1;
              message.arg2 = arg2;
              message.obj = obj;
              try {
                  mMessenger.send(message);
              } catch (RemoteException e) {
                  loge("Failed to send message: " + what);
              }
          }
  
          /**
           * Send a message to the client handler which should reroute the message to the appropriate
           * state machine.
           */
          public void sendRequestToClientHandler(int what, ScanSettings settings,
                  WorkSource workSource) {
              Message msg = Message.obtain();
              msg.what = what;
              msg.arg2 = INTERNAL_CLIENT_HANDLER;
              if (settings != null) {
                  Bundle bundle = new Bundle();
                  bundle.putParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
                  bundle.putParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
                  msg.obj = bundle;
              }
              msg.replyTo = mMessenger;
              msg.sendingUid = getUid();
              mClientHandler.sendMessage(msg);
          }
  
          /**
           * Send a message to the client handler which should reroute the message to the appropriate
           * state machine.
           */
          public void sendRequestToClientHandler(int what) {
              sendRequestToClientHandler(what, null, null);
          }
  
          @Override
          public String toString() {
              return "InternalClientInfo[]";
          }
      }
  
      void replySucceeded(Message msg) {
          if (msg.replyTo != null) {
              Message reply = Message.obtain();
              reply.what = WifiScanner.CMD_OP_SUCCEEDED;
              reply.arg2 = msg.arg2;
              if (msg.obj != null) {
                  reply.obj = msg.obj;
              }
              try {
                  msg.replyTo.send(reply);
                  mLog.trace("replySucceeded recvdMessage=%").c(msg.what).flush();
              } catch (RemoteException e) {
                  // There's not much we can do if reply can't be sent!
              }
          } else {
              // locally generated message; doesn't need a reply!
          }
      }
  
      void replyFailed(Message msg, int reason, String description) {
          if (msg.replyTo != null) {
              Message reply = Message.obtain();
              reply.what = WifiScanner.CMD_OP_FAILED;
              reply.arg2 = msg.arg2;
              reply.obj = new WifiScanner.OperationResult(reason, description);
              try {
                  msg.replyTo.send(reply);
                  mLog.trace("replyFailed recvdMessage=% reason=%")
                              .c(msg.what)
                              .c(reason)
                              .flush();
              } catch (RemoteException e) {
                  // There's not much we can do if reply can't be sent!
              }
          } else {
              // locally generated message; doesn't need a reply!
          }
      }
  
      private static String toString(int uid, ScanSettings settings) {
          StringBuilder sb = new StringBuilder();
          sb.append("ScanSettings[uid=").append(uid);
          sb.append(", period=").append(settings.periodInMs);
          sb.append(", report=").append(settings.reportEvents);
          if (settings.reportEvents == WifiScanner.REPORT_EVENT_AFTER_BUFFER_FULL
                  && settings.numBssidsPerScan > 0
                  && settings.maxScansToCache > 1) {
              sb.append(", batch=").append(settings.maxScansToCache);
              sb.append(", numAP=").append(settings.numBssidsPerScan);
          }
          sb.append(", ").append(ChannelHelper.toString(settings));
          sb.append("]");
  
          return sb.toString();
      }
  
      @Override
      protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
                  != PERMISSION_GRANTED) {
              pw.println("Permission Denial: can't dump WifiScanner from from pid="
                      + Binder.getCallingPid()
                      + ", uid=" + Binder.getCallingUid()
                      + " without permission "
                      + android.Manifest.permission.DUMP);
              return;
          }
          pw.println("WifiScanningService - Log Begin ----");
          mLocalLog.dump(fd, pw, args);
          pw.println("WifiScanningService - Log End ----");
          pw.println();
          pw.println("clients:");
          for (ClientInfo client : mClients.values()) {
              pw.println("  " + client);
          }
          pw.println("listeners:");
          for (ClientInfo client : mClients.values()) {
              Collection<ScanSettings> settingsList =
                      mBackgroundScanStateMachine.getBackgroundScanSettings(client);
              for (ScanSettings settings : settingsList) {
                  pw.println("  " + toString(client.mUid, settings));
              }
          }
          if (mBackgroundScheduler != null) {
              WifiNative.ScanSettings schedule = mBackgroundScheduler.getSchedule();
              if (schedule != null) {
                  pw.println("schedule:");
                  pw.println("  base period: " + schedule.base_period_ms);
                  pw.println("  max ap per scan: " + schedule.max_ap_per_scan);
                  pw.println("  batched scans: " + schedule.report_threshold_num_scans);
                  pw.println("  buckets:");
                  for (int b = 0; b < schedule.num_buckets; b++) {
                      WifiNative.BucketSettings bucket = schedule.buckets[b];
                      pw.println("    bucket " + bucket.bucket + " (" + bucket.period_ms + "ms)["
                              + bucket.report_events + "]: "
                              + ChannelHelper.toString(bucket));
                  }
              }
          }
          if (mPnoScanStateMachine != null) {
              mPnoScanStateMachine.dump(fd, pw, args);
          }
          pw.println();
  
          if (mSingleScanStateMachine != null) {
              mSingleScanStateMachine.dump(fd, pw, args);
              pw.println();
              pw.println("Latest scan results:");
              List<ScanResult> scanResults = mSingleScanStateMachine.getCachedScanResultsAsList();
              long nowMs = mClock.getElapsedSinceBootMillis();
              ScanResultUtil.dumpScanResults(pw, scanResults, nowMs);
              pw.println();
          }
          if (mScannerImpl != null) {
              mScannerImpl.dump(fd, pw, args);
          }
      }
  
      void logScanRequest(String request, ClientInfo ci, int id, WorkSource workSource,
              ScanSettings settings, PnoSettings pnoSettings) {
          StringBuilder sb = new StringBuilder();
          sb.append(request)
                  .append(": ")
                  .append((ci == null) ? "ClientInfo[unknown]" : ci.toString())
                  .append(",Id=")
                  .append(id);
          if (workSource != null) {
              sb.append(",").append(workSource);
          }
          if (settings != null) {
              sb.append(", ");
              describeTo(sb, settings);
          }
          if (pnoSettings != null) {
              sb.append(", ");
              describeTo(sb, pnoSettings);
          }
          localLog(sb.toString());
      }
  
      void logCallback(String callback, ClientInfo ci, int id, String extra) {
          StringBuilder sb = new StringBuilder();
          sb.append(callback)
                  .append(": ")
                  .append((ci == null) ? "ClientInfo[unknown]" : ci.toString())
                  .append(",Id=")
                  .append(id);
          if (extra != null) {
              sb.append(",").append(extra);
          }
          localLog(sb.toString());
      }
  
      static String describeForLog(ScanData[] results) {
          StringBuilder sb = new StringBuilder();
          sb.append("results=");
          for (int i = 0; i < results.length; ++i) {
              if (i > 0) sb.append(";");
              sb.append(results[i].getResults().length);
          }
          return sb.toString();
      }
  
      static String describeForLog(ScanResult[] results) {
          return "results=" + results.length;
      }
  
      static String getScanTypeString(int type) {
          switch(type) {
              case WifiScanner.TYPE_LOW_LATENCY:
                  return "LOW LATENCY";
              case WifiScanner.TYPE_LOW_POWER:
                  return "LOW POWER";
              case WifiScanner.TYPE_HIGH_ACCURACY:
                  return "HIGH ACCURACY";
              default:
                  // This should never happen becuase we've validated the incoming type in
                  // |validateScanType|.
                  throw new IllegalArgumentException("Invalid scan type " + type);
          }
      }
  
      static String describeTo(StringBuilder sb, ScanSettings scanSettings) {
          sb.append("ScanSettings { ")
            .append(" type:").append(getScanTypeString(scanSettings.type))
            .append(" band:").append(ChannelHelper.bandToString(scanSettings.band))
            .append(" ignoreLocationSettings:").append(scanSettings.ignoreLocationSettings)
            .append(" period:").append(scanSettings.periodInMs)
            .append(" reportEvents:").append(scanSettings.reportEvents)
            .append(" numBssidsPerScan:").append(scanSettings.numBssidsPerScan)
            .append(" maxScansToCache:").append(scanSettings.maxScansToCache)
            .append(" channels:[ ");
          if (scanSettings.channels != null) {
              for (int i = 0; i < scanSettings.channels.length; i++) {
                  sb.append(scanSettings.channels[i].frequency)
                    .append(" ");
              }
          }
          sb.append(" ] ")
            .append(" } ");
          return sb.toString();
      }
  
      static String describeTo(StringBuilder sb, PnoSettings pnoSettings) {
          sb.append("PnoSettings { ")
            .append(" min5GhzRssi:").append(pnoSettings.min5GHzRssi)
            .append(" min24GhzRssi:").append(pnoSettings.min24GHzRssi)
            .append(" initialScoreMax:").append(pnoSettings.initialScoreMax)
            .append(" currentConnectionBonus:").append(pnoSettings.currentConnectionBonus)
            .append(" sameNetworkBonus:").append(pnoSettings.sameNetworkBonus)
            .append(" secureBonus:").append(pnoSettings.secureBonus)
            .append(" band5GhzBonus:").append(pnoSettings.band5GHzBonus)
            .append(" isConnected:").append(pnoSettings.isConnected)
            .append(" networks:[ ");
          if (pnoSettings.networkList != null) {
              for (int i = 0; i < pnoSettings.networkList.length; i++) {
                  sb.append(pnoSettings.networkList[i].ssid).append(",");
              }
          }
          sb.append(" ] ")
            .append(" } ");
          return sb.toString();
      }
  }
  