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 android.net.wifi;
  
  import android.Manifest.permission;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.Context;
  import android.net.INetworkScoreCache;
  import android.net.NetworkKey;
  import android.net.ScoredNetwork;
  import android.os.Handler;
  import android.os.Process;
  import android.util.Log;
  import android.util.LruCache;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.util.Preconditions;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.List;
  
  /**
   * {@link INetworkScoreCache} implementation for Wifi Networks.
   *
   * @hide
   */
  public class WifiNetworkScoreCache extends INetworkScoreCache.Stub {
      private static final String TAG = "WifiNetworkScoreCache";
      private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
  
      // A Network scorer returns a score in the range [-128, +127]
      // We treat the lowest possible score as though there were no score, effectively allowing the
      // scorer to provide an RSSI threshold below which a network should not be used.
      public static final int INVALID_NETWORK_SCORE = Byte.MIN_VALUE;
  
      /** Default number entries to be stored in the {@link LruCache}. */
      private static final int DEFAULT_MAX_CACHE_SIZE = 100;
  
      // See {@link #CacheListener}.
      @Nullable
      @GuardedBy("mLock")
      private CacheListener mListener;
  
      private final Context mContext;
      private final Object mLock = new Object();
  
      // The key is of the form "<ssid>"<bssid>
      // TODO: What about SSIDs that can't be encoded as UTF-8?
      @GuardedBy("mLock")
      private final LruCache<String, ScoredNetwork> mCache;
  
      public WifiNetworkScoreCache(Context context) {
          this(context, null /* listener */);
      }
  
      /**
       * Instantiates a WifiNetworkScoreCache.
       *
       * @param context Application context
       * @param listener CacheListener for cache updates
       */
      public WifiNetworkScoreCache(Context context, @Nullable CacheListener listener) {
          this(context, listener, DEFAULT_MAX_CACHE_SIZE);
      }
  
      public WifiNetworkScoreCache(
              Context context, @Nullable CacheListener listener, int maxCacheSize) {
          mContext = context.getApplicationContext();
          mListener = listener;
          mCache = new LruCache<>(maxCacheSize);
      }
  
      @Override public final void updateScores(List<ScoredNetwork> networks) {
          if (networks == null || networks.isEmpty()) {
             return;
          }
          if (DBG) {
              Log.d(TAG, "updateScores list size=" + networks.size());
          }
  
          boolean changed = false;
  
          synchronized(mLock) {
              for (ScoredNetwork network : networks) {
                  String networkKey = buildNetworkKey(network);
                  if (networkKey == null) {
                      if (DBG) {
                          Log.d(TAG, "Failed to build network key for ScoredNetwork" + network);
                      }
                      continue;
                  }
                  mCache.put(networkKey, network);
                  changed = true;
              }
  
              if (mListener != null && changed) {
                  mListener.post(networks);
              }
          }
      }
  
      @Override public final void clearScores() {
          synchronized (mLock) {
              mCache.evictAll();
          }
      }
  
      /**
       * Returns whether there is any score info for the given ScanResult.
       *
       * This includes null-score info, so it should only be used when determining whether to request
       * scores from the network scorer.
       */
      public boolean isScoredNetwork(ScanResult result) {
          return getScoredNetwork(result) != null;
      }
  
      /**
       * Returns whether there is a non-null score curve for the given ScanResult.
       *
       * A null score curve has special meaning - we should never connect to an ephemeral network if
       * the score curve is null.
       */
      public boolean hasScoreCurve(ScanResult result) {
          ScoredNetwork network = getScoredNetwork(result);
          return network != null && network.rssiCurve != null;
      }
  
      public int getNetworkScore(ScanResult result) {
          int score = INVALID_NETWORK_SCORE;
  
          ScoredNetwork network = getScoredNetwork(result);
          if (network != null && network.rssiCurve != null) {
              score = network.rssiCurve.lookupScore(result.level);
              if (DBG) {
                  Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
                          + " score " + Integer.toString(score)
                          + " RSSI " + result.level);
              }
          }
          return score;
      }
  
      /**
       * Returns the ScoredNetwork metered hint for a given ScanResult.
       *
       * If there is no ScoredNetwork associated with the ScanResult then false will be returned.
       */
      public boolean getMeteredHint(ScanResult result) {
          ScoredNetwork network = getScoredNetwork(result);
          return network != null && network.meteredHint;
      }
  
      public int getNetworkScore(ScanResult result, boolean isActiveNetwork) {
          int score = INVALID_NETWORK_SCORE;
  
          ScoredNetwork network = getScoredNetwork(result);
          if (network != null && network.rssiCurve != null) {
              score = network.rssiCurve.lookupScore(result.level, isActiveNetwork);
              if (DBG) {
                  Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
                          + " score " + Integer.toString(score)
                          + " RSSI " + result.level
                          + " isActiveNetwork " + isActiveNetwork);
              }
          }
          return score;
      }
  
      @Nullable
      public ScoredNetwork getScoredNetwork(ScanResult result) {
          String key = buildNetworkKey(result);
          if (key == null) return null;
  
          synchronized(mLock) {
              ScoredNetwork network = mCache.get(key);
              return network;
          }
      }
  
      /** Returns the ScoredNetwork for the given key. */
      @Nullable
      public ScoredNetwork getScoredNetwork(NetworkKey networkKey) {
          String key = buildNetworkKey(networkKey);
          if (key == null) {
              if (DBG) {
                  Log.d(TAG, "Could not build key string for Network Key: " + networkKey);
              }
              return null;
          }
          synchronized (mLock) {
              return mCache.get(key);
          }
      }
  
      private String buildNetworkKey(ScoredNetwork network) {
          if (network == null) {
              return null;
          }
          return buildNetworkKey(network.networkKey);
      }
  
      private String buildNetworkKey(NetworkKey networkKey) {
          if (networkKey == null) {
              return null;
          }
          if (networkKey.wifiKey == null) return null;
          if (networkKey.type == NetworkKey.TYPE_WIFI) {
              String key = networkKey.wifiKey.ssid;
              if (key == null) return null;
              if (networkKey.wifiKey.bssid != null) {
                  key = key + networkKey.wifiKey.bssid;
              }
              return key;
          }
          return null;
      }
  
      private String buildNetworkKey(ScanResult result) {
          if (result == null || result.SSID == null) {
              return null;
          }
          StringBuilder key = new StringBuilder("\"");
          key.append(result.SSID);
          key.append("\"");
          if (result.BSSID != null) {
              key.append(result.BSSID);
          }
          return key.toString();
      }
  
      @Override protected final void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
          mContext.enforceCallingOrSelfPermission(permission.DUMP, TAG);
          String header = String.format("WifiNetworkScoreCache (%s/%d)",
                  mContext.getPackageName(), Process.myUid());
          writer.println(header);
          writer.println("  All score curves:");
          synchronized (mLock) {
              for (ScoredNetwork score : mCache.snapshot().values()) {
                  writer.println("    " + score);
              }
              writer.println("  Network scores for latest ScanResults:");
              WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
              for (ScanResult scanResult : wifiManager.getScanResults()) {
                  writer.println(
                          "    " + buildNetworkKey(scanResult) + ": " + getNetworkScore(scanResult));
              }
          }
      }
  
      /** Registers a CacheListener instance, replacing the previous listener if it existed. */
      public void registerListener(CacheListener listener) {
          synchronized (mLock) {
              mListener = listener;
          }
      }
  
      /** Removes the registered CacheListener. */
      public void unregisterListener() {
          synchronized (mLock) {
              mListener = null;
          }
      }
  
      /** Listener for updates to the cache inside WifiNetworkScoreCache. */
      public abstract static class CacheListener {
          private Handler mHandler;
  
          /**
           * Constructor for CacheListener.
           *
           * @param handler the Handler on which to invoke the {@link #networkCacheUpdated} method.
           *          This cannot be null.
           */
          public CacheListener(@NonNull Handler handler) {
              Preconditions.checkNotNull(handler);
              mHandler = handler;
          }
  
          /** Invokes the {@link #networkCacheUpdated(List<ScoredNetwork>)} method on the handler. */
          void post(List<ScoredNetwork> updatedNetworks) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      networkCacheUpdated(updatedNetworks);
                  }
              });
          }
  
          /**
           * Invoked whenever the cache is updated.
           *
           * <p>Clearing the cache does not invoke this method.
           *
           * @param updatedNetworks the networks that were updated
           */
          public abstract void networkCacheUpdated(List<ScoredNetwork> updatedNetworks);
      }
  }
  