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