content: /*
   * Copyright (C) 2014 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;
  
  import android.Manifest.permission;
  import android.annotation.Nullable;
  import android.content.BroadcastReceiver;
  import android.content.ComponentName;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.ServiceConnection;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManagerInternal;
  import android.database.ContentObserver;
  import android.location.LocationManager;
  import android.net.INetworkRecommendationProvider;
  import android.net.INetworkScoreCache;
  import android.net.INetworkScoreService;
  import android.net.NetworkKey;
  import android.net.NetworkScoreManager;
  import android.net.NetworkScorerAppData;
  import android.net.ScoredNetwork;
  import android.net.Uri;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiInfo;
  import android.net.wifi.WifiManager;
  import android.net.wifi.WifiScanner;
  import android.os.Binder;
  import android.os.Build;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.RemoteCallbackList;
  import android.os.RemoteException;
  import android.os.UserHandle;
  import android.provider.Settings.Global;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.IntArray;
  import android.util.Log;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.content.PackageMonitor;
  import com.android.internal.os.TransferPipe;
  import com.android.internal.telephony.SmsApplication;
  import com.android.internal.util.DumpUtils;
  
  import java.io.FileDescriptor;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.function.BiConsumer;
  import java.util.function.Function;
  import java.util.function.Supplier;
  import java.util.function.UnaryOperator;
  
  /**
   * Backing service for {@link android.net.NetworkScoreManager}.
   * @hide
   */
  public class NetworkScoreService extends INetworkScoreService.Stub {
      private static final String TAG = "NetworkScoreService";
      private static final boolean DBG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
      private static final boolean VERBOSE = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE);
  
      private final Context mContext;
      private final NetworkScorerAppManager mNetworkScorerAppManager;
      @GuardedBy("mScoreCaches")
      private final Map<Integer, RemoteCallbackList<INetworkScoreCache>> mScoreCaches;
      /** Lock used to update mPackageMonitor when scorer package changes occur. */
      private final Object mPackageMonitorLock = new Object();
      private final Object mServiceConnectionLock = new Object();
      private final Handler mHandler;
      private final DispatchingContentObserver mRecommendationSettingsObserver;
      private final ContentObserver mUseOpenWifiPackageObserver;
      private final Function<NetworkScorerAppData, ScoringServiceConnection> mServiceConnProducer;
  
      @GuardedBy("mPackageMonitorLock")
      private NetworkScorerPackageMonitor mPackageMonitor;
      @GuardedBy("mServiceConnectionLock")
      private ScoringServiceConnection mServiceConnection;
  
      private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              final String action = intent.getAction();
              final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
              if (DBG) Log.d(TAG, "Received " + action + " for userId " + userId);
              if (userId == UserHandle.USER_NULL) return;
  
              if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
                  onUserUnlocked(userId);
              }
          }
      };
  
      private BroadcastReceiver mLocationModeReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              final String action = intent.getAction();
              if (LocationManager.MODE_CHANGED_ACTION.equals(action)) {
                  refreshBinding();
              }
          }
      };
  
      public static final class Lifecycle extends SystemService {
          private final NetworkScoreService mService;
  
          public Lifecycle(Context context) {
              super(context);
              mService = new NetworkScoreService(context);
          }
  
          @Override
          public void onStart() {
              Log.i(TAG, "Registering " + Context.NETWORK_SCORE_SERVICE);
              publishBinderService(Context.NETWORK_SCORE_SERVICE, mService);
          }
  
          @Override
          public void onBootPhase(int phase) {
              if (phase == PHASE_SYSTEM_SERVICES_READY) {
                  mService.systemReady();
              } else if (phase == PHASE_BOOT_COMPLETED) {
                  mService.systemRunning();
              }
          }
      }
  
      /**
       * Clears scores when the active scorer package is no longer valid and
       * manages the service connection.
       */
      private class NetworkScorerPackageMonitor extends PackageMonitor {
          final String mPackageToWatch;
  
          private NetworkScorerPackageMonitor(String packageToWatch) {
              mPackageToWatch = packageToWatch;
          }
  
          @Override
          public void onPackageAdded(String packageName, int uid) {
              evaluateBinding(packageName, true /* forceUnbind */);
          }
  
          @Override
          public void onPackageRemoved(String packageName, int uid) {
              evaluateBinding(packageName, true /* forceUnbind */);
          }
  
          @Override
          public void onPackageModified(String packageName) {
              evaluateBinding(packageName, false /* forceUnbind */);
          }
  
          @Override
          public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
              if (doit) { // "doit" means the force stop happened instead of just being queried for.
                  for (String packageName : packages) {
                      evaluateBinding(packageName, true /* forceUnbind */);
                  }
              }
              return super.onHandleForceStop(intent, packages, uid, doit);
          }
  
          @Override
          public void onPackageUpdateFinished(String packageName, int uid) {
              evaluateBinding(packageName, true /* forceUnbind */);
          }
  
          private void evaluateBinding(String changedPackageName, boolean forceUnbind) {
              if (!mPackageToWatch.equals(changedPackageName)) {
                  // Early exit when we don't care about the package that has changed.
                  return;
              }
  
              if (DBG) {
                  Log.d(TAG, "Evaluating binding for: " + changedPackageName
                          + ", forceUnbind=" + forceUnbind);
              }
  
              final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer();
              if (activeScorer == null) {
                  // Package change has invalidated a scorer, this will also unbind any service
                  // connection.
                  if (DBG) Log.d(TAG, "No active scorers available.");
                  refreshBinding();
              } else { // The scoring service changed in some way.
                  if (forceUnbind) {
                      unbindFromScoringServiceIfNeeded();
                  }
                  if (DBG) {
                      Log.d(TAG, "Binding to " + activeScorer.getRecommendationServiceComponent()
                              + " if needed.");
                  }
                  bindToScoringServiceIfNeeded(activeScorer);
              }
          }
      }
  
      /**
       * Dispatches observed content changes to a handler for further processing.
       */
      @VisibleForTesting
      public static class DispatchingContentObserver extends ContentObserver {
          final private Map<Uri, Integer> mUriEventMap;
          final private Context mContext;
          final private Handler mHandler;
  
          public DispatchingContentObserver(Context context, Handler handler) {
              super(handler);
              mContext = context;
              mHandler = handler;
              mUriEventMap = new ArrayMap<>();
          }
  
          void observe(Uri uri, int what) {
              mUriEventMap.put(uri, what);
              final ContentResolver resolver = mContext.getContentResolver();
              resolver.registerContentObserver(uri, false /*notifyForDescendants*/, this);
          }
  
          @Override
          public void onChange(boolean selfChange) {
              onChange(selfChange, null);
          }
  
          @Override
          public void onChange(boolean selfChange, Uri uri) {
              if (DBG) Log.d(TAG, String.format("onChange(%s, %s)", selfChange, uri));
              final Integer what = mUriEventMap.get(uri);
              if (what != null) {
                  mHandler.obtainMessage(what).sendToTarget();
              } else {
                  Log.w(TAG, "No matching event to send for URI = " + uri);
              }
          }
      }
  
      public NetworkScoreService(Context context) {
        this(context, new NetworkScorerAppManager(context),
                ScoringServiceConnection::new, Looper.myLooper());
      }
  
      @VisibleForTesting
      NetworkScoreService(Context context, NetworkScorerAppManager networkScoreAppManager,
              Function<NetworkScorerAppData, ScoringServiceConnection> serviceConnProducer,
              Looper looper) {
          mContext = context;
          mNetworkScorerAppManager = networkScoreAppManager;
          mScoreCaches = new ArrayMap<>();
          IntentFilter filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
          // TODO: Need to update when we support per-user scorers. http://b/23422763
          mContext.registerReceiverAsUser(
                  mUserIntentReceiver, UserHandle.SYSTEM, filter, null /* broadcastPermission*/,
                  null /* scheduler */);
          mHandler = new ServiceHandler(looper);
          IntentFilter locationModeFilter = new IntentFilter(LocationManager.MODE_CHANGED_ACTION);
          mContext.registerReceiverAsUser(
                  mLocationModeReceiver, UserHandle.SYSTEM, locationModeFilter,
                  null /* broadcastPermission*/, mHandler);
          mRecommendationSettingsObserver = new DispatchingContentObserver(context, mHandler);
          mServiceConnProducer = serviceConnProducer;
          mUseOpenWifiPackageObserver = new ContentObserver(mHandler) {
              @Override
              public void onChange(boolean selfChange, Uri uri, int userId) {
                  Uri useOpenWifiPkgUri = Global.getUriFor(Global.USE_OPEN_WIFI_PACKAGE);
                  if (useOpenWifiPkgUri.equals(uri)) {
                      String useOpenWifiPackage = Global.getString(mContext.getContentResolver(),
                              Global.USE_OPEN_WIFI_PACKAGE);
                      if (!TextUtils.isEmpty(useOpenWifiPackage)) {
                          LocalServices.getService(PackageManagerInternal.class)
                                  .grantDefaultPermissionsToDefaultUseOpenWifiApp(useOpenWifiPackage,
                                          userId);
                      }
                  }
              }
          };
          mContext.getContentResolver().registerContentObserver(
                  Global.getUriFor(Global.USE_OPEN_WIFI_PACKAGE),
                  false /*notifyForDescendants*/,
                  mUseOpenWifiPackageObserver);
          // Set a callback for the package manager to query the use open wifi app.
          LocalServices.getService(PackageManagerInternal.class).setUseOpenWifiAppPackagesProvider(
                  new PackageManagerInternal.PackagesProvider() {
                      @Override
                      public String[] getPackages(int userId) {
                          String useOpenWifiPackage = Global.getString(mContext.getContentResolver(),
                                  Global.USE_OPEN_WIFI_PACKAGE);
                          if (!TextUtils.isEmpty(useOpenWifiPackage)) {
                              return new String[]{useOpenWifiPackage};
                          }
                          return null;
                      }
                  });
      }
  
      /** Called when the system is ready to run third-party code but before it actually does so. */
      void systemReady() {
          if (DBG) Log.d(TAG, "systemReady");
          registerRecommendationSettingsObserver();
      }
  
      /** Called when the system is ready for us to start third-party code. */
      void systemRunning() {
          if (DBG) Log.d(TAG, "systemRunning");
      }
  
      @VisibleForTesting
      void onUserUnlocked(int userId) {
          if (DBG) Log.d(TAG, "onUserUnlocked(" + userId + ")");
          refreshBinding();
      }
  
      private void refreshBinding() {
          if (DBG) Log.d(TAG, "refreshBinding()");
          // Make sure the scorer is up-to-date
          mNetworkScorerAppManager.updateState();
          mNetworkScorerAppManager.migrateNetworkScorerAppSettingIfNeeded();
          registerPackageMonitorIfNeeded();
          bindToScoringServiceIfNeeded();
      }
  
      private void registerRecommendationSettingsObserver() {
          final Uri packageNameUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_PACKAGE);
          mRecommendationSettingsObserver.observe(packageNameUri,
                  ServiceHandler.MSG_RECOMMENDATIONS_PACKAGE_CHANGED);
  
          final Uri settingUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_ENABLED);
          mRecommendationSettingsObserver.observe(settingUri,
                  ServiceHandler.MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED);
      }
  
      /**
       * Ensures the package manager is registered to monitor the current active scorer.
       * If a discrepancy is found any previous monitor will be cleaned up
       * and a new monitor will be created.
       *
       * This method is idempotent.
       */
      private void registerPackageMonitorIfNeeded() {
          if (DBG) Log.d(TAG, "registerPackageMonitorIfNeeded()");
          final NetworkScorerAppData appData = mNetworkScorerAppManager.getActiveScorer();
          synchronized (mPackageMonitorLock) {
              // Unregister the current monitor if needed.
              if (mPackageMonitor != null && (appData == null
                      || !appData.getRecommendationServicePackageName().equals(
                              mPackageMonitor.mPackageToWatch))) {
                  if (DBG) {
                      Log.d(TAG, "Unregistering package monitor for "
                              + mPackageMonitor.mPackageToWatch);
                  }
                  mPackageMonitor.unregister();
                  mPackageMonitor = null;
              }
  
              // Create and register the monitor if a scorer is active.
              if (appData != null && mPackageMonitor == null) {
                  mPackageMonitor = new NetworkScorerPackageMonitor(
                          appData.getRecommendationServicePackageName());
                  // TODO: Need to update when we support per-user scorers. http://b/23422763
                  mPackageMonitor.register(mContext, null /* thread */, UserHandle.SYSTEM,
                          false /* externalStorage */);
                  if (DBG) {
                      Log.d(TAG, "Registered package monitor for "
                              + mPackageMonitor.mPackageToWatch);
                  }
              }
          }
      }
  
      private void bindToScoringServiceIfNeeded() {
          if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded");
          NetworkScorerAppData scorerData = mNetworkScorerAppManager.getActiveScorer();
          bindToScoringServiceIfNeeded(scorerData);
      }
  
      /**
       * Ensures the service connection is bound to the current active scorer.
       * If a discrepancy is found any previous connection will be cleaned up
       * and a new connection will be created.
       *
       * This method is idempotent.
       */
      private void bindToScoringServiceIfNeeded(NetworkScorerAppData appData) {
          if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded(" + appData + ")");
          if (appData != null) {
              synchronized (mServiceConnectionLock) {
                  // If we're connected to a different component then drop it.
                  if (mServiceConnection != null
                          && !mServiceConnection.getAppData().equals(appData)) {
                      unbindFromScoringServiceIfNeeded();
                  }
  
                  // If we're not connected at all then create a new connection.
                  if (mServiceConnection == null) {
                      mServiceConnection = mServiceConnProducer.apply(appData);
                  }
  
                  // Make sure the connection is connected (idempotent)
                  mServiceConnection.bind(mContext);
              }
          } else { // otherwise make sure it isn't bound.
              unbindFromScoringServiceIfNeeded();
          }
      }
  
      private void unbindFromScoringServiceIfNeeded() {
          if (DBG) Log.d(TAG, "unbindFromScoringServiceIfNeeded");
          synchronized (mServiceConnectionLock) {
              if (mServiceConnection != null) {
                  mServiceConnection.unbind(mContext);
                  if (DBG) Log.d(TAG, "Disconnected from: "
                          + mServiceConnection.getAppData().getRecommendationServiceComponent());
              }
              mServiceConnection = null;
          }
          clearInternal();
      }
  
      @Override
      public boolean updateScores(ScoredNetwork[] networks) {
          if (!isCallerActiveScorer(getCallingUid())) {
              throw new SecurityException("Caller with UID " + getCallingUid() +
                      " is not the active scorer.");
          }
  
          final long token = Binder.clearCallingIdentity();
          try {
              // Separate networks by type.
              Map<Integer, List<ScoredNetwork>> networksByType = new ArrayMap<>();
              for (ScoredNetwork network : networks) {
                  List<ScoredNetwork> networkList = networksByType.get(network.networkKey.type);
                  if (networkList == null) {
                      networkList = new ArrayList<>();
                      networksByType.put(network.networkKey.type, networkList);
                  }
                  networkList.add(network);
              }
  
              // Pass the scores of each type down to the appropriate network scorer.
              for (final Map.Entry<Integer, List<ScoredNetwork>> entry : networksByType.entrySet()) {
                  final RemoteCallbackList<INetworkScoreCache> callbackList;
                  final boolean isEmpty;
                  synchronized (mScoreCaches) {
                      callbackList = mScoreCaches.get(entry.getKey());
                      isEmpty = callbackList == null
                              || callbackList.getRegisteredCallbackCount() == 0;
                  }
  
                  if (isEmpty) {
                      if (Log.isLoggable(TAG, Log.VERBOSE)) {
                          Log.v(TAG, "No scorer registered for type " + entry.getKey()
                                  + ", discarding");
                      }
                      continue;
                  }
  
                  final BiConsumer<INetworkScoreCache, Object> consumer =
                          FilteringCacheUpdatingConsumer.create(mContext, entry.getValue(),
                                  entry.getKey());
                  sendCacheUpdateCallback(consumer, Collections.singleton(callbackList));
              }
  
              return true;
          } finally {
              Binder.restoreCallingIdentity(token);
          }
      }
  
      /**
       * A {@link BiConsumer} implementation that filters the given {@link ScoredNetwork}
       * list (if needed) before invoking {@link INetworkScoreCache#updateScores(List)} on the
       * accepted {@link INetworkScoreCache} implementation.
       */
      @VisibleForTesting
      static class FilteringCacheUpdatingConsumer
              implements BiConsumer<INetworkScoreCache, Object> {
          private final Context mContext;
          private final List<ScoredNetwork> mScoredNetworkList;
          private final int mNetworkType;
          // TODO: 1/23/17 - Consider a Map if we implement more filters.
          // These are created on-demand to defer the construction cost until
          // an instance is actually needed.
          private UnaryOperator<List<ScoredNetwork>> mCurrentNetworkFilter;
          private UnaryOperator<List<ScoredNetwork>> mScanResultsFilter;
  
          static FilteringCacheUpdatingConsumer create(Context context,
                  List<ScoredNetwork> scoredNetworkList, int networkType) {
              return new FilteringCacheUpdatingConsumer(context, scoredNetworkList, networkType,
                      null, null);
          }
  
          @VisibleForTesting
          FilteringCacheUpdatingConsumer(Context context,
                  List<ScoredNetwork> scoredNetworkList, int networkType,
                  UnaryOperator<List<ScoredNetwork>> currentNetworkFilter,
                  UnaryOperator<List<ScoredNetwork>> scanResultsFilter) {
              mContext = context;
              mScoredNetworkList = scoredNetworkList;
              mNetworkType = networkType;
              mCurrentNetworkFilter = currentNetworkFilter;
              mScanResultsFilter = scanResultsFilter;
          }
  
          @Override
          public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
              int filterType = NetworkScoreManager.CACHE_FILTER_NONE;
              if (cookie instanceof Integer) {
                  filterType = (Integer) cookie;
              }
  
              try {
                  final List<ScoredNetwork> filteredNetworkList =
                          filterScores(mScoredNetworkList, filterType);
                  if (!filteredNetworkList.isEmpty()) {
                      networkScoreCache.updateScores(filteredNetworkList);
                  }
              } catch (RemoteException e) {
                  if (VERBOSE) {
                      Log.v(TAG, "Unable to update scores of type " + mNetworkType, e);
                  }
              }
          }
  
          /**
           * Applies the appropriate filter and returns the filtered results.
           */
          private List<ScoredNetwork> filterScores(List<ScoredNetwork> scoredNetworkList,
                  int filterType) {
              switch (filterType) {
                  case NetworkScoreManager.CACHE_FILTER_NONE:
                      return scoredNetworkList;
  
                  case NetworkScoreManager.CACHE_FILTER_CURRENT_NETWORK:
                      if (mCurrentNetworkFilter == null) {
                          mCurrentNetworkFilter =
                                  new CurrentNetworkScoreCacheFilter(new WifiInfoSupplier(mContext));
                      }
                      return mCurrentNetworkFilter.apply(scoredNetworkList);
  
                  case NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS:
                      if (mScanResultsFilter == null) {
                          mScanResultsFilter = new ScanResultsScoreCacheFilter(
                                  new ScanResultsSupplier(mContext));
                      }
                      return mScanResultsFilter.apply(scoredNetworkList);
  
                  default:
                      Log.w(TAG, "Unknown filter type: " + filterType);
                      return scoredNetworkList;
              }
          }
      }
  
      /**
       * Helper class that improves the testability of the cache filter Functions.
       */
      private static class WifiInfoSupplier implements Supplier<WifiInfo> {
          private final Context mContext;
  
          WifiInfoSupplier(Context context) {
              mContext = context;
          }
  
          @Override
          public WifiInfo get() {
              WifiManager wifiManager = mContext.getSystemService(WifiManager.class);
              if (wifiManager != null) {
                  return wifiManager.getConnectionInfo();
              }
              Log.w(TAG, "WifiManager is null, failed to return the WifiInfo.");
              return null;
          }
      }
  
      /**
       * Helper class that improves the testability of the cache filter Functions.
       */
      private static class ScanResultsSupplier implements Supplier<List<ScanResult>> {
          private final Context mContext;
  
          ScanResultsSupplier(Context context) {
              mContext = context;
          }
  
          @Override
          public List<ScanResult> get() {
              WifiScanner wifiScanner = mContext.getSystemService(WifiScanner.class);
              if (wifiScanner != null) {
                  return wifiScanner.getSingleScanResults();
              }
              Log.w(TAG, "WifiScanner is null, failed to return scan results.");
              return Collections.emptyList();
          }
      }
  
      /**
       * Filters the given set of {@link ScoredNetwork}s and returns a new List containing only the
       * {@link ScoredNetwork} associated with the current network. If no network is connected the
       * returned list will be empty.
       * <p>
       * Note: this filter performs some internal caching for consistency and performance. The
       *       current network is determined at construction time and never changed. Also, the
       *       last filtered list is saved so if the same input is provided multiple times in a row
       *       the computation is only done once.
       */
      @VisibleForTesting
      static class CurrentNetworkScoreCacheFilter implements UnaryOperator<List<ScoredNetwork>> {
          private final NetworkKey mCurrentNetwork;
  
          CurrentNetworkScoreCacheFilter(Supplier<WifiInfo> wifiInfoSupplier) {
              mCurrentNetwork = NetworkKey.createFromWifiInfo(wifiInfoSupplier.get());
          }
  
          @Override
          public List<ScoredNetwork> apply(List<ScoredNetwork> scoredNetworks) {
              if (mCurrentNetwork == null || scoredNetworks.isEmpty()) {
                  return Collections.emptyList();
              }
  
              for (int i = 0; i < scoredNetworks.size(); i++) {
                  final ScoredNetwork scoredNetwork = scoredNetworks.get(i);
                  if (scoredNetwork.networkKey.equals(mCurrentNetwork)) {
                      return Collections.singletonList(scoredNetwork);
                  }
              }
  
              return Collections.emptyList();
          }
      }
  
      /**
       * Filters the given set of {@link ScoredNetwork}s and returns a new List containing only the
       * {@link ScoredNetwork} associated with the current set of {@link ScanResult}s.
       * If there are no {@link ScanResult}s the returned list will be empty.
       * <p>
       * Note: this filter performs some internal caching for consistency and performance. The
       *       current set of ScanResults is determined at construction time and never changed.
       *       Also, the last filtered list is saved so if the same input is provided multiple
       *       times in a row the computation is only done once.
       */
      @VisibleForTesting
      static class ScanResultsScoreCacheFilter implements UnaryOperator<List<ScoredNetwork>> {
          private final Set<NetworkKey> mScanResultKeys;
  
          ScanResultsScoreCacheFilter(Supplier<List<ScanResult>> resultsSupplier) {
              List<ScanResult> scanResults = resultsSupplier.get();
              final int size = scanResults.size();
              mScanResultKeys = new ArraySet<>(size);
              for (int i = 0; i < size; i++) {
                  ScanResult scanResult = scanResults.get(i);
                  NetworkKey key = NetworkKey.createFromScanResult(scanResult);
                  if (key != null) {
                      mScanResultKeys.add(key);
                  }
              }
          }
  
          @Override
          public List<ScoredNetwork> apply(List<ScoredNetwork> scoredNetworks) {
              if (mScanResultKeys.isEmpty() || scoredNetworks.isEmpty()) {
                  return Collections.emptyList();
              }
  
              List<ScoredNetwork> filteredScores = new ArrayList<>();
              for (int i = 0; i < scoredNetworks.size(); i++) {
                  final ScoredNetwork scoredNetwork = scoredNetworks.get(i);
                  if (mScanResultKeys.contains(scoredNetwork.networkKey)) {
                      filteredScores.add(scoredNetwork);
                  }
              }
  
              return filteredScores;
          }
      }
  
      @Override
      public boolean clearScores() {
          // Only the active scorer or the system should be allowed to flush all scores.
          enforceSystemOrIsActiveScorer(getCallingUid());
          final long token = Binder.clearCallingIdentity();
          try {
              clearInternal();
              return true;
          } finally {
              Binder.restoreCallingIdentity(token);
          }
      }
  
      @Override
      public boolean setActiveScorer(String packageName) {
          enforceSystemOrHasScoreNetworks();
          return mNetworkScorerAppManager.setActiveScorer(packageName);
      }
  
      /**
       * Determine whether the application with the given UID is the enabled scorer.
       *
       * @param callingUid the UID to check
       * @return true if the provided UID is the active scorer, false otherwise.
       */
      @Override
      public boolean isCallerActiveScorer(int callingUid) {
          synchronized (mServiceConnectionLock) {
              return mServiceConnection != null
                      && mServiceConnection.getAppData().packageUid == callingUid;
          }
      }
  
      private void enforceSystemOnly() throws SecurityException {
          // REQUEST_NETWORK_SCORES is a signature only permission.
          mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES,
                  "Caller must be granted REQUEST_NETWORK_SCORES.");
      }
  
      private void enforceSystemOrHasScoreNetworks() throws SecurityException {
          if (mContext.checkCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES)
                  != PackageManager.PERMISSION_GRANTED
                  && mContext.checkCallingOrSelfPermission(permission.SCORE_NETWORKS)
                  != PackageManager.PERMISSION_GRANTED) {
              throw new SecurityException(
                      "Caller is neither the system process or a network scorer.");
          }
      }
  
      private void enforceSystemOrIsActiveScorer(int callingUid) throws SecurityException {
          if (mContext.checkCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES)
                  != PackageManager.PERMISSION_GRANTED
                  && !isCallerActiveScorer(callingUid)) {
              throw new SecurityException(
                      "Caller is neither the system process or the active network scorer.");
          }
      }
  
      /**
       * Obtain the package name of the current active network scorer.
       *
       * @return the full package name of the current active scorer, or null if there is no active
       *         scorer.
       */
      @Override
      public String getActiveScorerPackage() {
          enforceSystemOrHasScoreNetworks();
          synchronized (mServiceConnectionLock) {
              if (mServiceConnection != null) {
                  return mServiceConnection.getPackageName();
              }
          }
          return null;
      }
  
      /**
       * Returns metadata about the active scorer or <code>null</code> if there is no active scorer.
       */
      @Override
      public NetworkScorerAppData getActiveScorer() {
          // Only the system can access this data.
          enforceSystemOnly();
          synchronized (mServiceConnectionLock) {
              if (mServiceConnection != null) {
                  return mServiceConnection.getAppData();
              }
          }
  
          return null;
      }
  
      /**
       * Returns the list of available scorer apps. The list will be empty if there are
       * no valid scorers.
       */
      @Override
      public List<NetworkScorerAppData> getAllValidScorers() {
          // Only the system can access this data.
          enforceSystemOnly();
          return mNetworkScorerAppManager.getAllValidScorers();
      }
  
      @Override
      public void disableScoring() {
          // Only the active scorer or the system should be allowed to disable scoring.
          enforceSystemOrIsActiveScorer(getCallingUid());
          // no-op for now but we could write to the setting if needed.
      }
  
      /** Clear scores. Callers are responsible for checking permissions as appropriate. */
      private void clearInternal() {
          sendCacheUpdateCallback(new BiConsumer<INetworkScoreCache, Object>() {
              @Override
              public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
                  try {
                      networkScoreCache.clearScores();
                  } catch (RemoteException e) {
                      if (Log.isLoggable(TAG, Log.VERBOSE)) {
                          Log.v(TAG, "Unable to clear scores", e);
                      }
                  }
              }
          }, getScoreCacheLists());
      }
  
      @Override
      public void registerNetworkScoreCache(int networkType,
                                            INetworkScoreCache scoreCache,
                                            int filterType) {
          enforceSystemOnly();
          final long token = Binder.clearCallingIdentity();
          try {
              synchronized (mScoreCaches) {
                  RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
                  if (callbackList == null) {
                      callbackList = new RemoteCallbackList<>();
                      mScoreCaches.put(networkType, callbackList);
                  }
                  if (!callbackList.register(scoreCache, filterType)) {
                      if (callbackList.getRegisteredCallbackCount() == 0) {
                          mScoreCaches.remove(networkType);
                      }
                      if (Log.isLoggable(TAG, Log.VERBOSE)) {
                          Log.v(TAG, "Unable to register NetworkScoreCache for type " + networkType);
                      }
                  }
              }
          } finally {
              Binder.restoreCallingIdentity(token);
          }
      }
  
      @Override
      public void unregisterNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) {
          enforceSystemOnly();
          final long token = Binder.clearCallingIdentity();
          try {
              synchronized (mScoreCaches) {
                  RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
                  if (callbackList == null || !callbackList.unregister(scoreCache)) {
                      if (Log.isLoggable(TAG, Log.VERBOSE)) {
                          Log.v(TAG, "Unable to unregister NetworkScoreCache for type "
                                  + networkType);
                      }
                  } else if (callbackList.getRegisteredCallbackCount() == 0) {
                      mScoreCaches.remove(networkType);
                  }
              }
          } finally {
              Binder.restoreCallingIdentity(token);
          }
      }
  
      @Override
      public boolean requestScores(NetworkKey[] networks) {
          enforceSystemOnly();
          final long token = Binder.clearCallingIdentity();
          try {
              final INetworkRecommendationProvider provider = getRecommendationProvider();
              if (provider != null) {
                  try {
                      provider.requestScores(networks);
                      // TODO: 12/15/16 - Consider pushing null scores into the cache to
                      // prevent repeated requests for the same scores.
                      return true;
                  } catch (RemoteException e) {
                      Log.w(TAG, "Failed to request scores.", e);
                      // TODO: 12/15/16 - Keep track of failures.
                  }
              }
              return false;
          } finally {
              Binder.restoreCallingIdentity(token);
          }
      }
  
      @Override
      protected void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) {
          if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
          final long token = Binder.clearCallingIdentity();
          try {
              NetworkScorerAppData currentScorer = mNetworkScorerAppManager.getActiveScorer();
              if (currentScorer == null) {
                  writer.println("Scoring is disabled.");
                  return;
              }
              writer.println("Current scorer: " + currentScorer);
  
              sendCacheUpdateCallback(new BiConsumer<INetworkScoreCache, Object>() {
                  @Override
                  public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
                      try {
                          TransferPipe.dumpAsync(networkScoreCache.asBinder(), fd, args);
                      } catch (IOException | RemoteException e) {
                          writer.println("Failed to dump score cache: " + e);
                      }
                  }
              }, getScoreCacheLists());
  
              synchronized (mServiceConnectionLock) {
                  if (mServiceConnection != null) {
                      mServiceConnection.dump(fd, writer, args);
                  } else {
                      writer.println("ScoringServiceConnection: null");
                  }
              }
              writer.flush();
          } finally {
              Binder.restoreCallingIdentity(token);
          }
      }
  
      /**
       * Returns a {@link Collection} of all {@link RemoteCallbackList}s that are currently active.
       *
       * <p>May be used to perform an action on all score caches without potentially strange behavior
       * if a new scorer is registered during that action's execution.
       */
      private Collection<RemoteCallbackList<INetworkScoreCache>> getScoreCacheLists() {
          synchronized (mScoreCaches) {
              return new ArrayList<>(mScoreCaches.values());
          }
      }
  
      private void sendCacheUpdateCallback(BiConsumer<INetworkScoreCache, Object> consumer,
              Collection<RemoteCallbackList<INetworkScoreCache>> remoteCallbackLists) {
          for (RemoteCallbackList<INetworkScoreCache> callbackList : remoteCallbackLists) {
              synchronized (callbackList) { // Ensure only one active broadcast per RemoteCallbackList
                  final int count = callbackList.beginBroadcast();
                  try {
                      for (int i = 0; i < count; i++) {
                          consumer.accept(callbackList.getBroadcastItem(i),
                                  callbackList.getBroadcastCookie(i));
                      }
                  } finally {
                      callbackList.finishBroadcast();
                  }
              }
          }
      }
  
      @Nullable
      private INetworkRecommendationProvider getRecommendationProvider() {
          synchronized (mServiceConnectionLock) {
              if (mServiceConnection != null) {
                  return mServiceConnection.getRecommendationProvider();
              }
          }
          return null;
      }
  
      // The class and methods need to be public for Mockito to work.
      @VisibleForTesting
      public static class ScoringServiceConnection implements ServiceConnection {
          private final NetworkScorerAppData mAppData;
          private volatile boolean mBound = false;
          private volatile boolean mConnected = false;
          private volatile INetworkRecommendationProvider mRecommendationProvider;
  
          ScoringServiceConnection(NetworkScorerAppData appData) {
              mAppData = appData;
          }
  
          @VisibleForTesting
          public void bind(Context context) {
              if (!mBound) {
                  Intent service = new Intent(NetworkScoreManager.ACTION_RECOMMEND_NETWORKS);
                  service.setComponent(mAppData.getRecommendationServiceComponent());
                  mBound = context.bindServiceAsUser(service, this,
                          Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
                          UserHandle.SYSTEM);
                  if (!mBound) {
                      Log.w(TAG, "Bind call failed for " + service);
                      context.unbindService(this);
                  } else {
                      if (DBG) Log.d(TAG, "ScoringServiceConnection bound.");
                  }
              }
          }
  
          @VisibleForTesting
          public void unbind(Context context) {
              try {
                  if (mBound) {
                      mBound = false;
                      context.unbindService(this);
                      if (DBG) Log.d(TAG, "ScoringServiceConnection unbound.");
                  }
              } catch (RuntimeException e) {
                  Log.e(TAG, "Unbind failed.", e);
              }
  
              mConnected = false;
              mRecommendationProvider = null;
          }
  
          @VisibleForTesting
          public NetworkScorerAppData getAppData() {
              return mAppData;
          }
  
          @VisibleForTesting
          public INetworkRecommendationProvider getRecommendationProvider() {
              return mRecommendationProvider;
          }
  
          @VisibleForTesting
          public String getPackageName() {
              return mAppData.getRecommendationServiceComponent().getPackageName();
          }
  
          @VisibleForTesting
          public boolean isAlive() {
              return mBound && mConnected;
          }
  
          @Override
          public void onServiceConnected(ComponentName name, IBinder service) {
              if (DBG) Log.d(TAG, "ScoringServiceConnection: " + name.flattenToString());
              mConnected = true;
              mRecommendationProvider = INetworkRecommendationProvider.Stub.asInterface(service);
          }
  
          @Override
          public void onServiceDisconnected(ComponentName name) {
              if (DBG) {
                  Log.d(TAG, "ScoringServiceConnection, disconnected: " + name.flattenToString());
              }
              mConnected = false;
              mRecommendationProvider = null;
          }
  
          public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
              writer.println("ScoringServiceConnection: "
                      + mAppData.getRecommendationServiceComponent()
                      + ", bound: " + mBound
                      + ", connected: " + mConnected);
          }
      }
  
      @VisibleForTesting
      public final class ServiceHandler extends Handler {
          public static final int MSG_RECOMMENDATIONS_PACKAGE_CHANGED = 1;
          public static final int MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED = 2;
  
          public ServiceHandler(Looper looper) {
              super(looper);
          }
  
          @Override
          public void handleMessage(Message msg) {
              final int what = msg.what;
              switch (what) {
                  case MSG_RECOMMENDATIONS_PACKAGE_CHANGED:
                  case MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED:
                      refreshBinding();
                      break;
  
                  default:
                      Log.w(TAG,"Unknown message: " + what);
              }
          }
      }
  }
  