content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.wifi;
  
  import android.annotation.NonNull;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiConfiguration;
  import android.net.wifi.WifiNetworkSuggestion;
  import android.util.LocalLog;
  import android.util.Log;
  
  import com.android.server.wifi.util.ScanResultUtil;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.stream.Collectors;
  
  import javax.annotation.Nullable;
  import javax.annotation.concurrent.NotThreadSafe;
  
  /**
   * Evaluator to pick the best network to connect to from the list of active network suggestions
   * provided by apps.
   * Note:
   * <li> This class is not thread safe and meant to be used only from {@link WifiNetworkSelector}.
   * </li>
   *
   * This is a non-optimal implementation which picks any network suggestion which matches
   * the scan result with the highest RSSI.
   * TODO: More advanced implementation will follow!
   * Params to consider for evaluating network suggestions:
   *  - Regular network evaluator params like security, band, RSSI, etc.
   *  - Priority of suggestions provided by a single app.
   *  - Whether the network suggestions requires user/app interaction or if it is metered.
   *  - Historical quality of suggestions provided by the corresponding app.
   */
  @NotThreadSafe
  public class NetworkSuggestionEvaluator implements WifiNetworkSelector.NetworkEvaluator {
      private static final String TAG = "NetworkSuggestionEvaluator";
  
      private final WifiNetworkSuggestionsManager mWifiNetworkSuggestionsManager;
      private final WifiConfigManager mWifiConfigManager;
      private final LocalLog mLocalLog;
  
      NetworkSuggestionEvaluator(WifiNetworkSuggestionsManager networkSuggestionsManager,
              WifiConfigManager wifiConfigManager, LocalLog localLog) {
          mWifiNetworkSuggestionsManager = networkSuggestionsManager;
          mWifiConfigManager = wifiConfigManager;
          mLocalLog = localLog;
      }
  
      @Override
      public void update(List<ScanDetail> scanDetails) {
          // TODO(b/115504887): This could be used to re-evaluate any temporary blacklists.
      }
  
      @Override
      public WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
              WifiConfiguration currentNetwork, String currentBssid, boolean connected,
              boolean untrustedNetworkAllowed,
              @NonNull OnConnectableListener onConnectableListener) {
          MatchMetaInfo matchMetaInfo = new MatchMetaInfo();
          for (int i = 0; i < scanDetails.size(); i++) {
              ScanDetail scanDetail = scanDetails.get(i);
              ScanResult scanResult = scanDetail.getScanResult();
              // If the user previously forgot this network, don't select it.
              if (mWifiConfigManager.wasEphemeralNetworkDeleted(
                      ScanResultUtil.createQuotedSSID(scanResult.SSID))) {
                  mLocalLog.log("Ignoring disabled ephemeral SSID: "
                          + WifiNetworkSelector.toScanId(scanResult));
                  continue;
              }
              Set<WifiNetworkSuggestion> matchingNetworkSuggestions =
                      mWifiNetworkSuggestionsManager.getNetworkSuggestionsForScanDetail(scanDetail);
              if (matchingNetworkSuggestions == null || matchingNetworkSuggestions.isEmpty()) {
                  continue;
              }
              // All matching suggestions have the same network credentials type. So, use any one of
              // them to lookup/add the credentials to WifiConfigManager.
              // Note: Apps could provide different credentials (password, ceritificate) for the same
              // network, need to handle that in the future.
              WifiNetworkSuggestion matchingNetworkSuggestion =
                      matchingNetworkSuggestions.stream().findAny().get();
              // Check if we already have a network with the same credentials in WifiConfigManager
              // database. If yes, we should check if the network is currently blacklisted.
              WifiConfiguration wCmConfiguredNetwork =
                      mWifiConfigManager.getConfiguredNetwork(
                              matchingNetworkSuggestion.wifiConfiguration.configKey());
              if (wCmConfiguredNetwork != null) {
                  if (!wCmConfiguredNetwork.getNetworkSelectionStatus().isNetworkEnabled()
                          && !mWifiConfigManager.tryEnableNetwork(wCmConfiguredNetwork.networkId)) {
                      mLocalLog.log("Ignoring blacklisted network: "
                              + WifiNetworkSelector.toNetworkString(wCmConfiguredNetwork));
                      continue;
                  }
              }
              matchMetaInfo.putAll(matchingNetworkSuggestions, wCmConfiguredNetwork, scanDetail);
          }
          // Return early on no match.
          if (matchMetaInfo.isEmpty()) {
              mLocalLog.log("did not see any matching network suggestions.");
              return null;
          }
          // Note: These matched sets should be very small & hence these additional manipulations that
          // follow should not be very expensive.
          PerNetworkSuggestionMatchMetaInfo candidate =
                  matchMetaInfo.findConnectableNetworksAndPickBest(onConnectableListener);
          if (candidate == null) { // should never happen.
              Log.wtf(TAG, "Unexepectedly got null");
              return null;
          }
          return candidate.wCmConfiguredNetwork;
      }
  
      // Add and enable this network to the central database (i.e WifiConfigManager).
      // Returns the copy of WifiConfiguration with the allocated network ID filled in.
      private WifiConfiguration addCandidateToWifiConfigManager(
              @NonNull WifiConfiguration wifiConfiguration, int uid, @NonNull String packageName) {
          // Mark the network ephemeral because we don't want these persisted by WifiConfigManager.
          wifiConfiguration.ephemeral = true;
          wifiConfiguration.fromWifiNetworkSuggestion = true;
  
          NetworkUpdateResult result =
                  mWifiConfigManager.addOrUpdateNetwork(wifiConfiguration, uid, packageName);
          if (!result.isSuccess()) {
              mLocalLog.log("Failed to add network suggestion");
              return null;
          }
          if (!mWifiConfigManager.updateNetworkSelectionStatus(result.getNetworkId(),
                  WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE)) {
              mLocalLog.log("Failed to make network suggestion selectable");
              return null;
          }
          int candidateNetworkId = result.getNetworkId();
          return mWifiConfigManager.getConfiguredNetwork(candidateNetworkId);
      }
  
      @Override
      public @EvaluatorId int getId() {
          return EVALUATOR_ID_SUGGESTION;
      }
  
      @Override
      public String getName() {
          return TAG;
      }
  
      // Container classes to handle book-keeping while we're iterating through the scan list.
      private class PerNetworkSuggestionMatchMetaInfo {
          public final WifiNetworkSuggestion wifiNetworkSuggestion;
          public final ScanDetail matchingScanDetail;
          public WifiConfiguration wCmConfiguredNetwork; // Added to WifiConfigManager.
  
          PerNetworkSuggestionMatchMetaInfo(@NonNull WifiNetworkSuggestion wifiNetworkSuggestion,
                                            @Nullable WifiConfiguration wCmConfiguredNetwork,
                                            @NonNull ScanDetail matchingScanDetail) {
              this.wifiNetworkSuggestion = wifiNetworkSuggestion;
              this.wCmConfiguredNetwork = wCmConfiguredNetwork;
              this.matchingScanDetail = matchingScanDetail;
          }
      }
  
      private class PerAppMatchMetaInfo {
          public final List<PerNetworkSuggestionMatchMetaInfo> networkInfos = new ArrayList<>();
  
          /**
           * Add the network suggestion & associated info to this package meta info.
           */
          public void put(WifiNetworkSuggestion wifiNetworkSuggestion,
                          WifiConfiguration matchingWifiConfiguration,
                          ScanDetail matchingScanDetail) {
              networkInfos.add(new PerNetworkSuggestionMatchMetaInfo(
                      wifiNetworkSuggestion, matchingWifiConfiguration, matchingScanDetail));
          }
  
          /**
           * Pick the highest priority networks among the current match info candidates for this
           * app.
           */
          public List<PerNetworkSuggestionMatchMetaInfo> getHighestPriorityNetworks() {
              // Partition the list to a map of network suggestions keyed in by the priorities.
              // There can be multiple networks with the same priority, hence a list in the value.
              Map<Integer, List<PerNetworkSuggestionMatchMetaInfo>> matchedNetworkInfosPerPriority =
                      networkInfos.stream()
                              .collect(Collectors.toMap(
                                      e -> e.wifiNetworkSuggestion.wifiConfiguration.priority,
                                      e -> Arrays.asList(e),
                                      (v1, v2) -> { // concatenate networks with the same priority.
                                          List<PerNetworkSuggestionMatchMetaInfo> concatList =
                                                  new ArrayList<>(v1);
                                          concatList.addAll(v2);
                                          return concatList;
                                      }));
              if (matchedNetworkInfosPerPriority.isEmpty()) { // should never happen.
                  Log.wtf(TAG, "Unexepectedly got empty");
                  return Collections.EMPTY_LIST;
              }
              // Return the list associated with the highest priority value.
              return matchedNetworkInfosPerPriority.get(Collections.max(
                      matchedNetworkInfosPerPriority.keySet()));
          }
      }
  
      private class MatchMetaInfo {
          private Map<String, PerAppMatchMetaInfo> mAppInfos = new HashMap<>();
  
          /**
           * Add all the network suggestion & associated info.
           */
          public void putAll(Set<WifiNetworkSuggestion> wifiNetworkSuggestions,
                             WifiConfiguration wCmConfiguredNetwork,
                             ScanDetail matchingScanDetail) {
              // Separate the suggestions into buckets for each app to allow sorting based on
              // priorities set by app.
              for (WifiNetworkSuggestion wifiNetworkSuggestion : wifiNetworkSuggestions) {
                  PerAppMatchMetaInfo appInfo = mAppInfos.computeIfAbsent(
                          wifiNetworkSuggestion.suggestorPackageName, k -> new PerAppMatchMetaInfo());
                  appInfo.put(wifiNetworkSuggestion, wCmConfiguredNetwork, matchingScanDetail);
              }
          }
  
          /**
           * Are there any matched candidates?
           */
          public boolean isEmpty() {
              return mAppInfos.isEmpty();
          }
  
          /**
           * Find all the connectable networks and pick the best network among the current match info
           * candidates.
           *
           * Among the highest priority suggestions from different packages, choose the suggestion
           * with the highest RSSI.
           * Note: This should need to be replaced by a more sophisticated algorithm.
           */
          public PerNetworkSuggestionMatchMetaInfo findConnectableNetworksAndPickBest(
                  @NonNull OnConnectableListener onConnectableListener) {
              List<PerNetworkSuggestionMatchMetaInfo> allMatchedNetworkInfos = new ArrayList<>();
              for (PerAppMatchMetaInfo appInfo : mAppInfos.values()) {
                  List<PerNetworkSuggestionMatchMetaInfo> matchedNetworkInfos =
                          appInfo.getHighestPriorityNetworks();
                  for (PerNetworkSuggestionMatchMetaInfo matchedNetworkInfo : matchedNetworkInfos) {
                      // if the network does not already exist in WifiConfigManager, add now.
                      if (matchedNetworkInfo.wCmConfiguredNetwork == null) {
                          matchedNetworkInfo.wCmConfiguredNetwork = addCandidateToWifiConfigManager(
                                  matchedNetworkInfo.wifiNetworkSuggestion.wifiConfiguration,
                                  matchedNetworkInfo.wifiNetworkSuggestion.suggestorUid,
                                  matchedNetworkInfo.wifiNetworkSuggestion.suggestorPackageName);
                          if (matchedNetworkInfo.wCmConfiguredNetwork == null) continue;
                          mLocalLog.log(String.format("network suggestion candidate %s (new)",
                                  WifiNetworkSelector.toNetworkString(
                                          matchedNetworkInfo.wCmConfiguredNetwork)));
                      } else {
                          mLocalLog.log(String.format("network suggestion candidate %s (existing)",
                                  WifiNetworkSelector.toNetworkString(
                                          matchedNetworkInfo.wCmConfiguredNetwork)));
                      }
                      allMatchedNetworkInfos.add(matchedNetworkInfo);
                      // Invoke onConnectable for the best networks from each app.
                      onConnectableListener.onConnectable(
                              matchedNetworkInfo.matchingScanDetail,
                              matchedNetworkInfo.wCmConfiguredNetwork,
                              0);
                  }
              }
              PerNetworkSuggestionMatchMetaInfo networkInfo = allMatchedNetworkInfos
                      .stream()
                      .max(Comparator.comparing(e -> e.matchingScanDetail.getScanResult().level))
                      .orElse(null);
              if (networkInfo == null) { // should never happen.
                  Log.wtf(TAG, "Unexepectedly got null");
                  return null;
              }
              return networkInfo;
          }
      }
  
  }
  