• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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