content: /*
   * Copyright (C) 2016 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.devicepolicy;
  
  import static java.util.concurrent.TimeUnit.NANOSECONDS;
  
  import android.app.AlarmManager;
  import android.app.AlarmManager.OnAlarmListener;
  import android.app.admin.DeviceAdminReceiver;
  import android.app.admin.NetworkEvent;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.SystemClock;
  import android.util.LongSparseArray;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.concurrent.TimeUnit;
  
  /**
   * A Handler class for managing network logging on a background thread.
   */
  final class NetworkLoggingHandler extends Handler {
  
      private static final String TAG = NetworkLoggingHandler.class.getSimpleName();
  
      static final String NETWORK_EVENT_KEY = "network_event";
  
      // If this value changes, update DevicePolicyManager#retrieveNetworkLogs() javadoc
      private static final int MAX_EVENTS_PER_BATCH = 1200;
  
      /**
       * Maximum number of batches to store in memory. If more batches are generated and the DO
       * doesn't fetch them, we will discard the oldest one.
       */
      private static final int MAX_BATCHES = 5;
  
      private static final long BATCH_FINALIZATION_TIMEOUT_MS = 90 * 60 * 1000; // 1.5h
      private static final long BATCH_FINALIZATION_TIMEOUT_ALARM_INTERVAL_MS = 30 * 60 * 1000; // 30m
  
      private static final String NETWORK_LOGGING_TIMEOUT_ALARM_TAG = "NetworkLogging.batchTimeout";
  
      /** Delay after which older batches get discarded after a retrieval. */
      private static final long RETRIEVED_BATCH_DISCARD_DELAY_MS = 5 * 60 * 1000; // 5m
  
      /** Throttle batch finalization to 10 seconds.*/
      private static final long FORCE_FETCH_THROTTLE_NS = TimeUnit.SECONDS.toNanos(10);
      /** Timestamp of the last call to finalise a batch. Used for throttling forced finalization.*/
      @GuardedBy("this")
      private long mLastFinalizationNanos = -1;
  
      /** Do not call into mDpm with locks held */
      private final DevicePolicyManagerService mDpm;
      private final AlarmManager mAlarmManager;
  
      private long mId;
  
      private final OnAlarmListener mBatchTimeoutAlarmListener = new OnAlarmListener() {
          @Override
          public void onAlarm() {
              Slog.d(TAG, "Received a batch finalization timeout alarm, finalizing "
                      + mNetworkEvents.size() + " pending events.");
              Bundle notificationExtras = null;
              synchronized (NetworkLoggingHandler.this) {
                  notificationExtras = finalizeBatchAndBuildDeviceOwnerMessageLocked();
              }
              if (notificationExtras != null) {
                  notifyDeviceOwner(notificationExtras);
              }
          }
      };
  
      @VisibleForTesting
      static final int LOG_NETWORK_EVENT_MSG = 1;
  
      /** Network events accumulated so far to be finalized into a batch at some point. */
      @GuardedBy("this")
      private ArrayList<NetworkEvent> mNetworkEvents = new ArrayList<>();
  
      /**
       * Up to {@code MAX_BATCHES} finalized batches of logs ready to be retrieved by the DO. Already
       * retrieved batches are discarded after {@code RETRIEVED_BATCH_DISCARD_DELAY_MS}.
       */
      @GuardedBy("this")
      private final LongSparseArray<ArrayList<NetworkEvent>> mBatches =
              new LongSparseArray<>(MAX_BATCHES);
  
      @GuardedBy("this")
      private boolean mPaused = false;
  
      // each full batch is represented by its token, which the DPC has to provide back to retrieve it
      @GuardedBy("this")
      private long mCurrentBatchToken;
  
      @GuardedBy("this")
      private long mLastRetrievedBatchToken;
  
      NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm) {
          this(looper, dpm, 0 /* event id */);
      }
  
      @VisibleForTesting
      NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm, long id) {
          super(looper);
          this.mDpm = dpm;
          this.mAlarmManager = mDpm.mInjector.getAlarmManager();
          this.mId = id;
      }
  
      @Override
      public void handleMessage(Message msg) {
          switch (msg.what) {
              case LOG_NETWORK_EVENT_MSG: {
                  final NetworkEvent networkEvent = msg.getData().getParcelable(NETWORK_EVENT_KEY);
                  if (networkEvent != null) {
                      Bundle notificationExtras = null;
                      synchronized (NetworkLoggingHandler.this) {
                          mNetworkEvents.add(networkEvent);
                          if (mNetworkEvents.size() >= MAX_EVENTS_PER_BATCH) {
                              notificationExtras = finalizeBatchAndBuildDeviceOwnerMessageLocked();
                          }
                      }
                      if (notificationExtras != null) {
                          notifyDeviceOwner(notificationExtras);
                      }
                  }
                  break;
              }
              default: {
                  Slog.d(TAG, "NetworkLoggingHandler received an unknown of message.");
                  break;
              }
          }
      }
  
      void scheduleBatchFinalization() {
          final long when = SystemClock.elapsedRealtime() + BATCH_FINALIZATION_TIMEOUT_MS;
          // We use alarm manager and not just postDelayed here to ensure the batch gets finalized
          // even if the device goes to sleep.
          mAlarmManager.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, when,
                  BATCH_FINALIZATION_TIMEOUT_ALARM_INTERVAL_MS, NETWORK_LOGGING_TIMEOUT_ALARM_TAG,
                  mBatchTimeoutAlarmListener, this);
          Slog.d(TAG, "Scheduled a new batch finalization alarm " + BATCH_FINALIZATION_TIMEOUT_MS
                  + "ms from now.");
      }
  
      /**
       * Forces batch finalisation. Throttled to 10 seconds per batch finalisation.
       * @return the number of milliseconds to wait until batch finalisation can be forced.
       */
      long forceBatchFinalization() {
          Bundle notificationExtras;
          synchronized (this) {
              final long toWaitNanos =
                  mLastFinalizationNanos + FORCE_FETCH_THROTTLE_NS - System.nanoTime();
              if (toWaitNanos > 0) {
                  return NANOSECONDS.toMillis(toWaitNanos) + 1; // Round up.
              }
              notificationExtras = finalizeBatchAndBuildDeviceOwnerMessageLocked();
          }
          if (notificationExtras != null) {
              notifyDeviceOwner(notificationExtras);
          }
          return 0;
      }
  
      synchronized void pause() {
          Slog.d(TAG, "Paused network logging");
          mPaused = true;
      }
  
      void resume() {
          Bundle notificationExtras = null;
          synchronized (this) {
              if (!mPaused) {
                  Slog.d(TAG, "Attempted to resume network logging, but logging is not paused.");
                  return;
              }
  
              Slog.d(TAG, "Resumed network logging. Current batch=" + mCurrentBatchToken
                      + ", LastRetrievedBatch=" + mLastRetrievedBatchToken);
              mPaused = false;
  
              // If there is a batch ready that the device owner hasn't been notified about, do it now.
              if (mBatches.size() > 0 && mLastRetrievedBatchToken != mCurrentBatchToken) {
                  scheduleBatchFinalization();
                  notificationExtras = buildDeviceOwnerMessageLocked();
              }
          }
          if (notificationExtras != null) {
              notifyDeviceOwner(notificationExtras);
          }
      }
  
      synchronized void discardLogs() {
          mBatches.clear();
          mNetworkEvents = new ArrayList<>();
          Slog.d(TAG, "Discarded all network logs");
      }
  
      @GuardedBy("this")
      /** @returns extras if a message should be sent to the device owner */
      private Bundle finalizeBatchAndBuildDeviceOwnerMessageLocked() {
          mLastFinalizationNanos = System.nanoTime();
          Bundle notificationExtras = null;
          if (mNetworkEvents.size() > 0) {
              // Assign ids to the events.
              for (NetworkEvent event : mNetworkEvents) {
                  event.setId(mId);
                  if (mId == Long.MAX_VALUE) {
                      Slog.i(TAG, "Reached maximum id value; wrapping around ." + mCurrentBatchToken);
                      mId = 0;
                  } else {
                      mId++;
                  }
              }
              // Finalize the batch and start a new one from scratch.
              if (mBatches.size() >= MAX_BATCHES) {
                  // Remove the oldest batch if we hit the limit.
                  mBatches.removeAt(0);
              }
              mCurrentBatchToken++;
              mBatches.append(mCurrentBatchToken, mNetworkEvents);
              mNetworkEvents = new ArrayList<>();
              if (!mPaused) {
                  notificationExtras = buildDeviceOwnerMessageLocked();
              }
          } else {
              // Don't notify the DO, since there are no events; DPC can still retrieve
              // the last full batch if not paused.
              Slog.d(TAG, "Was about to finalize the batch, but there were no events to send to"
                      + " the DPC, the batchToken of last available batch: " + mCurrentBatchToken);
          }
          // Regardless of whether the batch was non-empty schedule a new finalization after timeout.
          scheduleBatchFinalization();
          return notificationExtras;
      }
  
      @GuardedBy("this")
      /** Build extras notification to the DO. Should only be called when there
          is a batch available. */
      private Bundle buildDeviceOwnerMessageLocked() {
          final Bundle extras = new Bundle();
          final int lastBatchSize = mBatches.valueAt(mBatches.size() - 1).size();
          extras.putLong(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_TOKEN, mCurrentBatchToken);
          extras.putInt(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_COUNT, lastBatchSize);
          return extras;
      }
  
      /** Sends a notification to the DO. Should not hold locks as DevicePolicyManagerService may
          call into NetworkLoggingHandler. */
      private void notifyDeviceOwner(Bundle extras) {
          Slog.d(TAG, "Sending network logging batch broadcast to device owner, batchToken: "
                  + extras.getLong(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_TOKEN, -1));
          if (Thread.holdsLock(this)) {
              Slog.wtfStack(TAG, "Shouldn't be called with NetworkLoggingHandler lock held");
              return;
          }
          mDpm.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE, extras);
      }
  
      synchronized List<NetworkEvent> retrieveFullLogBatch(final long batchToken) {
          final int index = mBatches.indexOfKey(batchToken);
          if (index < 0) {
              // Invalid token or batch has already been discarded.
              return null;
          }
  
          // Schedule this and older batches to be discarded after a delay to lessen memory load
          // without interfering with the admin's ability to collect logs out-of-order.
          // It isn't critical and we allow it to be delayed further if the phone sleeps, so we don't
          // use the alarm manager here.
          postDelayed(() -> {
              synchronized(this) {
                  while (mBatches.size() > 0 && mBatches.keyAt(0) <= batchToken) {
                      mBatches.removeAt(0);
                  }
              }
          }, RETRIEVED_BATCH_DISCARD_DELAY_MS);
  
          mLastRetrievedBatchToken = batchToken;
          return mBatches.valueAt(index);
      }
  }
  
  