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 
22 import com.android.internal.annotations.VisibleForTesting;
23 import com.android.internal.telephony.nano.PersistAtomsProto.PersistAtoms;
24 import com.android.internal.telephony.nano.PersistAtomsProto.RawVoiceCallRatUsage;
25 import com.android.internal.telephony.nano.PersistAtomsProto.VoiceCallSession;
26 import com.android.telephony.Rlog;
27 
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.nio.file.Files;
31 import java.security.SecureRandom;
32 import java.util.Arrays;
33 
34 /**
35  * Stores and aggregates metrics that should not be pulled at arbitrary frequency.
36  *
37  * <p>NOTE: while this class checks timestamp against {@code minIntervalMillis}, it is {@link
38  * MetricsCollector}'s responsibility to ensure {@code minIntervalMillis} is set correctly.
39  */
40 public class PersistAtomsStorage {
41     private static final String TAG = PersistAtomsStorage.class.getSimpleName();
42 
43     /** Name of the file where cached statistics are saved to. */
44     private static final String FILENAME = "persist_atoms.pb";
45 
46     /** Maximum number of call sessions to store during pulls. */
47     private static final int MAX_NUM_CALL_SESSIONS = 50;
48 
49     /** Stores persist atoms and persist states of the puller. */
50     @VisibleForTesting protected final PersistAtoms mAtoms;
51 
52     /** Aggregates RAT duration and call count. */
53     private final VoiceCallRatTracker mVoiceCallRatTracker;
54 
55     private final Context mContext;
56     private static final SecureRandom sRandom = new SecureRandom();
57 
PersistAtomsStorage(Context context)58     public PersistAtomsStorage(Context context) {
59         mContext = context;
60         mAtoms = loadAtomsFromFile();
61         mVoiceCallRatTracker = VoiceCallRatTracker.fromProto(mAtoms.rawVoiceCallRatUsage);
62     }
63 
64     /** Adds a call to the storage. */
addVoiceCallSession(VoiceCallSession call)65     public synchronized void addVoiceCallSession(VoiceCallSession call) {
66         int newLength = mAtoms.voiceCallSession.length + 1;
67         if (newLength > MAX_NUM_CALL_SESSIONS) {
68             // will evict one previous call randomly instead of making the array larger
69             newLength = MAX_NUM_CALL_SESSIONS;
70         } else {
71             mAtoms.voiceCallSession = Arrays.copyOf(mAtoms.voiceCallSession, newLength);
72         }
73         int insertAt = 0;
74         if (newLength > 1) {
75             // shuffle when each call is added, or randomly replace a previous call instead if
76             // MAX_NUM_CALL_SESSIONS is reached (call at the last index is evicted).
77             insertAt = sRandom.nextInt(newLength);
78             mAtoms.voiceCallSession[newLength - 1] = mAtoms.voiceCallSession[insertAt];
79         }
80         mAtoms.voiceCallSession[insertAt] = call;
81         saveAtomsToFile();
82     }
83 
84     /** Adds RAT usages to the storage when a call session ends. */
addVoiceCallRatUsage(VoiceCallRatTracker ratUsages)85     public synchronized void addVoiceCallRatUsage(VoiceCallRatTracker ratUsages) {
86         mVoiceCallRatTracker.mergeWith(ratUsages);
87         mAtoms.rawVoiceCallRatUsage = mVoiceCallRatTracker.toProto();
88         saveAtomsToFile();
89     }
90 
91     /**
92      * Returns and clears the voice call sessions if last pulled longer than {@code
93      * minIntervalMillis} ago, otherwise returns {@code null}.
94      */
95     @Nullable
getVoiceCallSessions(long minIntervalMillis)96     public synchronized VoiceCallSession[] getVoiceCallSessions(long minIntervalMillis) {
97         if (getWallTimeMillis() - mAtoms.voiceCallSessionPullTimestampMillis > minIntervalMillis) {
98             mAtoms.voiceCallSessionPullTimestampMillis = getWallTimeMillis();
99             VoiceCallSession[] previousCalls = mAtoms.voiceCallSession;
100             mAtoms.voiceCallSession = new VoiceCallSession[0];
101             saveAtomsToFile();
102             return previousCalls;
103         } else {
104             return null;
105         }
106     }
107 
108     /**
109      * Returns and clears the voice call RAT usages if last pulled longer than {@code
110      * minIntervalMillis} ago, otherwise returns {@code null}.
111      */
112     @Nullable
getVoiceCallRatUsages(long minIntervalMillis)113     public synchronized RawVoiceCallRatUsage[] getVoiceCallRatUsages(long minIntervalMillis) {
114         if (getWallTimeMillis() - mAtoms.rawVoiceCallRatUsagePullTimestampMillis
115                 > minIntervalMillis) {
116             mAtoms.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis();
117             RawVoiceCallRatUsage[] previousUsages = mAtoms.rawVoiceCallRatUsage;
118             mVoiceCallRatTracker.clear();
119             mAtoms.rawVoiceCallRatUsage = new RawVoiceCallRatUsage[0];
120             saveAtomsToFile();
121             return previousUsages;
122         } else {
123             return null;
124         }
125     }
126 
127     /** Loads {@link PersistAtoms} from a file in private storage. */
loadAtomsFromFile()128     private PersistAtoms loadAtomsFromFile() {
129         try {
130             PersistAtoms atomsFromFile =
131                     PersistAtoms.parseFrom(
132                             Files.readAllBytes(mContext.getFileStreamPath(FILENAME).toPath()));
133             // check all the fields in case of situations such as OTA or crash during saving
134             if (atomsFromFile.rawVoiceCallRatUsage == null) {
135                 atomsFromFile.rawVoiceCallRatUsage = new RawVoiceCallRatUsage[0];
136             }
137             if (atomsFromFile.voiceCallSession == null) {
138                 atomsFromFile.voiceCallSession = new VoiceCallSession[0];
139             }
140             if (atomsFromFile.voiceCallSession.length > MAX_NUM_CALL_SESSIONS) {
141                 atomsFromFile.voiceCallSession =
142                         Arrays.copyOf(atomsFromFile.voiceCallSession, MAX_NUM_CALL_SESSIONS);
143             }
144             // out of caution, set timestamps to now if they are missing
145             if (atomsFromFile.rawVoiceCallRatUsagePullTimestampMillis == 0L) {
146                 atomsFromFile.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis();
147             }
148             if (atomsFromFile.voiceCallSessionPullTimestampMillis == 0L) {
149                 atomsFromFile.voiceCallSessionPullTimestampMillis = getWallTimeMillis();
150             }
151             return atomsFromFile;
152         } catch (IOException | NullPointerException e) {
153             Rlog.e(TAG, "cannot load/parse PersistAtoms", e);
154             return makeNewPersistAtoms();
155         }
156     }
157 
158     /** Saves a copy of {@link PersistAtoms} to a file in private storage. */
saveAtomsToFile()159     private void saveAtomsToFile() {
160         try (FileOutputStream stream = mContext.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
161             stream.write(PersistAtoms.toByteArray(mAtoms));
162         } catch (IOException e) {
163             Rlog.e(TAG, "cannot save PersistAtoms", e);
164         }
165     }
166 
167     /** Returns an empty PersistAtoms with pull timestamp set to current time. */
makeNewPersistAtoms()168     private PersistAtoms makeNewPersistAtoms() {
169         PersistAtoms atoms = new PersistAtoms();
170         // allow pulling only after some time so data are sufficiently aggregated
171         atoms.rawVoiceCallRatUsagePullTimestampMillis = getWallTimeMillis();
172         atoms.voiceCallSessionPullTimestampMillis = getWallTimeMillis();
173         Rlog.d(TAG, "created new PersistAtoms");
174         return atoms;
175     }
176 
177     @VisibleForTesting
getWallTimeMillis()178     protected long getWallTimeMillis() {
179         // epoch time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP
180         return System.currentTimeMillis();
181     }
182 }
183