1 /* 2 * Copyright (C) 2020 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.internal.telephony.metrics; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.os.Build; 22 import android.os.Handler; 23 import android.os.HandlerThread; 24 import android.telephony.TelephonyManager; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.internal.telephony.nano.PersistAtomsProto.CarrierIdMismatch; 28 import com.android.internal.telephony.nano.PersistAtomsProto.CellularDataServiceSwitch; 29 import com.android.internal.telephony.nano.PersistAtomsProto.CellularServiceState; 30 import com.android.internal.telephony.nano.PersistAtomsProto.DataCallSession; 31 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationStats; 32 import com.android.internal.telephony.nano.PersistAtomsProto.ImsRegistrationTermination; 33 import com.android.internal.telephony.nano.PersistAtomsProto.IncomingSms; 34 import com.android.internal.telephony.nano.PersistAtomsProto.NetworkRequests; 35 import com.android.internal.telephony.nano.PersistAtomsProto.OutgoingSms; 36 import com.android.internal.telephony.nano.PersistAtomsProto.PersistAtoms; 37 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallRatUsage; 38 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession; 39 import com.android.internal.util.ArrayUtils; 40 import com.android.telephony.Rlog; 41 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.nio.file.Files; 45 import java.nio.file.NoSuchFileException; 46 import java.security.SecureRandom; 47 import java.util.Arrays; 48 import java.util.stream.IntStream; 49 50 /** 51 * Stores and aggregates metrics that should not be pulled at arbitrary frequency. 52 * 53 * <p>NOTE: while this class checks timestamp against {@code minIntervalMillis}, it is {@link 54 * MetricsCollector}'s responsibility to ensure {@code minIntervalMillis} is set correctly. 55 */ 56 public class PersistAtomsStorage { 57 private static final String TAG = PersistAtomsStorage.class.getSimpleName(); 58 59 /** Name of the file where cached statistics are saved to. */ 60 private static final String FILENAME = "persist_atoms.pb"; 61 62 /** Delay to store atoms to persistent storage to bundle multiple operations together. */ 63 private static final int SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS = 30000; 64 65 /** 66 * Delay to store atoms to persistent storage during pulls to avoid unnecessary operations. 67 * 68 * <p>This delay should be short to avoid duplicating atoms or losing pull timestamp in case of 69 * crash or power loss. 70 */ 71 private static final int SAVE_TO_FILE_DELAY_FOR_GET_MILLIS = 500; 72 73 /** Maximum number of call sessions to store between pulls. */ 74 private static final int MAX_NUM_CALL_SESSIONS = 50; 75 76 /** 77 * Maximum number of SMS to store between pulls. Incoming messages and outgoing messages are 78 * counted separately. 79 */ 80 private static final int MAX_NUM_SMS = 25; 81 82 /** 83 * Maximum number of carrier ID mismatch events stored on the device to avoid sending duplicated 84 * metrics. 85 */ 86 private static final int MAX_CARRIER_ID_MISMATCH = 40; 87 88 /** Maximum number of data call sessions to store during pulls. */ 89 private static final int MAX_NUM_DATA_CALL_SESSIONS = 15; 90 91 /** Maximum number of service states to store between pulls. */ 92 private static final int MAX_NUM_CELLULAR_SERVICE_STATES = 50; 93 94 /** Maximum number of data service switches to store between pulls. */ 95 private static final int MAX_NUM_CELLULAR_DATA_SERVICE_SWITCHES = 50; 96 97 /** Maximum number of IMS registration stats to store between pulls. */ 98 private static final int MAX_NUM_IMS_REGISTRATION_STATS = 10; 99 100 /** Maximum number of IMS registration terminations to store between pulls. */ 101 private static final int MAX_NUM_IMS_REGISTRATION_TERMINATIONS = 10; 102 103 /** Stores persist atoms and persist states of the puller. */ 104 @VisibleForTesting protected final PersistAtoms mAtoms; 105 106 /** Aggregates RAT duration and call count. */ 107 private final VoiceCallRatTracker mVoiceCallRatTracker; 108 109 /** Whether atoms should be saved immediately, skipping the delay. */ 110 @VisibleForTesting protected boolean mSaveImmediately; 111 112 private final Context mContext; 113 private final Handler mHandler; 114 private final HandlerThread mHandlerThread; 115 private static final SecureRandom sRandom = new SecureRandom(); 116 117 private Runnable mSaveRunnable = 118 new Runnable() { 119 @Override 120 public void run() { 121 saveAtomsToFileNow(); 122 } 123 }; 124 PersistAtomsStorage(Context context)125 public PersistAtomsStorage(Context context) { 126 mContext = context; 127 mAtoms = loadAtomsFromFile(); 128 mVoiceCallRatTracker = VoiceCallRatTracker.fromProto(mAtoms.voiceCallRatUsage); 129 130 mHandlerThread = new HandlerThread("PersistAtomsThread"); 131 mHandlerThread.start(); 132 mHandler = new Handler(mHandlerThread.getLooper()); 133 mSaveImmediately = false; 134 } 135 136 /** Adds a call to the storage. */ addVoiceCallSession(VoiceCallSession call)137 public synchronized void addVoiceCallSession(VoiceCallSession call) { 138 mAtoms.voiceCallSession = 139 insertAtRandomPlace(mAtoms.voiceCallSession, call, MAX_NUM_CALL_SESSIONS); 140 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 141 142 Rlog.d(TAG, "Add new voice call session: " + call.toString()); 143 } 144 145 /** Adds RAT usages to the storage when a call session ends. */ addVoiceCallRatUsage(VoiceCallRatTracker ratUsages)146 public synchronized void addVoiceCallRatUsage(VoiceCallRatTracker ratUsages) { 147 mVoiceCallRatTracker.mergeWith(ratUsages); 148 mAtoms.voiceCallRatUsage = mVoiceCallRatTracker.toProto(); 149 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 150 } 151 152 /** Adds an incoming SMS to the storage. */ addIncomingSms(IncomingSms sms)153 public synchronized void addIncomingSms(IncomingSms sms) { 154 mAtoms.incomingSms = insertAtRandomPlace(mAtoms.incomingSms, sms, MAX_NUM_SMS); 155 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 156 157 // To be removed 158 Rlog.d(TAG, "Add new incoming SMS atom: " + sms.toString()); 159 } 160 161 /** Adds an outgoing SMS to the storage. */ addOutgoingSms(OutgoingSms sms)162 public synchronized void addOutgoingSms(OutgoingSms sms) { 163 // Update the retry id, if needed, so that it's unique and larger than all 164 // previous ones. (this algorithm ignores the fact that some SMS atoms might 165 // be dropped due to limit in size of the array). 166 for (OutgoingSms storedSms : mAtoms.outgoingSms) { 167 if (storedSms.messageId == sms.messageId && storedSms.retryId >= sms.retryId) { 168 sms.retryId = storedSms.retryId + 1; 169 } 170 } 171 172 mAtoms.outgoingSms = insertAtRandomPlace(mAtoms.outgoingSms, sms, MAX_NUM_SMS); 173 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 174 175 // To be removed 176 Rlog.d(TAG, "Add new outgoing SMS atom: " + sms.toString()); 177 } 178 179 /** Adds a service state to the storage, together with data service switch if any. */ addCellularServiceStateAndCellularDataServiceSwitch( CellularServiceState state, @Nullable CellularDataServiceSwitch serviceSwitch)180 public synchronized void addCellularServiceStateAndCellularDataServiceSwitch( 181 CellularServiceState state, @Nullable CellularDataServiceSwitch serviceSwitch) { 182 CellularServiceState existingState = find(state); 183 if (existingState != null) { 184 existingState.totalTimeMillis += state.totalTimeMillis; 185 existingState.lastUsedMillis = getWallTimeMillis(); 186 } else { 187 state.lastUsedMillis = getWallTimeMillis(); 188 mAtoms.cellularServiceState = 189 insertAtRandomPlace( 190 mAtoms.cellularServiceState, state, MAX_NUM_CELLULAR_SERVICE_STATES); 191 } 192 193 if (serviceSwitch != null) { 194 CellularDataServiceSwitch existingSwitch = find(serviceSwitch); 195 if (existingSwitch != null) { 196 existingSwitch.switchCount += serviceSwitch.switchCount; 197 existingSwitch.lastUsedMillis = getWallTimeMillis(); 198 } else { 199 serviceSwitch.lastUsedMillis = getWallTimeMillis(); 200 mAtoms.cellularDataServiceSwitch = 201 insertAtRandomPlace( 202 mAtoms.cellularDataServiceSwitch, 203 serviceSwitch, 204 MAX_NUM_CELLULAR_DATA_SERVICE_SWITCHES); 205 } 206 } 207 208 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 209 } 210 211 /** Adds a data call session to the storage. */ addDataCallSession(DataCallSession dataCall)212 public synchronized void addDataCallSession(DataCallSession dataCall) { 213 mAtoms.dataCallSession = 214 insertAtRandomPlace(mAtoms.dataCallSession, dataCall, MAX_NUM_DATA_CALL_SESSIONS); 215 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 216 } 217 218 /** 219 * Adds a new carrier ID mismatch event to the storage. 220 * 221 * @return true if the item was not present and was added to the persistent storage, false 222 * otherwise. 223 */ addCarrierIdMismatch(CarrierIdMismatch carrierIdMismatch)224 public synchronized boolean addCarrierIdMismatch(CarrierIdMismatch carrierIdMismatch) { 225 // Check if the details of the SIM cards are already present and in case return. 226 if (find(carrierIdMismatch) != null) { 227 return false; 228 } 229 // Add the new CarrierIdMismatch at the end of the array, so that the same atom will not be 230 // sent again in future. 231 if (mAtoms.carrierIdMismatch.length == MAX_CARRIER_ID_MISMATCH) { 232 System.arraycopy( 233 mAtoms.carrierIdMismatch, 234 1, 235 mAtoms.carrierIdMismatch, 236 0, 237 MAX_CARRIER_ID_MISMATCH - 1); 238 mAtoms.carrierIdMismatch[MAX_CARRIER_ID_MISMATCH - 1] = carrierIdMismatch; 239 } else { 240 int newLength = mAtoms.carrierIdMismatch.length + 1; 241 mAtoms.carrierIdMismatch = Arrays.copyOf(mAtoms.carrierIdMismatch, newLength); 242 mAtoms.carrierIdMismatch[newLength - 1] = carrierIdMismatch; 243 } 244 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 245 return true; 246 } 247 248 /** Adds IMS registration stats to the storage. */ addImsRegistrationStats(ImsRegistrationStats stats)249 public synchronized void addImsRegistrationStats(ImsRegistrationStats stats) { 250 ImsRegistrationStats existingStats = find(stats); 251 if (existingStats != null) { 252 existingStats.registeredMillis += stats.registeredMillis; 253 existingStats.voiceCapableMillis += stats.voiceCapableMillis; 254 existingStats.voiceAvailableMillis += stats.voiceAvailableMillis; 255 existingStats.smsCapableMillis += stats.smsCapableMillis; 256 existingStats.smsAvailableMillis += stats.smsAvailableMillis; 257 existingStats.videoCapableMillis += stats.videoCapableMillis; 258 existingStats.videoAvailableMillis += stats.videoAvailableMillis; 259 existingStats.utCapableMillis += stats.utCapableMillis; 260 existingStats.utAvailableMillis += stats.utAvailableMillis; 261 existingStats.lastUsedMillis = getWallTimeMillis(); 262 } else { 263 stats.lastUsedMillis = getWallTimeMillis(); 264 mAtoms.imsRegistrationStats = 265 insertAtRandomPlace( 266 mAtoms.imsRegistrationStats, stats, MAX_NUM_IMS_REGISTRATION_STATS); 267 } 268 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 269 } 270 271 /** Adds IMS registration termination to the storage. */ addImsRegistrationTermination(ImsRegistrationTermination termination)272 public synchronized void addImsRegistrationTermination(ImsRegistrationTermination termination) { 273 ImsRegistrationTermination existingTermination = find(termination); 274 if (existingTermination != null) { 275 existingTermination.count += termination.count; 276 existingTermination.lastUsedMillis = getWallTimeMillis(); 277 } else { 278 termination.lastUsedMillis = getWallTimeMillis(); 279 mAtoms.imsRegistrationTermination = 280 insertAtRandomPlace( 281 mAtoms.imsRegistrationTermination, 282 termination, 283 MAX_NUM_IMS_REGISTRATION_TERMINATIONS); 284 } 285 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 286 } 287 288 /** 289 * Stores the version of the carrier ID matching table. 290 * 291 * @return true if the version is newer than last available version, false otherwise. 292 */ setCarrierIdTableVersion(int carrierIdTableVersion)293 public synchronized boolean setCarrierIdTableVersion(int carrierIdTableVersion) { 294 if (mAtoms.carrierIdTableVersion < carrierIdTableVersion) { 295 mAtoms.carrierIdTableVersion = carrierIdTableVersion; 296 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 297 return true; 298 } else { 299 return false; 300 } 301 } 302 303 /** Adds a new {@link NetworkRequests} to the storage. */ addNetworkRequests(NetworkRequests networkRequests)304 public synchronized void addNetworkRequests(NetworkRequests networkRequests) { 305 NetworkRequests existingMetrics = find(networkRequests); 306 if (existingMetrics != null) { 307 existingMetrics.enterpriseRequestCount += networkRequests.enterpriseRequestCount; 308 existingMetrics.enterpriseReleaseCount += networkRequests.enterpriseReleaseCount; 309 } else { 310 int newLength = mAtoms.networkRequests.length + 1; 311 mAtoms.networkRequests = Arrays.copyOf(mAtoms.networkRequests, newLength); 312 mAtoms.networkRequests[newLength - 1] = networkRequests; 313 } 314 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS); 315 } 316 317 /** 318 * Returns and clears the voice call sessions if last pulled longer than {@code 319 * minIntervalMillis} ago, otherwise returns {@code null}. 320 */ 321 @Nullable getVoiceCallSessions(long minIntervalMillis)322 public synchronized VoiceCallSession[] getVoiceCallSessions(long minIntervalMillis) { 323 if (getWallTimeMillis() - mAtoms.voiceCallSessionPullTimestampMillis > minIntervalMillis) { 324 mAtoms.voiceCallSessionPullTimestampMillis = getWallTimeMillis(); 325 VoiceCallSession[] previousCalls = mAtoms.voiceCallSession; 326 mAtoms.voiceCallSession = new VoiceCallSession[0]; 327 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 328 return previousCalls; 329 } else { 330 return null; 331 } 332 } 333 334 /** 335 * Returns and clears the voice call RAT usages if last pulled longer than {@code 336 * minIntervalMillis} ago, otherwise returns {@code null}. 337 */ 338 @Nullable getVoiceCallRatUsages(long minIntervalMillis)339 public synchronized VoiceCallRatUsage[] getVoiceCallRatUsages(long minIntervalMillis) { 340 if (getWallTimeMillis() - mAtoms.voiceCallRatUsagePullTimestampMillis > minIntervalMillis) { 341 mAtoms.voiceCallRatUsagePullTimestampMillis = getWallTimeMillis(); 342 VoiceCallRatUsage[] previousUsages = mAtoms.voiceCallRatUsage; 343 mVoiceCallRatTracker.clear(); 344 mAtoms.voiceCallRatUsage = new VoiceCallRatUsage[0]; 345 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 346 return previousUsages; 347 } else { 348 return null; 349 } 350 } 351 352 /** 353 * Returns and clears the incoming SMS if last pulled longer than {@code minIntervalMillis} ago, 354 * otherwise returns {@code null}. 355 */ 356 @Nullable getIncomingSms(long minIntervalMillis)357 public synchronized IncomingSms[] getIncomingSms(long minIntervalMillis) { 358 if (getWallTimeMillis() - mAtoms.incomingSmsPullTimestampMillis > minIntervalMillis) { 359 mAtoms.incomingSmsPullTimestampMillis = getWallTimeMillis(); 360 IncomingSms[] previousIncomingSms = mAtoms.incomingSms; 361 mAtoms.incomingSms = new IncomingSms[0]; 362 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 363 return previousIncomingSms; 364 } else { 365 return null; 366 } 367 } 368 369 /** 370 * Returns and clears the outgoing SMS if last pulled longer than {@code minIntervalMillis} ago, 371 * otherwise returns {@code null}. 372 */ 373 @Nullable getOutgoingSms(long minIntervalMillis)374 public synchronized OutgoingSms[] getOutgoingSms(long minIntervalMillis) { 375 if (getWallTimeMillis() - mAtoms.outgoingSmsPullTimestampMillis > minIntervalMillis) { 376 mAtoms.outgoingSmsPullTimestampMillis = getWallTimeMillis(); 377 OutgoingSms[] previousOutgoingSms = mAtoms.outgoingSms; 378 mAtoms.outgoingSms = new OutgoingSms[0]; 379 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 380 return previousOutgoingSms; 381 } else { 382 return null; 383 } 384 } 385 386 /** 387 * Returns and clears the data call session if last pulled longer than {@code minIntervalMillis} 388 * ago, otherwise returns {@code null}. 389 */ 390 @Nullable getDataCallSessions(long minIntervalMillis)391 public synchronized DataCallSession[] getDataCallSessions(long minIntervalMillis) { 392 if (getWallTimeMillis() - mAtoms.dataCallSessionPullTimestampMillis > minIntervalMillis) { 393 mAtoms.dataCallSessionPullTimestampMillis = getWallTimeMillis(); 394 DataCallSession[] previousDataCallSession = mAtoms.dataCallSession; 395 mAtoms.dataCallSession = new DataCallSession[0]; 396 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 397 return previousDataCallSession; 398 } else { 399 return null; 400 } 401 } 402 403 /** 404 * Returns and clears the service state durations if last pulled longer than {@code 405 * minIntervalMillis} ago, otherwise returns {@code null}. 406 */ 407 @Nullable getCellularServiceStates(long minIntervalMillis)408 public synchronized CellularServiceState[] getCellularServiceStates(long minIntervalMillis) { 409 if (getWallTimeMillis() - mAtoms.cellularServiceStatePullTimestampMillis 410 > minIntervalMillis) { 411 mAtoms.cellularServiceStatePullTimestampMillis = getWallTimeMillis(); 412 CellularServiceState[] previousStates = mAtoms.cellularServiceState; 413 Arrays.stream(previousStates).forEach(state -> state.lastUsedMillis = 0L); 414 mAtoms.cellularServiceState = new CellularServiceState[0]; 415 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 416 return previousStates; 417 } else { 418 return null; 419 } 420 } 421 422 /** 423 * Returns and clears the service state durations if last pulled longer than {@code 424 * minIntervalMillis} ago, otherwise returns {@code null}. 425 */ 426 @Nullable getCellularDataServiceSwitches( long minIntervalMillis)427 public synchronized CellularDataServiceSwitch[] getCellularDataServiceSwitches( 428 long minIntervalMillis) { 429 if (getWallTimeMillis() - mAtoms.cellularDataServiceSwitchPullTimestampMillis 430 > minIntervalMillis) { 431 mAtoms.cellularDataServiceSwitchPullTimestampMillis = getWallTimeMillis(); 432 CellularDataServiceSwitch[] previousSwitches = mAtoms.cellularDataServiceSwitch; 433 Arrays.stream(previousSwitches) 434 .forEach(serviceSwitch -> serviceSwitch.lastUsedMillis = 0L); 435 mAtoms.cellularDataServiceSwitch = new CellularDataServiceSwitch[0]; 436 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 437 return previousSwitches; 438 } else { 439 return null; 440 } 441 } 442 443 /** 444 * Returns and clears the IMS registration statistics if last pulled longer than {@code 445 * minIntervalMillis} ago, otherwise returns {@code null}. 446 */ 447 @Nullable getImsRegistrationStats(long minIntervalMillis)448 public synchronized ImsRegistrationStats[] getImsRegistrationStats(long minIntervalMillis) { 449 if (getWallTimeMillis() - mAtoms.imsRegistrationStatsPullTimestampMillis 450 > minIntervalMillis) { 451 mAtoms.imsRegistrationStatsPullTimestampMillis = getWallTimeMillis(); 452 ImsRegistrationStats[] previousStats = mAtoms.imsRegistrationStats; 453 Arrays.stream(previousStats).forEach(stats -> stats.lastUsedMillis = 0L); 454 mAtoms.imsRegistrationStats = new ImsRegistrationStats[0]; 455 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 456 return previousStats; 457 } else { 458 return null; 459 } 460 } 461 462 /** 463 * Returns and clears the IMS registration terminations if last pulled longer than {@code 464 * minIntervalMillis} ago, otherwise returns {@code null}. 465 */ 466 @Nullable getImsRegistrationTerminations( long minIntervalMillis)467 public synchronized ImsRegistrationTermination[] getImsRegistrationTerminations( 468 long minIntervalMillis) { 469 if (getWallTimeMillis() - mAtoms.imsRegistrationTerminationPullTimestampMillis 470 > minIntervalMillis) { 471 mAtoms.imsRegistrationTerminationPullTimestampMillis = getWallTimeMillis(); 472 ImsRegistrationTermination[] previousTerminations = mAtoms.imsRegistrationTermination; 473 Arrays.stream(previousTerminations) 474 .forEach(termination -> termination.lastUsedMillis = 0L); 475 mAtoms.imsRegistrationTermination = new ImsRegistrationTermination[0]; 476 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 477 return previousTerminations; 478 } else { 479 return null; 480 } 481 } 482 483 /** 484 * Returns and clears the network requests if last pulled longer than {@code 485 * minIntervalMillis} ago, otherwise returns {@code null}. 486 */ 487 @Nullable getNetworkRequests(long minIntervalMillis)488 public synchronized NetworkRequests[] getNetworkRequests(long minIntervalMillis) { 489 if (getWallTimeMillis() - mAtoms.networkRequestsPullTimestampMillis > minIntervalMillis) { 490 mAtoms.networkRequestsPullTimestampMillis = getWallTimeMillis(); 491 NetworkRequests[] previousNetworkRequests = mAtoms.networkRequests; 492 mAtoms.networkRequests = new NetworkRequests[0]; 493 saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS); 494 return previousNetworkRequests; 495 } else { 496 return null; 497 } 498 } 499 500 /** Loads {@link PersistAtoms} from a file in private storage. */ loadAtomsFromFile()501 private PersistAtoms loadAtomsFromFile() { 502 try { 503 PersistAtoms atoms = 504 PersistAtoms.parseFrom( 505 Files.readAllBytes(mContext.getFileStreamPath(FILENAME).toPath())); 506 // Start from scratch if build changes, since mixing atoms from different builds could 507 // produce strange results 508 if (!Build.FINGERPRINT.equals(atoms.buildFingerprint)) { 509 Rlog.d(TAG, "Build changed"); 510 return makeNewPersistAtoms(); 511 } 512 // check all the fields in case of situations such as OTA or crash during saving 513 atoms.voiceCallRatUsage = 514 sanitizeAtoms(atoms.voiceCallRatUsage, VoiceCallRatUsage.class); 515 atoms.voiceCallSession = 516 sanitizeAtoms( 517 atoms.voiceCallSession, VoiceCallSession.class, MAX_NUM_CALL_SESSIONS); 518 atoms.incomingSms = sanitizeAtoms(atoms.incomingSms, IncomingSms.class, MAX_NUM_SMS); 519 atoms.outgoingSms = sanitizeAtoms(atoms.outgoingSms, OutgoingSms.class, MAX_NUM_SMS); 520 atoms.carrierIdMismatch = 521 sanitizeAtoms( 522 atoms.carrierIdMismatch, 523 CarrierIdMismatch.class, 524 MAX_CARRIER_ID_MISMATCH); 525 atoms.dataCallSession = 526 sanitizeAtoms( 527 atoms.dataCallSession, 528 DataCallSession.class, 529 MAX_NUM_DATA_CALL_SESSIONS); 530 atoms.cellularServiceState = 531 sanitizeAtoms( 532 atoms.cellularServiceState, 533 CellularServiceState.class, 534 MAX_NUM_CELLULAR_SERVICE_STATES); 535 atoms.cellularDataServiceSwitch = 536 sanitizeAtoms( 537 atoms.cellularDataServiceSwitch, 538 CellularDataServiceSwitch.class, 539 MAX_NUM_CELLULAR_DATA_SERVICE_SWITCHES); 540 atoms.imsRegistrationStats = 541 sanitizeAtoms( 542 atoms.imsRegistrationStats, 543 ImsRegistrationStats.class, 544 MAX_NUM_IMS_REGISTRATION_STATS); 545 atoms.imsRegistrationTermination = 546 sanitizeAtoms( 547 atoms.imsRegistrationTermination, 548 ImsRegistrationTermination.class, 549 MAX_NUM_IMS_REGISTRATION_TERMINATIONS); 550 atoms.networkRequests = sanitizeAtoms(atoms.networkRequests, NetworkRequests.class); 551 // out of caution, sanitize also the timestamps 552 atoms.voiceCallRatUsagePullTimestampMillis = 553 sanitizeTimestamp(atoms.voiceCallRatUsagePullTimestampMillis); 554 atoms.voiceCallSessionPullTimestampMillis = 555 sanitizeTimestamp(atoms.voiceCallSessionPullTimestampMillis); 556 atoms.incomingSmsPullTimestampMillis = 557 sanitizeTimestamp(atoms.incomingSmsPullTimestampMillis); 558 atoms.outgoingSmsPullTimestampMillis = 559 sanitizeTimestamp(atoms.outgoingSmsPullTimestampMillis); 560 atoms.dataCallSessionPullTimestampMillis = 561 sanitizeTimestamp(atoms.dataCallSessionPullTimestampMillis); 562 atoms.cellularServiceStatePullTimestampMillis = 563 sanitizeTimestamp(atoms.cellularServiceStatePullTimestampMillis); 564 atoms.cellularDataServiceSwitchPullTimestampMillis = 565 sanitizeTimestamp(atoms.cellularDataServiceSwitchPullTimestampMillis); 566 atoms.imsRegistrationStatsPullTimestampMillis = 567 sanitizeTimestamp(atoms.imsRegistrationStatsPullTimestampMillis); 568 atoms.imsRegistrationTerminationPullTimestampMillis = 569 sanitizeTimestamp(atoms.imsRegistrationTerminationPullTimestampMillis); 570 atoms.networkRequestsPullTimestampMillis = 571 sanitizeTimestamp(atoms.networkRequestsPullTimestampMillis); 572 return atoms; 573 } catch (NoSuchFileException e) { 574 Rlog.d(TAG, "PersistAtoms file not found"); 575 } catch (IOException | NullPointerException e) { 576 Rlog.e(TAG, "cannot load/parse PersistAtoms", e); 577 } 578 return makeNewPersistAtoms(); 579 } 580 581 /** 582 * Posts message to save a copy of {@link PersistAtoms} to a file after a delay. 583 * 584 * <p>The delay is introduced to avoid too frequent operations to disk, which would negatively 585 * impact the power consumption. 586 */ saveAtomsToFile(int delayMillis)587 private void saveAtomsToFile(int delayMillis) { 588 if (delayMillis > 0 && !mSaveImmediately) { 589 mHandler.removeCallbacks(mSaveRunnable); 590 if (mHandler.postDelayed(mSaveRunnable, delayMillis)) { 591 return; 592 } 593 } 594 // In case of error posting the event or if delay is 0, save immediately 595 saveAtomsToFileNow(); 596 } 597 598 /** Saves a copy of {@link PersistAtoms} to a file in private storage. */ saveAtomsToFileNow()599 private synchronized void saveAtomsToFileNow() { 600 try (FileOutputStream stream = mContext.openFileOutput(FILENAME, Context.MODE_PRIVATE)) { 601 stream.write(PersistAtoms.toByteArray(mAtoms)); 602 } catch (IOException e) { 603 Rlog.e(TAG, "cannot save PersistAtoms", e); 604 } 605 } 606 607 /** 608 * Returns the service state that has the same dimension values with the given one, or {@code 609 * null} if it does not exist. 610 */ find(CellularServiceState key)611 private @Nullable CellularServiceState find(CellularServiceState key) { 612 for (CellularServiceState state : mAtoms.cellularServiceState) { 613 if (state.voiceRat == key.voiceRat 614 && state.dataRat == key.dataRat 615 && state.voiceRoamingType == key.voiceRoamingType 616 && state.dataRoamingType == key.dataRoamingType 617 && state.isEndc == key.isEndc 618 && state.simSlotIndex == key.simSlotIndex 619 && state.isMultiSim == key.isMultiSim 620 && state.carrierId == key.carrierId) { 621 return state; 622 } 623 } 624 return null; 625 } 626 627 /** 628 * Returns the data service switch that has the same dimension values with the given one, or 629 * {@code null} if it does not exist. 630 */ find(CellularDataServiceSwitch key)631 private @Nullable CellularDataServiceSwitch find(CellularDataServiceSwitch key) { 632 for (CellularDataServiceSwitch serviceSwitch : mAtoms.cellularDataServiceSwitch) { 633 if (serviceSwitch.ratFrom == key.ratFrom 634 && serviceSwitch.ratTo == key.ratTo 635 && serviceSwitch.simSlotIndex == key.simSlotIndex 636 && serviceSwitch.isMultiSim == key.isMultiSim 637 && serviceSwitch.carrierId == key.carrierId) { 638 return serviceSwitch; 639 } 640 } 641 return null; 642 } 643 644 /** 645 * Returns the carrier ID mismatch event that has the same dimension values with the given one, 646 * or {@code null} if it does not exist. 647 */ find(CarrierIdMismatch key)648 private @Nullable CarrierIdMismatch find(CarrierIdMismatch key) { 649 for (CarrierIdMismatch mismatch : mAtoms.carrierIdMismatch) { 650 if (mismatch.mccMnc.equals(key.mccMnc) 651 && mismatch.gid1.equals(key.gid1) 652 && mismatch.spn.equals(key.spn) 653 && mismatch.pnn.equals(key.pnn)) { 654 return mismatch; 655 } 656 } 657 return null; 658 } 659 660 /** 661 * Returns the IMS registration stats that has the same dimension values with the given one, or 662 * {@code null} if it does not exist. 663 */ find(ImsRegistrationStats key)664 private @Nullable ImsRegistrationStats find(ImsRegistrationStats key) { 665 for (ImsRegistrationStats stats : mAtoms.imsRegistrationStats) { 666 if (stats.carrierId == key.carrierId 667 && stats.simSlotIndex == key.simSlotIndex 668 && stats.rat == key.rat) { 669 return stats; 670 } 671 } 672 return null; 673 } 674 675 /** 676 * Returns the IMS registration termination that has the same dimension values with the given 677 * one, or {@code null} if it does not exist. 678 */ find(ImsRegistrationTermination key)679 private @Nullable ImsRegistrationTermination find(ImsRegistrationTermination key) { 680 for (ImsRegistrationTermination termination : mAtoms.imsRegistrationTermination) { 681 if (termination.carrierId == key.carrierId 682 && termination.isMultiSim == key.isMultiSim 683 && termination.ratAtEnd == key.ratAtEnd 684 && termination.setupFailed == key.setupFailed 685 && termination.reasonCode == key.reasonCode 686 && termination.extraCode == key.extraCode 687 && termination.extraMessage.equals(key.extraMessage)) { 688 return termination; 689 } 690 } 691 return null; 692 } 693 694 /** 695 * Returns the network requests event that has the same carrier id as the given one, 696 * or {@code null} if it does not exist. 697 */ find(NetworkRequests key)698 private @Nullable NetworkRequests find(NetworkRequests key) { 699 for (NetworkRequests item : mAtoms.networkRequests) { 700 if (item.carrierId == key.carrierId) { 701 return item; 702 } 703 } 704 return null; 705 } 706 707 /** 708 * Inserts a new element in a random position in an array with a maximum size, replacing the 709 * least recent item if possible. 710 */ insertAtRandomPlace(T[] storage, T instance, int maxLength)711 private static <T> T[] insertAtRandomPlace(T[] storage, T instance, int maxLength) { 712 final int newLength = storage.length + 1; 713 final boolean arrayFull = (newLength > maxLength); 714 T[] result = Arrays.copyOf(storage, arrayFull ? maxLength : newLength); 715 if (newLength == 1) { 716 result[0] = instance; 717 } else if (arrayFull) { 718 result[findItemToEvict(storage)] = instance; 719 } else { 720 // insert at random place (by moving the item at the random place to the end) 721 int insertAt = sRandom.nextInt(newLength); 722 result[newLength - 1] = result[insertAt]; 723 result[insertAt] = instance; 724 } 725 return result; 726 } 727 728 /** Returns index of the item suitable for eviction when the array is full. */ findItemToEvict(T[] array)729 private static <T> int findItemToEvict(T[] array) { 730 if (array instanceof CellularServiceState[]) { 731 CellularServiceState[] arr = (CellularServiceState[]) array; 732 return IntStream.range(0, arr.length) 733 .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j) 734 .getAsInt(); 735 } 736 737 if (array instanceof CellularDataServiceSwitch[]) { 738 CellularDataServiceSwitch[] arr = (CellularDataServiceSwitch[]) array; 739 return IntStream.range(0, arr.length) 740 .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j) 741 .getAsInt(); 742 } 743 744 if (array instanceof ImsRegistrationStats[]) { 745 ImsRegistrationStats[] arr = (ImsRegistrationStats[]) array; 746 return IntStream.range(0, arr.length) 747 .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j) 748 .getAsInt(); 749 } 750 751 if (array instanceof ImsRegistrationTermination[]) { 752 ImsRegistrationTermination[] arr = (ImsRegistrationTermination[]) array; 753 return IntStream.range(0, arr.length) 754 .reduce((i, j) -> arr[i].lastUsedMillis < arr[j].lastUsedMillis ? i : j) 755 .getAsInt(); 756 } 757 758 return sRandom.nextInt(array.length); 759 } 760 761 /** Sanitizes the loaded array of atoms to avoid null values. */ sanitizeAtoms(T[] array, Class<T> cl)762 private <T> T[] sanitizeAtoms(T[] array, Class<T> cl) { 763 return ArrayUtils.emptyIfNull(array, cl); 764 } 765 766 /** Sanitizes the loaded array of atoms loaded to avoid null values and enforce max length. */ sanitizeAtoms(T[] array, Class<T> cl, int maxLength)767 private <T> T[] sanitizeAtoms(T[] array, Class<T> cl, int maxLength) { 768 array = sanitizeAtoms(array, cl); 769 if (array.length > maxLength) { 770 return Arrays.copyOf(array, maxLength); 771 } 772 return array; 773 } 774 775 /** Sanitizes the timestamp of the last pull loaded from persistent storage. */ sanitizeTimestamp(long timestamp)776 private long sanitizeTimestamp(long timestamp) { 777 return timestamp <= 0L ? getWallTimeMillis() : timestamp; 778 } 779 780 /** Returns an empty PersistAtoms with pull timestamp set to current time. */ makeNewPersistAtoms()781 private PersistAtoms makeNewPersistAtoms() { 782 PersistAtoms atoms = new PersistAtoms(); 783 // allow pulling only after some time so data are sufficiently aggregated 784 long currentTime = getWallTimeMillis(); 785 atoms.buildFingerprint = Build.FINGERPRINT; 786 atoms.voiceCallRatUsagePullTimestampMillis = currentTime; 787 atoms.voiceCallSessionPullTimestampMillis = currentTime; 788 atoms.incomingSmsPullTimestampMillis = currentTime; 789 atoms.outgoingSmsPullTimestampMillis = currentTime; 790 atoms.carrierIdTableVersion = TelephonyManager.UNKNOWN_CARRIER_ID_LIST_VERSION; 791 atoms.dataCallSessionPullTimestampMillis = currentTime; 792 atoms.cellularServiceStatePullTimestampMillis = currentTime; 793 atoms.cellularDataServiceSwitchPullTimestampMillis = currentTime; 794 atoms.imsRegistrationStatsPullTimestampMillis = currentTime; 795 atoms.imsRegistrationTerminationPullTimestampMillis = currentTime; 796 atoms.networkRequestsPullTimestampMillis = currentTime; 797 Rlog.d(TAG, "created new PersistAtoms"); 798 return atoms; 799 } 800 801 @VisibleForTesting getWallTimeMillis()802 protected long getWallTimeMillis() { 803 // Epoch time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP 804 return System.currentTimeMillis(); 805 } 806 } 807