Merge "export broadcast receiver in BlockedNumberActivity" into udc-dev
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 32b5556..e4dc6af 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -3914,6 +3914,10 @@
         return mCallDirection == CALL_DIRECTION_UNKNOWN;
     }
 
+    public boolean isOutgoing() {
+        return mCallDirection == CALL_DIRECTION_OUTGOING;
+    }
+
     /**
      * Determines if this call is in a disconnecting state.
      *
diff --git a/src/com/android/server/telecom/CallAnomalyWatchdog.java b/src/com/android/server/telecom/CallAnomalyWatchdog.java
index f7020f4..045671e 100644
--- a/src/com/android/server/telecom/CallAnomalyWatchdog.java
+++ b/src/com/android/server/telecom/CallAnomalyWatchdog.java
@@ -43,6 +43,8 @@
  * Watchdog class responsible for detecting potential anomalous conditions for {@link Call}s.
  */
 public class CallAnomalyWatchdog extends CallsManagerListenerBase implements Call.Listener {
+    private final EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
+
     /**
      * Class used to track the call state as it pertains to the watchdog. The watchdog cares about
      * both the call state and whether a {@link ConnectionService} has finished creating the
@@ -146,11 +148,13 @@
 
     public CallAnomalyWatchdog(ScheduledExecutorService executorService,
             TelecomSystem.SyncRoot lock,
-            Timeouts.Adapter timeoutAdapter, ClockProxy clockProxy) {
+            Timeouts.Adapter timeoutAdapter, ClockProxy clockProxy,
+            EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger) {
         mScheduledExecutorService = executorService;
         mLock = lock;
         mTimeoutAdapter = timeoutAdapter;
         mClockProxy = clockProxy;
+        mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
     }
 
     /**
@@ -357,6 +361,7 @@
                         mAnomalyReporter.reportAnomaly(
                                 WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_UUID,
                                 WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_MSG);
+                        mEmergencyCallDiagnosticLogger.reportStuckCall(call);
                     } else {
                         mAnomalyReporter.reportAnomaly(
                                 WATCHDOG_DISCONNECTED_STUCK_CALL_UUID,
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index f36ba1b..b9a83e5 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -117,6 +117,7 @@
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
 import com.android.server.telecom.callfiltering.BlockCheckerFilter;
+import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
 import com.android.server.telecom.callfiltering.CallFilterResultCallback;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
 import com.android.server.telecom.callfiltering.CallFilteringResult.Builder;
@@ -124,11 +125,9 @@
 import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
 import com.android.server.telecom.callfiltering.DndCallFilter;
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
-import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
 import com.android.server.telecom.callredirection.CallRedirectionProcessor;
 import com.android.server.telecom.components.ErrorDialogActivity;
 import com.android.server.telecom.components.TelecomBroadcastReceiver;
-import com.android.server.telecom.settings.BlockedNumbersUtil;
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
@@ -440,6 +439,8 @@
     private final RoleManagerAdapter mRoleManagerAdapter;
     private final CallEndpointController mCallEndpointController;
     private final CallAnomalyWatchdog mCallAnomalyWatchdog;
+
+    private final EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
     private final CallStreamingController mCallStreamingController;
     private final BlockedNumbersAdapter mBlockedNumbersAdapter;
     private final TransactionManager mTransactionManager;
@@ -552,7 +553,9 @@
             Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
             Executor asyncTaskExecutor,
             BlockedNumbersAdapter blockedNumbersAdapter,
-            TransactionManager transactionManager) {
+            TransactionManager transactionManager,
+            EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger) {
+
         mContext = context;
         mLock = lock;
         mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
@@ -569,6 +572,7 @@
         mTimeoutsAdapter = timeoutsAdapter;
         mEmergencyCallHelper = emergencyCallHelper;
         mCallerInfoLookupHelper = callerInfoLookupHelper;
+        mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
 
         mDtmfLocalTonePlayer =
                 new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
@@ -653,6 +657,7 @@
         mListeners.add(mProximitySensorManager);
         mListeners.add(audioProcessingNotification);
         mListeners.add(callAnomalyWatchdog);
+        mListeners.add(mEmergencyCallDiagnosticLogger);
         mListeners.add(mCallStreamingController);
 
         // this needs to be after the mCallAudioManager
@@ -1277,6 +1282,10 @@
         return mEmergencyCallHelper;
     }
 
+    EmergencyCallDiagnosticLogger getEmergencyCallDiagnosticLogger() {
+        return mEmergencyCallDiagnosticLogger;
+    }
+
     public DefaultDialerCache getDefaultDialerCache() {
         return mDefaultDialerCache;
     }
@@ -3178,7 +3187,10 @@
                         isEmergency);
         // Only one SIM PhoneAccount can be active at one time for DSDS. Only that SIM PhoneAccount
         // should be available if a call is already active on the SIM account.
-        if (!isDsdaCallingPossible()) {
+        // Similarly, the emergency call should be attempted over the same PhoneAccount as the
+        // ongoing call. However, if the ongoing call is over cross-SIM registration, then the
+        // emergency call will be attempted over a different Phone object at a later stage.
+        if (isEmergency || !isDsdaCallingPossible()) {
             List<PhoneAccountHandle> simAccounts =
                     mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser();
             PhoneAccountHandle ongoingCallAccount = null;
@@ -5378,7 +5390,7 @@
      *
      * @param pw The {@code IndentingPrintWriter} to write the state to.
      */
-    public void dump(IndentingPrintWriter pw) {
+    public void dump(IndentingPrintWriter pw, String[] args) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
         if (mCalls != null) {
             pw.println("mCalls: ");
@@ -5440,6 +5452,14 @@
             mCallAnomalyWatchdog.dump(pw);
             pw.decreaseIndent();
         }
+
+        if (mEmergencyCallDiagnosticLogger != null) {
+            pw.println("mEmergencyCallDiagnosticLogger:");
+            pw.increaseIndent();
+            mEmergencyCallDiagnosticLogger.dump(pw, args);
+            pw.decreaseIndent();
+        }
+
         if (mDefaultDialerCache != null) {
             pw.println("mDefaultDialerCache:");
             pw.increaseIndent();
diff --git a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
new file mode 100644
index 0000000..af79da3
--- /dev/null
+++ b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+
+import android.os.BugreportManager;
+import android.os.DropBoxManager;
+import android.provider.DeviceConfig;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.telephony.TelephonyManager;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * The EmergencyCallDiagnosticsLogger monitors information required to diagnose potential outgoing
+ * ecall failures on the device. When a potential failure is detected, it calls a Telephony API to
+ * persist relevant information (dumpsys, logcat etc.) to the dropbox. This acts as a central place
+ * to determine when and what to collect.
+ *
+ * <p>When a bugreport is triggered, this module will read the dropbox entries and add them to the
+ * telecom dump.
+ */
+public class EmergencyCallDiagnosticLogger extends CallsManagerListenerBase
+        implements Call.Listener {
+
+    public static final int REPORT_REASON_RANGE_START = -1; //!!DO NOT CHANGE
+    public static final int REPORT_REASON_RANGE_END = 5; //increment this and add new reason above
+    public static final int COLLECTION_TYPE_BUGREPORT = 10;
+    public static final int COLLECTION_TYPE_TELECOM_STATE = 11;
+    public static final int COLLECTION_TYPE_TELEPHONY_STATE = 12;
+    public static final int COLLECTION_TYPE_LOGCAT_BUFFERS = 13;
+    private static final int REPORT_REASON_STUCK_CALL_DETECTED = 0;
+    private static final int REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY = 1;
+    private static final int REPORT_REASON_CALL_FAILED = 2;
+    private static final int REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED = 3;
+    private static final int REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE = 4;
+    private static final String DROPBOX_TAG = "ecall_diagnostic_data";
+    private static final String ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+            "enable_bugreport_collection_for_emergency_call_diagnostics";
+    private static final String ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+            "enable_telecom_dump_collection_for_emergency_call_diagnostics";
+
+    private static final String ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+            "enable_logcat_collection_for_emergency_call_diagnostics";
+    private static final String ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+            "enable_telephony_dump_collection_for_emergency_call_diagnostics";
+
+    private static final String DUMPSYS_ARG_FOR_DIAGNOSTICS = "EmergencyDiagnostics";
+
+    // max text size to read from dropbox entry
+    private static final int DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY = 500000;
+    private static final String MAX_BYTES_PER_DROP_BOX_ENTRY = "max_bytes_per_dropbox_entry";
+    private static final int MAX_DROPBOX_ENTRIES_TO_DUMP = 6;
+
+    private final Timeouts.Adapter mTimeoutAdapter;
+    // This map holds all calls, but keeps pruning non-emergency calls when we can determine it
+    private final Map<Call, CallEventTimestamps> mEmergencyCallsMap = new ConcurrentHashMap<>(2);
+    private final DropBoxManager mDropBoxManager;
+    private final LocalLog mLocalLog = new LocalLog(10);
+    private final TelephonyManager mTelephonyManager;
+    private final BugreportManager mBugreportManager;
+    private final Executor mAsyncTaskExecutor;
+    private final ClockProxy mClockProxy;
+
+    public EmergencyCallDiagnosticLogger(
+            TelephonyManager tm,
+            BugreportManager brm,
+            Timeouts.Adapter timeoutAdapter, DropBoxManager dropBoxManager,
+            Executor asyncTaskExecutor, ClockProxy clockProxy) {
+        mTimeoutAdapter = timeoutAdapter;
+        mDropBoxManager = dropBoxManager;
+        mTelephonyManager = tm;
+        mBugreportManager = brm;
+        mAsyncTaskExecutor = asyncTaskExecutor;
+        mClockProxy = clockProxy;
+    }
+
+    // this calculates time from ACTIVE --> removed
+    private static long getCallTimeInActiveStateSec(CallEventTimestamps ts) {
+        if (ts.getCallActiveTime() == 0 || ts.getCallRemovedTime() == 0) {
+            return 0;
+        } else {
+            return (ts.getCallRemovedTime() - ts.getCallActiveTime()) / 1000;
+        }
+    }
+
+    // this calculates time from call created --> removed
+    private static long getTotalCallTimeSec(CallEventTimestamps ts) {
+        if (ts.getCallRemovedTime() == 0 || ts.getCallCreatedTime() == 0) {
+            return 0;
+        } else {
+            return (ts.getCallRemovedTime() - ts.getCallCreatedTime()) / 1000;
+        }
+    }
+
+    //determines what to collect based on fail reason
+    //if COLLECTION_TYPE_BUGREPORT is present in the returned list, then that
+    //should be the only collection type in the list
+    @VisibleForTesting
+    public static List<Integer> getDataCollectionTypes(int reason) {
+        switch (reason) {
+            case REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE:
+                return Arrays.asList(COLLECTION_TYPE_TELECOM_STATE);
+            case REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED:
+                return Arrays.asList(
+                        COLLECTION_TYPE_TELECOM_STATE, COLLECTION_TYPE_TELEPHONY_STATE);
+            case REPORT_REASON_CALL_FAILED:
+            case REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY:
+            case REPORT_REASON_STUCK_CALL_DETECTED:
+                return Arrays.asList(
+                        COLLECTION_TYPE_TELECOM_STATE,
+                        COLLECTION_TYPE_TELEPHONY_STATE,
+                        COLLECTION_TYPE_LOGCAT_BUFFERS);
+            default:
+        }
+        return new ArrayList<>();
+    }
+
+    private int getMaxBytesPerDropboxEntry() {
+        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
+                MAX_BYTES_PER_DROP_BOX_ENTRY, DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY);
+    }
+
+    @VisibleForTesting
+    public Map<Call, CallEventTimestamps> getEmergencyCallsMap() {
+        return mEmergencyCallsMap;
+    }
+
+    private void triggerDiagnosticsCollection(Call call, int reason) {
+        Log.i(this, "Triggering diagnostics for call %s reason: %d", call.getId(), reason);
+        List<Integer> dataCollectionTypes = getDataCollectionTypes(reason);
+        boolean invokeTelephonyPersistApi = false;
+        CallEventTimestamps ts = mEmergencyCallsMap.get(call);
+        EmergencyCallDiagnosticParams dp =
+                new EmergencyCallDiagnosticParams();
+        for (Integer dataCollectionType : dataCollectionTypes) {
+            switch (dataCollectionType) {
+                case COLLECTION_TYPE_TELECOM_STATE:
+                    if (isTelecomDumpCollectionEnabled()) {
+                        dp.setTelecomDumpSysCollection(true);
+                        invokeTelephonyPersistApi = true;
+                    }
+                    break;
+                case COLLECTION_TYPE_TELEPHONY_STATE:
+                    if (isTelephonyDumpCollectionEnabled()) {
+                        dp.setTelephonyDumpSysCollection(true);
+                        invokeTelephonyPersistApi = true;
+                    }
+                    break;
+                case COLLECTION_TYPE_LOGCAT_BUFFERS:
+                    if (isLogcatCollectionEnabled()) {
+                        dp.setLogcatCollection(true, ts.getCallCreatedTime());
+                        invokeTelephonyPersistApi = true;
+                    }
+                    break;
+                case COLLECTION_TYPE_BUGREPORT:
+                    if (isBugreportCollectionEnabled()) {
+                        mAsyncTaskExecutor.execute(new Runnable() {
+                            @Override
+                            public void run() {
+                                persistBugreport();
+                            }
+                        });
+                    }
+                    break;
+                default:
+            }
+        }
+        if (invokeTelephonyPersistApi) {
+            mAsyncTaskExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    Log.i(this, "Requesting Telephony to persist data %s", dp.toString());
+                    try {
+                        mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, dp);
+                    } catch (Exception e) {
+                        Log.w(this,
+                                "Exception while invoking "
+                                        + "Telephony#persistEmergencyCallDiagnosticData  %s",
+                                e.toString());
+                    }
+                }
+            });
+        }
+    }
+
+    private boolean isBugreportCollectionEnabled() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_TELEPHONY,
+                ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+                false);
+    }
+
+    private boolean isTelecomDumpCollectionEnabled() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_TELEPHONY,
+                ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+                true);
+    }
+
+    private boolean isLogcatCollectionEnabled() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_TELEPHONY,
+                ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+                true);
+    }
+
+    private boolean isTelephonyDumpCollectionEnabled() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_TELEPHONY,
+                ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+                true);
+    }
+
+    private void persistBugreport() {
+        if (isBugreportCollectionEnabled()) {
+            // TODO:
+        }
+    }
+
+    private boolean shouldTrackCall(Call call) {
+        return (call != null && call.isEmergencyCall() && call.isOutgoing());
+    }
+
+    public void reportStuckCall(Call call) {
+        if (shouldTrackCall(call)) {
+            Log.i(this, "Triggering diagnostics for stuck call %s", call.getId());
+            triggerDiagnosticsCollection(call, REPORT_REASON_STUCK_CALL_DETECTED);
+            call.removeListener(this);
+            mEmergencyCallsMap.remove(call);
+        }
+    }
+
+    @Override
+    public void onStartCreateConnection(Call call) {
+        if (shouldTrackCall(call)) {
+            long currentTime = mClockProxy.currentTimeMillis();
+            call.addListener(this);
+            Log.i(this, "Tracking call %s timestamp: %d", call.getId(), currentTime);
+            mEmergencyCallsMap.put(call, new CallEventTimestamps(currentTime));
+        }
+    }
+
+    @Override
+    public void onCreateConnectionFailed(Call call) {
+        if (shouldTrackCall(call)) {
+            Log.i(this, "Triggering diagnostics for  call %s that was never added", call.getId());
+            triggerDiagnosticsCollection(call, REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED);
+            call.removeListener(this);
+            mEmergencyCallsMap.remove(call);
+        }
+    }
+
+    /**
+     * Override of {@link CallsManagerListenerBase} to track when calls are removed
+     *
+     * @param call the call
+     */
+    @Override
+    public void onCallRemoved(Call call) {
+        if (call != null && (mEmergencyCallsMap.get(call) != null)) {
+            call.removeListener(this);
+
+            CallEventTimestamps ts = mEmergencyCallsMap.get(call);
+            long currentTime = mClockProxy.currentTimeMillis();
+            ts.setCallRemovedTime(currentTime);
+
+            maybeTriggerDiagnosticsCollection(call, ts);
+            mEmergencyCallsMap.remove(call);
+        }
+    }
+
+    // !NOTE!: this method should only be called after we get onCallRemoved
+    private void maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts) {
+        Log.i(this, "Evaluating emergency call for diagnostic logging: %s", removedCall.getId());
+        boolean wentActive = (ts.getCallActiveTime() != 0);
+        long callActiveTimeSec = (wentActive ? getCallTimeInActiveStateSec(ts) : 0);
+        long timeSinceCallCreatedSec = getTotalCallTimeSec(ts);
+        int dc = removedCall.getDisconnectCause().getCode();
+
+        if (wentActive) {
+            if (callActiveTimeSec
+                    < mTimeoutAdapter.getEmergencyCallActiveTimeThresholdMillis() / 1000) {
+                // call connected but did not go on for long
+                triggerDiagnosticsCollection(
+                        removedCall, REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE);
+            }
+        } else {
+
+            if (dc == DisconnectCause.LOCAL
+                    && timeSinceCallCreatedSec
+                    > mTimeoutAdapter.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis()
+                    / 1000) {
+                // call was disconnected by the user (but not immediately)
+                triggerDiagnosticsCollection(
+                        removedCall, REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY);
+            } else if (dc != DisconnectCause.LOCAL) {
+                // this can be a case for a full bugreport
+                triggerDiagnosticsCollection(removedCall, REPORT_REASON_CALL_FAILED);
+            }
+        }
+    }
+
+    /**
+     * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track
+     * call state changes.
+     *
+     * @param call     the call
+     * @param oldState its old state
+     * @param newState the new state
+     */
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+
+        if (call != null && mEmergencyCallsMap.get(call) != null && newState == CallState.ACTIVE) {
+            CallEventTimestamps ts = mEmergencyCallsMap.get(call);
+            if (ts != null) {
+                long currentTime = mClockProxy.currentTimeMillis();
+                ts.setCallActiveTime(currentTime);
+            }
+        }
+    }
+
+    private void dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw) {
+        pw.increaseIndent();
+        pw.println("PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
+        int totalEntriesDumped = 0;
+        long currentTime = mClockProxy.currentTimeMillis();
+        long entriesAfterTime =
+                currentTime - (mTimeoutAdapter.getDaysBackToSearchEmergencyDiagnosticEntries() * 24
+                        * 60L * 60L * 1000L);
+        Log.i(this, "current time: %d entriesafter: %d", currentTime, entriesAfterTime);
+        DropBoxManager.Entry entry;
+        entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entriesAfterTime);
+        while (entry != null) {
+            Log.i(this, "found entry with ts: %d", entry.getTimeMillis());
+            String content[] = entry.getText(getMaxBytesPerDropboxEntry()).split(
+                    System.lineSeparator());
+            long entryTime = entry.getTimeMillis();
+            if (content != null) {
+                pw.increaseIndent();
+                pw.println("------------BEGIN ENTRY (" + entryTime + ")--------");
+                for (String line : content) {
+                    pw.println(line);
+                }
+                pw.println("--------END ENTRY--------");
+                pw.decreaseIndent();
+                totalEntriesDumped++;
+            }
+            entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entryTime);
+            if (totalEntriesDumped > MAX_DROPBOX_ENTRIES_TO_DUMP) {
+                /*
+                Since Emergency calls are a rare/once in a lifetime time occurrence for most users,
+                we should not be seeing too many entries. This code just guards against edge case
+                like load testing, b2b failures etc. We may accumulate a lot of dropbox entries in
+                such cases, but we limit to dumping only MAX_DROPBOX_ENTRIES_TO_DUMP in the
+                bugreport
+
+                The Dropbox API in its current state does not allow to query Entries in reverse
+                chronological order efficiently.
+                 */
+
+                Log.i(this, "Skipping dump for remaining entries. dumped :%d", totalEntriesDumped);
+                break;
+            }
+        }
+        pw.println("END OF PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
+        pw.decreaseIndent();
+    }
+
+    public void dump(IndentingPrintWriter pw, String[] args) {
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+        if (args != null && args.length > 0 && args[0].equals(DUMPSYS_ARG_FOR_DIAGNOSTICS)) {
+            //dont read dropbox entries since this dump is triggered by telephony for diagnostics
+            Log.i(this, "skipped dumping diagnostic data");
+            return;
+        }
+        dumpDiagnosticDataFromDropbox(pw);
+    }
+
+    private static class CallEventTimestamps {
+
+        private final long mCallCreatedTime;
+        private long mCallActiveTime;
+        private long mCallRemovedTime;
+
+        public CallEventTimestamps(long createdTime) {
+            mCallCreatedTime = createdTime;
+        }
+
+        public long getCallActiveTime() {
+            return mCallActiveTime;
+        }
+
+        public void setCallActiveTime(long callActiveTime) {
+            this.mCallActiveTime = callActiveTime;
+        }
+
+        public long getCallCreatedTime() {
+            return mCallCreatedTime;
+        }
+
+        public long getCallRemovedTime() {
+            return mCallRemovedTime;
+        }
+
+        public void setCallRemovedTime(long callRemovedTime) {
+            this.mCallRemovedTime = callRemovedTime;
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 253f5c5..c5f50e5 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.CALL_PHONE;
 import static android.Manifest.permission.CALL_PRIVILEGED;
 import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.MANAGE_OWN_CALLS;
 import static android.Manifest.permission.MODIFY_PHONE_STATE;
 import static android.Manifest.permission.READ_PHONE_NUMBERS;
 import static android.Manifest.permission.READ_PHONE_STATE;
@@ -26,11 +27,10 @@
 import static android.Manifest.permission.READ_SMS;
 import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static android.Manifest.permission.MANAGE_OWN_CALLS;
 import static android.telecom.CallAttributes.DIRECTION_INCOMING;
 import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
-import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 
 import android.Manifest;
 import android.app.ActivityManager;
@@ -58,7 +58,6 @@
 import android.provider.BlockedNumberContract;
 import android.provider.Settings;
 import android.telecom.CallAttributes;
-
 import android.telecom.CallException;
 import android.telecom.Log;
 import android.telecom.PhoneAccount;
@@ -1929,19 +1928,21 @@
             }
 
 
-            if (args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(args[0])) {
+            if (args != null && args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(
+                    args[0])) {
                 Binder.withCleanCallingIdentity(() ->
                         Analytics.dumpToEncodedProto(mContext, writer, args));
                 return;
             }
 
-            boolean isTimeLineView = (args.length > 0 && TIME_LINE_ARG.equalsIgnoreCase(args[0]));
+            boolean isTimeLineView =
+                    (args != null && args.length > 0 && TIME_LINE_ARG.equalsIgnoreCase(args[0]));
 
             final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
             if (mCallsManager != null) {
                 pw.println("CallsManager: ");
                 pw.increaseIndent();
-                mCallsManager.dump(pw);
+                mCallsManager.dump(pw, args);
                 pw.decreaseIndent();
 
                 pw.println("PhoneAccountRegistrar: ");
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 8984b2b..8477d49 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -26,11 +26,14 @@
 import android.content.ServiceConnection;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.os.BugreportManager;
+import android.os.DropBoxManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.AnomalyReporter;
+import android.telephony.TelephonyManager;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
@@ -336,11 +339,17 @@
                 }
             };
 
+            EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger =
+                    new EmergencyCallDiagnosticLogger(mContext.getSystemService(
+                            TelephonyManager.class), mContext.getSystemService(
+                            BugreportManager.class), timeoutsAdapter, mContext.getSystemService(
+                            DropBoxManager.class), asyncTaskExecutor, clockProxy);
+
             CallAnomalyWatchdog callAnomalyWatchdog = new CallAnomalyWatchdog(
                     Executors.newSingleThreadScheduledExecutor(),
-                    mLock, timeoutsAdapter, clockProxy);
-            TransactionManager transactionManager = TransactionManager.getInstance();
+                    mLock, timeoutsAdapter, clockProxy, emergencyCallDiagnosticLogger);
 
