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.mms.service.metrics;
18 
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 import android.os.Build;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.util.Log;
25 import androidx.annotation.Nullable;
26 import androidx.annotation.VisibleForTesting;
27 
28 import com.android.mms.IncomingMms;
29 import com.android.mms.OutgoingMms;
30 import com.android.mms.PersistMmsAtoms;
31 
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.nio.file.Files;
35 import java.nio.file.NoSuchFileException;
36 import java.security.SecureRandom;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.List;
40 
41 public class PersistMmsAtomsStorage {
42     private static final String TAG = PersistMmsAtomsStorage.class.getSimpleName();
43 
44     /** Name of the file where cached statistics are saved to. */
45     private static final String FILENAME = "persist_mms_atoms.pb";
46 
47     /** Delay to store atoms to persistent storage to bundle multiple operations together. */
48     private static final int SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS = 30000;
49 
50     /**
51      * Delay to store atoms to persistent storage during pulls to avoid unnecessary operations.
52      *
53      * <p>This delay should be short to avoid duplicating atoms or losing pull timestamp in case of
54      * crash or power loss.
55      */
56     private static final int SAVE_TO_FILE_DELAY_FOR_GET_MILLIS = 500;
57     private static final SecureRandom sRandom = new SecureRandom();
58     /**
59      * Maximum number of MMS to store between pulls.
60      * Incoming MMS and outgoing MMS are counted separately.
61      */
62     private final int mMaxNumMms;
63     private final Context mContext;
64     private final Handler mHandler;
65     private final HandlerThread mHandlerThread;
66     /** Stores persist atoms and persist states of the puller. */
67     @VisibleForTesting
68     protected PersistMmsAtoms mPersistMmsAtoms;
69     private final Runnable mSaveRunnable =
70             new Runnable() {
71                 @Override
72                 public void run() {
73                     saveAtomsToFileNow();
74                 }
75             };
76     /** Whether atoms should be saved immediately, skipping the delay. */
77     @VisibleForTesting
78     protected boolean mSaveImmediately;
79 
PersistMmsAtomsStorage(Context context)80     public PersistMmsAtomsStorage(Context context) {
81         mContext = context;
82 
83         if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_RAM_LOW)) {
84             Log.i(TAG, "[PersistMmsAtomsStorage]: Low RAM device");
85             mMaxNumMms = 5;
86         } else {
87             mMaxNumMms = 25;
88         }
89         mPersistMmsAtoms = loadAtomsFromFile();
90         mHandlerThread = new HandlerThread("PersistMmsAtomsThread");
91         mHandlerThread.start();
92         mHandler = new Handler(mHandlerThread.getLooper());
93         mSaveImmediately = false;
94     }
95 
96     /** Loads {@link  PersistMmsAtoms} from a file in private storage. */
loadAtomsFromFile()97     private PersistMmsAtoms loadAtomsFromFile() {
98         try {
99             PersistMmsAtoms atoms = PersistMmsAtoms.parseFrom(
100                     Files.readAllBytes(mContext.getFileStreamPath(FILENAME).toPath()));
101 
102             // Start from scratch if build changes, since mixing atoms from different builds could
103             // produce strange results.
104             if (!Build.FINGERPRINT.equals(atoms.getBuildFingerprint())) {
105                 Log.d(TAG, "[loadAtomsFromFile]: Build changed");
106                 return makeNewPersistMmsAtoms();
107             }
108             // check all the fields in case of situations such as OTA or crash during saving.
109             List<IncomingMms> incomingMms = sanitizeAtoms(atoms.getIncomingMmsList(), mMaxNumMms);
110             List<OutgoingMms> outgoingMms = sanitizeAtoms(atoms.getOutgoingMmsList(), mMaxNumMms);
111             long incomingMmsPullTimestamp = sanitizeTimestamp(
112                     atoms.getIncomingMmsPullTimestampMillis());
113             long outgoingMmsPullTimestamp = sanitizeTimestamp(
114                     atoms.getOutgoingMmsPullTimestampMillis());
115 
116             // Rebuild atoms after sanitizing.
117             atoms = atoms.toBuilder()
118                     .clearIncomingMms()
119                     .clearOutgoingMms()
120                     .addAllIncomingMms(incomingMms)
121                     .addAllOutgoingMms(outgoingMms)
122                     .setIncomingMmsPullTimestampMillis(incomingMmsPullTimestamp)
123                     .setOutgoingMmsPullTimestampMillis(outgoingMmsPullTimestamp)
124                     .build();
125             return atoms;
126         } catch (NoSuchFileException e) {
127             Log.e(TAG, "[loadAtomsFromFile]: PersistMmsAtoms file not found");
128         } catch (IOException | NullPointerException e) {
129             Log.e(TAG, "[loadAtomsFromFile]: cannot load/parse PersistMmsAtoms", e);
130         }
131         return makeNewPersistMmsAtoms();
132     }
133 
134     /** Adds an IncomingMms to the storage. */
addIncomingMms(IncomingMms mms)135     public synchronized void addIncomingMms(IncomingMms mms) {
136         int existingMmsIndex = findIndex(mms);
137         if (existingMmsIndex != -1) {
138             // Update mmsCount and avgIntervalMillis of existingMms.
139             IncomingMms existingMms = mPersistMmsAtoms.getIncomingMms(existingMmsIndex);
140             long updatedMmsCount = existingMms.getMmsCount() + 1;
141             long updatedAvgIntervalMillis =
142                     (((existingMms.getAvgIntervalMillis() * existingMms.getMmsCount())
143                             + mms.getAvgIntervalMillis()) / updatedMmsCount);
144             existingMms = existingMms.toBuilder()
145                     .setMmsCount(updatedMmsCount)
146                     .setAvgIntervalMillis(updatedAvgIntervalMillis)
147                     .build();
148 
149             mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
150                     .setIncomingMms(existingMmsIndex, existingMms)
151                     .build();
152         } else {
153             // Insert new mms at random place.
154             List<IncomingMms> incomingMmsList = insertAtRandomPlace(
155                     mPersistMmsAtoms.getIncomingMmsList(), mms, mMaxNumMms);
156             mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
157                     .clearIncomingMms()
158                     .addAllIncomingMms(incomingMmsList)
159                     .build();
160         }
161         saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
162     }
163 
164     /** Adds an OutgoingMms to the storage. */
addOutgoingMms(OutgoingMms mms)165     public synchronized void addOutgoingMms(OutgoingMms mms) {
166         int existingMmsIndex = findIndex(mms);
167         if (existingMmsIndex != -1) {
168             // Update mmsCount and avgIntervalMillis of existingMms.
169             OutgoingMms existingMms = mPersistMmsAtoms.getOutgoingMms(existingMmsIndex);
170             long updatedMmsCount = existingMms.getMmsCount() + 1;
171             long updatedAvgIntervalMillis =
172                     (((existingMms.getAvgIntervalMillis() * existingMms.getMmsCount())
173                             + mms.getAvgIntervalMillis()) / updatedMmsCount);
174             existingMms = existingMms.toBuilder()
175                     .setMmsCount(updatedMmsCount)
176                     .setAvgIntervalMillis(updatedAvgIntervalMillis)
177                     .build();
178 
179             mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
180                     .setOutgoingMms(existingMmsIndex, existingMms)
181                     .build();
182         } else {
183             // Insert new mms at random place.
184             List<OutgoingMms> outgoingMmsList = insertAtRandomPlace(
185                     mPersistMmsAtoms.getOutgoingMmsList(), mms, mMaxNumMms);
186             mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
187                     .clearOutgoingMms()
188                     .addAllOutgoingMms(outgoingMmsList)
189                     .build();
190         }
191         saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
192     }
193 
194     /**
195      * Returns and clears the IncomingMms if last pulled longer than {@code minIntervalMillis} ago,
196      * otherwise returns {@code null}.
197      */
198     @Nullable
getIncomingMms(long minIntervalMillis)199     public synchronized List<IncomingMms> getIncomingMms(long minIntervalMillis) {
200         if ((getWallTimeMillis() - mPersistMmsAtoms.getIncomingMmsPullTimestampMillis())
201                 > minIntervalMillis) {
202             List<IncomingMms> previousIncomingMmsList = mPersistMmsAtoms.getIncomingMmsList();
203             mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
204                     .setIncomingMmsPullTimestampMillis(getWallTimeMillis())
205                     .clearIncomingMms()
206                     .build();
207             saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
208             return previousIncomingMmsList;
209         } else {
210             return null;
211         }
212     }
213 
214     /**
215      * Returns and clears the OutgoingMms if last pulled longer than {@code minIntervalMillis} ago,
216      * otherwise returns {@code null}.
217      */
218     @Nullable
getOutgoingMms(long minIntervalMillis)219     public synchronized List<OutgoingMms> getOutgoingMms(long minIntervalMillis) {
220         if ((getWallTimeMillis() - mPersistMmsAtoms.getOutgoingMmsPullTimestampMillis())
221                 > minIntervalMillis) {
222             List<OutgoingMms> previousOutgoingMmsList = mPersistMmsAtoms.getOutgoingMmsList();
223             mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
224                     .setOutgoingMmsPullTimestampMillis(getWallTimeMillis())
225                     .clearOutgoingMms()
226                     .build();
227             saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
228             return previousOutgoingMmsList;
229         } else {
230             return null;
231         }
232     }
233 
234     /** Saves a pending {@link PersistMmsAtoms} to a file in private storage immediately. */
flushAtoms()235     public void flushAtoms() {
236         if (mHandler.hasCallbacks(mSaveRunnable)) {
237             mHandler.removeCallbacks(mSaveRunnable);
238             saveAtomsToFileNow();
239         }
240     }
241 
242     /** Returns an empty PersistMmsAtoms with pull timestamp set to current time. */
makeNewPersistMmsAtoms()243     private PersistMmsAtoms makeNewPersistMmsAtoms() {
244         // allow pulling only after some time so data are sufficiently aggregated.
245         long currentTime = getWallTimeMillis();
246         PersistMmsAtoms atoms = PersistMmsAtoms.newBuilder()
247                 .setBuildFingerprint(Build.FINGERPRINT)
248                 .setIncomingMmsPullTimestampMillis(currentTime)
249                 .setOutgoingMmsPullTimestampMillis(currentTime)
250                 .build();
251         return atoms;
252     }
253 
254     /**
255      * Posts message to save a copy of {@link PersistMmsAtoms} to a file after a delay.
256      *
257      * <p>The delay is introduced to avoid too frequent operations to disk, which would negatively
258      * impact the power consumption.
259      */
saveAtomsToFile(int delayMillis)260     private void saveAtomsToFile(int delayMillis) {
261         if (delayMillis > 0 && !mSaveImmediately) {
262             mHandler.removeCallbacks(mSaveRunnable);
263             if (mHandler.postDelayed(mSaveRunnable, delayMillis)) {
264                 return;
265             }
266         }
267         // In case of error posting the event or if delay is 0, save immediately.
268         saveAtomsToFileNow();
269     }
270 
271     /** Saves a copy of {@link PersistMmsAtoms} to a file in private storage. */
saveAtomsToFileNow()272     private synchronized void saveAtomsToFileNow() {
273         try (FileOutputStream stream = mContext.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
274             stream.write(mPersistMmsAtoms.toByteArray());
275         } catch (IOException e) {
276             Log.e(TAG, "[saveAtomsToFileNow]: Cannot save PersistMmsAtoms", e);
277         }
278     }
279 
280     /**
281      * Inserts a new element in a random position.
282      */
insertAtRandomPlace(List<T> storage, T instance, int maxSize)283     private static <T> List<T> insertAtRandomPlace(List<T> storage, T instance, int maxSize) {
284         final int storage_size = storage.size();
285         List<T> result = new ArrayList<>(storage);
286         if (storage_size == 0) {
287             result.add(instance);
288         } else if (storage_size == maxSize) {
289             // Index of the item suitable for eviction is chosen randomly when the array is full.
290             int insertAt = sRandom.nextInt(maxSize);
291             result.set(insertAt, instance);
292         } else {
293             // Insert at random place (by moving the item at the random place to the end).
294             int insertAt = sRandom.nextInt(storage_size);
295             result.add(result.get(insertAt));
296             result.set(insertAt, instance);
297         }
298         return result;
299     }
300 
301     /**
302      * Returns IncomingMms atom index that has the same dimension values with the given one,
303      * or {@code -1} if it does not exist.
304      */
findIndex(IncomingMms key)305     private int findIndex(IncomingMms key) {
306         for (int i = 0; i < mPersistMmsAtoms.getIncomingMmsCount(); i++) {
307             IncomingMms mms = mPersistMmsAtoms.getIncomingMms(i);
308             if (mms.getRat() == key.getRat()
309                     && mms.getResult() == key.getResult()
310                     && mms.getRoaming() == key.getRoaming()
311                     && mms.getSimSlotIndex() == key.getSimSlotIndex()
312                     && mms.getIsMultiSim() == key.getIsMultiSim()
313                     && mms.getIsEsim() == key.getIsEsim()
314                     && mms.getCarrierId() == key.getCarrierId()
315                     && mms.getRetryId() == key.getRetryId()
316                     && mms.getHandledByCarrierApp() == key.getHandledByCarrierApp()) {
317                 return i;
318             }
319         }
320         return -1;
321     }
322 
323     /**
324      * Returns OutgoingMms atom index that has the same dimension values with the given one,
325      * or {@code -1} if it does not exist.
326      */
findIndex(OutgoingMms key)327     private int findIndex(OutgoingMms key) {
328         for (int i = 0; i < mPersistMmsAtoms.getOutgoingMmsCount(); i++) {
329             OutgoingMms mms = mPersistMmsAtoms.getOutgoingMms(i);
330             if (mms.getRat() == key.getRat()
331                     && mms.getResult() == key.getResult()
332                     && mms.getRoaming() == key.getRoaming()
333                     && mms.getSimSlotIndex() == key.getSimSlotIndex()
334                     && mms.getIsMultiSim() == key.getIsMultiSim()
335                     && mms.getIsEsim() == key.getIsEsim()
336                     && mms.getCarrierId() == key.getCarrierId()
337                     && mms.getIsFromDefaultApp() == key.getIsFromDefaultApp()
338                     && mms.getRetryId() == key.getRetryId()
339                     && mms.getHandledByCarrierApp() == key.getHandledByCarrierApp()) {
340                 return i;
341             }
342         }
343         return -1;
344     }
345 
346     /** Sanitizes the loaded list of atoms to avoid null values. */
sanitizeAtoms(List<T> list)347     private <T> List<T> sanitizeAtoms(List<T> list) {
348         return list == null ? Collections.emptyList() : list;
349     }
350 
351     /** Sanitizes the loaded list of atoms loaded to avoid null values and enforce max length. */
sanitizeAtoms(List<T> list, int maxSize)352     private <T> List<T> sanitizeAtoms(List<T> list, int maxSize) {
353         list = sanitizeAtoms(list);
354         if (list.size() > maxSize) {
355             return list.subList(0, maxSize);
356         }
357         return list;
358     }
359 
360     /** Sanitizes the timestamp of the last pull loaded from persistent storage. */
sanitizeTimestamp(long timestamp)361     private long sanitizeTimestamp(long timestamp) {
362         return timestamp <= 0L ? getWallTimeMillis() : timestamp;
363     }
364 
365     @VisibleForTesting
getWallTimeMillis()366     protected long getWallTimeMillis() {
367         // Epoch time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP.
368         return System.currentTimeMillis();
369     }
370 }