content: /*
   * Copyright (C) 2019 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.p2p;
  
  import android.net.wifi.p2p.WifiP2pConfig;
  import android.net.wifi.p2p.WifiP2pGroup;
  import android.net.wifi.p2p.WifiP2pGroupList;
  import android.util.Log;
  
  import com.android.server.wifi.Clock;
  import com.android.server.wifi.nano.WifiMetricsProto.GroupEvent;
  import com.android.server.wifi.nano.WifiMetricsProto.P2pConnectionEvent;
  import com.android.server.wifi.nano.WifiMetricsProto.WifiP2pStats;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Calendar;
  import java.util.Collection;
  import java.util.List;
  
  /**
   * Provides storage for wireless connectivity P2p metrics, as they are generated.
   * Metrics logged by this class include:
   *   Aggregated connection stats (num of connections, num of failures, ...)
   *   Discrete connection event stats (time, duration, failure codes, ...)
   */
  public class WifiP2pMetrics {
      private static final String TAG = "WifiP2pMetrics";
      private static final boolean DBG = false;
  
      private static final int MAX_CONNECTION_EVENTS = 256;
      private static final int MAX_GROUP_EVENTS = 256;
  
      private Clock mClock;
      private final Object mLock = new Object();
  
      /**
       * Metrics are stored within an instance of the WifiP2pStats proto during runtime,
       * The P2pConnectionEvent and GroupEvent metrics are stored during runtime in member
       * lists of this WifiP2pMetrics class, with the final WifiLog proto being pieced
       * together at dump-time
       */
      private final WifiP2pStats mWifiP2pStatsProto =
              new WifiP2pStats();
  
      /**
       * Connection information that gets logged for every P2P connection attempt.
       */
      private final List<P2pConnectionEvent> mConnectionEventList =
              new ArrayList<>();
  
      /**
       * The latest started (but un-ended) connection attempt
       */
      private P2pConnectionEvent mCurrentConnectionEvent;
  
      /**
       * The latest started (but un-ended) connection attempt start time
       */
      private long mCurrentConnectionEventStartTime;
  
      /**
       * Group Session information that gets logged for every formed group.
       */
      private final List<GroupEvent> mGroupEventList =
              new ArrayList<>();
  
      /**
       * The latest started (but un-ended) group
       */
      private GroupEvent mCurrentGroupEvent;
  
      /**
       * The latest started (but un-ended) group start time
       */
      private long mCurrentGroupEventStartTime;
  
      /**
       * The latest started (but un-ended) group idle start time.
       * The group is idle if there is no connected client.
       */
      private long mCurrentGroupEventIdleStartTime;
  
      /**
       * The current number of persistent groups.
       * This should be persisted after a dump.
       */
      private int mNumPersistentGroup;
  
      public WifiP2pMetrics(Clock clock) {
          mClock = clock;
  
          mNumPersistentGroup = 0;
      }
  
      /**
       * Clear all WifiP2pMetrics, except for currentConnectionEvent.
       */
      public void clear() {
          synchronized (mLock) {
              mConnectionEventList.clear();
              if (mCurrentConnectionEvent != null) {
                  mConnectionEventList.add(mCurrentConnectionEvent);
              }
              mGroupEventList.clear();
              if (mCurrentGroupEvent != null) {
                  mGroupEventList.add(mCurrentGroupEvent);
              }
              mWifiP2pStatsProto.clear();
          }
      }
  
      /**
       * Put all metrics that were being tracked separately into mWifiP2pStatsProto
       */
      public WifiP2pStats consolidateProto() {
          synchronized (mLock) {
              mWifiP2pStatsProto.numPersistentGroup = mNumPersistentGroup;
              int connectionEventCount = mConnectionEventList.size();
              if (mCurrentConnectionEvent != null) {
                  connectionEventCount--;
              }
              mWifiP2pStatsProto.connectionEvent =
                      new P2pConnectionEvent[connectionEventCount];
              for (int i = 0; i < connectionEventCount; i++) {
                  mWifiP2pStatsProto.connectionEvent[i] = mConnectionEventList.get(i);
              }
  
              int groupEventCount = mGroupEventList.size();
              if (mCurrentGroupEvent != null) {
                  groupEventCount--;
              }
              mWifiP2pStatsProto.groupEvent =
                      new GroupEvent[groupEventCount];
              for (int i = 0; i < groupEventCount; i++) {
                  mWifiP2pStatsProto.groupEvent[i] = mGroupEventList.get(i);
              }
              return mWifiP2pStatsProto;
          }
      }
  
      /**
       * Dump all WifiP2pMetrics. Collects some metrics at this time.
       *
       * @param pw PrintWriter for writing dump to
       */
      public void dump(PrintWriter pw) {
          synchronized (mLock) {
              pw.println("WifiP2pMetrics:");
              pw.println("mConnectionEvents:");
              for (P2pConnectionEvent event : mConnectionEventList) {
                  StringBuilder sb = new StringBuilder();
                  Calendar c = Calendar.getInstance();
                  c.setTimeInMillis(event.startTimeMillis);
                  sb.append("startTime=");
                  sb.append(event.startTimeMillis == 0 ? "            <null>" :
                          String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c));
                  sb.append(", connectionType=");
                  switch (event.connectionType) {
                      case P2pConnectionEvent.CONNECTION_FRESH:
                          sb.append("FRESH");
                          break;
                      case P2pConnectionEvent.CONNECTION_REINVOKE:
                          sb.append("REINVOKE");
                          break;
                      case P2pConnectionEvent.CONNECTION_LOCAL:
                          sb.append("LOCAL");
                          break;
                      case P2pConnectionEvent.CONNECTION_FAST:
                          sb.append("FAST");
                          break;
                      default:
                          sb.append("UNKNOWN");
                          break;
                  }
                  sb.append(", wpsMethod=");
                  switch (event.wpsMethod) {
                      case P2pConnectionEvent.WPS_NA:
                          sb.append("NA");
                          break;
                      case P2pConnectionEvent.WPS_PBC:
                          sb.append("PBC");
                          break;
                      case P2pConnectionEvent.WPS_DISPLAY:
                          sb.append("DISPLAY");
                          break;
                      case P2pConnectionEvent.WPS_KEYPAD:
                          sb.append("KEYPAD");
                          break;
                      case P2pConnectionEvent.WPS_LABEL:
                          sb.append("LABLE");
                          break;
                      default:
                          sb.append("UNKNOWN");
                          break;
                  }
                  sb.append(", durationTakenToConnectMillis=");
                  sb.append(event.durationTakenToConnectMillis);
                  sb.append(", connectivityLevelFailureCode=");
                  switch (event.connectivityLevelFailureCode) {
                      case P2pConnectionEvent.CLF_NONE:
                          sb.append("NONE");
                          break;
                      case P2pConnectionEvent.CLF_TIMEOUT:
                          sb.append("TIMEOUT");
                          break;
                      case P2pConnectionEvent.CLF_CANCEL:
                          sb.append("CANCEL");
                          break;
                      case P2pConnectionEvent.CLF_PROV_DISC_FAIL:
                          sb.append("PROV_DISC_FAIL");
                          break;
                      case P2pConnectionEvent.CLF_INVITATION_FAIL:
                          sb.append("INVITATION_FAIL");
                          break;
                      case P2pConnectionEvent.CLF_USER_REJECT:
                          sb.append("USER_REJECT");
                          break;
                      case P2pConnectionEvent.CLF_NEW_CONNECTION_ATTEMPT:
                          sb.append("NEW_CONNECTION_ATTEMPT");
                          break;
                      case P2pConnectionEvent.CLF_UNKNOWN:
                      default:
                          sb.append("UNKNOWN");
                          break;
                  }
  
                  if (event == mCurrentConnectionEvent) {
                      sb.append(" CURRENTLY OPEN EVENT");
                  }
                  pw.println(sb.toString());
              }
              pw.println("mGroupEvents:");
              for (GroupEvent event : mGroupEventList) {
                  StringBuilder sb = new StringBuilder();
                  Calendar c = Calendar.getInstance();
                  c.setTimeInMillis(event.startTimeMillis);
                  sb.append("netId=");
                  sb.append(event.netId);
                  sb.append(", startTime=");
                  sb.append(event.startTimeMillis == 0 ? "            <null>" :
                          String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c));
                  sb.append(", channelFrequency=");
                  sb.append(event.channelFrequency);
                  sb.append(", groupRole=");
                  switch (event.groupRole) {
                      case GroupEvent.GROUP_CLIENT:
                          sb.append("GroupClient");
                          break;
                      case GroupEvent.GROUP_OWNER:
                      default:
                          sb.append("GroupOwner");
                          break;
                  }
                  sb.append(", numConnectedClients=");
                  sb.append(event.numConnectedClients);
                  sb.append(", numCumulativeClients=");
                  sb.append(event.numCumulativeClients);
                  sb.append(", sessionDurationMillis=");
                  sb.append(event.sessionDurationMillis);
                  sb.append(", idleDurationMillis=");
                  sb.append(event.idleDurationMillis);
  
                  if (event == mCurrentGroupEvent) {
                      sb.append(" CURRENTLY OPEN EVENT");
                  }
                  pw.println(sb.toString());
              }
              pw.println("mWifiP2pStatsProto.numPersistentGroup="
                      + mNumPersistentGroup);
              pw.println("mWifiP2pStatsProto.numTotalPeerScans="
                      + mWifiP2pStatsProto.numTotalPeerScans);
              pw.println("mWifiP2pStatsProto.numTotalServiceScans="
                      + mWifiP2pStatsProto.numTotalServiceScans);
          }
      }
  
      /** Increment total number of peer scans */
      public void incrementPeerScans() {
          synchronized (mLock) {
              mWifiP2pStatsProto.numTotalPeerScans++;
          }
      }
  
      /** Increment total number of service scans */
      public void incrementServiceScans() {
          synchronized (mLock) {
              mWifiP2pStatsProto.numTotalServiceScans++;
          }
      }
  
      /** Set the number of saved persistent group */
      public void updatePersistentGroup(WifiP2pGroupList groups) {
          synchronized (mLock) {
              final Collection<WifiP2pGroup> list = groups.getGroupList();
              mNumPersistentGroup = list.size();
          }
      }
  
      /**
       * Create a new connection event. Call when p2p attmpts to make a new connection to
       * another peer. If there is a current 'un-ended' connection event, it will be ended with
       * P2pConnectionEvent.CLF_NEW_CONNNECTION_ATTEMPT.
       *
       * @param connectionType indicate this connection is fresh or reinvoke.
       * @param config configuration used for this connection.
       */
      public void startConnectionEvent(int connectionType, WifiP2pConfig config) {
          synchronized (mLock) {
              // handle overlapping connection event first.
              if (mCurrentConnectionEvent != null) {
                  endConnectionEvent(P2pConnectionEvent.CLF_NEW_CONNECTION_ATTEMPT);
              }
  
              while (mConnectionEventList.size() >= MAX_CONNECTION_EVENTS) {
                  mConnectionEventList.remove(0);
              }
              mCurrentConnectionEventStartTime = mClock.getElapsedSinceBootMillis();
  
              mCurrentConnectionEvent = new P2pConnectionEvent();
              mCurrentConnectionEvent.startTimeMillis = mClock.getWallClockMillis();
              mCurrentConnectionEvent.connectionType = connectionType;
              if (config != null) {
                  mCurrentConnectionEvent.wpsMethod = config.wps.setup;
              }
  
              mConnectionEventList.add(mCurrentConnectionEvent);
          }
      }
  
      /**
       * End a Connection event record. Call when p2p connection attempt succeeds or fails.
       * If a Connection event has not been started when .end is called,
       * a new one is created with zero duration.
       *
       * @param failure indicate the failure with WifiMetricsProto.P2pConnectionEvent.CLF_X.
       */
      public void endConnectionEvent(int failure) {
          synchronized (mLock) {
              if (mCurrentConnectionEvent == null) {
                  // Reinvoking a group with invitation will be handled in supplicant.
                  // There won't be a connection starting event in framework.
                  // THe framework only get the connection ending event in GroupStarted state.
                  startConnectionEvent(P2pConnectionEvent.CONNECTION_REINVOKE, null);
              }
  
              mCurrentConnectionEvent.durationTakenToConnectMillis = (int)
                      (mClock.getElapsedSinceBootMillis()
                      - mCurrentConnectionEventStartTime);
              mCurrentConnectionEvent.connectivityLevelFailureCode = failure;
  
              mCurrentConnectionEvent = null;
          }
      }
  
      /**
       * Create a new group event.
       *
       * @param group the information of started group.
       */
      public void startGroupEvent(WifiP2pGroup group) {
          if (group == null) {
              if (DBG) Log.d(TAG, "Cannot start group event due to null group");
              return;
          }
          synchronized (mLock) {
              // handle overlapping group event first.
              if (mCurrentGroupEvent != null) {
                  if (DBG) Log.d(TAG, "Overlapping group event!");
                  endGroupEvent();
              }
  
              while (mGroupEventList.size() >= MAX_GROUP_EVENTS) {
                  mGroupEventList.remove(0);
              }
              mCurrentGroupEventStartTime = mClock.getElapsedSinceBootMillis();
              if (group.getClientList().size() == 0) {
                  mCurrentGroupEventIdleStartTime = mClock.getElapsedSinceBootMillis();
              } else {
                  mCurrentGroupEventIdleStartTime = 0;
              }
  
              mCurrentGroupEvent = new GroupEvent();
              mCurrentGroupEvent.netId = group.getNetworkId();
              mCurrentGroupEvent.startTimeMillis = mClock.getWallClockMillis();
              mCurrentGroupEvent.numConnectedClients = group.getClientList().size();
              mCurrentGroupEvent.channelFrequency = group.getFrequency();
              mCurrentGroupEvent.groupRole = group.isGroupOwner()
                      ? GroupEvent.GROUP_OWNER
                      : GroupEvent.GROUP_CLIENT;
              mGroupEventList.add(mCurrentGroupEvent);
          }
      }
  
      /**
       * Update the information of started group.
       */
      public void updateGroupEvent(WifiP2pGroup group) {
          if (group == null) {
              if (DBG) Log.d(TAG, "Cannot update group event due to null group.");
              return;
          }
          synchronized (mLock) {
              if (mCurrentGroupEvent == null) {
                  Log.w(TAG, "Cannot update group event due to no current group.");
                  return;
              }
  
              if (mCurrentGroupEvent.netId != group.getNetworkId()) {
                  Log.w(TAG, "Updating group id " + group.getNetworkId()
                          + " is different from current group id " + mCurrentGroupEvent.netId
                          + ".");
                  return;
              }
  
              int delta = group.getClientList().size() - mCurrentGroupEvent.numConnectedClients;
              mCurrentGroupEvent.numConnectedClients = group.getClientList().size();
              if (delta > 0) {
                  mCurrentGroupEvent.numCumulativeClients += delta;
              }
  
              // if new client comes during idle period, cumulate idle duration and reset idle timer.
              // if the last client disconnected during non-idle period, start idle timer.
              if (mCurrentGroupEventIdleStartTime > 0) {
                  if (group.getClientList().size() > 0) {
                      mCurrentGroupEvent.idleDurationMillis +=
                              (mClock.getElapsedSinceBootMillis()
                              - mCurrentGroupEventIdleStartTime);
                      mCurrentGroupEventIdleStartTime = 0;
                  }
              } else {
                  if (group.getClientList().size() == 0) {
                      mCurrentGroupEventIdleStartTime = mClock.getElapsedSinceBootMillis();
                  }
              }
          }
      }
  
      /**
       * End a group event.
       */
      public void endGroupEvent() {
          synchronized (mLock) {
              if (mCurrentGroupEvent != null) {
                  mCurrentGroupEvent.sessionDurationMillis = (int)
                          (mClock.getElapsedSinceBootMillis()
                          - mCurrentGroupEventStartTime);
                  if (mCurrentGroupEventIdleStartTime > 0) {
                      mCurrentGroupEvent.idleDurationMillis +=
                              (mClock.getElapsedSinceBootMillis()
                              - mCurrentGroupEventIdleStartTime);
                      mCurrentGroupEventIdleStartTime = 0;
                  }
              } else {
                  Log.e(TAG, "No current group!");
              }
              mCurrentGroupEvent = null;
          }
      }
  
      /* Log Metrics */
  }
  