+            TransactionManager transactionManager = TransactionManager.getInstance();
             mCallsManager = new CallsManager(
                     mContext,
                     mLock,
@@ -376,7 +385,9 @@
                     accessibilityManagerAdapter,
                     asyncTaskExecutor,
                     blockedNumbersAdapter,
-                    transactionManager);
+                    transactionManager,
+                    emergencyCallDiagnosticLogger);
+
             mIncomingCallNotifier = incomingCallNotifier;
             incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
                 @Override
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 39c86ad..1ba06ec 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -20,8 +20,8 @@
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.telecom.CallDiagnosticService;
-import android.telecom.CallRedirectionService;
 import android.telecom.CallDiagnostics;
+import android.telecom.CallRedirectionService;
 import android.telephony.ims.ImsReasonInfo;
 
 import java.util.concurrent.TimeUnit;
@@ -112,12 +112,46 @@
         public long getNonVoipEmergencyCallIntermediateStateTimeoutMillis() {
             return Timeouts.getNonVoipEmergencyCallIntermediateStateTimeoutMillis();
         }
+
+        public long getEmergencyCallTimeBeforeUserDisconnectThresholdMillis(){
+            return Timeouts.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis();
+        }
+
+        public long getEmergencyCallActiveTimeThresholdMillis(){
+            return Timeouts.getEmergencyCallActiveTimeThresholdMillis();
+        }
+
+        public int getDaysBackToSearchEmergencyDiagnosticEntries(){
+            return Timeouts.getDaysBackToSearchEmergencyDiagnosticEntries();
+
+        }
     }
 
     /** A prefix to use for all keys so to not clobber the global namespace. */
     private static final String PREFIX = "telecom.";
 
     /**
+     * threshold used to filter out ecalls that the user may have dialed by mistake
+     * It is used only when the disconnect cause is LOCAL by EmergencyDiagnosticLogger
+     */
+    private static final String EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS =
+            "emergency_call_time_before_user_disconnect_threshold_millis";
+
+    /**
+     * Returns the threshold used to detect ecalls that transition to active but only for a very
+     * short duration. These short duration active calls can result in Diagnostic data collection.
+     */
+    private static final String EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS =
+            "emergency_call_active_time_threshold_millis";
+
+    /**
+     * Time in Days that is used to filter out old dropbox entries for emergency call diagnostic
+     * data. Entries older than this are ignored
+     */
+    private static final String DAYS_BACK_TO_SEARCH_EMERGENCY_DROP_BOX_ENTRIES =
+            "days_back_to_search_emergency_drop_box_entries";
+
+    /**
      * A prefix to use for {@link DeviceConfig} for the transitory state timeout of
      * VoIP Call, in millis.
      */
