Support pulled atoms for Telecom metrics
Flag: com.android.server.telecom.flags.telecom_metrics_support
Bug: 362394177
Test: manual
Test: atest TelecomUnitTests
Change-Id: I50cbc15b8f81bc86be5398cd605804743428f473
diff --git a/flags/Android.bp b/flags/Android.bp
index 45acacf..e374a4d 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -43,5 +43,6 @@
"telecom_profile_user_flags.aconfig",
"telecom_bluetoothdevicemanager_flags.aconfig",
"telecom_non_critical_security_flags.aconfig",
+ "telecom_metrics_flags.aconfig",
],
}
diff --git a/flags/telecom_metrics_flags.aconfig b/flags/telecom_metrics_flags.aconfig
new file mode 100644
index 0000000..e582e9e
--- /dev/null
+++ b/flags/telecom_metrics_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=huiwang TARGET=25Q1
+flag {
+ name: "telecom_metrics_support"
+ namespace: "telecom"
+ description: "Support telecom metrics"
+ bug: "362394177"
+}
diff --git a/proto/pulled_atoms.proto b/proto/pulled_atoms.proto
new file mode 100644
index 0000000..7360b6a
--- /dev/null
+++ b/proto/pulled_atoms.proto
@@ -0,0 +1,114 @@
+syntax = "proto2";
+
+package com.android.server.telecom;
+
+option java_package = "com.android.server.telecom";
+option java_outer_classname = "PulledAtomsClass";
+
+message PulledAtoms {
+ repeated CallStats call_stats = 1;
+ optional int64 call_stats_pull_timestamp_millis = 2;
+ repeated CallAudioRouteStats call_audio_route_stats = 3;
+ optional int64 call_audio_route_stats_pull_timestamp_millis = 4;
+ repeated TelecomApiStats telecom_api_stats = 5;
+ optional int64 telecom_api_stats_pull_timestamp_millis = 6;
+ repeated TelecomErrorStats telecom_error_stats = 7;
+ optional int64 telecom_error_stats_pull_timestamp_millis = 8;
+}
+
+/**
+ * Pulled atom to capture stats of the calls
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message CallStats {
+ // The value should be converted to android.telecom.CallDirectionEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 call_direction = 1;
+
+ // True if call is external. External calls are calls on connected Wear
+ // devices but show up in Telecom so the user can pull them onto the device.
+ optional bool external_call = 2;
+
+ // True if call is emergency call.
+ optional bool emergency_call = 3;
+
+ // True if there are multiple audio routes available
+ optional bool multiple_audio_available = 4;
+
+ // The value should be converted to android.telecom.AccountTypeEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 account_type = 5;
+
+ // UID of the package to init the call. This should always be -1/unknown for
+ // the private space calls
+ optional int32 uid = 6;
+
+ // Total number of the calls
+ optional int32 count = 7;
+
+ // Average elapsed time between CALL_STATE_ACTIVE to CALL_STATE_DISCONNECTED.
+ optional int32 average_duration_ms = 8;
+}
+
+/**
+ * Pulled atom to capture stats of the call audio route
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message CallAudioRouteStats {
+ // The value should be converted to android.telecom.CallAudioEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 call_audio_route_source = 1;
+
+ // The value should be converted to android.telecom.CallAudioEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 call_audio_route_dest = 2;
+
+ // True if the route is successful.
+ optional bool success = 3;
+
+ // True if the route is revert
+ optional bool revert = 4;
+
+ // Total number of the audio route
+ optional int32 count = 5;
+
+ // Average time from the audio route start to complete
+ optional int32 average_latency_ms = 6;
+}
+
+/**
+ * Pulled atom to capture stats of Telecom API usage
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message TelecomApiStats {
+ // The value should be converted to android.telecom.ApiNameEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 api_name = 1;
+
+ // UID of the caller. This is always -1/unknown for the private space.
+ optional int32 uid = 2;
+
+ // The value should be converted to android.telecom.ApiResultEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 api_result = 3;
+
+ // The number of times this event occurs
+ optional int32 count = 4;
+}
+
+/**
+ * Pulled atom to capture stats of Telecom module errors
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message TelecomErrorStats {
+ // The value should be converted to android.telecom.SubmoduleNameEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 submodule_name = 1;
+
+ // The value should be converted to android.telecom.ErrorNameEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 error_name = 2;
+
+ // The number of times this error occurs
+ optional int32 count = 3;
+}
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 4f563a1..f402099 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -137,6 +137,7 @@
private @AudioRouteType int mAudioRouteType;
private String mBluetoothAddress;
private AudioDeviceInfo mInfo;
+ private boolean mIsDestRouteForWatch;
public static final Set<Integer> BT_AUDIO_DEVICE_INFO_TYPES = Set.of(
AudioDeviceInfo.TYPE_BLE_HEADSET,
AudioDeviceInfo.TYPE_BLE_SPEAKER,
@@ -241,6 +242,10 @@
return mAudioRouteType;
}
+ public boolean isWatch() {
+ return mIsDestRouteForWatch;
+ }
+
String getBluetoothAddress() {
return mBluetoothAddress;
}
@@ -264,6 +269,8 @@
audioManager, bluetoothRouteManager);
// Special handling for SCO case.
if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+ // Set whether the dest route is for the watch
+ mIsDestRouteForWatch = bluetoothRouteManager.isWatch(device);
// Check if the communication device was set for the device, even if
// BluetoothHeadset#connectAudio reports that the SCO connection wasn't
// successfully established.
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index dcd9486..c391641 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -1980,7 +1980,6 @@
PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, false);
}
- @VisibleForTesting
public boolean isIncoming() {
return mCallDirection == CALL_DIRECTION_INCOMING;
}
@@ -2272,7 +2271,6 @@
* @return The "age" of this call object in milliseconds, which typically also represents the
* period since this call was added to the set pending outgoing calls.
*/
- @VisibleForTesting
public long getAgeMillis() {
if (mState == CallState.DISCONNECTED &&
(mDisconnectCause.getCode() == DisconnectCause.REJECTED ||
diff --git a/src/com/android/server/telecom/CallAnomalyWatchdog.java b/src/com/android/server/telecom/CallAnomalyWatchdog.java
index 497d7e6..384110c 100644
--- a/src/com/android/server/telecom/CallAnomalyWatchdog.java
+++ b/src/com/android/server/telecom/CallAnomalyWatchdog.java
@@ -32,6 +32,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.stats.CallStateChangedAtomWriter;
import com.android.server.telecom.flags.FeatureFlags;
@@ -130,6 +131,7 @@
private final Set<Call> mCallsPendingDestruction = Collections.newSetFromMap(
new ConcurrentHashMap<>(2));
private final LocalLog mLocalLog = new LocalLog(20);
+ private final TelecomMetricsController mMetricsController;
/**
* Enables the action to disconnect the call when the Transitory state and Intermediate state
@@ -163,13 +165,15 @@
TelecomSystem.SyncRoot lock,
FeatureFlags featureFlags,
Timeouts.Adapter timeoutAdapter, ClockProxy clockProxy,
- EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger) {
+ EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
+ TelecomMetricsController metricsController) {
mScheduledExecutorService = executorService;
mLock = lock;
mFeatureFlags = featureFlags;
mTimeoutAdapter = timeoutAdapter;
mClockProxy = clockProxy;
mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
+ mMetricsController = metricsController;
}
/**
@@ -185,6 +189,9 @@
@Override
public void onCallAdded(Call call) {
maybeTrackCall(call);
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getCallStats().onCallStart(call);
+ }
}
/**
@@ -206,6 +213,9 @@
public void onCallRemoved(Call call) {
Log.i(this, "onCallRemoved: call=%s", call.toString());
stopTrackingCall(call);
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getCallStats().onCallEnd(call);
+ }
}
/**
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 0ed4308..d07d554 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -52,6 +52,7 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import java.util.ArrayList;
import java.util.HashMap;
@@ -172,13 +173,14 @@
private boolean mIsMute;
private boolean mIsPending;
private boolean mIsActive;
+ private final TelecomMetricsController mMetricsController;
public CallAudioRouteController(
Context context, CallsManager callsManager,
CallAudioManager.AudioServiceFactory audioServiceFactory,
AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager,
BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags, TelecomMetricsController metricsController) {
mContext = context;
mCallsManager = callsManager;
mAudioManager = context.getSystemService(AudioManager.class);
@@ -189,6 +191,7 @@
mBluetoothRouteManager = bluetoothRouteManager;
mStatusBarNotifier = statusBarNotifier;
mFeatureFlags = featureFlags;
+ mMetricsController = metricsController;
mFocusType = NO_FOCUS;
mIsScoAudioConnected = false;
mTelecomLock = callsManager.getLock();
@@ -542,6 +545,9 @@
mIsScoAudioConnected);
mIsActive = active;
mPendingAudioRoute.evaluatePendingState();
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getAudioRouteStats().onRouteEnter(mPendingAudioRoute);
+ }
}
private void handleWiredHeadsetConnected() {
@@ -973,6 +979,9 @@
mIsPending = false;
mPendingAudioRoute.clearPendingMessages();
onCurrentRouteChanged();
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute);
+ }
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index a1b8a50..028d8c1 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -134,6 +134,7 @@
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.stats.CallFailureCause;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
@@ -616,7 +617,8 @@
BluetoothDeviceManager bluetoothDeviceManager,
FeatureFlags featureFlags,
com.android.internal.telephony.flags.FeatureFlags telephonyFlags,
- IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) {
+ IncomingCallFilterGraphProvider incomingCallFilterGraphProvider,
+ TelecomMetricsController metricsController) {
mContext = context;
mLock = lock;
@@ -659,7 +661,7 @@
} else {
callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
- statusBarNotifier, featureFlags);
+ statusBarNotifier, featureFlags, metricsController);
}
callAudioRouteAdapter.initialize();
bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 396aca0..a544258 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -52,6 +52,7 @@
private AudioRoute mDestRoute;
private Set<Pair<Integer, String>> mPendingMessages;
private boolean mActive;
+ private boolean mIsFailed;
/**
* The device that has been set for communication by Telecom
*/
@@ -72,7 +73,7 @@
mOrigRoute = origRoute;
}
- AudioRoute getOrigRoute() {
+ public AudioRoute getOrigRoute() {
return mOrigRoute;
}
@@ -95,6 +96,7 @@
public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
Log.i(this, "onMessageReceived: message - %s", message);
if (message.first == PENDING_ROUTE_FAILED) {
+ mIsFailed = true;
// Fallback to base route
mCallAudioRouteController.sendMessageWithSessionInfo(
SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, btAddressToExclude);
@@ -139,4 +141,8 @@
public void overrideDestRoute(AudioRoute route) {
mDestRoute = route;
}
+
+ public boolean isFailed() {
+ return mIsFailed;
+ }
}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 1cbe846..fd1053f 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -48,6 +48,7 @@
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
@@ -373,10 +374,13 @@
BugreportManager.class), timeoutsAdapter, mContext.getSystemService(
DropBoxManager.class), asyncTaskExecutor, clockProxy);
+ TelecomMetricsController metricsController = featureFlags.telecomMetricsSupport()
+ ? TelecomMetricsController.make(mContext) : null;
+
CallAnomalyWatchdog callAnomalyWatchdog = new CallAnomalyWatchdog(
Executors.newSingleThreadScheduledExecutor(),
mLock, mFeatureFlags, timeoutsAdapter, clockProxy,
- emergencyCallDiagnosticLogger);
+ emergencyCallDiagnosticLogger, metricsController);
TransactionManager transactionManager = TransactionManager.getInstance();
@@ -428,7 +432,8 @@
bluetoothDeviceManager,
featureFlags,
telephonyFlags,
- IncomingCallFilterGraph::new);
+ IncomingCallFilterGraph::new,
+ metricsController);
mIncomingCallNotifier = incomingCallNotifier;
incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
diff --git a/src/com/android/server/telecom/metrics/ApiStats.java b/src/com/android/server/telecom/metrics/ApiStats.java
new file mode 100644
index 0000000..b37569f
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/ApiStats.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2024 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.telecom.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class ApiStats extends TelecomPulledAtom {
+
+ private static final String FILE_NAME = "api_stats";
+ private Map<ApiStatsKey, Integer> mApiStatsMap;
+
+ public ApiStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return TELECOM_API_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.telecomApiStats.length != 0) {
+ Arrays.stream(mPulledAtoms.telecomApiStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getApiName(), v.getUid(), v.getApiResult(), v.getCount())));
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.telecomApiStats != null) {
+ mApiStatsMap = new HashMap<>();
+ for (PulledAtomsClass.TelecomApiStats v : mPulledAtoms.telecomApiStats) {
+ mApiStatsMap.put(new ApiStatsKey(v.getApiName(), v.getUid(), v.getApiResult()),
+ v.getCount());
+ }
+ mLastPulledTimestamps = mPulledAtoms.getTelecomApiStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ clearAtoms();
+ if (mApiStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setTelecomApiStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.telecomApiStats =
+ new PulledAtomsClass.TelecomApiStats[mApiStatsMap.size()];
+ int[] index = new int[1];
+ mApiStatsMap.forEach((k, v) -> {
+ mPulledAtoms.telecomApiStats[index[0]] = new PulledAtomsClass.TelecomApiStats();
+ mPulledAtoms.telecomApiStats[index[0]].setApiName(k.mApiId);
+ mPulledAtoms.telecomApiStats[index[0]].setUid(k.mCallerUid);
+ mPulledAtoms.telecomApiStats[index[0]].setApiResult(k.mResult);
+ mPulledAtoms.telecomApiStats[index[0]].setCount(v);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ public void log(int apiId, int callerUid, int result) {
+ post(() -> {
+ ApiStatsKey key = new ApiStatsKey(apiId, callerUid, result);
+ mApiStatsMap.put(key, mApiStatsMap.getOrDefault(key, 0) + 1);
+ onAggregate();
+ });
+ }
+
+ static class ApiStatsKey {
+
+ int mApiId;
+ int mCallerUid;
+ int mResult;
+
+ ApiStatsKey(int apiId, int callerUid, int result) {
+ mApiId = apiId;
+ mCallerUid = callerUid;
+ mResult = result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || !(other instanceof ApiStatsKey obj)) {
+ return false;
+ }
+ return this.mApiId == obj.mApiId && this.mCallerUid == obj.mCallerUid
+ && this.mResult == obj.mResult;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mApiId, mCallerUid, mResult);
+ }
+
+ @Override
+ public String toString() {
+ return "[ApiStatsKey: mApiId=" + mApiId + ", mCallerUid=" + mCallerUid
+ + ", mResult=" + mResult + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/AudioRouteStats.java b/src/com/android/server/telecom/metrics/AudioRouteStats.java
new file mode 100644
index 0000000..8755402
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/AudioRouteStats.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2024 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.telecom.metrics;
+
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_LE;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+import static com.android.server.telecom.AudioRoute.TYPE_DOCK;
+import static com.android.server.telecom.AudioRoute.TYPE_EARPIECE;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
+import static com.android.server.telecom.AudioRoute.TYPE_STREAMING;
+import static com.android.server.telecom.AudioRoute.TYPE_WIRED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH_LE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_EARPIECE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_HEARING_AID;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_PHONE_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_UNSPECIFIED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WATCH_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WIRED_HEADSET;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_HEARING_AID;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_PHONE_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_UNSPECIFIED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WATCH_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WIRED_HEADSET;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.telecom.Log;
+import android.util.Pair;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.PendingAudioRoute;
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class AudioRouteStats extends TelecomPulledAtom {
+ @VisibleForTesting
+ public static final long THRESHOLD_REVERT_MS = 5000;
+ @VisibleForTesting
+ public static final int EVENT_REVERT_THRESHOLD_EXPIRED = EVENT_SUB_BASE + 1;
+ private static final String TAG = AudioRouteStats.class.getSimpleName();
+ private static final String FILE_NAME = "audio_route_stats";
+ private Map<AudioRouteStatsKey, AudioRouteStatsData> mAudioRouteStatsMap;
+ private Pair<AudioRouteStatsKey, long[]> mCur;
+ private boolean mIsOngoing;
+
+ public AudioRouteStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return CALL_AUDIO_ROUTE_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.callAudioRouteStats.length != 0) {
+ Arrays.stream(mPulledAtoms.callAudioRouteStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getCallAudioRouteSource(), v.getCallAudioRouteDest(),
+ v.getSuccess(), v.getRevert(), v.getCount(), v.getAverageLatencyMs())));
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.callAudioRouteStats != null) {
+ mAudioRouteStatsMap = new HashMap<>();
+ for (PulledAtomsClass.CallAudioRouteStats v : mPulledAtoms.callAudioRouteStats) {
+ mAudioRouteStatsMap.put(new AudioRouteStatsKey(v.getCallAudioRouteSource(),
+ v.getCallAudioRouteDest(), v.getSuccess(), v.getRevert()),
+ new AudioRouteStatsData(v.getCount(), v.getAverageLatencyMs()));
+ }
+ mLastPulledTimestamps = mPulledAtoms.getCallAudioRouteStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ Log.d(TAG, "onAggregate: %s", mAudioRouteStatsMap);
+ clearAtoms();
+ if (mAudioRouteStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setCallAudioRouteStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.callAudioRouteStats =
+ new PulledAtomsClass.CallAudioRouteStats[mAudioRouteStatsMap.size()];
+ int[] index = new int[1];
+ mAudioRouteStatsMap.forEach((k, v) -> {
+ mPulledAtoms.callAudioRouteStats[index[0]] = new PulledAtomsClass.CallAudioRouteStats();
+ mPulledAtoms.callAudioRouteStats[index[0]].setCallAudioRouteSource(k.mSource);
+ mPulledAtoms.callAudioRouteStats[index[0]].setCallAudioRouteDest(k.mDest);
+ mPulledAtoms.callAudioRouteStats[index[0]].setSuccess(k.mIsSuccess);
+ mPulledAtoms.callAudioRouteStats[index[0]].setRevert(k.mIsRevert);
+ mPulledAtoms.callAudioRouteStats[index[0]].setCount(v.mCount);
+ mPulledAtoms.callAudioRouteStats[index[0]].setAverageLatencyMs(v.mAverageLatency);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ @VisibleForTesting
+ public void log(int source, int target, boolean isSuccess, boolean isRevert, int latency) {
+ post(() -> onLog(new AudioRouteStatsKey(source, target, isSuccess, isRevert), latency));
+ }
+
+ public void onRouteEnter(PendingAudioRoute pendingRoute) {
+ int sourceType = convertAudioType(pendingRoute.getOrigRoute(), true);
+ int destType = convertAudioType(pendingRoute.getDestRoute(), false);
+ long curTime = SystemClock.elapsedRealtime();
+
+ post(() -> {
+ // Ignore the transition route
+ if (!mIsOngoing) {
+ mIsOngoing = true;
+ // Check if the previous route is reverted as the revert time has not been expired.
+ if (mCur != null) {
+ if (destType == mCur.first.getSource() && curTime - mCur.second[0]
+ < THRESHOLD_REVERT_MS) {
+ mCur.first.setRevert(true);
+ }
+ if (mCur.second[1] < 0) {
+ mCur.second[1] = curTime;
+ }
+ onLog();
+ }
+ mCur = new Pair<>(new AudioRouteStatsKey(sourceType, destType), new long[]{curTime,
+ -1});
+ if (hasMessages(EVENT_REVERT_THRESHOLD_EXPIRED)) {
+ // Only keep the latest event
+ removeMessages(EVENT_REVERT_THRESHOLD_EXPIRED);
+ }
+ sendMessageDelayed(
+ obtainMessage(EVENT_REVERT_THRESHOLD_EXPIRED), THRESHOLD_REVERT_MS);
+ }
+ });
+ }
+
+ public void onRouteExit(PendingAudioRoute pendingRoute) {
+ // Check the dest type on the route exiting as it may be different as the enter
+ int destType = convertAudioType(pendingRoute.getDestRoute(), false);
+ boolean isSuccess = !pendingRoute.isFailed();
+ long curTime = SystemClock.elapsedRealtime();
+ post(() -> {
+ if (mIsOngoing) {
+ mIsOngoing = false;
+ // Should not be null unless the route is not done before the revert timer expired.
+ if (mCur != null) {
+ mCur.first.setDestType(destType);
+ mCur.first.setSuccess(isSuccess);
+ mCur.second[1] = curTime;
+ }
+ }
+ });
+ }
+
+ private void onLog() {
+ if (mCur != null) {
+ // Ignore the case if the source and dest types are same
+ if (mCur.first.mSource != mCur.first.mDest) {
+ // The route should have been done before the revert timer expires. Otherwise, it
+ // would be logged as the failed case
+ if (mCur.second[1] < 0) {
+ mCur.second[1] = SystemClock.elapsedRealtime();
+ }
+ onLog(mCur.first, (int) (mCur.second[1] - mCur.second[0]));
+ }
+ mCur = null;
+ }
+ }
+
+ private void onLog(AudioRouteStatsKey key, int latency) {
+ AudioRouteStatsData data = mAudioRouteStatsMap.computeIfAbsent(key,
+ k -> new AudioRouteStatsData(0, 0));
+ data.add(latency);
+ onAggregate();
+ }
+
+ private int convertAudioType(AudioRoute route, boolean isSource) {
+ if (route != null) {
+ switch (route.getType()) {
+ case TYPE_EARPIECE:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_EARPIECE;
+ case TYPE_WIRED:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WIRED_HEADSET
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WIRED_HEADSET;
+ case TYPE_SPEAKER:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_PHONE_SPEAKER
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_PHONE_SPEAKER;
+ case TYPE_BLUETOOTH_LE:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH_LE;
+ case TYPE_BLUETOOTH_SCO:
+ if (isSource) {
+ return route.isWatch()
+ ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WATCH_SPEAKER
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH;
+ } else {
+ return route.isWatch()
+ ? CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WATCH_SPEAKER
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH;
+ }
+ case TYPE_BLUETOOTH_HA:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_HEARING_AID
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_HEARING_AID;
+ case TYPE_DOCK:
+ // Reserved for the future
+ case TYPE_STREAMING:
+ // Reserved for the future
+ default:
+ break;
+ }
+ }
+
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_UNSPECIFIED
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_UNSPECIFIED;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_REVERT_THRESHOLD_EXPIRED:
+ onLog();
+ break;
+ default:
+ super.handleMessage(msg);
+ }
+ }
+
+ static class AudioRouteStatsKey {
+
+ final int mSource;
+ int mDest;
+ boolean mIsSuccess;
+ boolean mIsRevert;
+
+ AudioRouteStatsKey(int source, int dest) {
+ mSource = source;
+ mDest = dest;
+ }
+
+ AudioRouteStatsKey(int source, int dest, boolean isSuccess, boolean isRevert) {
+ mSource = source;
+ mDest = dest;
+ mIsSuccess = isSuccess;
+ mIsRevert = isRevert;
+ }
+
+ void setDestType(int dest) {
+ mDest = dest;
+ }
+
+ void setSuccess(boolean isSuccess) {
+ mIsSuccess = isSuccess;
+ }
+
+ void setRevert(boolean isRevert) {
+ mIsRevert = isRevert;
+ }
+
+ int getSource() {
+ return mSource;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AudioRouteStatsKey obj)) {
+ return false;
+ }
+ return this.mSource == obj.mSource && this.mDest == obj.mDest
+ && this.mIsSuccess == obj.mIsSuccess && this.mIsRevert == obj.mIsRevert;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSource, mDest, mIsSuccess, mIsRevert);
+ }
+
+ @Override
+ public String toString() {
+ return "[AudioRouteStatsKey: mSource=" + mSource + ", mDest=" + mDest
+ + ", mIsSuccess=" + mIsSuccess + ", mIsRevert=" + mIsRevert + "]";
+ }
+ }
+
+ static class AudioRouteStatsData {
+
+ int mCount;
+ int mAverageLatency;
+
+ AudioRouteStatsData(int count, int averageLatency) {
+ mCount = count;
+ mAverageLatency = averageLatency;
+ }
+
+ void add(int latency) {
+ mCount++;
+ mAverageLatency += (latency - mAverageLatency) / mCount;
+ }
+
+ @Override
+ public String toString() {
+ return "[AudioRouteStatsData: mCount=" + mCount + ", mAverageLatency:"
+ + mAverageLatency + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/CallStats.java b/src/com/android/server/telecom/metrics/CallStats.java
new file mode 100644
index 0000000..39b0e6d
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/CallStats.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2024 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.telecom.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_MANAGED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SELFMANAGED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_VOIP_API;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_INCOMING;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_OUTGOING;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class CallStats extends TelecomPulledAtom {
+ private static final String TAG = CallStats.class.getSimpleName();
+
+ private static final String FILE_NAME = "call_stats";
+ private final Set<String> mOngoingCallsWithoutMultipleAudioDevices = new HashSet<>();
+ private final Set<String> mOngoingCallsWithMultipleAudioDevices = new HashSet<>();
+ private Map<CallStatsKey, CallStatsData> mCallStatsMap;
+ private boolean mHasMultipleAudioDevices;
+
+ public CallStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return CALL_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.callStats.length != 0) {
+ Arrays.stream(mPulledAtoms.callStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getCallDirection(), v.getExternalCall(), v.getEmergencyCall(),
+ v.getMultipleAudioAvailable(), v.getAccountType(), v.getUid(),
+ v.getCount(), v.getAverageDurationMs())));
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.callStats != null) {
+ mCallStatsMap = new HashMap<>();
+ for (PulledAtomsClass.CallStats v : mPulledAtoms.callStats) {
+ mCallStatsMap.put(new CallStatsKey(v.getCallDirection(),
+ v.getExternalCall(), v.getEmergencyCall(),
+ v.getMultipleAudioAvailable(),
+ v.getAccountType(), v.getUid()),
+ new CallStatsData(v.getCount(), v.getAverageDurationMs()));
+ }
+ mLastPulledTimestamps = mPulledAtoms.getCallStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ Log.d(TAG, "onAggregate: %s", mCallStatsMap);
+ clearAtoms();
+ if (mCallStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setCallStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.callStats = new PulledAtomsClass.CallStats[mCallStatsMap.size()];
+ int[] index = new int[1];
+ mCallStatsMap.forEach((k, v) -> {
+ mPulledAtoms.callStats[index[0]] = new PulledAtomsClass.CallStats();
+ mPulledAtoms.callStats[index[0]].setCallDirection(k.mDirection);
+ mPulledAtoms.callStats[index[0]].setExternalCall(k.mIsExternal);
+ mPulledAtoms.callStats[index[0]].setEmergencyCall(k.mIsEmergency);
+ mPulledAtoms.callStats[index[0]].setMultipleAudioAvailable(k.mIsMultipleAudioAvailable);
+ mPulledAtoms.callStats[index[0]].setAccountType(k.mAccountType);
+ mPulledAtoms.callStats[index[0]].setUid(k.mUid);
+ mPulledAtoms.callStats[index[0]].setCount(v.mCount);
+ mPulledAtoms.callStats[index[0]].setAverageDurationMs(v.mAverageDuration);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ public void log(int direction, boolean isExternal, boolean isEmergency,
+ boolean isMultipleAudioAvailable, int accountType, int uid, int duration) {
+ post(() -> {
+ CallStatsKey key = new CallStatsKey(direction, isExternal, isEmergency,
+ isMultipleAudioAvailable, accountType, uid);
+ CallStatsData data = mCallStatsMap.computeIfAbsent(key, k -> new CallStatsData(0, 0));
+ data.add(duration);
+ onAggregate();
+ });
+ }
+
+ public void onCallStart(Call call) {
+ post(() -> {
+ if (mHasMultipleAudioDevices) {
+ mOngoingCallsWithMultipleAudioDevices.add(call.getId());
+ } else {
+ mOngoingCallsWithoutMultipleAudioDevices.add(call.getId());
+ }
+ });
+ }
+
+ public void onCallEnd(Call call) {
+ final int duration = (int) (call.getAgeMillis());
+ post(() -> {
+ final boolean hasMultipleAudioDevices = mOngoingCallsWithMultipleAudioDevices.remove(
+ call.getId());
+ final int direction = call.isIncoming() ? CALL_STATS__CALL_DIRECTION__DIR_INCOMING
+ : (call.isOutgoing() ? CALL_STATS__CALL_DIRECTION__DIR_OUTGOING
+ : CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN);
+ final int accountType = getAccountType(call.getPhoneAccountFromHandle());
+ final int uid = call.getAssociatedUser().getIdentifier();
+ log(direction, call.isExternalCall(), call.isEmergencyCall(), hasMultipleAudioDevices,
+ accountType, uid, duration);
+ });
+ }
+
+ private int getAccountType(PhoneAccount account) {
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+ return account.hasCapabilities(
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
+ ? CALL_STATS__ACCOUNT_TYPE__ACCOUNT_VOIP_API
+ : CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SELFMANAGED;
+ }
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
+ return account.hasCapabilities(
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+ ? CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM
+ : CALL_STATS__ACCOUNT_TYPE__ACCOUNT_MANAGED;
+ }
+ return CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
+ }
+
+ public void onAudioDevicesChange(boolean hasMultipleAudioDevices) {
+ post(() -> {
+ if (mHasMultipleAudioDevices != hasMultipleAudioDevices) {
+ mHasMultipleAudioDevices = hasMultipleAudioDevices;
+ if (mHasMultipleAudioDevices) {
+ mOngoingCallsWithMultipleAudioDevices.addAll(
+ mOngoingCallsWithoutMultipleAudioDevices);
+ mOngoingCallsWithoutMultipleAudioDevices.clear();
+ }
+ }
+ });
+ }
+
+ static class CallStatsKey {
+ final int mDirection;
+ final boolean mIsExternal;
+ final boolean mIsEmergency;
+ final boolean mIsMultipleAudioAvailable;
+ final int mAccountType;
+ final int mUid;
+
+ CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
+ boolean isMultipleAudioAvailable, int accountType, int uid) {
+ mDirection = direction;
+ mIsExternal = isExternal;
+ mIsEmergency = isEmergency;
+ mIsMultipleAudioAvailable = isMultipleAudioAvailable;
+ mAccountType = accountType;
+ mUid = uid;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CallStatsKey obj)) {
+ return false;
+ }
+ return this.mDirection == obj.mDirection && this.mIsExternal == obj.mIsExternal
+ && this.mIsEmergency == obj.mIsEmergency
+ && this.mIsMultipleAudioAvailable == obj.mIsMultipleAudioAvailable
+ && this.mAccountType == obj.mAccountType && this.mUid == obj.mUid;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDirection, mIsExternal, mIsEmergency, mIsMultipleAudioAvailable,
+ mAccountType, mUid);
+ }
+
+ @Override
+ public String toString() {
+ return "[CallStatsKey: mDirection=" + mDirection + ", mIsExternal=" + mIsExternal
+ + ", mIsEmergency=" + mIsEmergency + ", mIsMultipleAudioAvailable="
+ + mIsMultipleAudioAvailable + ", mAccountType=" + mAccountType + ", mUid="
+ + mUid + "]";
+ }
+ }
+
+ static class CallStatsData {
+
+ int mCount;
+ int mAverageDuration;
+
+ CallStatsData(int count, int averageDuration) {
+ mCount = count;
+ mAverageDuration = averageDuration;
+ }
+
+ void add(int duration) {
+ mCount++;
+ mAverageDuration += (duration - mAverageDuration) / mCount;
+ }
+
+ @Override
+ public String toString() {
+ return "[CallStatsData: mCount=" + mCount + ", mAverageDuration:" + mAverageDuration
+ + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/ErrorStats.java b/src/com/android/server/telecom/metrics/ErrorStats.java
new file mode 100644
index 0000000..e4d0a51
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/ErrorStats.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 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.telecom.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class ErrorStats extends TelecomPulledAtom {
+
+ private static final String FILE_NAME = "error_stats";
+ private Map<ErrorStatsKey, Integer> mErrorStatsMap;
+
+ public ErrorStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return TELECOM_ERROR_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.telecomErrorStats.length != 0) {
+ Arrays.stream(mPulledAtoms.telecomErrorStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getSubmoduleName(), v.getErrorName(), v.getCount())));
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.telecomErrorStats != null) {
+ mErrorStatsMap = new HashMap<>();
+ for (PulledAtomsClass.TelecomErrorStats v : mPulledAtoms.telecomErrorStats) {
+ mErrorStatsMap.put(new ErrorStatsKey(v.getSubmoduleName(), v.getErrorName()),
+ v.getCount());
+ }
+ mLastPulledTimestamps = mPulledAtoms.getTelecomErrorStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ clearAtoms();
+ if (mErrorStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setTelecomErrorStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.telecomErrorStats =
+ new PulledAtomsClass.TelecomErrorStats[mErrorStatsMap.size()];
+ int[] index = new int[1];
+ mErrorStatsMap.forEach((k, v) -> {
+ mPulledAtoms.telecomErrorStats[index[0]] = new PulledAtomsClass.TelecomErrorStats();
+ mPulledAtoms.telecomErrorStats[index[0]].setSubmoduleName(k.mModuleId);
+ mPulledAtoms.telecomErrorStats[index[0]].setErrorName(k.mErrorId);
+ mPulledAtoms.telecomErrorStats[index[0]].setCount(v);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ public void log(int moduleId, int errorId) {
+ post(() -> {
+ ErrorStatsKey key = new ErrorStatsKey(moduleId, errorId);
+ mErrorStatsMap.put(key, mErrorStatsMap.getOrDefault(key, 0) + 1);
+ onAggregate();
+ });
+ }
+
+ static class ErrorStatsKey {
+
+ final int mModuleId;
+ final int mErrorId;
+
+ ErrorStatsKey(int moduleId, int errorId) {
+ mModuleId = moduleId;
+ mErrorId = errorId;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ErrorStatsKey obj)) {
+ return false;
+ }
+ return this.mModuleId == obj.mModuleId && this.mErrorId == obj.mErrorId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mModuleId, mErrorId);
+ }
+
+ @Override
+ public String toString() {
+ return "[ErrorStatsKey: mModuleId=" + mModuleId + ", mErrorId=" + mErrorId + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/TelecomMetricsController.java b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
new file mode 100644
index 0000000..8903b02
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 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.telecom.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class TelecomMetricsController implements StatsManager.StatsPullAtomCallback {
+
+ private static final String TAG = TelecomMetricsController.class.getSimpleName();
+
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+ private final ConcurrentHashMap<Integer, TelecomPulledAtom> mStats = new ConcurrentHashMap<>();
+
+ private TelecomMetricsController(@NonNull Context context,
+ @NonNull HandlerThread handlerThread) {
+ mContext = context;
+ mHandlerThread = handlerThread;
+ }
+
+ @NonNull
+ public static TelecomMetricsController make(@NonNull Context context) {
+ Log.i(TAG, "TMC.iN1");
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ return make(context, handlerThread);
+ }
+
+ @VisibleForTesting
+ @NonNull
+ public static TelecomMetricsController make(@NonNull Context context,
+ @NonNull HandlerThread handlerThread) {
+ Log.i(TAG, "TMC.iN2");
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(handlerThread);
+ return new TelecomMetricsController(context, handlerThread);
+ }
+
+ @NonNull
+ public ApiStats getApiStats() {
+ ApiStats stats = (ApiStats) mStats.get(TELECOM_API_STATS);
+ if (stats == null) {
+ stats = new ApiStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @NonNull
+ public AudioRouteStats getAudioRouteStats() {
+ AudioRouteStats stats = (AudioRouteStats) mStats.get(CALL_AUDIO_ROUTE_STATS);
+ if (stats == null) {
+ stats = new AudioRouteStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @NonNull
+ public CallStats getCallStats() {
+ CallStats stats = (CallStats) mStats.get(CALL_STATS);
+ if (stats == null) {
+ stats = new CallStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @NonNull
+ public ErrorStats getErrorStats() {
+ ErrorStats stats = (ErrorStats) mStats.get(TELECOM_ERROR_STATS);
+ if (stats == null) {
+ stats = new ErrorStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @Override
+ public int onPullAtom(final int atomTag, final List<StatsEvent> data) {
+ if (mStats.containsKey(atomTag)) {
+ return Objects.requireNonNull(mStats.get(atomTag)).pull(data);
+ }
+ return StatsManager.PULL_SKIP;
+ }
+
+ @VisibleForTesting
+ public Map<Integer, TelecomPulledAtom> getStats() {
+ return mStats;
+ }
+
+ @VisibleForTesting
+ public void registerAtom(int tag, TelecomPulledAtom atom) {
+ mStats.put(tag, atom);
+ }
+
+ public void destroy() {
+ mStats.clear();
+ mHandlerThread.quitSafely();
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/TelecomPulledAtom.java b/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
new file mode 100644
index 0000000..d6eb039
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 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.telecom.metrics;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.nano.PulledAtomsClass.PulledAtoms;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.util.List;
+
+public abstract class TelecomPulledAtom extends Handler {
+ /**
+ * Min interval to persist the data.
+ */
+ protected static final int DELAY_FOR_PERSISTENT_MILLIS = 30000;
+ protected static final int EVENT_SUB_BASE = 1000;
+ private static final String TAG = TelecomPulledAtom.class.getSimpleName();
+ private static final long MIN_PULL_INTERVAL_MILLIS = 23L * 60 * 60 * 1000;
+ private static final int EVENT_SAVE = 1;
+ private final Context mContext;
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public PulledAtoms mPulledAtoms;
+ protected long mLastPulledTimestamps;
+
+ protected TelecomPulledAtom(@NonNull Context context, @NonNull Looper looper) {
+ super(looper);
+ mContext = context;
+ mPulledAtoms = loadAtomsFromFile();
+ onLoad();
+ }
+
+ public synchronized int pull(final List<StatsEvent> data) {
+ long cur = System.currentTimeMillis();
+ if (cur - mLastPulledTimestamps < MIN_PULL_INTERVAL_MILLIS) {
+ return StatsManager.PULL_SKIP;
+ }
+ mLastPulledTimestamps = cur;
+ return onPull(data);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ abstract public int getTag();
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public abstract int onPull(List<StatsEvent> data);
+
+ protected abstract void onLoad();
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public abstract void onAggregate();
+
+ public void onFlush() {
+ save(0);
+ }
+
+ protected abstract String getFileName();
+
+ private synchronized PulledAtoms loadAtomsFromFile() {
+ try {
+ return
+ PulledAtoms.parseFrom(
+ Files.readAllBytes(mContext.getFileStreamPath(getFileName()).toPath()));
+ } catch (NoSuchFileException e) {
+ Log.e(TAG, e, "the atom file not found");
+ } catch (IOException | NullPointerException e) {
+ Log.e(TAG, e, "cannot load/parse the atom file");
+ }
+ return makeNewPulledAtoms();
+ }
+
+ protected synchronized void clearAtoms() {
+ mPulledAtoms = makeNewPulledAtoms();
+ }
+
+ private synchronized void onSave() {
+ try (FileOutputStream stream = mContext.openFileOutput(getFileName(),
+ Context.MODE_PRIVATE)) {
+ Log.d(TAG, "save " + getTag());
+ stream.write(PulledAtoms.toByteArray(mPulledAtoms));
+ } catch (IOException e) {
+ Log.e(TAG, e, "cannot save the atom to file");
+ } catch (UnsupportedOperationException e) {
+ Log.e(TAG, e, "cannot open the file");
+ }
+ }
+
+ private PulledAtoms makeNewPulledAtoms() {
+ return new PulledAtoms();
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public void save(int delayMillis) {
+ if (delayMillis > 0) {
+ if (!hasMessages(EVENT_SAVE)) {
+ sendMessageDelayed(obtainMessage(EVENT_SAVE), delayMillis);
+ }
+ } else {
+ onSave();
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_SAVE) {
+ onSave();
+ }
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
index a6f63bc..d608d0a 100644
--- a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
@@ -45,6 +45,7 @@
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.ui.ToastFactory;
import org.junit.After;
@@ -90,6 +91,7 @@
@Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
@Mock private EmergencyCallDiagnosticLogger mMockEmergencyCallDiagnosticLogger;
+ @Mock private TelecomMetricsController mMockTelecomMetricsController;
@Override
@Before
@@ -122,7 +124,8 @@
doReturn(new ComponentName(mContext, CallTest.class))
.when(mMockConnectionService).getComponentName();
mCallAnomalyWatchdog = new CallAnomalyWatchdog(mTestScheduledExecutorService, mLock,
- mFeatureFlags, mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger);
+ mFeatureFlags, mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger,
+ mMockTelecomMetricsController);
mCallAnomalyWatchdog.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 1254b8a..ade2a22 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -85,6 +85,7 @@
import com.android.server.telecom.WiredHeadsetManager;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import org.junit.After;
import org.junit.Before;
@@ -116,6 +117,7 @@
@Mock CallAudioManager mCallAudioManager;
@Mock Call mCall;
@Mock private TelecomSystem.SyncRoot mLock;
+ @Mock private TelecomMetricsController mMockTelecomMetricsController;
private AudioRoute mEarpieceRoute;
private AudioRoute mSpeakerRoute;
private boolean mOverrideSpeakerToBus;
@@ -174,8 +176,8 @@
.thenReturn(BLUETOOTH_DEVICE_1);
when(mAudioDeviceInfo.getAddress()).thenReturn(BT_ADDRESS_1);
mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
- mAudioRouteFactory, mWiredHeadsetManager,
- mBluetoothRouteManager, mockStatusBarNotifier, mFeatureFlags);
+ mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager,
+ mockStatusBarNotifier, mFeatureFlags, mMockTelecomMetricsController);
mController.setAudioRouteFactory(mAudioRouteFactory);
mController.setAudioManager(mAudioManager);
mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 74f33c3..c2acfd6 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -134,6 +134,7 @@
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
@@ -315,6 +316,7 @@
@Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
@Mock private Context mMockCreateContextAsUser;
@Mock private UserManager mMockCurrentUserManager;
+ @Mock private TelecomMetricsController mMockTelecomMetricsController;
private CallsManager mCallsManager;
@Override
@@ -393,7 +395,8 @@
mFeatureFlags,
mTelephonyFlags,
(call, listener, context, timeoutsAdapter,
- mFeatureFlags, lock) -> mIncomingCallFilterGraph);
+ mFeatureFlags, lock) -> mIncomingCallFilterGraph,
+ mMockTelecomMetricsController);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
diff --git a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
new file mode 100644
index 0000000..e2ab8d6
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 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.telecom.tests;
+
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.StatsManager;
+import android.os.HandlerThread;
+import android.util.StatsEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.AudioRouteStats;
+import com.android.server.telecom.metrics.CallStats;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class TelecomMetricsControllerTest extends TelecomTestCase {
+
+ @Mock
+ ApiStats mApiStats;
+ @Mock
+ AudioRouteStats mAudioRouteStats;
+ @Mock
+ CallStats mCallStats;
+ @Mock
+ ErrorStats mErrorStats;
+
+ HandlerThread mHandlerThread;
+
+ TelecomMetricsController mTelecomMetricsController;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mHandlerThread = new HandlerThread("TelecomMetricsControllerTest");
+ mHandlerThread.start();
+ mTelecomMetricsController = TelecomMetricsController.make(mContext, mHandlerThread);
+ assertThat(mTelecomMetricsController).isNotNull();
+ setUpStats();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ mTelecomMetricsController.destroy();
+ mHandlerThread.quitSafely();
+ super.tearDown();
+ }
+
+ @Test
+ public void testGetApiStatsReturnsSameInstance() {
+ ApiStats stats1 = mTelecomMetricsController.getApiStats();
+ ApiStats stats2 = mTelecomMetricsController.getApiStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testGetAudioRouteStatsReturnsSameInstance() {
+ AudioRouteStats stats1 = mTelecomMetricsController.getAudioRouteStats();
+ AudioRouteStats stats2 = mTelecomMetricsController.getAudioRouteStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testGetCallStatsReturnsSameInstance() {
+ CallStats stats1 = mTelecomMetricsController.getCallStats();
+ CallStats stats2 = mTelecomMetricsController.getCallStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testGetErrorStatsReturnsSameInstance() {
+ ErrorStats stats1 = mTelecomMetricsController.getErrorStats();
+ ErrorStats stats2 = mTelecomMetricsController.getErrorStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testOnPullAtomReturnsPullSkipIfAtomNotRegistered() {
+ mTelecomMetricsController.getStats().clear();
+
+ int result = mTelecomMetricsController.onPullAtom(TELECOM_API_STATS, null);
+ assertThat(result).isEqualTo(StatsManager.PULL_SKIP);
+ }
+
+ @Test
+ public void testRegisterAtomIsSameInstance() {
+ ApiStats stats = mock(ApiStats.class);
+
+ mTelecomMetricsController.registerAtom(TELECOM_API_STATS, stats);
+
+ assertThat(mTelecomMetricsController.getStats().get(TELECOM_API_STATS))
+ .isSameInstanceAs(stats);
+ }
+
+ @Test
+ public void testDestroy() {
+ mTelecomMetricsController.destroy();
+ assertThat(mTelecomMetricsController.getStats()).isEmpty();
+ }
+
+ @Test
+ public void testOnPullAtomIsPulled() {
+ final List<StatsEvent> data = new ArrayList<>();
+ final ArgumentCaptor<List<StatsEvent>> captor = ArgumentCaptor.forClass((Class) List.class);
+ doReturn(StatsManager.PULL_SUCCESS).when(mApiStats).pull(any());
+
+ int result = mTelecomMetricsController.onPullAtom(TELECOM_API_STATS, data);
+
+ verify(mApiStats).pull(captor.capture());
+ assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+ assertThat(captor.getValue()).isEqualTo(data);
+ }
+
+ private void setUpStats() {
+ mTelecomMetricsController.getStats().put(CALL_AUDIO_ROUTE_STATS,
+ mAudioRouteStats);
+ mTelecomMetricsController.getStats().put(CALL_STATS, mCallStats);
+ mTelecomMetricsController.getStats().put(TELECOM_API_STATS, mApiStats);
+ mTelecomMetricsController.getStats().put(TELECOM_ERROR_STATS, mErrorStats);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
new file mode 100644
index 0000000..d188054
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
@@ -0,0 +1,815 @@
+/*
+ * Copyright (C) 2024 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.telecom.tests;
+
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_LE;
+import static com.android.server.telecom.AudioRoute.TYPE_EARPIECE;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_INCOMING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.telecom.PhoneAccount;
+import android.util.StatsEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.PendingAudioRoute;
+import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.AudioRouteStats;
+import com.android.server.telecom.metrics.CallStats;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class TelecomPulledAtomTest extends TelecomTestCase {
+ private static final long MIN_PULL_INTERVAL_MILLIS = 23L * 60 * 60 * 1000;
+ private static final long DEFAULT_TIMESTAMPS_MILLIS = 3000;
+ private static final int DELAY_FOR_PERSISTENT_MILLIS = 30000;
+ private static final int DELAY_TOLERANCE = 50;
+ private static final int TEST_TIMEOUT = (int) AudioRouteStats.THRESHOLD_REVERT_MS + 1000;
+ private static final String FILE_NAME_TEST_ATOM = "test_atom.pb";
+
+ private static final int VALUE_ATOM_COUNT = 1;
+
+ private static final int VALUE_UID = 10000 + 1;
+ private static final int VALUE_API_ID = 1;
+ private static final int VALUE_API_RESULT = 1;
+ private static final int VALUE_API_COUNT = 1;
+
+ private static final int VALUE_AUDIO_ROUTE_TYPE1 = 1;
+ private static final int VALUE_AUDIO_ROUTE_TYPE2 = 2;
+ private static final int VALUE_AUDIO_ROUTE_COUNT = 1;
+ private static final int VALUE_AUDIO_ROUTE_LATENCY = 300;
+
+ private static final int VALUE_CALL_DIRECTION = 1;
+ private static final int VALUE_CALL_ACCOUNT_TYPE = 1;
+ private static final int VALUE_CALL_COUNT = 1;
+ private static final int VALUE_CALL_DURATION = 3000;
+
+ private static final int VALUE_MODULE_ID = 1;
+ private static final int VALUE_ERROR_ID = 1;
+ private static final int VALUE_ERROR_COUNT = 1;
+
+ @Rule
+ public TemporaryFolder mTempFolder = new TemporaryFolder();
+ @Mock
+ FileOutputStream mFileOutputStream;
+ @Mock
+ PendingAudioRoute mMockPendingAudioRoute;
+ @Mock
+ AudioRoute mMockSourceRoute;
+ @Mock
+ AudioRoute mMockDestRoute;
+ private File mTempFile;
+ private Looper mLooper;
+ private Context mSpyContext;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mSpyContext = spy(mContext);
+ mLooper = Looper.getMainLooper();
+ mTempFile = mTempFolder.newFile(FILE_NAME_TEST_ATOM);
+ doReturn(mTempFile).when(mSpyContext).getFileStreamPath(anyString());
+ doReturn(mFileOutputStream).when(mSpyContext).openFileOutput(anyString(), anyInt());
+ doReturn(false).when(mMockPendingAudioRoute).isFailed();
+ doReturn(mMockSourceRoute).when(mMockPendingAudioRoute).getOrigRoute();
+ doReturn(mMockDestRoute).when(mMockPendingAudioRoute).getDestRoute();
+ doReturn(TYPE_EARPIECE).when(mMockSourceRoute).getType();
+ doReturn(TYPE_BLUETOOTH_LE).when(mMockDestRoute).getType();
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ mTempFile.delete();
+ super.tearDown();
+ }
+
+ @Test
+ public void testNewPulledAtomsFromFileInvalid() throws Exception {
+ mTempFile.delete();
+
+ ApiStats apiStats = new ApiStats(mSpyContext, mLooper);
+
+ assertNotNull(apiStats.mPulledAtoms);
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 0);
+
+ AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper);
+
+ assertNotNull(audioRouteStats.mPulledAtoms);
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 0);
+
+ CallStats callStats = new CallStats(mSpyContext, mLooper);
+
+ assertNotNull(callStats.mPulledAtoms);
+ assertEquals(callStats.mPulledAtoms.callStats.length, 0);
+
+ ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper);
+
+ assertNotNull(errorStats.mPulledAtoms);
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 0);
+ }
+
+ @Test
+ public void testNewPulledAtomsFromFileValid() throws Exception {
+ createTestFileForApiStats(DEFAULT_TIMESTAMPS_MILLIS);
+ ApiStats apiStats = new ApiStats(mSpyContext, mLooper);
+
+ verifyTestDataForApiStats(apiStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+ createTestFileForAudioRouteStats(DEFAULT_TIMESTAMPS_MILLIS);
+ AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper);
+
+ verifyTestDataForAudioRouteStats(audioRouteStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+ createTestFileForCallStats(DEFAULT_TIMESTAMPS_MILLIS);
+ CallStats callStats = new CallStats(mSpyContext, mLooper);
+
+ verifyTestDataForCallStats(callStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+ createTestFileForErrorStats(DEFAULT_TIMESTAMPS_MILLIS);
+ ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper);
+
+ verifyTestDataForErrorStats(errorStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+ }
+
+ @Test
+ public void testPullApiStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForApiStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = apiStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(apiStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullApiStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForApiStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = apiStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(apiStats).onPull(eq(data));
+ assertEquals(data.size(), apiStats.mPulledAtoms.telecomApiStats.length);
+ }
+
+ @Test
+ public void testPullAudioRouteStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForAudioRouteStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = audioRouteStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(audioRouteStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullAudioRouteStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForAudioRouteStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = audioRouteStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(audioRouteStats).onPull(eq(data));
+ assertEquals(data.size(), audioRouteStats.mPulledAtoms.callAudioRouteStats.length);
+ }
+
+ @Test
+ public void testPullCallStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForCallStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = callStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(callStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullCallStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForCallStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = callStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(callStats).onPull(eq(data));
+ assertEquals(data.size(), callStats.mPulledAtoms.callStats.length);
+ }
+
+ @Test
+ public void testPullErrorStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForErrorStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = errorStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(errorStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullErrorStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForErrorStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = errorStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(errorStats).onPull(eq(data));
+ assertEquals(data.size(), errorStats.mPulledAtoms.telecomErrorStats.length);
+ }
+
+ @Test
+ public void testApiStatsLog() throws Exception {
+ ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+
+ apiStats.log(VALUE_API_ID, VALUE_UID, VALUE_API_RESULT);
+ waitForHandlerAction(apiStats, TEST_TIMEOUT);
+
+ verify(apiStats, times(1)).onAggregate();
+ verify(apiStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 1);
+ verifyMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats[0], VALUE_API_ID,
+ VALUE_UID, VALUE_API_RESULT, 1);
+
+ apiStats.log(VALUE_API_ID, VALUE_UID, VALUE_API_RESULT);
+ waitForHandlerAction(apiStats, TEST_TIMEOUT);
+
+ verify(apiStats, times(2)).onAggregate();
+ verify(apiStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 1);
+ verifyMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats[0], VALUE_API_ID,
+ VALUE_UID, VALUE_API_RESULT, 2);
+ }
+
+ @Test
+ public void testAudioRouteStatsLog() throws Exception {
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.log(VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ waitForHandlerAction(audioRouteStats, TEST_TIMEOUT);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false, 1,
+ VALUE_AUDIO_ROUTE_LATENCY);
+
+ audioRouteStats.log(VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ waitForHandlerAction(audioRouteStats, TEST_TIMEOUT);
+
+ verify(audioRouteStats, times(2)).onAggregate();
+ verify(audioRouteStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false, 2,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnEnterThenExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, 100);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the stats should be saved when the revert threshold is expired
+ waitForHandlerActionDelayed(
+ audioRouteStats, TEST_TIMEOUT, AudioRouteStats.THRESHOLD_REVERT_MS);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, false, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRevertToSourceInThreshold() throws Exception {
+ int delay = 100;
+ int latency = 500;
+ int duration = 1000;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the event should be saved as revert when routing back to the source before
+ // the revert threshold is expired
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, duration);
+
+ // Reverse the audio types
+ doReturn(TYPE_BLUETOOTH_LE).when(mMockSourceRoute).getType();
+ doReturn(TYPE_EARPIECE).when(mMockDestRoute).getType();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, true, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRevertToSourceBeyondThreshold() throws Exception {
+ int delay = 100;
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the event should not be saved as revert when routing back to the source
+ // after the revert threshold is expired
+ waitForHandlerActionDelayed(
+ audioRouteStats, TEST_TIMEOUT, AudioRouteStats.THRESHOLD_REVERT_MS);
+
+ // Reverse the audio types
+ doReturn(TYPE_BLUETOOTH_LE).when(mMockSourceRoute).getType();
+ doReturn(TYPE_EARPIECE).when(mMockDestRoute).getType();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, false, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRouteToAnotherDestInThreshold() throws Exception {
+ int delay = 100;
+ int latency = 500;
+ int duration = 1000;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the event should not be saved as revert when routing to a type different
+ // as the source before the revert threshold is expired
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, duration);
+
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, false, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnMultipleEnterWithoutExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ doReturn(mMockDestRoute).when(mMockPendingAudioRoute).getOrigRoute();
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Verify that the stats should not be saved without exit
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+ }
+
+ @Test
+ public void testAudioRouteStatsOnMultipleEnterWithExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, 100);
+
+ doReturn(mMockDestRoute).when(mMockPendingAudioRoute).getOrigRoute();
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Verify that the stats should be saved after exit
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRouteToSameDestWithExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+ doReturn(mMockSourceRoute).when(mMockPendingAudioRoute).getDestRoute();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Enter again to trigger the log
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Verify that the stats should not be saved without exit
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+ }
+
+ @Test
+ public void testCallStatsLog() throws Exception {
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+
+ callStats.log(VALUE_CALL_DIRECTION, false, false, true, VALUE_CALL_ACCOUNT_TYPE,
+ VALUE_UID, VALUE_CALL_DURATION);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(1)).onAggregate();
+ verify(callStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(callStats.mPulledAtoms.callStats.length, 1);
+ verifyMessageForCallStats(callStats.mPulledAtoms.callStats[0], VALUE_CALL_DIRECTION,
+ false, false, true, VALUE_CALL_ACCOUNT_TYPE, VALUE_UID, 1, VALUE_CALL_DURATION);
+
+ callStats.log(VALUE_CALL_DIRECTION, false, false, true, VALUE_CALL_ACCOUNT_TYPE,
+ VALUE_UID, VALUE_CALL_DURATION);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(2)).onAggregate();
+ verify(callStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(callStats.mPulledAtoms.callStats.length, 1);
+ verifyMessageForCallStats(callStats.mPulledAtoms.callStats[0], VALUE_CALL_DIRECTION,
+ false, false, true, VALUE_CALL_ACCOUNT_TYPE, VALUE_UID, 2, VALUE_CALL_DURATION);
+ }
+
+ @Test
+ public void testCallStatsOnStartThenEnd() throws Exception {
+ int duration = 1000;
+ UserHandle uh = UserHandle.of(UserHandle.USER_SYSTEM);
+ PhoneAccount account = mock(PhoneAccount.class);
+ Call call = mock(Call.class);
+ doReturn(true).when(call).isIncoming();
+ doReturn(account).when(call).getPhoneAccountFromHandle();
+ doReturn((long) duration).when(call).getAgeMillis();
+ doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_CALL_PROVIDER));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
+ doReturn(uh).when(call).getAssociatedUser();
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+
+ callStats.onCallStart(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ callStats.onCallEnd(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(1)).log(eq(CALL_STATS__CALL_DIRECTION__DIR_INCOMING),
+ eq(false), eq(false), eq(false), eq(CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM),
+ eq(UserHandle.USER_SYSTEM), eq(duration));
+ }
+
+ @Test
+ public void testCallStatsOnMultipleAudioDevices() throws Exception {
+ int duration = 1000;
+ UserHandle uh = UserHandle.of(UserHandle.USER_SYSTEM);
+ PhoneAccount account = mock(PhoneAccount.class);
+ Call call = mock(Call.class);
+ doReturn(true).when(call).isIncoming();
+ doReturn(account).when(call).getPhoneAccountFromHandle();
+ doReturn((long) duration).when(call).getAgeMillis();
+ doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_CALL_PROVIDER));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
+ doReturn(uh).when(call).getAssociatedUser();
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+
+ callStats.onCallStart(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ callStats.onAudioDevicesChange(true);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ callStats.onCallEnd(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(1)).log(eq(CALL_STATS__CALL_DIRECTION__DIR_INCOMING),
+ eq(false), eq(false), eq(true), eq(CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM),
+ eq(UserHandle.USER_SYSTEM), eq(duration));
+ }
+
+ @Test
+ public void testErrorStatsLog() throws Exception {
+ ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+
+ errorStats.log(VALUE_MODULE_ID, VALUE_ERROR_ID);
+ waitForHandlerAction(errorStats, TEST_TIMEOUT);
+
+ verify(errorStats, times(1)).onAggregate();
+ verify(errorStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 1);
+ verifyMessageForErrorStats(errorStats.mPulledAtoms.telecomErrorStats[0], VALUE_MODULE_ID,
+ VALUE_ERROR_ID, 1);
+
+ errorStats.log(VALUE_MODULE_ID, VALUE_ERROR_ID);
+ waitForHandlerAction(errorStats, TEST_TIMEOUT);
+
+ verify(errorStats, times(2)).onAggregate();
+ verify(errorStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 1);
+ verifyMessageForErrorStats(errorStats.mPulledAtoms.telecomErrorStats[0], VALUE_MODULE_ID,
+ VALUE_ERROR_ID, 2);
+ }
+
+ private void createTestFileForApiStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.telecomApiStats =
+ new PulledAtomsClass.TelecomApiStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.telecomApiStats[i] = new PulledAtomsClass.TelecomApiStats();
+ atom.telecomApiStats[i].setApiName(VALUE_API_ID + i);
+ atom.telecomApiStats[i].setUid(VALUE_UID);
+ atom.telecomApiStats[i].setApiResult(VALUE_API_RESULT);
+ atom.telecomApiStats[i].setCount(VALUE_API_COUNT);
+ }
+ atom.setTelecomApiStatsPullTimestampMillis(timestamps);
+
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForApiStats(final PulledAtomsClass.PulledAtoms atom,
+ long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getTelecomApiStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.telecomApiStats);
+ assertEquals(atom.telecomApiStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.telecomApiStats[i]);
+ verifyMessageForApiStats(atom.telecomApiStats[i], VALUE_API_ID + i, VALUE_UID,
+ VALUE_API_RESULT, VALUE_API_COUNT);
+ }
+ }
+
+ private void verifyMessageForApiStats(final PulledAtomsClass.TelecomApiStats msg, int apiId,
+ int uid, int result, int count) {
+ assertEquals(msg.getApiName(), apiId);
+ assertEquals(msg.getUid(), uid);
+ assertEquals(msg.getApiResult(), result);
+ assertEquals(msg.getCount(), count);
+ }
+
+ private void createTestFileForAudioRouteStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.callAudioRouteStats =
+ new PulledAtomsClass.CallAudioRouteStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.callAudioRouteStats[i] = new PulledAtomsClass.CallAudioRouteStats();
+ atom.callAudioRouteStats[i].setCallAudioRouteSource(VALUE_AUDIO_ROUTE_TYPE1);
+ atom.callAudioRouteStats[i].setCallAudioRouteDest(VALUE_AUDIO_ROUTE_TYPE2);
+ atom.callAudioRouteStats[i].setSuccess(true);
+ atom.callAudioRouteStats[i].setRevert(false);
+ atom.callAudioRouteStats[i].setCount(VALUE_AUDIO_ROUTE_COUNT);
+ atom.callAudioRouteStats[i].setAverageLatencyMs(VALUE_AUDIO_ROUTE_LATENCY);
+ }
+ atom.setCallAudioRouteStatsPullTimestampMillis(timestamps);
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForAudioRouteStats(final PulledAtomsClass.PulledAtoms atom,
+ long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getCallAudioRouteStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.callAudioRouteStats);
+ assertEquals(atom.callAudioRouteStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.callAudioRouteStats[i]);
+ verifyMessageForAudioRouteStats(atom.callAudioRouteStats[i], VALUE_AUDIO_ROUTE_TYPE1,
+ VALUE_AUDIO_ROUTE_TYPE2, true, false, VALUE_AUDIO_ROUTE_COUNT,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ }
+ }
+
+ private void verifyMessageForAudioRouteStats(
+ final PulledAtomsClass.CallAudioRouteStats msg, int source, int dest, boolean success,
+ boolean revert, int count, int latency) {
+ assertEquals(msg.getCallAudioRouteSource(), source);
+ assertEquals(msg.getCallAudioRouteDest(), dest);
+ assertEquals(msg.getSuccess(), success);
+ assertEquals(msg.getRevert(), revert);
+ assertEquals(msg.getCount(), count);
+ assertTrue(Math.abs(latency - msg.getAverageLatencyMs()) < DELAY_TOLERANCE);
+ }
+
+ private void createTestFileForCallStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.callStats =
+ new PulledAtomsClass.CallStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.callStats[i] = new PulledAtomsClass.CallStats();
+ atom.callStats[i].setCallDirection(VALUE_CALL_DIRECTION);
+ atom.callStats[i].setExternalCall(false);
+ atom.callStats[i].setEmergencyCall(false);
+ atom.callStats[i].setMultipleAudioAvailable(false);
+ atom.callStats[i].setAccountType(VALUE_CALL_ACCOUNT_TYPE);
+ atom.callStats[i].setUid(VALUE_UID);
+ atom.callStats[i].setCount(VALUE_CALL_COUNT);
+ atom.callStats[i].setAverageDurationMs(VALUE_CALL_DURATION);
+ }
+ atom.setCallStatsPullTimestampMillis(timestamps);
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForCallStats(final PulledAtomsClass.PulledAtoms atom,
+ long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getCallStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.callStats);
+ assertEquals(atom.callStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.callStats[i]);
+ verifyMessageForCallStats(atom.callStats[i], VALUE_CALL_DIRECTION, false, false,
+ false, VALUE_CALL_ACCOUNT_TYPE, VALUE_UID, VALUE_CALL_COUNT,
+ VALUE_CALL_DURATION);
+ }
+ }
+
+ private void verifyMessageForCallStats(final PulledAtomsClass.CallStats msg,
+ int direction, boolean external, boolean emergency, boolean multipleAudio,
+ int accountType, int uid, int count, int duration) {
+ assertEquals(msg.getCallDirection(), direction);
+ assertEquals(msg.getExternalCall(), external);
+ assertEquals(msg.getEmergencyCall(), emergency);
+ assertEquals(msg.getMultipleAudioAvailable(), multipleAudio);
+ assertEquals(msg.getAccountType(), accountType);
+ assertEquals(msg.getUid(), uid);
+ assertEquals(msg.getCount(), count);
+ assertEquals(msg.getAverageDurationMs(), duration);
+ }
+
+ private void createTestFileForErrorStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.telecomErrorStats =
+ new PulledAtomsClass.TelecomErrorStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.telecomErrorStats[i] = new PulledAtomsClass.TelecomErrorStats();
+ atom.telecomErrorStats[i].setSubmoduleName(VALUE_MODULE_ID);
+ atom.telecomErrorStats[i].setErrorName(VALUE_ERROR_ID);
+ atom.telecomErrorStats[i].setCount(VALUE_ERROR_COUNT);
+ }
+ atom.setTelecomErrorStatsPullTimestampMillis(timestamps);
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForErrorStats(
+ final PulledAtomsClass.PulledAtoms atom, long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getTelecomErrorStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.telecomErrorStats);
+ assertEquals(atom.telecomErrorStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.telecomErrorStats[i]);
+ verifyMessageForErrorStats(atom.telecomErrorStats[i], VALUE_MODULE_ID, VALUE_ERROR_ID
+ , VALUE_ERROR_COUNT);
+ }
+ }
+
+ private void verifyMessageForErrorStats(final PulledAtomsClass.TelecomErrorStats msg,
+ int moduleId, int errorId, int count) {
+ assertEquals(msg.getSubmoduleName(), moduleId);
+ assertEquals(msg.getErrorName(), errorId);
+ assertEquals(msg.getCount(), count);
+ }
+}