Add logs for Telecom metrics stage 2

Flag: com.android.server.telecom.flags.telecom_metrics_support
Bug: 397554282
Test: atest TeleServiceTests
Test: manual
Change-Id: I47300ddf9205ee2f8812b840445402d4093e176a
diff --git a/proto/pulled_atoms.proto b/proto/pulled_atoms.proto
index 6c9af46..2916dad 100644
--- a/proto/pulled_atoms.proto
+++ b/proto/pulled_atoms.proto
@@ -14,6 +14,8 @@
   optional int64 telecom_api_stats_pull_timestamp_millis = 6;
   repeated TelecomErrorStats telecom_error_stats = 7;
   optional int64 telecom_error_stats_pull_timestamp_millis = 8;
+  repeated TelecomEventStats telecom_event_stats = 9;
+  optional int64 telecom_event_stats_pull_timestamp_millis = 10;
 }
 
 /**
@@ -48,6 +50,15 @@
 
     // Average elapsed time between CALL_STATE_ACTIVE to CALL_STATE_DISCONNECTED.
     optional int32 average_duration_ms = 8;
+
+    // The disconnect cause of the call. Eg. ERROR, LOCAL, REMOTE, etc.
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 disconnect_cause = 9;
+
+    // The type of simultaneous call type. Eg. SINGLE, DUAL_SAME_ACCOUNT,
+    // DUAL_DIFF_ACCOUNT, etc.
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 simultaneous_type = 10;
 }
 
 /**
@@ -112,3 +123,22 @@
     // The number of times this error occurs
     optional int32 count = 3;
 }
+
+/**
+ * Pulled atom to capture stats of Telecom critical events
+ */
+message TelecomEventStats {
+    // The event name
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 event = 1;
+
+    // UID of the caller. This is always -1/unknown for the private space.
+    optional int32 uid = 2;
+
+    // The cause related to the event
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 event_cause = 3;
+
+    // The number of times this event occurs
+    optional int32 count = 4;
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 3b3a35a..8cd51d0 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -131,6 +131,10 @@
 
     private static final char NO_DTMF_TONE = '\0';
 
+    public static final int CALL_SIMULTANEOUS_UNKNOWN = 0;
+    public static final int CALL_SIMULTANEOUS_SINGLE = 1;
+    public static final int CALL_DIRECTION_DUAL_SAME_ACCOUNT = 2;
+    public static final int CALL_DIRECTION_DUAL_DIFF_ACCOUNT = 3;
 
     /**
      * Listener for CallState changes which can be leveraged by a Transaction.
@@ -501,6 +505,11 @@
      */
     private DisconnectCause mOverrideDisconnectCause = new DisconnectCause(DisconnectCause.UNKNOWN);
 
+    /**
+     * Simultaneous type of the call.
+     */
+    private int mSimultaneousType = CALL_SIMULTANEOUS_UNKNOWN;
+
     private Bundle mIntentExtras = new Bundle();
 
     /**
@@ -5064,4 +5073,12 @@
             }
         }
     }
+
+    public void setSimultaneousType(int simultaneousType) {
+        mSimultaneousType = simultaneousType;
+    }
+
+    public int getSimultaneousType() {
+        return mSimultaneousType;
+    }
 }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index ef8210d..9d1a382 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -84,6 +84,8 @@
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.EventStats;
+import com.android.server.telecom.metrics.EventStats.CriticalEvent;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
 import com.android.server.telecom.callsequencing.TransactionManager;
@@ -195,8 +197,9 @@
         @Override
         public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
                 String callId, String callingPackage) {
+            int uid = Binder.getCallingUid();
             ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDCALL,
-                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
+                    uid, ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aC", Log.getPackageAbbreviation(callingPackage));
                 Log.i(TAG, "addCall: id=[%s], attributes=[%s]", callId, callAttributes);
@@ -213,8 +216,8 @@
 
                 // add extras about info used for FGS delegation
                 Bundle extras = new Bundle();
-                extras.putInt(CallAttributes.CALLER_UID_KEY, Binder.getCallingUid());
-                extras.putInt(CallAttributes.CALLER_PID_KEY, Binder.getCallingPid());
+                extras.putInt(CallAttributes.CALLER_UID_KEY, uid);
+                extras.putInt(CallAttributes.CALLER_PID_KEY, uid);
 
 
                 CompletableFuture<CallTransaction> transactionFuture;
@@ -233,6 +236,11 @@
                             public void onResult(CallTransactionResult result) {
                                 Log.d(TAG, "addCall: onResult");
                                 Call call = result.getCall();
+                                if (mFeatureFlags.telecomMetricsSupport()) {
+                                    mMetricsController.getEventStats().log(new CriticalEvent(
+                                            EventStats.ID_ADD_CALL, uid,
+                                            EventStats.CAUSE_CALL_TRANSACTION_SUCCESS));
+                                }
 
                                 if (call == null || !call.getId().equals(callId)) {
                                     Log.i(TAG, "addCall: onResult: call is null or id mismatch");
@@ -278,6 +286,12 @@
                                             ADD_CALL_ON_ERROR_UUID,
                                             exception.getMessage());
                                 }
+                                if (mFeatureFlags.telecomMetricsSupport()) {
+                                    mMetricsController.getEventStats().log(new CriticalEvent(
+                                            EventStats.ID_ADD_CALL, uid,
+                                            EventStats.CAUSE_CALL_TRANSACTION_BASE
+                                                    + exception.getCode()));
+                                }
                             }
                         });
                     }
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index b66fb10..00341a5 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -49,6 +49,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.EventStats;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallStreamingNotification;
diff --git a/src/com/android/server/telecom/metrics/CallStats.java b/src/com/android/server/telecom/metrics/CallStats.java
index f518557..5f00446 100644
--- a/src/com/android/server/telecom/metrics/CallStats.java
+++ b/src/com/android/server/telecom/metrics/CallStats.java
@@ -42,6 +42,7 @@
 import com.android.server.telecom.TelecomStatsLog;
 import com.android.server.telecom.nano.PulledAtomsClass;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -82,7 +83,8 @@
                     TelecomStatsLog.buildStatsEvent(getTag(),
                             v.getCallDirection(), v.getExternalCall(), v.getEmergencyCall(),
                             v.getMultipleAudioAvailable(), v.getAccountType(), v.getUid(),
-                            v.getCount(), v.getAverageDurationMs())));
+                            v.getCount(), v.getAverageDurationMs(), v.getDisconnectCause(),
+                            v.getSimultaneousType())));
             mCallStatsMap.clear();
             onAggregate();
             return StatsManager.PULL_SUCCESS;
@@ -97,10 +99,11 @@
             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()));
+                        v.getExternalCall(), v.getEmergencyCall(),
+                        v.getMultipleAudioAvailable(), v.getAccountType(),
+                        v.getUid(), v.getDisconnectCause(), v.getSimultaneousType()),
+                        new CallStatsData(
+                                v.getCount(), v.getAverageDurationMs()));
             }
             mLastPulledTimestamps = mPulledAtoms.getCallStatsPullTimestampMillis();
         }
@@ -125,6 +128,8 @@
             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]].setDisconnectCause(k.mCause);
+            mPulledAtoms.callStats[index[0]].setSimultaneousType(k.mSimultaneousType);
             mPulledAtoms.callStats[index[0]].setCount(v.mCount);
             mPulledAtoms.callStats[index[0]].setAverageDurationMs(v.mAverageDuration);
             index[0]++;
@@ -133,10 +138,16 @@
     }
 
     public void log(int direction, boolean isExternal, boolean isEmergency,
-            boolean isMultipleAudioAvailable, int accountType, int uid, int duration) {
+        boolean isMultipleAudioAvailable, int accountType, int uid, int duration) {
+        log(direction, isExternal, isEmergency, isMultipleAudioAvailable, accountType, uid,
+                0, 0, duration);
+    }
+    public void log(int direction, boolean isExternal, boolean isEmergency,
+            boolean isMultipleAudioAvailable, int accountType, int uid,
+            int disconnectCause, int simultaneousType, int duration) {
         post(() -> {
             CallStatsKey key = new CallStatsKey(direction, isExternal, isEmergency,
-                    isMultipleAudioAvailable, accountType, uid);
+                    isMultipleAudioAvailable, accountType, uid, disconnectCause, simultaneousType);
             CallStatsData data = mCallStatsMap.computeIfAbsent(key, k -> new CallStatsData(0, 0));
             data.add(duration);
             onAggregate();
@@ -171,7 +182,8 @@
             }
 
             log(direction, call.isExternalCall(), call.isEmergencyCall(), hasMultipleAudioDevices,
-                    accountType, uid, duration);
+                    accountType, uid, call.getDisconnectCause().getCode(),
+                    call.getSimultaneousType(), duration);
         });
     }
 
@@ -236,15 +248,26 @@
         final boolean mIsMultipleAudioAvailable;
         final int mAccountType;
         final int mUid;
+        final int mCause;
+        final int mSimultaneousType;
 
         CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
-                boolean isMultipleAudioAvailable, int accountType, int uid) {
+            boolean isMultipleAudioAvailable, int accountType, int uid) {
+            this(direction, isExternal, isEmergency, isMultipleAudioAvailable, accountType, uid,
+                    0, 0);
+        }
+
+        CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
+                boolean isMultipleAudioAvailable, int accountType, int uid,
+                int cause, int simultaneousType) {
             mDirection = direction;
             mIsExternal = isExternal;
             mIsEmergency = isEmergency;
             mIsMultipleAudioAvailable = isMultipleAudioAvailable;
             mAccountType = accountType;
             mUid = uid;
+            mCause = cause;
+            mSimultaneousType = simultaneousType;
         }
 
         @Override
@@ -258,13 +281,14 @@
             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;
+                    && this.mAccountType == obj.mAccountType && this.mUid == obj.mUid
+                    && this.mCause == obj.mCause && this.mSimultaneousType == obj.mSimultaneousType;
         }
 
         @Override
         public int hashCode() {
             return Objects.hash(mDirection, mIsExternal, mIsEmergency, mIsMultipleAudioAvailable,
-                    mAccountType, mUid);
+                    mAccountType, mUid, mCause, mSimultaneousType);
         }
 
         @Override
@@ -272,7 +296,7 @@
             return "[CallStatsKey: mDirection=" + mDirection + ", mIsExternal=" + mIsExternal
                     + ", mIsEmergency=" + mIsEmergency + ", mIsMultipleAudioAvailable="
                     + mIsMultipleAudioAvailable + ", mAccountType=" + mAccountType + ", mUid="
-                    + mUid + "]";
+                    + mUid + ", mCause=" + mCause + ", mScType=" + mSimultaneousType + "]";
         }
     }
 
diff --git a/src/com/android/server/telecom/metrics/EventStats.java b/src/com/android/server/telecom/metrics/EventStats.java
new file mode 100644
index 0000000..18e68fb
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/EventStats.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2025 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_EVENT_STATS;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.telecom.CallException;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.metrics.ApiStats.ApiEvent;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class EventStats extends TelecomPulledAtom {
+    public static final int ID_UNKNOWN = TelecomStatsLog.TELECOM_EVENT_STATS__EVENT__EVENT_UNKNOWN;
+    public static final int ID_INIT = TelecomStatsLog.TELECOM_EVENT_STATS__EVENT__EVENT_INIT;
+    public static final int ID_DEFAULT_DIALER_CHANGED = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT__EVENT_DEFAULT_DIALER_CHANGED;
+    public static final int ID_ADD_CALL = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT__EVENT_ADD_CALL;
+
+    public static final int CAUSE_UNKNOWN = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CAUSE_UNKNOWN;
+    public static final int CAUSE_GENERIC_SUCCESS = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CAUSE_GENERIC_SUCCESS;
+    public static final int CAUSE_GENERIC_FAILURE = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CAUSE_GENERIC_FAILURE;
+    public static final int CAUSE_CALL_TRANSACTION_SUCCESS = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CALL_TRANSACTION_SUCCESS;
+    public static final int CAUSE_CALL_TRANSACTION_BASE = CAUSE_CALL_TRANSACTION_SUCCESS;
+    public static final int CAUSE_CALL_TRANSACTION_ERROR_UNKNOWN =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_ERROR_UNKNOWN;
+    public static final int CAUSE_CALL_TRANSACTION_CANNOT_HOLD_CURRENT_ACTIVE_CALL =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL;
+    public static final int CAUSE_CALL_TRANSACTION_CALL_IS_NOT_BEING_TRACKED =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CALL_IS_NOT_BEING_TRACKED;
+    public static final int CAUSE_CALL_TRANSACTION_CALL_CANNOT_BE_SET_TO_ACTIVE =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE;
+    public static final int CAUSE_CALL_TRANSACTION_CALL_NOT_PERMITTED_AT_PRESENT_TIME =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
+    public static final int CAUSE_CALL_TRANSACTION_OPERATION_TIMED_OUT =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_OPERATION_TIMED_OUT;
+    private static final String TAG = EventStats.class.getSimpleName();
+    private static final String FILE_NAME = "event_stats";
+    private Map<CriticalEvent, Integer> mEventStatsMap;
+
+    public EventStats(@NonNull Context context, @NonNull Looper looper,
+                      boolean isTestMode) {
+        super(context, looper, isTestMode);
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    @Override
+    public int getTag() {
+        return TELECOM_EVENT_STATS;
+    }
+
+    @Override
+    protected String getFileName() {
+        return FILE_NAME;
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    @Override
+    public synchronized int onPull(final List<StatsEvent> data) {
+        if (mPulledAtoms.telecomEventStats.length != 0) {
+            Arrays.stream(mPulledAtoms.telecomEventStats).forEach(v -> data.add(
+                    TelecomStatsLog.buildStatsEvent(getTag(),
+                            v.getEvent(), v.getUid(), v.getEventCause(), v.getCount())));
+            mEventStatsMap.clear();
+            onAggregate();
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    @Override
+    protected synchronized void onLoad() {
+        if (mPulledAtoms.telecomEventStats != null) {
+            mEventStatsMap = new HashMap<>();
+            for (PulledAtomsClass.TelecomEventStats v : mPulledAtoms.telecomEventStats) {
+                mEventStatsMap.put(new CriticalEvent(v.getEvent(), v.getUid(),
+                        v.getEventCause()), v.getCount());
+            }
+            mLastPulledTimestamps = mPulledAtoms.getTelecomEventStatsPullTimestampMillis();
+        }
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    @Override
+    public synchronized void onAggregate() {
+        Log.d(TAG, "onAggregate: %s", mEventStatsMap);
+        clearAtoms();
+        if (mEventStatsMap.isEmpty()) {
+            return;
+        }
+        mPulledAtoms.setTelecomEventStatsPullTimestampMillis(mLastPulledTimestamps);
+        mPulledAtoms.telecomEventStats =
+                new PulledAtomsClass.TelecomEventStats[mEventStatsMap.size()];
+        int[] index = new int[1];
+        mEventStatsMap.forEach((k, v) -> {
+            mPulledAtoms.telecomEventStats[index[0]] = new PulledAtomsClass.TelecomEventStats();
+            mPulledAtoms.telecomEventStats[index[0]].setEvent(k.mId);
+            mPulledAtoms.telecomEventStats[index[0]].setUid(k.mUid);
+            mPulledAtoms.telecomEventStats[index[0]].setEventCause(k.mCause);
+            mPulledAtoms.telecomEventStats[index[0]].setCount(v);
+            index[0]++;
+        });
+        save(DELAY_FOR_PERSISTENT_MILLIS);
+    }
+
+    public void log(@NonNull CriticalEvent event) {
+        post(() -> {
+            mEventStatsMap.put(event, mEventStatsMap.getOrDefault(event, 0) + 1);
+            onAggregate();
+        });
+    }
+
+    @IntDef(prefix = "ID_", value = {
+            ID_UNKNOWN,
+            ID_INIT,
+            ID_DEFAULT_DIALER_CHANGED,
+            ID_ADD_CALL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventId {
+    }
+
+    @IntDef(prefix = "CAUSE_", value = {
+            CAUSE_UNKNOWN,
+            CAUSE_GENERIC_SUCCESS,
+            CAUSE_GENERIC_FAILURE,
+            CAUSE_CALL_TRANSACTION_SUCCESS,
+            CAUSE_CALL_TRANSACTION_ERROR_UNKNOWN,
+            CAUSE_CALL_TRANSACTION_CANNOT_HOLD_CURRENT_ACTIVE_CALL,
+            CAUSE_CALL_TRANSACTION_CALL_IS_NOT_BEING_TRACKED,
+            CAUSE_CALL_TRANSACTION_CALL_CANNOT_BE_SET_TO_ACTIVE,
+            CAUSE_CALL_TRANSACTION_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+            CAUSE_CALL_TRANSACTION_OPERATION_TIMED_OUT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CauseId {
+    }
+
+    public static class CriticalEvent {
+
+        @EventId
+        int mId;
+        int mUid;
+        @CauseId
+        int mCause;
+
+        public CriticalEvent(@EventId int id, int uid, @CauseId int cause) {
+            mId = id;
+            mUid = uid;
+            mCause = cause;
+        }
+
+        public void setUid(int uid) {
+            this.mUid = uid;
+        }
+
+        public void setResult(@CauseId int result) {
+            this.mCause = result;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof ApiEvent obj)) {
+                return false;
+            }
+            return this.mId == obj.mId && this.mUid == obj.mCallerUid
+                    && this.mCause == obj.mResult;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mId, mUid, mCause);
+        }
+
+        @Override
+        public String toString() {
+            return "[CriticalEvent: mId=" + mId + ", m"
+                    + "Uid=" + mUid
+                    + ", mResult=" + mCause + "]";
+        }
+    }
+
+
+}
diff --git a/src/com/android/server/telecom/metrics/TelecomMetricsController.java b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
index 23673ca..980c180 100644
--- a/src/com/android/server/telecom/metrics/TelecomMetricsController.java
+++ b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
@@ -20,6 +20,7 @@
 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.android.server.telecom.TelecomStatsLog.TELECOM_EVENT_STATS;
 
 import android.annotation.NonNull;
 import android.app.StatsManager;
@@ -117,6 +118,16 @@
         return stats;
     }
 
+    @NonNull
+    public EventStats getEventStats() {
+        EventStats stats = (EventStats) mStats.get(TELECOM_EVENT_STATS);
+        if (stats == null) {
+            stats = new EventStats(mContext, mHandlerThread.getLooper(), isTestMode());
+            registerAtom(stats.getTag(), stats);
+        }
+        return stats;
+    }
+
     @Override
     public int onPullAtom(final int atomTag, final List<StatsEvent> data) {
         if (mStats.containsKey(atomTag)) {
diff --git a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
index 3836a41..3716b4d 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
@@ -19,6 +19,7 @@
 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.android.server.telecom.TelecomStatsLog.TELECOM_EVENT_STATS;
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyObject;
@@ -38,6 +39,7 @@
 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.EventStats;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 
 import org.junit.After;
@@ -61,6 +63,8 @@
     CallStats mCallStats;
     @Mock
     ErrorStats mErrorStats;
+    @Mock
+    EventStats mEventStats;
 
     HandlerThread mHandlerThread;
 
@@ -114,6 +118,13 @@
     }
 
     @Test
+    public void testGetEventStatsReturnsSameInstance() {
+        EventStats stats1 = mTelecomMetricsController.getEventStats();
+        EventStats stats2 = mTelecomMetricsController.getEventStats();
+        assertThat(stats1).isSameInstanceAs(stats2);
+    }
+
+    @Test
     public void testOnPullAtomReturnsPullSkipIfAtomNotRegistered() {
         mTelecomMetricsController.getStats().clear();
 
@@ -143,6 +154,7 @@
         verify(statsManager, times(1)).clearPullAtomCallback(eq(CALL_STATS));
         verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_API_STATS));
         verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_ERROR_STATS));
+        verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_EVENT_STATS));
         assertThat(mTelecomMetricsController.getStats()).isEmpty();
     }
 
@@ -195,5 +207,6 @@
         mTelecomMetricsController.getStats().put(CALL_STATS, mCallStats);
         mTelecomMetricsController.getStats().put(TELECOM_API_STATS, mApiStats);
         mTelecomMetricsController.getStats().put(TELECOM_ERROR_STATS, mErrorStats);
+        mTelecomMetricsController.getStats().put(TELECOM_EVENT_STATS, mEventStats);
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
index 4f7569e..875617f 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
@@ -42,6 +42,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Looper;
+import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.util.StatsEvent;
@@ -55,6 +56,7 @@
 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.EventStats;
 import com.android.server.telecom.nano.PulledAtomsClass;
 
 import org.junit.After;
@@ -104,6 +106,10 @@
     private static final int VALUE_ERROR_ID = 1;
     private static final int VALUE_ERROR_COUNT = 1;
 
+    private static final int VALUE_EVENT_ID = 1;
+    private static final int VALUE_CAUSE_ID = 1;
+    private static final int VALUE_EVENT_COUNT = 1;
+
     @Rule
     public TemporaryFolder mTempFolder = new TemporaryFolder();
     @Mock
@@ -187,6 +193,11 @@
         ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper, false);
 
         verifyTestDataForErrorStats(errorStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+        createTestFileForEventStats(DEFAULT_TIMESTAMPS_MILLIS);
+        EventStats eventStats = new EventStats(mSpyContext, mLooper, false);
+
+        verifyTestDataForEventStats(eventStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
     }
 
     @Test
@@ -681,6 +692,8 @@
         doReturn(cn).when(handle).getComponentName();
         Call call = mock(Call.class);
         doReturn(true).when(call).isIncoming();
+        doReturn(new DisconnectCause(0)).when(call).getDisconnectCause();
+        doReturn(0).when(call).getSimultaneousType();
         doReturn(account).when(call).getPhoneAccountFromHandle();
         doReturn((long) duration).when(call).getAgeMillis();
         doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
@@ -698,7 +711,7 @@
 
         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(fakeUid), eq(duration));
+                eq(fakeUid), eq(0), eq(0), eq(duration));
     }
 
     @Test
@@ -719,6 +732,8 @@
         doReturn(cn).when(handle).getComponentName();
         Call call = mock(Call.class);
         doReturn(true).when(call).isIncoming();
+        doReturn(new DisconnectCause(0)).when(call).getDisconnectCause();
+        doReturn(0).when(call).getSimultaneousType();
         doReturn(account).when(call).getPhoneAccountFromHandle();
         doReturn((long) duration).when(call).getAgeMillis();
         doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
@@ -739,7 +754,7 @@
 
         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(fakeUid), eq(duration));
+                eq(fakeUid), eq(0), eq(0), eq(duration));
     }
 
     @Test
@@ -871,6 +886,94 @@
         verify(mSpyContext, never()).openFileOutput(anyString(), anyInt());
     }
 
+    @Test
+    public void testPullEventStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+        createTestFileForEventStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        final List<StatsEvent> data = new ArrayList<>();
+
+        int result = eventStats.pull(data);
+
+        assertEquals(StatsManager.PULL_SKIP, result);
+        verify(eventStats, never()).onPull(any());
+        assertEquals(data.size(), 0);
+    }
+
+    @Test
+    public void testPullEventStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+        createTestFileForEventStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        final List<StatsEvent> data = new ArrayList<>();
+        int sizePulled = eventStats.mPulledAtoms.telecomEventStats.length;
+
+        int result = eventStats.pull(data);
+
+        assertEquals(StatsManager.PULL_SUCCESS, result);
+        verify(eventStats).onPull(eq(data));
+        assertEquals(data.size(), sizePulled);
+        assertEquals(eventStats.mPulledAtoms.telecomEventStats.length, 0);
+    }
+
+    @Test
+    public void testEventStatsLogCount() throws Exception {
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        EventStats.CriticalEvent event = new EventStats.CriticalEvent(
+                VALUE_EVENT_ID, VALUE_UID, VALUE_CAUSE_ID);
+
+        for (int i = 0; i < 10; i++) {
+            eventStats.log(event);
+            waitForHandlerAction(eventStats, TEST_TIMEOUT);
+
+            verify(eventStats, times(i + 1)).onAggregate();
+            verify(eventStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(eventStats.mPulledAtoms.telecomEventStats.length, 1);
+            verifyMessageForEventStats(eventStats.mPulledAtoms.telecomEventStats[0],
+                    VALUE_EVENT_ID, VALUE_UID, VALUE_CAUSE_ID, i + 1);
+        }
+    }
+
+    @Test
+    public void testEventStatsLogEvent() throws Exception {
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        int[] events = {
+                EventStats.ID_UNKNOWN,
+                EventStats.ID_INIT,
+                EventStats.ID_DEFAULT_DIALER_CHANGED,
+                EventStats.ID_ADD_CALL,
+        };
+        int[] causes = {
+                EventStats.CAUSE_UNKNOWN,
+                EventStats.CAUSE_GENERIC_SUCCESS,
+                EventStats.CAUSE_GENERIC_FAILURE,
+                EventStats.CAUSE_CALL_TRANSACTION_SUCCESS,
+                EventStats.CAUSE_CALL_TRANSACTION_ERROR_UNKNOWN,
+                EventStats.CAUSE_CALL_TRANSACTION_CALL_CANNOT_BE_SET_TO_ACTIVE,
+                EventStats.CAUSE_CALL_TRANSACTION_CALL_IS_NOT_BEING_TRACKED,
+                EventStats.CAUSE_CALL_TRANSACTION_CANNOT_HOLD_CURRENT_ACTIVE_CALL,
+                EventStats.CAUSE_CALL_TRANSACTION_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+                EventStats.CAUSE_CALL_TRANSACTION_OPERATION_TIMED_OUT,
+        };
+        Random rand = new Random();
+        Map<EventStats.CriticalEvent, Integer> eventMap = new HashMap<>();
+
+        for (int i = 0; i < 10; i++) {
+            int e = events[rand.nextInt(events.length)];
+            int uid = rand.nextInt(65535);
+            int cause = causes[rand.nextInt(causes.length)];
+            EventStats.CriticalEvent ce = new EventStats.CriticalEvent(e, uid, cause);
+            eventMap.put(ce, eventMap.getOrDefault(ce, 0) + 1);
+
+            eventStats.log(ce);
+            waitForHandlerAction(eventStats, TEST_TIMEOUT);
+
+            verify(eventStats, times(i + 1)).onAggregate();
+            verify(eventStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(eventStats.mPulledAtoms.telecomEventStats.length, eventMap.size());
+            assertTrue(hasMessageForEventStats(eventStats.mPulledAtoms.telecomEventStats,
+                    e, uid, cause, eventMap.get(ce)));
+        }
+    }
+
     private void createTestFileForApiStats(long timestamps) throws IOException {
         PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
         atom.telecomApiStats =
@@ -1037,8 +1140,8 @@
         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);
+            verifyMessageForErrorStats(atom.telecomErrorStats[i], VALUE_MODULE_ID,
+                    VALUE_ERROR_ID, VALUE_ERROR_COUNT);
         }
     }
 
@@ -1059,4 +1162,53 @@
         }
         return false;
     }
+
+    private void createTestFileForEventStats(long timestamps) throws IOException {
+        PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+        atom.telecomEventStats =
+                new PulledAtomsClass.TelecomEventStats[VALUE_ATOM_COUNT];
+        for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+            atom.telecomEventStats[i] = new PulledAtomsClass.TelecomEventStats();
+            atom.telecomEventStats[i].setEvent(VALUE_EVENT_ID + i);
+            atom.telecomEventStats[i].setUid(VALUE_UID);
+            atom.telecomEventStats[i].setEventCause(VALUE_CAUSE_ID);
+            atom.telecomEventStats[i].setCount(VALUE_EVENT_COUNT);
+        }
+        atom.setTelecomEventStatsPullTimestampMillis(timestamps);
+        FileOutputStream stream = new FileOutputStream(mTempFile);
+        stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+        stream.close();
+    }
+
+    private void verifyTestDataForEventStats(
+            final PulledAtomsClass.PulledAtoms atom, long timestamps) {
+        assertNotNull(atom);
+        assertEquals(atom.getTelecomEventStatsPullTimestampMillis(), timestamps);
+        assertNotNull(atom.telecomEventStats);
+        assertEquals(atom.telecomEventStats.length, VALUE_ATOM_COUNT);
+        for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+            assertNotNull(atom.telecomEventStats[i]);
+            verifyMessageForEventStats(atom.telecomEventStats[i], VALUE_EVENT_ID + i,
+                    VALUE_UID, VALUE_CAUSE_ID, VALUE_EVENT_COUNT);
+        }
+    }
+
+    private void verifyMessageForEventStats(final PulledAtomsClass.TelecomEventStats msg,
+                                            int eventId, int uid, int causeId, int count) {
+        assertEquals(msg.getEvent(), eventId);
+        assertEquals(msg.getUid(), uid);
+        assertEquals(msg.getEventCause(), causeId);
+        assertEquals(msg.getCount(), count);
+    }
+
+    private boolean hasMessageForEventStats(final PulledAtomsClass.TelecomEventStats[] msgs,
+                                            int eventId, int uid, int causeId, int count) {
+        for (PulledAtomsClass.TelecomEventStats msg : msgs) {
+            if (msg.getEvent() == eventId && msg.getUid() == uid
+                    && msg.getEventCause() == causeId && msg.getCount() == count) {
+                return true;
+            }
+        }
+        return false;
+    }
 }