@@ -335,6 +369,36 @@
                 TRANSITORY_STATE_VOIP_NORMAL_TIMEOUT_MILLIS, 5000L);
     }
 
+
+    /**
+     * Returns the threshold used to filter out ecalls that the user may have dialed by mistake
+     * It is used only when the disconnect cause is LOCAL by EmergencyDiagnosticLogger
+     * @return the threshold in milliseconds
+     */
+    public static long getEmergencyCallTimeBeforeUserDisconnectThresholdMillis() {
+        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+                EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS, 20000L);
+    }
+
+    /**
+     * Returns the threshold used to detect ecalls that transition to active but only for a very
+     * short duration. These short duration active calls can result in Diagnostic data collection.
+     * @return the threshold in milliseconds
+     */
+    public static long getEmergencyCallActiveTimeThresholdMillis() {
+        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+                EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS, 15000L);
+    }
+
+    /**
+     * Time in Days that is used to filter out old dropbox entries for emergency call diagnostic
+     * data. Entries older than this are ignored
+     */
+    public static int getDaysBackToSearchEmergencyDiagnosticEntries() {
+        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
+                DAYS_BACK_TO_SEARCH_EMERGENCY_DROP_BOX_ENTRIES, 30);
+    }
+
     /**
      * Returns the duration of time an emergency VoIP call can be in a transitory state before
      * Telecom will try to clean up the call.
diff --git a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
index 531d194..7e197fe 100644
--- a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
@@ -39,6 +39,7 @@
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.EmergencyCallDiagnosticLogger;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
@@ -87,6 +88,8 @@
     @Mock private ConnectionServiceWrapper mMockConnectionService;
     @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
 
+    @Mock private EmergencyCallDiagnosticLogger mMockEmergencyCallDiagnosticLogger;
+
     @Override
     @Before
     public void setUp() throws Exception {
@@ -118,7 +121,7 @@
         doReturn(new ComponentName(mContext, CallTest.class))
                 .when(mMockConnectionService).getComponentName();
         mCallAnomalyWatchdog = new CallAnomalyWatchdog(mTestScheduledExecutorService, mLock,
-                mTimeouts, mMockClockProxy);
+                mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger);
         mCallAnomalyWatchdog.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index c467671..7ea3568 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -59,17 +59,18 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.BlockedNumberContract;
-import android.telecom.CallerInfo;
+import android.telecom.CallException;
 import android.telecom.CallScreeningService;
+import android.telecom.CallerInfo;
 import android.telecom.Connection;
 import android.telecom.DisconnectCause;
 import android.telecom.GatewayInfo;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
-import android.telecom.CallException;
 import android.telecom.VideoProfile;
 import android.telephony.CarrierConfigManager;
+import android.telephony.PhoneCapability;
 import android.telephony.TelephonyManager;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -83,9 +84,9 @@
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.CallDiagnosticServiceController;
 import com.android.server.telecom.CallEndpointController;
 import com.android.server.telecom.CallEndpointControllerFactory;
-import com.android.server.telecom.CallDiagnosticServiceController;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManager;
@@ -94,6 +95,7 @@
 import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
 import com.android.server.telecom.ConnectionServiceWrapper;
 import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.EmergencyCallDiagnosticLogger;
 import com.android.server.telecom.EmergencyCallHelper;
 import com.android.server.telecom.HandoverState;
 import com.android.server.telecom.HeadsetMediaButton;
@@ -116,8 +118,8 @@
 import com.android.server.telecom.WiredHeadsetManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
-import com.android.server.telecom.callfiltering.CallFilteringResult;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.callfiltering.CallFilteringResult;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
@@ -237,9 +239,12 @@
     @Mock private ToastFactory mToastFactory;
     @Mock private Toast mToast;
     @Mock private CallAnomalyWatchdog mCallAnomalyWatchdog;
+
+    @Mock private EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
     @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
     @Mock private Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
     @Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
+    @Mock private PhoneCapability mPhoneCapability;
 
     private CallsManager mCallsManager;
 
@@ -306,7 +311,8 @@
                 // Just do async tasks synchronously to support testing.
                 command -> command.run(),
                 mBlockedNumbersAdapter,
-                TransactionManager.getTestInstance());
+                TransactionManager.getTestInstance(),
+                mEmergencyCallDiagnosticLogger);
 
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -331,6 +337,26 @@
         assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null, false, false).size());
     }
 
+    private Call constructOngoingCall(String callId, PhoneAccountHandle phoneAccountHandle) {
+        Call ongoingCall = new Call(
+                callId,
+                mContext,
+                mCallsManager,
+                mLock,
+                null /* ConnectionServiceRepository */,
+                mPhoneNumberUtilsAdapter,
+                TEST_ADDRESS,
+                null /* GatewayInfo */,
+                null /* connectionManagerPhoneAccountHandle */,
+                phoneAccountHandle,
+                Call.CALL_DIRECTION_INCOMING,
+                false /* shouldAttachToExistingConnection*/,
+                false /* isConference */,
+                mClockProxy,
+                mToastFactory);
+        ongoingCall.setState(CallState.ACTIVE, "just cuz");
+        return ongoingCall;
+    }
     /**
      * Verify behavior for multisim devices where we want to ensure that the active sim is used for
      * placing a new call.
@@ -341,23 +367,7 @@
     public void testConstructPossiblePhoneAccountsMultiSimActive() throws Exception {
         setupMsimAccounts();
 
-        Call ongoingCall = new Call(
-                "1", /* callId */
-                mContext,
-                mCallsManager,
-                mLock,
-                null /* ConnectionServiceRepository */,
-                mPhoneNumberUtilsAdapter,
-                TEST_ADDRESS,
-                null /* GatewayInfo */,
-                null /* connectionManagerPhoneAccountHandle */,
-                SIM_2_HANDLE,
-                Call.CALL_DIRECTION_INCOMING,
-                false /* shouldAttachToExistingConnection*/,
-                false /* isConference */,
-                mClockProxy,
-                mToastFactory);
-        ongoingCall.setState(CallState.ACTIVE, "just cuz");
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
         mCallsManager.addCall(ongoingCall);
 
         List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
