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.EmergencyCallDiagnosticData; 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 EmergencyCallDiagnosticData.Builder callDiagnosticBuilder = 160 new EmergencyCallDiagnosticData.Builder(); 161 for (Integer dataCollectionType : dataCollectionTypes) { 162 switch (dataCollectionType) { 163 case COLLECTION_TYPE_TELECOM_STATE: 164 if (isTelecomDumpCollectionEnabled()) { 165 callDiagnosticBuilder.setTelecomDumpsysCollectionEnabled(true); 166 invokeTelephonyPersistApi = true; 167 } 168 break; 169 case COLLECTION_TYPE_TELEPHONY_STATE: 170 if (isTelephonyDumpCollectionEnabled()) { 171 callDiagnosticBuilder.setTelephonyDumpsysCollectionEnabled(true); 172 invokeTelephonyPersistApi = true; 173 } 174 break; 175 case COLLECTION_TYPE_LOGCAT_BUFFERS: 176 if (isLogcatCollectionEnabled()) { 177 callDiagnosticBuilder.setLogcatCollectionStartTimeMillis( 178 ts.getCallCreatedTime()); 179 invokeTelephonyPersistApi = true; 180 } 181 break; 182 case COLLECTION_TYPE_BUGREPORT: 183 if (isBugreportCollectionEnabled()) { 184 mAsyncTaskExecutor.execute(new Runnable() { 185 @Override 186 public void run() { 187 persistBugreport(); 188 } 189 }); 190 } 191 break; 192 default: 193 } 194 } 195 EmergencyCallDiagnosticData ecdData = callDiagnosticBuilder.build(); 196 if (invokeTelephonyPersistApi) { 197 mAsyncTaskExecutor.execute(new Runnable() { 198 @Override 199 public void run() { 200 Log.i(this, "Requesting Telephony to persist data %s", ecdData.toString()); 201 try { 202 mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, ecdData); 203 } catch (Exception e) { 204 Log.w(this, 205 "Exception while invoking " 206 + "Telephony#persistEmergencyCallDiagnosticData %s", 207 e.toString()); 208 } 209 } 210 }); 211 } 212 } 213 isBugreportCollectionEnabled()214 private boolean isBugreportCollectionEnabled() { 215 return DeviceConfig.getBoolean( 216 DeviceConfig.NAMESPACE_TELEPHONY, 217 ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 218 false); 219 } 220 isTelecomDumpCollectionEnabled()221 private boolean isTelecomDumpCollectionEnabled() { 222 return DeviceConfig.getBoolean( 223 DeviceConfig.NAMESPACE_TELEPHONY, 224 ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 225 true); 226 } 227 isLogcatCollectionEnabled()228 private boolean isLogcatCollectionEnabled() { 229 return DeviceConfig.getBoolean( 230 DeviceConfig.NAMESPACE_TELEPHONY, 231 ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 232 true); 233 } 234 isTelephonyDumpCollectionEnabled()235 private boolean isTelephonyDumpCollectionEnabled() { 236 return DeviceConfig.getBoolean( 237 DeviceConfig.NAMESPACE_TELEPHONY, 238 ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS, 239 true); 240 } 241 persistBugreport()242 private void persistBugreport() { 243 if (isBugreportCollectionEnabled()) { 244 // TODO: 245 } 246 } 247 shouldTrackCall(Call call)248 private boolean shouldTrackCall(Call call) { 249 return (call != null && call.isEmergencyCall() && call.isOutgoing()); 250 } 251 reportStuckCall(Call call)252 public void reportStuckCall(Call call) { 253 if (shouldTrackCall(call)) { 254 Log.i(this, "Triggering diagnostics for stuck call %s", call.getId()); 255 triggerDiagnosticsCollection(call, REPORT_REASON_STUCK_CALL_DETECTED); 256 call.removeListener(this); 257 mEmergencyCallsMap.remove(call); 258 } 259 } 260 261 @Override onStartCreateConnection(Call call)262 public void onStartCreateConnection(Call call) { 263 if (shouldTrackCall(call)) { 264 long currentTime = mClockProxy.currentTimeMillis(); 265 call.addListener(this); 266 Log.i(this, "Tracking call %s timestamp: %d", call.getId(), currentTime); 267 mEmergencyCallsMap.put(call, new CallEventTimestamps(currentTime)); 268 } 269 } 270 271 @Override onCreateConnectionFailed(Call call)272 public void onCreateConnectionFailed(Call call) { 273 if (shouldTrackCall(call)) { 274 Log.i(this, "Triggering diagnostics for call %s that was never added", call.getId()); 275 triggerDiagnosticsCollection(call, REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED); 276 call.removeListener(this); 277 mEmergencyCallsMap.remove(call); 278 } 279 } 280 281 /** 282 * Override of {@link CallsManagerListenerBase} to track when calls are removed 283 * 284 * @param call the call 285 */ 286 @Override onCallRemoved(Call call)287 public void onCallRemoved(Call call) { 288 if (call != null && (mEmergencyCallsMap.get(call) != null)) { 289 call.removeListener(this); 290 291 CallEventTimestamps ts = mEmergencyCallsMap.get(call); 292 long currentTime = mClockProxy.currentTimeMillis(); 293 ts.setCallRemovedTime(currentTime); 294 295 maybeTriggerDiagnosticsCollection(call, ts); 296 mEmergencyCallsMap.remove(call); 297 } 298 } 299 300 // !NOTE!: this method should only be called after we get onCallRemoved maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts)301 private void maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts) { 302 Log.i(this, "Evaluating emergency call for diagnostic logging: %s", removedCall.getId()); 303 boolean wentActive = (ts.getCallActiveTime() != 0); 304 long callActiveTimeSec = (wentActive ? getCallTimeInActiveStateSec(ts) : 0); 305 long timeSinceCallCreatedSec = getTotalCallTimeSec(ts); 306 int dc = removedCall.getDisconnectCause().getCode(); 307 308 if (wentActive) { 309 if (callActiveTimeSec 310 < mTimeoutAdapter.getEmergencyCallActiveTimeThresholdMillis() / 1000) { 311 // call connected but did not go on for long 312 triggerDiagnosticsCollection( 313 removedCall, REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE); 314 } 315 } else { 316 317 if (dc == DisconnectCause.LOCAL 318 && timeSinceCallCreatedSec 319 > mTimeoutAdapter.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis() 320 / 1000) { 321 // call was disconnected by the user (but not immediately) 322 triggerDiagnosticsCollection( 323 removedCall, REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY); 324 } else if (dc != DisconnectCause.LOCAL) { 325 // this can be a case for a full bugreport 326 triggerDiagnosticsCollection(removedCall, REPORT_REASON_CALL_FAILED); 327 } 328 } 329 } 330 331 /** 332 * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track 333 * call state changes. 334 * 335 * @param call the call 336 * @param oldState its old state 337 * @param newState the new state 338 */ 339 @Override onCallStateChanged(Call call, int oldState, int newState)340 public void onCallStateChanged(Call call, int oldState, int newState) { 341 342 CallEventTimestamps ts = mEmergencyCallsMap.get(call); 343 if (call != null && ts != null && newState == CallState.ACTIVE 344 && ts.getCallActiveTime() == 0) { 345 long currentTime = mClockProxy.currentTimeMillis(); 346 ts.setCallActiveTime(currentTime); 347 } 348 } 349 dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw)350 private void dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw) { 351 pw.increaseIndent(); 352 pw.println("PERSISTED DIAGNOSTIC DATA FROM DROP BOX"); 353 int totalEntriesDumped = 0; 354 long currentTime = mClockProxy.currentTimeMillis(); 355 long entriesAfterTime = 356 currentTime - (mTimeoutAdapter.getDaysBackToSearchEmergencyDiagnosticEntries() * 24 357 * 60L * 60L * 1000L); 358 Log.i(this, "current time: %d entriesafter: %d", currentTime, entriesAfterTime); 359 DropBoxManager.Entry entry; 360 entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entriesAfterTime); 361 while (entry != null) { 362 Log.i(this, "found entry with ts: %d", entry.getTimeMillis()); 363 String content[] = entry.getText(getMaxBytesPerDropboxEntry()).split( 364 System.lineSeparator()); 365 long entryTime = entry.getTimeMillis(); 366 if (content != null) { 367 pw.increaseIndent(); 368 pw.println("------------BEGIN ENTRY (" + entryTime + ")--------"); 369 for (String line : content) { 370 pw.println(line); 371 } 372 pw.println("--------END ENTRY--------"); 373 pw.decreaseIndent(); 374 totalEntriesDumped++; 375 } 376 entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entryTime); 377 if (totalEntriesDumped > MAX_DROPBOX_ENTRIES_TO_DUMP) { 378 /* 379 Since Emergency calls are a rare/once in a lifetime time occurrence for most users, 380 we should not be seeing too many entries. This code just guards against edge case 381 like load testing, b2b failures etc. We may accumulate a lot of dropbox entries in 382 such cases, but we limit to dumping only MAX_DROPBOX_ENTRIES_TO_DUMP in the 383 bugreport 384 385 The Dropbox API in its current state does not allow to query Entries in reverse 386 chronological order efficiently. 387 */ 388 389 Log.i(this, "Skipping dump for remaining entries. dumped :%d", totalEntriesDumped); 390 break; 391 } 392 } 393 pw.println("END OF PERSISTED DIAGNOSTIC DATA FROM DROP BOX"); 394 pw.decreaseIndent(); 395 } 396 dump(IndentingPrintWriter pw, String[] args)397 public void dump(IndentingPrintWriter pw, String[] args) { 398 pw.increaseIndent(); 399 mLocalLog.dump(pw); 400 pw.decreaseIndent(); 401 if (args != null && args.length > 0 && args[0].equals(DUMPSYS_ARG_FOR_DIAGNOSTICS)) { 402 //dont read dropbox entries since this dump is triggered by telephony for diagnostics 403 Log.i(this, "skipped dumping diagnostic data"); 404 return; 405 } 406 try { 407 dumpDiagnosticDataFromDropbox(pw); 408 } catch (Exception e) { 409 pw.println("Exception was thrown while dumping diagnostic data from DropBox"); 410 e.printStackTrace(); 411 } 412 } 413 414 private static class CallEventTimestamps { 415 416 private final long mCallCreatedTime; 417 private long mCallActiveTime; 418 private long mCallRemovedTime; 419 CallEventTimestamps(long createdTime)420 public CallEventTimestamps(long createdTime) { 421 mCallCreatedTime = createdTime; 422 } 423 getCallActiveTime()424 public long getCallActiveTime() { 425 return mCallActiveTime; 426 } 427 setCallActiveTime(long callActiveTime)428 public void setCallActiveTime(long callActiveTime) { 429 this.mCallActiveTime = callActiveTime; 430 } 431 getCallCreatedTime()432 public long getCallCreatedTime() { 433 return mCallCreatedTime; 434 } 435 getCallRemovedTime()436 public long getCallRemovedTime() { 437 return mCallRemovedTime; 438 } 439 setCallRemovedTime(long callRemovedTime)440 public void setCallRemovedTime(long callRemovedTime) { 441 this.mCallRemovedTime = callRemovedTime; 442 } 443 } 444 } 445