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 }