blob: 0a7070ffe4f6b58d153476389a46111afba5cf93 [file] [log] [blame]
/*
* 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);
}
}