blob: 4eef77f5e5a93e4b067a40f9e9a242714dc50523 [file] [log] [blame]
/*
* Copyright (C) 2022 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.car.telemetry.publisher;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkTemplate.OEM_MANAGED_NO;
import static android.net.NetworkTemplate.OEM_MANAGED_YES;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import android.annotation.NonNull;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkIdentity;
import android.net.NetworkTemplate;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.telephony.TelephonyManager;
import com.android.car.telemetry.TelemetryProto;
import com.android.car.telemetry.TelemetryProto.ConnectivityPublisher.OemType;
import com.android.car.telemetry.TelemetryProto.ConnectivityPublisher.Transport;
import com.android.car.telemetry.databroker.DataSubscriber;
import com.android.car.telemetry.publisher.net.FakeNetworkStats;
import com.android.car.telemetry.publisher.net.NetworkStatsServiceProxy;
import com.android.car.telemetry.publisher.net.RefinedStats;
import com.android.car.test.FakeHandlerWrapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Tests for {@link ConnectivityPublisher}.
*
* <p>Note that {@link TAG_NONE} is a total value across all the tags. NetworkStatsService returns 2
* types of netstats: summary for all (tag is equal to 0, i.e. TAG_NONE), and summary per tag.
*/
@RunWith(MockitoJUnitRunner.class)
public class ConnectivityPublisherTest {
private static final TelemetryProto.Publisher PUBLISHER_WIFI_OEM_NONE =
TelemetryProto.Publisher.newBuilder()
.setConnectivity(
TelemetryProto.ConnectivityPublisher.newBuilder()
.setTransport(Transport.TRANSPORT_WIFI)
.setOemType(OemType.OEM_NONE))
.build();
private static final TelemetryProto.Publisher PUBLISHER_WIFI_OEM_MANAGED =
TelemetryProto.Publisher.newBuilder()
.setConnectivity(
TelemetryProto.ConnectivityPublisher.newBuilder()
.setTransport(Transport.TRANSPORT_WIFI)
.setOemType(OemType.OEM_MANAGED))
.build();
private static final TelemetryProto.Publisher PUBLISHER_CELL_OEM_NONE =
TelemetryProto.Publisher.newBuilder()
.setConnectivity(
TelemetryProto.ConnectivityPublisher.newBuilder()
.setTransport(Transport.TRANSPORT_CELLULAR)
.setOemType(OemType.OEM_NONE))
.build();
private static final TelemetryProto.Subscriber SUBSCRIBER_WIFI_OEM_NONE =
TelemetryProto.Subscriber.newBuilder()
.setHandler("empty_handler")
.setPublisher(PUBLISHER_WIFI_OEM_NONE)
.build();
private static final TelemetryProto.Subscriber SUBSCRIBER_WIFI_OEM_MANAGED =
TelemetryProto.Subscriber.newBuilder()
.setHandler("empty_handler")
.setPublisher(PUBLISHER_WIFI_OEM_MANAGED)
.build();
private static final TelemetryProto.Subscriber SUBSCRIBER_CELL_OEM_NONE =
TelemetryProto.Subscriber.newBuilder()
.setHandler("empty_handler")
.setPublisher(PUBLISHER_CELL_OEM_NONE)
.build();
private static final TelemetryProto.MetricsConfig METRICS_CONFIG =
TelemetryProto.MetricsConfig.newBuilder()
.setName("myconfig")
.setVersion(1)
.addSubscribers(SUBSCRIBER_WIFI_OEM_NONE)
.addSubscribers(SUBSCRIBER_WIFI_OEM_MANAGED)
.addSubscribers(SUBSCRIBER_CELL_OEM_NONE)
.setScript("function empty_handler()\nend")
.build();
/** See {@code ConnectivityPublisher#pullInitialNetstats()}. */
private static final int BASELINE_PULL_COUNT = 8;
// Test network usage tags.
private static final int TAG_1 = 1;
private static final int TAG_2 = 2;
// Test network usage uids.
private static final int UID_1 = 1;
private static final int UID_2 = 2;
private static final int UID_3 = 3;
private static final int UID_4 = 4;
private final long mNow = System.currentTimeMillis(); // since epoch
private final FakeHandlerWrapper mFakeHandler =
new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.QUEUEING);
private final FakeDataSubscriber mDataSubscriberWifi =
new FakeDataSubscriber(METRICS_CONFIG, SUBSCRIBER_WIFI_OEM_NONE);
private final FakeDataSubscriber mDataSubscriberWifiOemManaged =
new FakeDataSubscriber(METRICS_CONFIG, SUBSCRIBER_WIFI_OEM_MANAGED);
private final FakeDataSubscriber mDataSubscriberCell =
new FakeDataSubscriber(METRICS_CONFIG, SUBSCRIBER_CELL_OEM_NONE);
private final FakeNetworkStatsManager mFakeManager = new FakeNetworkStatsManager();
private ConnectivityPublisher mPublisher; // subject
@Before
public void setUp() throws Exception {
mPublisher =
new ConnectivityPublisher(
this::onPublisherFailure,
mFakeManager,
mFakeHandler.getMockHandler(),
mock(Context.class));
}
@Test
public void testAddDataSubscriber_storesIt_andStartsPeriodicPull() {
mPublisher.addDataSubscriber(mDataSubscriberWifi);
assertThat(mPublisher.hasDataSubscriber(mDataSubscriberWifi)).isTrue();
// One for initial pull, second one for this addDataSubscriber.
assertThat(mFakeHandler.getQueuedMessages()).hasSize(2);
}
@Test
public void testRemoveDataSubscriber_removesIt() {
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mPublisher.removeDataSubscriber(mDataSubscriberWifi);
assertThat(mPublisher.hasDataSubscriber(mDataSubscriberWifi)).isFalse();
// Only initial pull left, because we removed the last subscriber.
assertThat(mFakeHandler.getQueuedMessages()).hasSize(1);
}
@Test
public void testRemoveDataSubscriber_leavesPeriodicTask_ifOtherSubscriberExists() {
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mPublisher.addDataSubscriber(mDataSubscriberCell);
mPublisher.removeDataSubscriber(mDataSubscriberWifi);
assertThat(mPublisher.hasDataSubscriber(mDataSubscriberWifi)).isFalse();
// Only initial pull left + periodic puller.
assertThat(mFakeHandler.getQueuedMessages()).hasSize(2);
}
@Test
public void testRemoveDataSubscriber_givenWrongSubscriber_doesNothing() {
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mPublisher.removeDataSubscriber(mDataSubscriberCell);
assertThat(mPublisher.hasDataSubscriber(mDataSubscriberWifi)).isTrue();
// One for initial pull, second one for this addDataSubscriber.
assertThat(mFakeHandler.getQueuedMessages()).hasSize(2);
}
@Test
public void testRemoveAllDataSubscribers_removesAll_andStopsPeriodicPull() {
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mPublisher.addDataSubscriber(mDataSubscriberCell);
mPublisher.removeAllDataSubscribers();
assertThat(mPublisher.hasDataSubscriber(mDataSubscriberWifi)).isFalse();
// Only initial pull left, because we removed the subscribers.
assertThat(mFakeHandler.getQueuedMessages()).hasSize(1);
}
@Test
public void testSchedulesNextPeriodicPullInHandler() {
mFakeHandler.dispatchQueuedMessages(); // pulls empty baseline netstats
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mFakeHandler.dispatchQueuedMessages(); // Current pull.
assertThat(mFakeHandler.getQueuedMessages()).hasSize(1); // Next pull.
}
@Test
public void testPullsOnlyNecessaryData() {
mFakeHandler.dispatchQueuedMessages(); // pulls empty baseline netstats
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mFakeHandler.dispatchQueuedMessages();
// Pulls netstats only once for wifi.
assertThat(mFakeManager.getMethodCallCount("querySummary"))
.isEqualTo(BASELINE_PULL_COUNT + 1);
}
@Test
public void testPullsOnlyNecessaryData_wifiAndMobile() {
mFakeHandler.dispatchQueuedMessages(); // pulls empty baseline netstats
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mPublisher.addDataSubscriber(mDataSubscriberCell);
mFakeHandler.dispatchQueuedMessages();
assertThat(mFakeManager.getMethodCallCount("querySummary"))
.isEqualTo(BASELINE_PULL_COUNT + 2);
}
@Test
public void testPullsTaggedAndUntaggedMobileStats() {
mFakeHandler.dispatchQueuedMessages(); // pulls empty baseline netstats
mFakeManager.addMobileStats(UID_1, TAG_1, 2500L, 3500L, OEM_MANAGED_NO, mNow);
mFakeManager.addMobileStats(UID_1, TAG_NONE, 2502L, 3502L, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_1, TAG_2, 30, 30, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_2, TAG_2, 10, 10, OEM_MANAGED_YES, mNow);
mFakeManager.addWifiStats(UID_3, TAG_2, 6, 6, OEM_MANAGED_YES, mNow);
mPublisher.addDataSubscriber(mDataSubscriberCell);
mFakeHandler.dispatchQueuedMessages();
assertThat(mDataSubscriberCell.mPushedData).hasSize(1);
PersistableBundle result = mDataSubscriberCell.get(0);
// Matches only "UID_1/TAG_1" and "UID_1/TAG_NONE" above.
assertThat(result.getLong("startMillis")).isLessThan(mNow);
assertThat(result.getLong("endMillis")).isGreaterThan(result.getLong("startMillis"));
assertThat(result.getInt("size")).isEqualTo(2);
assertThat(result.getIntArray("uid")).asList().containsExactly(UID_1, UID_1);
assertThat(result.getIntArray("tag")).asList().containsExactly(TAG_1, TAG_NONE);
assertThat(result.getLongArray("rxBytes")).asList().containsExactly(2500L, 2502L);
assertThat(result.getLongArray("txBytes")).asList().containsExactly(3500L, 3502L);
}
@Test
public void testPullsOemManagedWifiStats() {
mFakeHandler.dispatchQueuedMessages(); // pulls empty baseline netstats
mFakeManager.addMobileStats(UID_1, TAG_2, 5000, 5000, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_1, TAG_1, 30, 30, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_2, TAG_NONE, 100L, 200L, OEM_MANAGED_YES, mNow);
mFakeManager.addWifiStats(UID_3, TAG_2, 6L, 7L, OEM_MANAGED_YES, mNow);
mPublisher.addDataSubscriber(mDataSubscriberWifiOemManaged);
mFakeHandler.dispatchQueuedMessages();
assertThat(mDataSubscriberWifiOemManaged.mPushedData).hasSize(1);
PersistableBundle result = mDataSubscriberWifiOemManaged.get(0);
// Matches only UID_2 + UID_3.
assertThat(result.getInt("size")).isEqualTo(2);
assertThat(result.getIntArray("uid")).asList().containsExactly(UID_2, UID_3);
assertThat(result.getIntArray("tag")).asList().containsExactly(TAG_NONE, TAG_2);
assertThat(result.getLongArray("rxBytes")).asList().containsExactly(100L, 6L);
assertThat(result.getLongArray("txBytes")).asList().containsExactly(200L, 7L);
}
@Test
public void testPullsOemNotManagedWifiStats() {
mFakeHandler.dispatchQueuedMessages(); // pulls empty baseline netstats
mFakeManager.addMobileStats(UID_4, TAG_2, 5000, 5000, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_1, TAG_1, 30, 30, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_2, TAG_1, 10, 10, OEM_MANAGED_YES, mNow);
mFakeManager.addWifiStats(UID_3, TAG_1, 6, 6, OEM_MANAGED_YES, mNow);
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mFakeHandler.dispatchQueuedMessages();
assertThat(mDataSubscriberWifi.mPushedData).hasSize(1);
PersistableBundle result = mDataSubscriberWifi.get(0);
// Matches only UID_1.
assertThat(result.getInt("size")).isEqualTo(1);
assertThat(result.getIntArray("uid")).asList().containsExactly(UID_1);
assertThat(result.getIntArray("tag")).asList().containsExactly(TAG_1);
assertThat(result.getLongArray("txBytes")).asList().containsExactly(30L);
assertThat(result.getLongArray("rxBytes")).asList().containsExactly(30L);
}
@Test
public void testPullsStatsOnlyBetweenBootTimeMinus2HoursAndNow() {
mFakeHandler.dispatchQueuedMessages(); // pulls empty baseline netstats
long mBootTimeMillis = mNow - SystemClock.elapsedRealtime(); // since epoch
long bootMinus30Mins = mBootTimeMillis - Duration.ofMinutes(30).toMillis();
long bootMinus5Hours = mBootTimeMillis - Duration.ofHours(5).toMillis();
mFakeManager.addMobileStats(UID_1, TAG_2, 5000, 5000, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_1, TAG_1, 10, 10, OEM_MANAGED_NO, mNow);
mFakeManager.addWifiStats(UID_2, TAG_1, 10, 10, OEM_MANAGED_NO, bootMinus30Mins);
mFakeManager.addWifiStats(UID_3, TAG_1, 7, 7, OEM_MANAGED_NO, bootMinus5Hours);
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mFakeHandler.dispatchQueuedMessages();
assertThat(mDataSubscriberWifi.mPushedData).hasSize(1);
PersistableBundle result = mDataSubscriberWifi.get(0);
// Only UID_1 and UID_2 are fetched, because other stats are outside
// of the time range.
assertThat(result.getInt("size")).isEqualTo(2);
assertThat(result.getIntArray("uid")).asList().containsExactly(UID_1, UID_2);
assertThat(result.getIntArray("tag")).asList().containsExactly(TAG_1, TAG_1);
assertThat(result.getLongArray("txBytes")).asList().containsExactly(10L, 10L);
assertThat(result.getLongArray("rxBytes")).asList().containsExactly(10L, 10L);
}
@Test
public void testPushesDataAsNotLarge() {
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mFakeManager.addWifiStats(UID_1, TAG_1, 10, 10, OEM_MANAGED_NO, mNow);
mFakeHandler.dispatchQueuedMessages();
// The final data should not be marked "large".
assertThat(mDataSubscriberWifi.mPushedData.get(0).mIsLargeData).isFalse();
}
@Test
public void testSubtractsFromInitialPull() {
long someTimeAgo = mNow - Duration.ofMinutes(1).toMillis();
mFakeManager.addWifiStats(UID_4, TAG_1, 10, 10, OEM_MANAGED_YES, someTimeAgo);
mFakeManager.addWifiStats(UID_4, TAG_1, 11, 11, OEM_MANAGED_YES, someTimeAgo);
mFakeManager.addWifiStats(UID_4, TAG_1, 12, 12, OEM_MANAGED_NO, someTimeAgo);
mFakeHandler.dispatchQueuedMessages(); // pulls 10, 11, 12 bytes.
// A hack to force the publisher to compute the diff from the initial pull.
// Otherwise we'll get "(100 + 12) - 12".
mFakeManager.clearNetworkStats();
mFakeManager.addWifiStats(UID_4, TAG_1, 100, 100, OEM_MANAGED_NO, mNow);
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mFakeHandler.dispatchQueuedMessages();
assertThat(mDataSubscriberWifi.mPushedData).hasSize(1);
PersistableBundle result = mDataSubscriberWifi.get(0);
assertThat(result.getInt("size")).isEqualTo(1);
assertThat(result.getIntArray("uid")).asList().containsExactly(UID_4);
assertThat(result.getIntArray("tag")).asList().containsExactly(TAG_1);
assertThat(result.getLongArray("txBytes")).asList().containsExactly(100L - 12L);
assertThat(result.getLongArray("rxBytes")).asList().containsExactly(100L - 12L);
}
@Test
public void testSubtractsFromThePreviousPull() {
// ==== 0th (initial) pull.
long someTimeAgo = mNow - Duration.ofMinutes(1).toMillis();
mFakeManager.addWifiStats(UID_4, TAG_1, 12, 12, OEM_MANAGED_NO, someTimeAgo);
mFakeHandler.dispatchQueuedMessages(); // pulls 12 bytes
// ==== 1st pull.
mFakeManager.addWifiStats(UID_4, TAG_1, 200, 200, OEM_MANAGED_NO, someTimeAgo);
mPublisher.addDataSubscriber(mDataSubscriberWifi);
mFakeHandler.dispatchQueuedMessages(); // pulls 200 + 12 bytes
assertThat(mDataSubscriberWifi.mPushedData).hasSize(1);
PersistableBundle result = mDataSubscriberWifi.get(0);
assertThat(result.getInt("size")).isEqualTo(1);
assertThat(result.getIntArray("uid")).asList().containsExactly(UID_4);
assertThat(result.getIntArray("tag")).asList().containsExactly(TAG_1);
// It's 200, because it subtracts previous pull 12 from (200 + 12).
assertThat(result.getLongArray("txBytes")).asList().containsExactly(200L);
// ==== 2nd pull.
mFakeManager.addWifiStats(UID_4, TAG_1, 1000, 1000, OEM_MANAGED_NO, mNow);
mFakeHandler.dispatchQueuedMessages(); // pulls 200 + 12 + 1000 bytes
assertThat(mDataSubscriberWifi.mPushedData).hasSize(2);
result = mDataSubscriberWifi.get(1);
assertThat(result.getInt("size")).isEqualTo(1);
assertThat(result.getIntArray("uid")).asList().containsExactly(UID_4);
assertThat(result.getIntArray("tag")).asList().containsExactly(TAG_1);
// It's 1000, because it subtracts previous pull (200 + 12) from (200 + 12 + 1000).
assertThat(result.getLongArray("txBytes")).asList().containsExactly(1000L);
}
private void onPublisherFailure(
AbstractPublisher publisher,
List<TelemetryProto.MetricsConfig> affectedConfigs,
Throwable error) {}
private static class FakeDataSubscriber extends DataSubscriber {
private final ArrayList<PushedData> mPushedData = new ArrayList<>();
FakeDataSubscriber(
TelemetryProto.MetricsConfig metricsConfig, TelemetryProto.Subscriber subscriber) {
super(/* dataBroker= */ null, metricsConfig, subscriber);
}
@Override
public void push(PersistableBundle data, boolean isLargeData) {
mPushedData.add(new PushedData(data, isLargeData));
}
/** Returns the pushed data by the given index. */
PersistableBundle get(int index) {
return mPushedData.get(index).mData;
}
}
/** Data pushed to a subscriber. */
private static class PushedData {
private final PersistableBundle mData;
private final boolean mIsLargeData;
PushedData(PersistableBundle data, boolean isLargeData) {
mData = data;
mIsLargeData = isLargeData;
}
}
/** A fake for {@link INetworkStatsService}. */
private static class FakeNetworkStatsManager extends NetworkStatsServiceProxy {
private final ArrayList<FakeNetworkStats.CustomBucket> mBuckets = new ArrayList<>();
private final HashMap<String, Integer> mMethodCallCount = new HashMap<>();
private FakeNetworkStatsManager() {
super(/* networkStatsManager= */ null);
}
/** Adds {@code NetworkStats.Bucket} that will be returned by {@code querySummary()}. */
public void addWifiStats(
int uid, int tag, long rx, long tx, int oemManaged, long timestampMillis) {
NetworkIdentity identity =
new NetworkIdentity(
ConnectivityManager.TYPE_WIFI,
/* subType= */ NetworkTemplate.NETWORK_TYPE_ALL,
/* subscriberId= */ null,
/* networkId= */ "guest-wifi",
/* roaming= */ false,
/* metered= */ false,
/* defaultNetwork= */ false,
oemManaged);
FakeNetworkStats.CustomBucket bucket =
new FakeNetworkStats.CustomBucket(
identity,
uid,
tag,
/* rxBytes= */ rx,
/* txBytes= */ tx,
timestampMillis);
mBuckets.add(bucket);
}
/** Adds {@code NetworkStats.Bucket} that will be returned by {@code querySummary()}. */
public void addMobileStats(
int uid, int tag, long rx, long tx, int oemManaged, long timestampMillis) {
NetworkIdentity identity =
new NetworkIdentity(
ConnectivityManager.TYPE_MOBILE,
/* subType= */ TelephonyManager.NETWORK_TYPE_GPRS,
/* subscriberId= */ null,
/* networkId= */ null,
/* roaming= */ false,
/* metered= */ false,
/* defaultNetwork= */ true,
oemManaged);
FakeNetworkStats.CustomBucket bucket =
new FakeNetworkStats.CustomBucket(
identity,
uid,
tag,
/* rxBytes= */ rx,
/* txBytes= */ tx,
timestampMillis);
mBuckets.add(bucket);
}
public void clearNetworkStats() {
mBuckets.clear();
}
/** Returns the API method call count. */
public int getMethodCallCount(String name) {
return mMethodCallCount.getOrDefault(name, 0);
}
@Override
@NonNull
public RefinedStats querySummary(
NetworkTemplate template, long start, long end, String callingPackage)
throws RemoteException {
increaseMethodCall("querySummary", 1);
FakeNetworkStats result = new FakeNetworkStats();
for (FakeNetworkStats.CustomBucket bucket : mBuckets) {
// NOTE: the actual implementation calculates bucket ratio in the given time range
// instead of this simple time range checking.
if (bucket.getStartTimeStamp() < start || bucket.getStartTimeStamp() > end) {
continue;
}
if (template.matches(bucket.getIdentity())) {
result.add(bucket);
}
}
RefinedStats stats = new RefinedStats(start, end);
stats.addNetworkStats(result.getNetworkStats());
return stats;
}
private void increaseMethodCall(String methodName, int count) {
mMethodCallCount.put(methodName, mMethodCallCount.getOrDefault(methodName, 0) + count);
}
}
}