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 }