1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.telecom; 18 19 import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams; 20 21 import android.os.BugreportManager; 22 import android.os.DropBoxManager; 23 import android.provider.DeviceConfig; 24 import android.telecom.DisconnectCause; 25 import android.telecom.Log; 26 import android.telephony.TelephonyManager; 27 import android.util.LocalLog; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.internal.util.IndentingPrintWriter; 31 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.concurrent.ConcurrentHashMap; 37 import java.util.concurrent.Executor; 38 39 /** 40 * The EmergencyCallDiagnosticsLogger monitors information required to diagnose potential outgoing 41 * ecall failures on the device. When a potential failure is detected, it calls a Telephony API to 42 * persist relevant information (dumpsys, logcat etc.) to the dropbox. This acts as a central place 43 * to determine when and what to collect. 44 * 45 * <p>When a bugreport is triggered, this module will read the dropbox entries and add them to the 46 * telecom dump. 47 */ 48 public class EmergencyCallDiagnosticLogger extends CallsManagerListenerBase 49 implements Call.Listener { 50 51 public static final int REPORT_REASON_RANGE_START = -1; //!!DO NOT CHANGE 52 public static final int REPORT_REASON_RANGE_END = 5; //increment this and add new reason above 53 public static final int COLLECTION_TYPE_BUGREPORT = 10; 54 public static final int COLLECTION_TYPE_TELECOM_STATE = 11; 55 public static final int COLLECTION_TYPE_TELEPHONY_STATE = 12; 56 public static final int COLLECTION_TYPE_LOGCAT_BUFFERS = 13; 57 private static final int REPORT_REASON_STUCK_CALL_DETECTED = 0; 58 private static final int REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY = 1; 59 private static final int REPORT_REASON_CALL_FAILED = 2; 60 private static final int REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED = 3; 61 private static final int REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE = 4; 62 private static final String DROPBOX_TAG = "ecall_diagnostic_data"; 63 private static final String ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS = 64 "enable_bugreport_collection_for_emergency_call_diagnostics"; 65 private static final String ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS = 66 "enable_telecom_dump_collection_for_emergency_call_diagnostics"; 67 68 private static final String ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS = 69 "enable_logcat_collection_for_emergency_call_diagnostics"; 70 private static final String ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS = 71 "enable_telephony_dump_collection_for_emergency_call_diagnostics"; 72 73 private static final String DUMPSYS_ARG_FOR_DIAGNOSTICS = "EmergencyDiagnostics"; 74 75 // max text size to read from dropbox entry 76 private static final int DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY = 500000; 77 private static final String MAX_BYTES_PER_DROP_BOX_ENTRY = "max_bytes_per_dropbox_entry"; 78 private static final int MAX_DROPBOX_ENTRIES_TO_DUMP = 6; 79 80 private final Timeouts.Adapter mTimeoutAdapter; 81 // This map holds all calls, but keeps pruning non-emergency calls when we can determine it 82 private final Map<Call, CallEventTimestamps> mEmergencyCallsMap = new ConcurrentHashMap<>(2); 83 private final DropBoxManager mDropBoxManager; 84 private final LocalLog mLocalLog = new LocalLog(10); 85 private final TelephonyManager mTelephonyManager; 86 private final BugreportManager mBugreportManager; 87 private final Executor mAsyncTaskExecutor; 88 private final ClockProxy mClockProxy; 89 EmergencyCallDiagnosticLogger( TelephonyManager tm, BugreportManager brm, Timeouts.Adapter timeoutAdapter, DropBoxManager dropBoxManager, Executor asyncTaskExecutor, ClockProxy clockProxy)90 public EmergencyCallDiagnosticLogger( 91 TelephonyManager tm, 92 BugreportManager brm, 93 Timeouts.Adapter timeoutAdapter, DropBoxManager dropBoxManager, 94 Executor asyncTaskExecutor, ClockProxy clockProxy) { 95 mTimeoutAdapter = timeoutAdapter; 96 mDropBoxManager = dropBoxManager; 97 mTelephonyManager = tm; 98 mBugreportManager = brm; 99 mAsyncTaskExecutor = asyncTaskExecutor; 100 mClockProxy = clockProxy; 101 } 102 103 // this calculates time from ACTIVE --> removed getCallTimeInActiveStateSec(CallEventTimestamps ts)104 private static long getCallTimeInActiveStateSec(CallEventTimestamps ts) { 105 if (ts.getCallActiveTime() == 0 || ts.getCallRemovedTime() == 0) { 106 return 0; 107 } else { 108 return (ts.getCallRemovedTime() - ts.getCallActiveTime()) / 1000; 109 } 110 } 111 112 // this calculates time from call created --> removed getTotalCallTimeSec(CallEventTimestamps ts)113 private static long getTotalCallTimeSec(CallEventTimestamps ts) { 114 if (ts.getCallRemovedTime() == 0 || ts.getCallCreatedTime() == 0) { 115 return 0; 116 } else { 117 return (ts.getCallRemovedTime() - ts.getCallCreatedTime()) / 1000; 118 } 119 } 120 121 //determines what to collect based on fail reason 122 //if COLLECTION_TYPE_BUGREPORT is present in the returned list, then that 123 //should be the only collection type in the list 124 @VisibleForTesting getDataCollectionTypes(int reason)125 public static List<Integer> getDataCollectionTypes(int reason) { 126 switch (reason) { 127 case REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE: 128 return Arrays.asList(COLLECTION_TYPE_TELECOM_STATE); 129 case REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED: 130 return Arrays.asList( 131 COLLECTION_TYPE_TELECOM_STATE, COLLECTION_TYPE_TELEPHONY_STATE); 132 case REPORT_REASON_CALL_FAILED: 133 case REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY: 134 case REPORT_REASON_STUCK_CALL_DETECTED: 135 return Arrays.asList( 136 COLLECTION_TYPE_TELECOM_STATE, 137 COLLECTION_TYPE_TELEPHONY_STATE, 138 COLLECTION_TYPE_LOGCAT_BUFFERS); 139 default: 140 } 141 return new ArrayList<>(); 142 } 143 getMaxBytesPerDropboxEntry()144 private int getMaxBytesPerDropboxEntry() { 145 return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY, 146 MAX_BYTES_PER_DROP_BOX_ENTRY, DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY); 147 } 148 149 @VisibleForTesting getEmergencyCallsMap()150 public Map<Call, CallEventTimestamps> getEmergencyCallsMap() { 151 return mEmergencyCallsMap; 152 } 153 triggerDiagnosticsCollection(Call call, int reason)154 private void triggerDiagnosticsCollection(Call call, int reason) { 155 Log.i(this, "Triggering diagnostics for call %s reason: %d", call.getId(), reason); 156 List<Integer> dataCollectionTypes = getDataCollectionTypes(reason); 157 boolean invokeTelephonyPersistApi = false; 158 CallEventTimestamps ts = mEmergencyCallsMap.get(call); 159 EmergencyCallDiagnosticParams dp = 160 new EmergencyCallDiagnosticParams(); 161 for (Integer dataCollectionType : dataCollectionTypes) { 162 switch (dataCollectionType) { 163 case COLLECTION_TYPE_TELECOM_STATE: 164 if (isTelecomDumpCollectionEnabled()) { 165 dp.setTelecomDumpSysCollection(true); 166 invokeTelephonyPersistApi = true; 167 } 168 break; 169 case COLLECTION_TYPE_TELEPHONY_STATE: 170 if (isTelephonyDumpCollectionEnabled()) { 171 dp.setTelephonyDumpSysCollection(true); 172 invokeTelephonyPersistApi = true; 173 } 174 break; 175 case COLLECTION_TYPE_LOGCAT_BUFFERS: 176 if (isLogcatCollectionEnabled()) { 177 dp.setLogcatCollection(true, ts.getCallCreatedTime()); 178 invokeTelephonyPersistApi = true; 179 } 180 break; 181 case COLLECTION_TYPE_BUGREPORT: 182 if (isBugreportCollectionEnabled()) { 183 mAsyncTaskExecutor.execute(new Runnable() { 184 @Override 185 public void run() { 186 persistBugreport(); 187 } 188 }); 189 } 190 break; 191 default: 192 } 193 } 194 if (invokeTelephonyPersistApi) { 195 mAsyncTaskExecutor.execute(new Runnable() { 196 @Override 197 public void run() { 198 Log.i(this, "Requesting Telephony to persist data %s", dp.toString()); 199 try { 200 mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, dp); 201 } catch (Exception e) { 202 Log.w(this, 203 "Exception while invoking " 204 + "Telephony#persistEmergencyCallDiagnosticData %s", 205 e.toString()); 206 } 207 } 208 }); 209 } 210 } 211 isBugreportCollectionEnabled()212 private boolean isBugreportCollectionEnabled() { 213 return DeviceConfig.getBoolean( 214 DeviceConfig.NAMESPACE_TELEPHONY, 215 ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 216 false); 217 } 218 isTelecomDumpCollectionEnabled()219 private boolean isTelecomDumpCollectionEnabled() { 220 return DeviceConfig.getBoolean( 221 DeviceConfig.NAMESPACE_TELEPHONY, 222 ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 223 true); 224 } 225 isLogcatCollectionEnabled()226 private boolean isLogcatCollectionEnabled() { 227 return DeviceConfig.getBoolean( 228 DeviceConfig.NAMESPACE_TELEPHONY, 229 ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 230 true); 231 } 232 isTelephonyDumpCollectionEnabled()233 private boolean isTelephonyDumpCollectionEnabled() { 234 return DeviceConfig.getBoolean( 235 DeviceConfig.NAMESPACE_TELEPHONY, 236 ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 237 true); 238 } 239 persistBugreport()240 private void persistBugreport() { 241 if (isBugreportCollectionEnabled()) { 242 // TODO: 243 } 244 } 245 shouldTrackCall(Call call)246 private boolean shouldTrackCall(Call call) { 247 return (call != null && call.isEmergencyCall() && call.isOutgoing()); 248 } 249 reportStuckCall(Call call)250 public void reportStuckCall(Call call) { 251 if (shouldTrackCall(call)) { 252 Log.i(this, "Triggering diagnostics for stuck call %s", call.getId()); 253 triggerDiagnosticsCollection(call, REPORT_REASON_STUCK_CALL_DETECTED); 254 call.removeListener(this); 255 mEmergencyCallsMap.remove(call); 256 } 257 } 258 259 @Override onStartCreateConnection(Call call)260 public void onStartCreateConnection(Call call) { 261 if (shouldTrackCall(call)) { 262 long currentTime = mClockProxy.currentTimeMillis(); 263 call.addListener(this); 264 Log.i(this, "Tracking call %s timestamp: %d", call.getId(), currentTime); 265 mEmergencyCallsMap.put(call, new CallEventTimestamps(currentTime)); 266 } 267 } 268 269 @Override onCreateConnectionFailed(Call call)270 public void onCreateConnectionFailed(Call call) { 271 if (shouldTrackCall(call)) { 272 Log.i(this, "Triggering diagnostics for call %s that was never added", call.getId()); 273 triggerDiagnosticsCollection(call, REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED); 274 call.removeListener(this); 275 mEmergencyCallsMap.remove(call); 276 } 277 } 278 279 /** 280 * Override of {@link CallsManagerListenerBase} to track when calls are removed 281 * 282 * @param call the call 283 */ 284 @Override onCallRemoved(Call call)285 public void onCallRemoved(Call call) { 286 if (call != null && (mEmergencyCallsMap.get(call) != null)) { 287 call.removeListener(this); 288 289 CallEventTimestamps ts = mEmergencyCallsMap.get(call); 290 long currentTime = mClockProxy.currentTimeMillis(); 291 ts.setCallRemovedTime(currentTime); 292 293 maybeTriggerDiagnosticsCollection(call, ts); 294 mEmergencyCallsMap.remove(call); 295 } 296 } 297 298 // !NOTE!: this method should only be called after we get onCallRemoved maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts)299 private void maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts) { 300 Log.i(this, "Evaluating emergency call for diagnostic logging: %s", removedCall.getId()); 301 boolean wentActive = (ts.getCallActiveTime() != 0); 302 long callActiveTimeSec = (wentActive ? getCallTimeInActiveStateSec(ts) : 0); 303 long timeSinceCallCreatedSec = getTotalCallTimeSec(ts); 304 int dc = removedCall.getDisconnectCause().getCode(); 305 306 if (wentActive) { 307 if (callActiveTimeSec 308 < mTimeoutAdapter.getEmergencyCallActiveTimeThresholdMillis() / 1000) { 309 // call connected but did not go on for long 310 triggerDiagnosticsCollection( 311 removedCall, REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE); 312 } 313 } else { 314 315 if (dc == DisconnectCause.LOCAL 316 && timeSinceCallCreatedSec 317 > mTimeoutAdapter.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis() 318 / 1000) { 319 // call was disconnected by the user (but not immediately) 320 triggerDiagnosticsCollection( 321 removedCall, REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY); 322 } else if (dc != DisconnectCause.LOCAL) { 323 // this can be a case for a full bugreport 324 triggerDiagnosticsCollection(removedCall, REPORT_REASON_CALL_FAILED); 325 } 326 } 327 } 328 329 /** 330 * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track 331 * call state changes. 332 * 333 * @param call the call 334 * @param oldState its old state 335 * @param newState the new state 336 */ 337 @Override onCallStateChanged(Call call, int oldState, int newState)338 public void onCallStateChanged(Call call, int oldState, int newState) { 339 340 if (call != null && mEmergencyCallsMap.get(call) != null && newState == CallState.ACTIVE) { 341 CallEventTimestamps ts = mEmergencyCallsMap.get(call); 342 if (ts != null) { 343 long currentTime = mClockProxy.currentTimeMillis(); 344 ts.setCallActiveTime(currentTime); 345 } 346 } 347 } 348 dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw)349 private void dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw) { 350 pw.increaseIndent(); 351 pw.println("PERSISTED DIAGNOSTIC DATA FROM DROP BOX"); 352 int totalEntriesDumped = 0; 353 long currentTime = mClockProxy.currentTimeMillis(); 354 long entriesAfterTime = 355 currentTime - (mTimeoutAdapter.getDaysBackToSearchEmergencyDiagnosticEntries() * 24 356 * 60L * 60L * 1000L); 357 Log.i(this, "current time: %d entriesafter: %d", currentTime, entriesAfterTime); 358 DropBoxManager.Entry entry; 359 entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entriesAfterTime); 360 while (entry != null) { 361 Log.i(this, "found entry with ts: %d", entry.getTimeMillis()); 362 String content[] = entry.getText(getMaxBytesPerDropboxEntry()).split( 363 System.lineSeparator()); 364 long entryTime = entry.getTimeMillis(); 365 if (content != null) { 366 pw.increaseIndent(); 367 pw.println("------------BEGIN ENTRY (" + entryTime + ")--------"); 368 for (String line : content) { 369 pw.println(line); 370 } 371 pw.println("--------END ENTRY--------"); 372 pw.decreaseIndent(); 373 totalEntriesDumped++; 374 } 375 entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entryTime); 376 if (totalEntriesDumped > MAX_DROPBOX_ENTRIES_TO_DUMP) { 377 /* 378 Since Emergency calls are a rare/once in a lifetime time occurrence for most users, 379 we should not be seeing too many entries. This code just guards against edge case 380 like load testing, b2b failures etc. We may accumulate a lot of dropbox entries in 381 such cases, but we limit to dumping only MAX_DROPBOX_ENTRIES_TO_DUMP in the 382 bugreport 383 384 The Dropbox API in its current state does not allow to query Entries in reverse 385 chronological order efficiently. 386 */ 387 388 Log.i(this, "Skipping dump for remaining entries. dumped :%d", totalEntriesDumped); 389 break; 390 } 391 } 392 pw.println("END OF PERSISTED DIAGNOSTIC DATA FROM DROP BOX"); 393 pw.decreaseIndent(); 394 } 395 dump(IndentingPrintWriter pw, String[] args)396 public void dump(IndentingPrintWriter pw, String[] args) { 397 pw.increaseIndent(); 398 mLocalLog.dump(pw); 399 pw.decreaseIndent(); 400 if (args != null && args.length > 0 && args[0].equals(DUMPSYS_ARG_FOR_DIAGNOSTICS)) { 401 //dont read dropbox entries since this dump is triggered by telephony for diagnostics 402 Log.i(this, "skipped dumping diagnostic data"); 403 return; 404 } 405 dumpDiagnosticDataFromDropbox(pw); 406 } 407 408 private static class CallEventTimestamps { 409 410 private final long mCallCreatedTime; 411 private long mCallActiveTime; 412 private long mCallRemovedTime; 413 CallEventTimestamps(long createdTime)414 public CallEventTimestamps(long createdTime) { 415 mCallCreatedTime = createdTime; 416 } 417 getCallActiveTime()418 public long getCallActiveTime() { 419 return mCallActiveTime; 420 } 421 setCallActiveTime(long callActiveTime)422 public void setCallActiveTime(long callActiveTime) { 423 this.mCallActiveTime = callActiveTime; 424 } 425 getCallCreatedTime()426 public long getCallCreatedTime() { 427 return mCallCreatedTime; 428 } 429 getCallRemovedTime()430 public long getCallRemovedTime() { 431 return mCallRemovedTime; 432 } 433 setCallRemovedTime(long callRemovedTime)434 public void setCallRemovedTime(long callRemovedTime) { 435 this.mCallRemovedTime = callRemovedTime; 436 } 437 } 438 } 439