/*
* Copyright (C) 2023 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.connectivity;
import static android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkSpecifier;
import android.net.TelephonyNetworkSpecifier;
import android.net.TransportInfo;
import android.net.wifi.WifiInfo;
import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.VisibleForTesting;
import com.android.metrics.DailykeepaliveInfoReported;
import com.android.metrics.DurationForNumOfKeepalive;
import com.android.metrics.DurationPerNumOfKeepalive;
import com.android.metrics.KeepaliveLifetimeForCarrier;
import com.android.metrics.KeepaliveLifetimePerCarrier;
import com.android.modules.utils.BackgroundThread;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.server.ConnectivityStatsLog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Tracks carrier and duration metrics of automatic on/off keepalives.
*
*
This class follows AutomaticOnOffKeepaliveTracker closely and its on*Keepalive methods needs
* to be called in a timely manner to keep the metrics accurate. It is also not thread-safe and all
* public methods must be called by the same thread, namely the ConnectivityService handler thread.
*
*
In the case that the keepalive state becomes out of sync with the hardware, the tracker will
* be disabled. e.g. Calling onStartKeepalive on a given network, slot pair twice without calling
* onStopKeepalive is unexpected and will disable the tracker.
*/
public class KeepaliveStatsTracker {
private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
private static final int INVALID_KEEPALIVE_ID = -1;
// 2 hour acceptable deviation in metrics collection duration time to account for the 1 hour
// window of AlarmManager.
private static final long MAX_EXPECTED_DURATION_MS =
AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 2 * 60 * 60 * 1_000L;
@NonNull private final Handler mConnectivityServiceHandler;
@NonNull private final Dependencies mDependencies;
// Mapping of subId to carrierId. Updates are received from OnSubscriptionsChangedListener
private final SparseIntArray mCachedCarrierIdPerSubId = new SparseIntArray();
// The default subscription id obtained from SubscriptionManager.getDefaultSubscriptionId.
// Updates are received from the ACTION_DEFAULT_SUBSCRIPTION_CHANGED broadcast.
private int mCachedDefaultSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
// Boolean to track whether the KeepaliveStatsTracker is enabled.
// Use a final AtomicBoolean to ensure initialization is seen on the handler thread.
// Repeated fields in metrics are only supported on T+ so this is enabled only on T+.
private final AtomicBoolean mEnabled = new AtomicBoolean(SdkLevel.isAtLeastT());
// Class to store network information, lifetime durations and active state of a keepalive.
private static final class KeepaliveStats {
// The carrier ID for a keepalive, or TelephonyManager.UNKNOWN_CARRIER_ID(-1) if not set.
public final int carrierId;
// The transport types of the underlying network for each keepalive. A network may include
// multiple transport types. Each transport type is represented by a different bit, defined
// in NetworkCapabilities
public final int transportTypes;
// The keepalive interval in millis.
public final int intervalMs;
// The uid of the app that requested the keepalive.
public final int appUid;
// Indicates if the keepalive is an automatic keepalive.
public final boolean isAutoKeepalive;
// Snapshot of the lifetime stats
public static class LifetimeStats {
public final int lifetimeMs;
public final int activeLifetimeMs;
LifetimeStats(int lifetimeMs, int activeLifetimeMs) {
this.lifetimeMs = lifetimeMs;
this.activeLifetimeMs = activeLifetimeMs;
}
}
// The total time since the keepalive is started until it is stopped.
private int mLifetimeMs = 0;
// The total time the keepalive is active (not suspended).
private int mActiveLifetimeMs = 0;
// A timestamp of the most recent time the lifetime metrics was updated.
private long mLastUpdateLifetimeTimestamp;
// A flag to indicate if the keepalive is active.
private boolean mKeepaliveActive = true;
/**
* Gets the lifetime stats for the keepalive, updated to timeNow, and then resets it.
*
* @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
*/
public LifetimeStats getAndResetLifetimeStats(long timeNow) {
updateLifetimeStatsAndSetActive(timeNow, mKeepaliveActive);
// Get a snapshot of the stats
final LifetimeStats lifetimeStats = new LifetimeStats(mLifetimeMs, mActiveLifetimeMs);
// Reset the stats
resetLifetimeStats(timeNow);
return lifetimeStats;
}
public boolean isKeepaliveActive() {
return mKeepaliveActive;
}
KeepaliveStats(
int carrierId,
int transportTypes,
int intervalSeconds,
int appUid,
boolean isAutoKeepalive,
long timeNow) {
this.carrierId = carrierId;
this.transportTypes = transportTypes;
this.intervalMs = intervalSeconds * 1000;
this.appUid = appUid;
this.isAutoKeepalive = isAutoKeepalive;
mLastUpdateLifetimeTimestamp = timeNow;
}
/**
* Updates the lifetime metrics to the given time and sets the active state. This should be
* called whenever the active state of the keepalive changes.
*
* @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
*/
public void updateLifetimeStatsAndSetActive(long timeNow, boolean keepaliveActive) {
final int durationIncrease = (int) (timeNow - mLastUpdateLifetimeTimestamp);
mLifetimeMs += durationIncrease;
if (mKeepaliveActive) mActiveLifetimeMs += durationIncrease;
mLastUpdateLifetimeTimestamp = timeNow;
mKeepaliveActive = keepaliveActive;
}
/**
* Resets the lifetime metrics but does not reset the active/stopped state of the keepalive.
* This also updates the time to timeNow, ensuring stats will start from this time.
*
* @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
*/
public void resetLifetimeStats(long timeNow) {
mLifetimeMs = 0;
mActiveLifetimeMs = 0;
mLastUpdateLifetimeTimestamp = timeNow;
}
}
// List of duration stats metric where the index is the number of concurrent keepalives.
// Each DurationForNumOfKeepalive message stores a registered duration and an active duration.
// Registered duration is the total time spent with mNumRegisteredKeepalive == index.
// Active duration is the total time spent with mNumActiveKeepalive == index.
private final List mDurationPerNumOfKeepalive =
new ArrayList<>();
// Map of keepalives identified by the id from getKeepaliveId to their stats information.
private final SparseArray mKeepaliveStatsPerId = new SparseArray<>();
// Generate and return a unique integer using a given network's netId and the slot number.
// This is possible because netId is a 16 bit integer, so an integer with the first 16 bits as
// the netId and the last 16 bits as the slot number can be created. This allows slot numbers to
// be up to 2^16.
// Returns INVALID_KEEPALIVE_ID if the netId or slot is not as expected above.
private int getKeepaliveId(@NonNull Network network, int slot) {
final int netId = network.getNetId();
// Since there is no enforcement that a Network's netId is valid check for it here.
if (netId < 0 || netId >= (1 << 16)) {
disableTracker("Unexpected netId value: " + netId);
return INVALID_KEEPALIVE_ID;
}
if (slot < 0 || slot >= (1 << 16)) {
disableTracker("Unexpected slot value: " + slot);
return INVALID_KEEPALIVE_ID;
}
return (netId << 16) + slot;
}
// Class to act as the key to aggregate the KeepaliveLifetimeForCarrier stats.
private static final class LifetimeKey {
public final int carrierId;
public final int transportTypes;
public final int intervalMs;
LifetimeKey(int carrierId, int transportTypes, int intervalMs) {
this.carrierId = carrierId;
this.transportTypes = transportTypes;
this.intervalMs = intervalMs;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final LifetimeKey that = (LifetimeKey) o;
return carrierId == that.carrierId && transportTypes == that.transportTypes
&& intervalMs == that.intervalMs;
}
@Override
public int hashCode() {
return carrierId + 3 * transportTypes + 5 * intervalMs;
}
}
// Map to aggregate the KeepaliveLifetimeForCarrier stats using LifetimeKey as the key.
final Map mAggregateKeepaliveLifetime =
new HashMap<>();
private final Set mAppUids = new HashSet();
private int mNumKeepaliveRequests = 0;
private int mNumAutomaticKeepaliveRequests = 0;
private int mNumRegisteredKeepalive = 0;
private int mNumActiveKeepalive = 0;
// A timestamp of the most recent time the duration metrics was updated.
private long mLastUpdateDurationsTimestamp;
/** Dependency class */
@VisibleForTesting
public static class Dependencies {
// Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
// relative to start time and avoid timezone change, including time spent in deep sleep.
public long getElapsedRealtime() {
return SystemClock.elapsedRealtime();
}
/**
* Writes a DAILY_KEEPALIVE_INFO_REPORTED to ConnectivityStatsLog.
*
* @param dailyKeepaliveInfoReported the proto to write to statsD.
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void writeStats(DailykeepaliveInfoReported dailyKeepaliveInfoReported) {
ConnectivityStatsLog.write(
ConnectivityStatsLog.DAILY_KEEPALIVE_INFO_REPORTED,
dailyKeepaliveInfoReported.getDurationPerNumOfKeepalive().toByteArray(),
dailyKeepaliveInfoReported.getKeepaliveLifetimePerCarrier().toByteArray(),
dailyKeepaliveInfoReported.getKeepaliveRequests(),
dailyKeepaliveInfoReported.getAutomaticKeepaliveRequests(),
dailyKeepaliveInfoReported.getDistinctUserCount(),
CollectionUtils.toIntArray(dailyKeepaliveInfoReported.getUidList()));
}
}
public KeepaliveStatsTracker(@NonNull Context context, @NonNull Handler handler) {
this(context, handler, new Dependencies());
}
private final Context mContext;
private final SubscriptionManager mSubscriptionManager;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mCachedDefaultSubscriptionId =
intent.getIntExtra(
SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
}
};
private final CompletableFuture mListenerFuture =
new CompletableFuture<>();
@VisibleForTesting
public KeepaliveStatsTracker(
@NonNull Context context,
@NonNull Handler handler,
@NonNull Dependencies dependencies) {
mContext = Objects.requireNonNull(context);
mDependencies = Objects.requireNonNull(dependencies);
mConnectivityServiceHandler = Objects.requireNonNull(handler);
mSubscriptionManager =
Objects.requireNonNull(context.getSystemService(SubscriptionManager.class));
mLastUpdateDurationsTimestamp = mDependencies.getElapsedRealtime();
if (!isEnabled()) {
return;
}
context.registerReceiver(
mBroadcastReceiver,
new IntentFilter(SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED),
/* broadcastPermission= */ null,
mConnectivityServiceHandler);
// The default constructor for OnSubscriptionsChangedListener will always implicitly grab
// the looper of the current thread. In the case the current thread does not have a looper,
// this will throw. Therefore, post a runnable that creates it there.
// When the callback is called on the BackgroundThread, post a message on the CS handler
// thread to update the caches, which can only be touched there.
BackgroundThread.getHandler().post(() -> {
final OnSubscriptionsChangedListener listener =
new OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
final List activeSubInfoList =
mSubscriptionManager.getActiveSubscriptionInfoList();
// A null subInfo list here indicates the current state is unknown
// but not necessarily empty, simply ignore it. Another call to the
// listener will be invoked in the future.
if (activeSubInfoList == null) return;
mConnectivityServiceHandler.post(() -> {
mCachedCarrierIdPerSubId.clear();
for (final SubscriptionInfo subInfo : activeSubInfoList) {
mCachedCarrierIdPerSubId.put(subInfo.getSubscriptionId(),
subInfo.getCarrierId());
}
});
}
};
mListenerFuture.complete(listener);
mSubscriptionManager.addOnSubscriptionsChangedListener(r -> r.run(), listener);
});
}
/** Ensures the list of duration metrics is large enough for number of registered keepalives. */
private void ensureDurationPerNumOfKeepaliveSize() {
if (mNumActiveKeepalive < 0 || mNumRegisteredKeepalive < 0) {
disableTracker("Number of active or registered keepalives is negative");
return;
}
if (mNumActiveKeepalive > mNumRegisteredKeepalive) {
disableTracker("Number of active keepalives greater than registered keepalives");
return;
}
while (mDurationPerNumOfKeepalive.size() <= mNumRegisteredKeepalive) {
final DurationForNumOfKeepalive.Builder durationForNumOfKeepalive =
DurationForNumOfKeepalive.newBuilder();
durationForNumOfKeepalive.setNumOfKeepalive(mDurationPerNumOfKeepalive.size());
durationForNumOfKeepalive.setKeepaliveRegisteredDurationsMsec(0);
durationForNumOfKeepalive.setKeepaliveActiveDurationsMsec(0);
mDurationPerNumOfKeepalive.add(durationForNumOfKeepalive);
}
}
/**
* Updates the durations metrics to the given time. This should always be called before making a
* change to mNumRegisteredKeepalive or mNumActiveKeepalive to keep the duration metrics
* correct.
*
* @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
*/
private void updateDurationsPerNumOfKeepalive(long timeNow) {
if (mDurationPerNumOfKeepalive.size() < mNumRegisteredKeepalive) {
Log.e(TAG, "Unexpected jump in number of registered keepalive");
}
ensureDurationPerNumOfKeepaliveSize();
final int durationIncrease = (int) (timeNow - mLastUpdateDurationsTimestamp);
final DurationForNumOfKeepalive.Builder durationForNumOfRegisteredKeepalive =
mDurationPerNumOfKeepalive.get(mNumRegisteredKeepalive);
durationForNumOfRegisteredKeepalive.setKeepaliveRegisteredDurationsMsec(
durationForNumOfRegisteredKeepalive.getKeepaliveRegisteredDurationsMsec()
+ durationIncrease);
final DurationForNumOfKeepalive.Builder durationForNumOfActiveKeepalive =
mDurationPerNumOfKeepalive.get(mNumActiveKeepalive);
durationForNumOfActiveKeepalive.setKeepaliveActiveDurationsMsec(
durationForNumOfActiveKeepalive.getKeepaliveActiveDurationsMsec()
+ durationIncrease);
mLastUpdateDurationsTimestamp = timeNow;
}
// TODO: Move this function to frameworks/libs/net/.../NetworkCapabilitiesUtils.java
private static int getSubId(@NonNull NetworkCapabilities nc, int defaultSubId) {
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
final NetworkSpecifier networkSpecifier = nc.getNetworkSpecifier();
if (networkSpecifier instanceof TelephonyNetworkSpecifier) {
return ((TelephonyNetworkSpecifier) networkSpecifier).getSubscriptionId();
}
// Use the default subscriptionId.
return defaultSubId;
}
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
final TransportInfo info = nc.getTransportInfo();
if (info instanceof WifiInfo) {
return ((WifiInfo) info).getSubscriptionId();
}
}
return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
private int getCarrierId(@NonNull NetworkCapabilities networkCapabilities) {
// Try to get the correct subscription id.
final int subId = getSubId(networkCapabilities, mCachedDefaultSubscriptionId);
if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
return TelephonyManager.UNKNOWN_CARRIER_ID;
}
return mCachedCarrierIdPerSubId.get(subId, TelephonyManager.UNKNOWN_CARRIER_ID);
}
private int getTransportTypes(@NonNull NetworkCapabilities networkCapabilities) {
// Transport types are internally packed as bits starting from bit 0. Casting to int works
// fine since for now and the foreseeable future, there will be less than 32 transports.
return (int) networkCapabilities.getTransportTypesInternal();
}
/** Inform the KeepaliveStatsTracker a keepalive has just started and is active. */
public void onStartKeepalive(
@NonNull Network network,
int slot,
@NonNull NetworkCapabilities nc,
int intervalSeconds,
int appUid,
boolean isAutoKeepalive) {
ensureRunningOnHandlerThread();
if (!isEnabled()) return;
final int keepaliveId = getKeepaliveId(network, slot);
if (keepaliveId == INVALID_KEEPALIVE_ID) return;
if (mKeepaliveStatsPerId.contains(keepaliveId)) {
disableTracker("Attempt to start keepalive stats on a known network, slot pair");
return;
}
mNumKeepaliveRequests++;
if (isAutoKeepalive) mNumAutomaticKeepaliveRequests++;
mAppUids.add(appUid);
final long timeNow = mDependencies.getElapsedRealtime();
updateDurationsPerNumOfKeepalive(timeNow);
mNumRegisteredKeepalive++;
mNumActiveKeepalive++;
final KeepaliveStats newKeepaliveStats =
new KeepaliveStats(
getCarrierId(nc),
getTransportTypes(nc),
intervalSeconds,
appUid,
isAutoKeepalive,
timeNow);
mKeepaliveStatsPerId.put(keepaliveId, newKeepaliveStats);
}
/**
* Inform the KeepaliveStatsTracker that the keepalive with the given network, slot pair has
* updated its active state to keepaliveActive.
*/
private void onKeepaliveActive(
@NonNull Network network, int slot, boolean keepaliveActive) {
final long timeNow = mDependencies.getElapsedRealtime();
onKeepaliveActive(network, slot, keepaliveActive, timeNow);
}
/**
* Inform the KeepaliveStatsTracker that the keepalive with the given network, slot pair has
* updated its active state to keepaliveActive.
*
* @param network the network of the keepalive
* @param slot the slot number of the keepalive
* @param keepaliveActive the new active state of the keepalive
* @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
*/
private void onKeepaliveActive(
@NonNull Network network, int slot, boolean keepaliveActive, long timeNow) {
final int keepaliveId = getKeepaliveId(network, slot);
if (keepaliveId == INVALID_KEEPALIVE_ID) return;
final KeepaliveStats keepaliveStats = mKeepaliveStatsPerId.get(keepaliveId, null);
if (keepaliveStats == null) {
disableTracker("Attempt to set active keepalive on an unknown network, slot pair");
return;
}
updateDurationsPerNumOfKeepalive(timeNow);
if (keepaliveActive != keepaliveStats.isKeepaliveActive()) {
mNumActiveKeepalive += keepaliveActive ? 1 : -1;
}
keepaliveStats.updateLifetimeStatsAndSetActive(timeNow, keepaliveActive);
}
/** Inform the KeepaliveStatsTracker a keepalive has just been paused. */
public void onPauseKeepalive(@NonNull Network network, int slot) {
ensureRunningOnHandlerThread();
if (!isEnabled()) return;
onKeepaliveActive(network, slot, /* keepaliveActive= */ false);
}
/** Inform the KeepaliveStatsTracker a keepalive has just been resumed. */
public void onResumeKeepalive(@NonNull Network network, int slot) {
ensureRunningOnHandlerThread();
if (!isEnabled()) return;
onKeepaliveActive(network, slot, /* keepaliveActive= */ true);
}
/** Inform the KeepaliveStatsTracker a keepalive has just been stopped. */
public void onStopKeepalive(@NonNull Network network, int slot) {
ensureRunningOnHandlerThread();
if (!isEnabled()) return;
final int keepaliveId = getKeepaliveId(network, slot);
if (keepaliveId == INVALID_KEEPALIVE_ID) return;
final long timeNow = mDependencies.getElapsedRealtime();
onKeepaliveActive(network, slot, /* keepaliveActive= */ false, timeNow);
final KeepaliveStats keepaliveStats = mKeepaliveStatsPerId.get(keepaliveId, null);
if (keepaliveStats == null) return;
mNumRegisteredKeepalive--;
// add to the aggregate since it will be removed.
addToAggregateKeepaliveLifetime(keepaliveStats, timeNow);
// free up the slot.
mKeepaliveStatsPerId.remove(keepaliveId);
}
/**
* Updates and adds the lifetime metric of keepaliveStats to the aggregate.
*
* @param keepaliveStats the stats to add to the aggregate
* @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
*/
private void addToAggregateKeepaliveLifetime(
@NonNull KeepaliveStats keepaliveStats, long timeNow) {
final KeepaliveStats.LifetimeStats lifetimeStats =
keepaliveStats.getAndResetLifetimeStats(timeNow);
final LifetimeKey key =
new LifetimeKey(
keepaliveStats.carrierId,
keepaliveStats.transportTypes,
keepaliveStats.intervalMs);
KeepaliveLifetimeForCarrier.Builder keepaliveLifetimeForCarrier =
mAggregateKeepaliveLifetime.get(key);
if (keepaliveLifetimeForCarrier == null) {
keepaliveLifetimeForCarrier =
KeepaliveLifetimeForCarrier.newBuilder()
.setCarrierId(keepaliveStats.carrierId)
.setTransportTypes(keepaliveStats.transportTypes)
.setIntervalsMsec(keepaliveStats.intervalMs);
mAggregateKeepaliveLifetime.put(key, keepaliveLifetimeForCarrier);
}
keepaliveLifetimeForCarrier.setLifetimeMsec(
keepaliveLifetimeForCarrier.getLifetimeMsec() + lifetimeStats.lifetimeMs);
keepaliveLifetimeForCarrier.setActiveLifetimeMsec(
keepaliveLifetimeForCarrier.getActiveLifetimeMsec()
+ lifetimeStats.activeLifetimeMs);
}
/**
* Builds and returns DailykeepaliveInfoReported proto.
*
* @return the DailykeepaliveInfoReported proto that was built.
*/
@VisibleForTesting
public @NonNull DailykeepaliveInfoReported buildKeepaliveMetrics() {
ensureRunningOnHandlerThread();
final long timeNow = mDependencies.getElapsedRealtime();
return buildKeepaliveMetrics(timeNow);
}
/**
* Updates the metrics to timeNow and builds and returns DailykeepaliveInfoReported proto.
*
* @param timeNow a timestamp obtained using Dependencies.getElapsedRealtime
*/
private @NonNull DailykeepaliveInfoReported buildKeepaliveMetrics(long timeNow) {
updateDurationsPerNumOfKeepalive(timeNow);
final DurationPerNumOfKeepalive.Builder durationPerNumOfKeepalive =
DurationPerNumOfKeepalive.newBuilder();
mDurationPerNumOfKeepalive.forEach(
durationForNumOfKeepalive ->
durationPerNumOfKeepalive.addDurationForNumOfKeepalive(
durationForNumOfKeepalive));
final KeepaliveLifetimePerCarrier.Builder keepaliveLifetimePerCarrier =
KeepaliveLifetimePerCarrier.newBuilder();
for (int i = 0; i < mKeepaliveStatsPerId.size(); i++) {
final KeepaliveStats keepaliveStats = mKeepaliveStatsPerId.valueAt(i);
addToAggregateKeepaliveLifetime(keepaliveStats, timeNow);
}
// Fill keepalive carrier stats to the proto
mAggregateKeepaliveLifetime
.values()
.forEach(
keepaliveLifetimeForCarrier ->
keepaliveLifetimePerCarrier.addKeepaliveLifetimeForCarrier(
keepaliveLifetimeForCarrier));
final DailykeepaliveInfoReported.Builder dailyKeepaliveInfoReported =
DailykeepaliveInfoReported.newBuilder();
dailyKeepaliveInfoReported.setDurationPerNumOfKeepalive(durationPerNumOfKeepalive);
dailyKeepaliveInfoReported.setKeepaliveLifetimePerCarrier(keepaliveLifetimePerCarrier);
dailyKeepaliveInfoReported.setKeepaliveRequests(mNumKeepaliveRequests);
dailyKeepaliveInfoReported.setAutomaticKeepaliveRequests(mNumAutomaticKeepaliveRequests);
dailyKeepaliveInfoReported.setDistinctUserCount(mAppUids.size());
dailyKeepaliveInfoReported.addAllUid(mAppUids);
return dailyKeepaliveInfoReported.build();
}
/**
* Builds and resets the stored metrics. Similar to buildKeepaliveMetrics but also resets the
* metrics while maintaining the state of the keepalives.
*
* @return the DailykeepaliveInfoReported proto that was built.
*/
@VisibleForTesting
public @NonNull DailykeepaliveInfoReported buildAndResetMetrics() {
ensureRunningOnHandlerThread();
final long timeNow = mDependencies.getElapsedRealtime();
final DailykeepaliveInfoReported metrics = buildKeepaliveMetrics(timeNow);
mDurationPerNumOfKeepalive.clear();
mAggregateKeepaliveLifetime.clear();
mAppUids.clear();
mNumKeepaliveRequests = 0;
mNumAutomaticKeepaliveRequests = 0;
// Update the metrics with the existing keepalives.
ensureDurationPerNumOfKeepaliveSize();
mAggregateKeepaliveLifetime.clear();
// Reset the stats for existing keepalives
for (int i = 0; i < mKeepaliveStatsPerId.size(); i++) {
final KeepaliveStats keepaliveStats = mKeepaliveStatsPerId.valueAt(i);
keepaliveStats.resetLifetimeStats(timeNow);
mAppUids.add(keepaliveStats.appUid);
mNumKeepaliveRequests++;
if (keepaliveStats.isAutoKeepalive) mNumAutomaticKeepaliveRequests++;
}
return metrics;
}
private void disableTracker(String msg) {
if (!mEnabled.compareAndSet(/* expectedValue= */ true, /* newValue= */ false)) {
// already disabled
return;
}
Log.wtf(TAG, msg + ". Disabling KeepaliveStatsTracker");
mContext.unregisterReceiver(mBroadcastReceiver);
// The returned future is ignored since it is void and the is never completed exceptionally.
final CompletableFuture unused = mListenerFuture.thenAcceptAsync(
listener -> mSubscriptionManager.removeOnSubscriptionsChangedListener(listener),
BackgroundThread.getExecutor());
}
/** Whether this tracker is enabled. This method is thread safe. */
public boolean isEnabled() {
return mEnabled.get();
}
/**
* Checks the DailykeepaliveInfoReported for the following:
* 1. total active durations/lifetimes <= total registered durations/lifetimes.
* 2. Total time in Durations == total time in Carrier lifetime stats
* 3. The total elapsed real time spent is within expectations.
*/
@VisibleForTesting
public boolean allMetricsExpected(DailykeepaliveInfoReported dailyKeepaliveInfoReported) {
int totalRegistered = 0;
int totalActiveDurations = 0;
int totalTimeSpent = 0;
for (DurationForNumOfKeepalive durationForNumOfKeepalive: dailyKeepaliveInfoReported
.getDurationPerNumOfKeepalive().getDurationForNumOfKeepaliveList()) {
final int n = durationForNumOfKeepalive.getNumOfKeepalive();
totalRegistered += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec() * n;
totalActiveDurations += durationForNumOfKeepalive.getKeepaliveActiveDurationsMsec() * n;
totalTimeSpent += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec();
}
int totalLifetimes = 0;
int totalActiveLifetimes = 0;
for (KeepaliveLifetimeForCarrier keepaliveLifetimeForCarrier: dailyKeepaliveInfoReported
.getKeepaliveLifetimePerCarrier().getKeepaliveLifetimeForCarrierList()) {
totalLifetimes += keepaliveLifetimeForCarrier.getLifetimeMsec();
totalActiveLifetimes += keepaliveLifetimeForCarrier.getActiveLifetimeMsec();
}
return totalActiveDurations <= totalRegistered && totalActiveLifetimes <= totalLifetimes
&& totalLifetimes == totalRegistered && totalActiveLifetimes == totalActiveDurations
&& totalTimeSpent <= MAX_EXPECTED_DURATION_MS;
}
/** Writes the stored metrics to ConnectivityStatsLog and resets. */
public void writeAndResetMetrics() {
ensureRunningOnHandlerThread();
// Keepalive stats use repeated atoms, which are only supported on T+. If written to statsd
// on S- they will bootloop the system, so they must not be sent on S-. See b/289471411.
if (!SdkLevel.isAtLeastT()) {
Log.d(TAG, "KeepaliveStatsTracker is disabled before T, skipping write");
return;
}
if (!isEnabled()) {
Log.d(TAG, "KeepaliveStatsTracker is disabled, skipping write");
return;
}
final DailykeepaliveInfoReported dailyKeepaliveInfoReported = buildAndResetMetrics();
if (!allMetricsExpected(dailyKeepaliveInfoReported)) {
Log.wtf(TAG, "Unexpected metrics values: " + dailyKeepaliveInfoReported.toString());
}
mDependencies.writeStats(dailyKeepaliveInfoReported);
}
/** Dump KeepaliveStatsTracker state. */
public void dump(IndentingPrintWriter pw) {
ensureRunningOnHandlerThread();
pw.println("KeepaliveStatsTracker enabled: " + isEnabled());
pw.increaseIndent();
pw.println(buildKeepaliveMetrics().toString());
pw.decreaseIndent();
}
private void ensureRunningOnHandlerThread() {
if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
throw new IllegalStateException(
"Not running on handler thread: " + Thread.currentThread().getName());
}
}
}