@@ -380,6 +390,47 @@
         assertEquals(2, phoneAccountHandles.size());
     }
 
+    /**
+     * For DSDA-enabled multisim devices with an ongoing call, verify that both SIMs'
+     * PhoneAccountHandles are constructed while placing a new call.
+     * @throws Exception
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible() throws
+            Exception {
+        setupMsimAccounts();
+        setMaxActiveVoiceSubscriptions(2);
+
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false);
+        assertEquals(2, phoneAccountHandles.size());
+    }
+
+    /**
+     * For DSDA-enabled multisim devices with an ongoing call, verify that only the active SIMs'
+     * PhoneAccountHandle is constructed while placing an emergency call.
+     * @throws Exception
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible_emergencyCall()
+            throws Exception {
+        setupMsimAccounts();
+        setMaxActiveVoiceSubscriptions(2);
+
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, true /* isEmergency */);
+        assertEquals(1, phoneAccountHandles.size());
+        assertEquals(SIM_2_HANDLE, phoneAccountHandles.get(0));
+    }
+
     private void setupCallerInfoLookupHelper() {
         doAnswer(invocation -> {
             Uri handle = invocation.getArgument(0);
@@ -2862,4 +2913,10 @@
         when(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser()).thenReturn(
                 new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
     }
+
+    private void setMaxActiveVoiceSubscriptions(int num) {
+        TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
+        when(mockTelephonyManager.getPhoneCapability()).thenReturn(mPhoneCapability);
+        when(mPhoneCapability.getMaxActiveVoiceSubscriptions()).thenReturn(num);
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 8ff882b..49ad6bb 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -55,7 +55,9 @@
 import android.location.LocationManager;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.os.BugreportManager;
 import android.os.Bundle;
+import android.os.DropBoxManager;
 import android.os.Handler;
 import android.os.IInterface;
 import android.os.PersistableBundle;
@@ -275,8 +277,12 @@
                 return Context.NOTIFICATION_SERVICE;
             } else if (svcClass == AccessibilityManager.class) {
                 return Context.ACCESSIBILITY_SERVICE;
+            } else if (svcClass == DropBoxManager.class) {
+                return Context.DROPBOX_SERVICE;
+            } else if (svcClass == BugreportManager.class) {
+                return Context.BUGREPORT_SERVICE;
             }
-            throw new UnsupportedOperationException();
+            throw new UnsupportedOperationException(svcClass.getName());
         }
 
         @Override
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
new file mode 100644
index 0000000..3cb8196
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+
+import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.BugreportManager;
+import android.os.DropBoxManager;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallerInfoLookupHelper;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.EmergencyCallDiagnosticLogger;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.ui.ToastFactory;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class EmergencyCallDiagnosticLoggerTest extends TelecomTestCase {
+
+    private static final ComponentName COMPONENT_NAME_1 = ComponentName
+            .unflattenFromString("com.foo/.Blah");
+    private static final PhoneAccountHandle SIM_1_HANDLE = new PhoneAccountHandle(
+            COMPONENT_NAME_1, "Sim1");
+    private static final PhoneAccount SIM_1_ACCOUNT = new PhoneAccount.
+            Builder(SIM_1_HANDLE, "Sim1")
+            .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                    | PhoneAccount.CAPABILITY_CALL_PROVIDER)
+            .setIsEnabled(true)
+            .build();
+    private static final String DROP_BOX_TAG = "ecall_diagnostic_data";
+
+    private static final long EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS = 100L;
+
+    private static final long EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS = 120L;
+
+    private static final int DAYS_BACK_TO_SEARCH_EMERGENCY_DIAGNOSTIC_ENTRIES = 1;
+    private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {
+    };
+    EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
+    @Mock
+    private Timeouts.Adapter mTimeouts;
+    @Mock
+    private CallsManager mMockCallsManager;
+    @Mock
+    private CallerInfoLookupHelper mMockCallerInfoLookupHelper;
+    @Mock
+    private PhoneAccountRegistrar mMockPhoneAccountRegistrar;
+    @Mock
+    private ClockProxy mMockClockProxy;
+    @Mock
+    private ToastFactory mMockToastProxy;
+    @Mock
+    private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
+
+    @Mock
+    private TelephonyManager mTm;
+    @Mock
+    private BugreportManager mBrm;
+    @Mock
+    private DropBoxManager mDbm;
+
+    @Mock
+    private ClockProxy mClockProxy;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        doReturn(mMockCallerInfoLookupHelper).when(mMockCallsManager).getCallerInfoLookupHelper();
+        doReturn(mMockPhoneAccountRegistrar).when(mMockCallsManager).getPhoneAccountRegistrar();
+        doReturn(SIM_1_ACCOUNT).when(mMockPhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(SIM_1_HANDLE));
+        when(mTimeouts.getEmergencyCallActiveTimeThresholdMillis()).
+                thenReturn(EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS);
+        when(mTimeouts.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis()).
+                thenReturn(EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS);
+        when(mTimeouts.getDaysBackToSearchEmergencyDiagnosticEntries()).
+                thenReturn(DAYS_BACK_TO_SEARCH_EMERGENCY_DIAGNOSTIC_ENTRIES);
+        when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
+
+        mEmergencyCallDiagnosticLogger = new EmergencyCallDiagnosticLogger(mTm, mBrm,
+                mTimeouts, mDbm, Runnable::run, mClockProxy);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+        //reset(mTm);
+    }
+
+    /**
+     * Helper function that creates the call being tested.
+     * Also invokes onStartCreateConnection
+     */
+    private Call createCall(boolean isEmergencyCall, int direction) {
+        Call call = getCall();
+        call.setCallDirection(direction);
+        call.setIsEmergencyCall(isEmergencyCall);
+        mEmergencyCallDiagnosticLogger.onStartCreateConnection(call);
+        return call;
+    }
+
+    /**
+     * @return an instance of {@link Call} for testing purposes.
+     */
+    private Call getCall() {
+        return new Call(
+                "1", /* callId */
+                mContext,
+                mMockCallsManager,
+                mLock,
+                null /* ConnectionServiceRepository */,
+                mMockPhoneNumberUtilsAdapter,
+                Uri.parse("tel:6505551212"),
+                null /* GatewayInfo */,
+                null /* connectionManagerPhoneAccountHandle */,
+                SIM_1_HANDLE,
+                Call.CALL_DIRECTION_OUTGOING,
+                false /* shouldAttachToExistingConnection*/,
+                false /* isConference */,
+                mMockClockProxy,
+                mMockToastProxy);
+    }
+
+    /**
+     * Test that only outgoing emergency calls are tracked
+     */
+    @Test
+    public void testNonEmergencyCallNotTracked() {
+        //should not be tracked
+        createCall(false, Call.CALL_DIRECTION_OUTGOING);
+        assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+
+        //should not be tracked (not in scope)
+        createCall(false, Call.CALL_DIRECTION_INCOMING);
+        assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+    }
+
+    /**
+     * Test that incoming emergency calls are not tracked (not in scope right now)
+     */
+    @Test
+    public void testIncomingEmergencyCallsNotTracked() {
+        //should not be tracked
+        createCall(true, Call.CALL_DIRECTION_INCOMING);
+        assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+    }
+
+
+    /**
+     * Test getDataCollectionTypes(reason)
+     */
+    @Test
+    public void testCollectionTypeForReasonDoesNotReturnUnreasonableValues() {
+        int reason = EmergencyCallDiagnosticLogger.REPORT_REASON_RANGE_START + 1;
+        while (reason < EmergencyCallDiagnosticLogger.REPORT_REASON_RANGE_END) {
+            List<Integer> ctypes = EmergencyCallDiagnosticLogger.getDataCollectionTypes(reason);
+            assertNotNull(ctypes);
+            Set<Integer> ctypesSet = new HashSet<>(ctypes);
+
+            //assert that list is not empty
+            assertNotEquals(0, ctypes.size());
+
+            //assert no repeated values
+            assertEquals(ctypes.size(), ctypesSet.size());
+
+            //if bugreport type is present, that should be the only collection type
+            if (ctypesSet.contains(EmergencyCallDiagnosticLogger.COLLECTION_TYPE_BUGREPORT)) {
+                assertEquals(1, ctypes.size());
+            }
+            reason++;
+        }
+    }
+
+
+    /**
+     * Test emergency call reported stuck
+     */
+    @Test
+    public void testStuckEmergencyCall() {
+        Call call = createCall(true, Call.CALL_DIRECTION_OUTGOING);
+        mEmergencyCallDiagnosticLogger.onCallAdded(call);
+        mEmergencyCallDiagnosticLogger.reportStuckCall(call);
+
+        //for stuck calls, we should always be persisting some data
+        ArgumentCaptor<EmergencyCallDiagnosticParams> captor =
+                ArgumentCaptor.forClass(EmergencyCallDiagnosticParams.class);
+        verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
+                captor.capture());
+        EmergencyCallDiagnosticParams dp = captor.getValue();
+
+        assertNotNull(dp);
+        assertTrue(
+                dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
+                        || dp.isTelephonyDumpSysCollectionEnabled());
+
+        //tracking should end
+        assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+    }
+
+    @Test
+    public void testEmergencyCallNeverWentActiveWithNonLocalDisconnectCause() {
+        Call call = createCall(true, Call.CALL_DIRECTION_OUTGOING);
+        mEmergencyCallDiagnosticLogger.onCallAdded(call);
+
+        //call is tracked
+        assertEquals(1, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+
+        call.setDisconnectCause(new DisconnectCause(DisconnectCause.REJECTED));
+        mEmergencyCallDiagnosticLogger.onCallRemoved(call);
+
+        //for non-local disconnect of non-active call,  we should always be persisting some data
+        ArgumentCaptor<TelephonyManager.EmergencyCallDiagnosticParams> captor =
+                ArgumentCaptor.forClass(
+                        TelephonyManager.EmergencyCallDiagnosticParams.class);
+        verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
+                captor.capture());
+        TelephonyManager.EmergencyCallDiagnosticParams dp = captor.getValue();
+
+        assertNotNull(dp);
+        assertTrue(
+                dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
+                        || dp.isTelephonyDumpSysCollectionEnabled());
+
+        //tracking should end
+        assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+    }
+
+    @Test
+    public void testEmergencyCallWentActiveForLongDuration_shouldNotCollectDiagnostics()
+            throws Exception {
+        Call call = createCall(true, Call.CALL_DIRECTION_OUTGOING);
+        mEmergencyCallDiagnosticLogger.onCallAdded(call);
+
+        //call went active
+        mEmergencyCallDiagnosticLogger.onCallStateChanged(call, CallState.DIALING,
+                CallState.ACTIVE);
+
+        //return large value for time when call is disconnected
+        when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis() + 10000L);
+
+        call.setDisconnectCause(new DisconnectCause(DisconnectCause.ERROR));
+        mEmergencyCallDiagnosticLogger.onCallRemoved(call);
+
+        //no diagnostic data should be persisted
+        verify(mTm, never()).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
+                any());
+
+        //tracking should end
+        assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+    }
+
+}