blob: 4e9ffc72141046e5fed7c1595ed620a9efa4fb94 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mms.service.metrics;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.mms.IncomingMms;
import com.android.mms.OutgoingMms;
import com.android.mms.PersistMmsAtoms;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PersistMmsAtomsStorage {
private static final String TAG = PersistMmsAtomsStorage.class.getSimpleName();
/** Name of the file where cached statistics are saved to. */
private static final String FILENAME = "persist_mms_atoms.pb";
/** Delay to store atoms to persistent storage to bundle multiple operations together. */
private static final int SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS = 30000;
/**
* Delay to store atoms to persistent storage during pulls to avoid unnecessary operations.
*
* <p>This delay should be short to avoid duplicating atoms or losing pull timestamp in case of
* crash or power loss.
*/
private static final int SAVE_TO_FILE_DELAY_FOR_GET_MILLIS = 500;
private static final SecureRandom sRandom = new SecureRandom();
/**
* Maximum number of MMS to store between pulls.
* Incoming MMS and outgoing MMS are counted separately.
*/
private final int mMaxNumMms;
private final Context mContext;
private final Handler mHandler;
private final HandlerThread mHandlerThread;
/** Stores persist atoms and persist states of the puller. */
@VisibleForTesting
protected PersistMmsAtoms mPersistMmsAtoms;
private final Runnable mSaveRunnable =
new Runnable() {
@Override
public void run() {
saveAtomsToFileNow();
}
};
/** Whether atoms should be saved immediately, skipping the delay. */
@VisibleForTesting
protected boolean mSaveImmediately;
public PersistMmsAtomsStorage(Context context) {
mContext = context;
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_RAM_LOW)) {
Log.i(TAG, "[PersistMmsAtomsStorage]: Low RAM device");
mMaxNumMms = 5;
} else {
mMaxNumMms = 25;
}
mPersistMmsAtoms = loadAtomsFromFile();
mHandlerThread = new HandlerThread("PersistMmsAtomsThread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mSaveImmediately = false;
}
/** Loads {@link PersistMmsAtoms} from a file in private storage. */
private PersistMmsAtoms loadAtomsFromFile() {
try {
PersistMmsAtoms atoms = PersistMmsAtoms.parseFrom(
Files.readAllBytes(mContext.getFileStreamPath(FILENAME).toPath()));
// Start from scratch if build changes, since mixing atoms from different builds could
// produce strange results.
if (!Build.FINGERPRINT.equals(atoms.getBuildFingerprint())) {
Log.d(TAG, "[loadAtomsFromFile]: Build changed");
return makeNewPersistMmsAtoms();
}
// check all the fields in case of situations such as OTA or crash during saving.
List<IncomingMms> incomingMms = sanitizeAtoms(atoms.getIncomingMmsList(), mMaxNumMms);
List<OutgoingMms> outgoingMms = sanitizeAtoms(atoms.getOutgoingMmsList(), mMaxNumMms);
long incomingMmsPullTimestamp = sanitizeTimestamp(
atoms.getIncomingMmsPullTimestampMillis());
long outgoingMmsPullTimestamp = sanitizeTimestamp(
atoms.getOutgoingMmsPullTimestampMillis());
// Rebuild atoms after sanitizing.
atoms = atoms.toBuilder()
.clearIncomingMms()
.clearOutgoingMms()
.addAllIncomingMms(incomingMms)
.addAllOutgoingMms(outgoingMms)
.setIncomingMmsPullTimestampMillis(incomingMmsPullTimestamp)
.setOutgoingMmsPullTimestampMillis(outgoingMmsPullTimestamp)
.build();
return atoms;
} catch (NoSuchFileException e) {
Log.e(TAG, "[loadAtomsFromFile]: PersistMmsAtoms file not found");
} catch (IOException | NullPointerException e) {
Log.e(TAG, "[loadAtomsFromFile]: cannot load/parse PersistMmsAtoms", e);
}
return makeNewPersistMmsAtoms();
}
/** Adds an IncomingMms to the storage. */
public synchronized void addIncomingMms(IncomingMms mms) {
int existingMmsIndex = findIndex(mms);
if (existingMmsIndex != -1) {
// Update mmsCount and avgIntervalMillis of existingMms.
IncomingMms existingMms = mPersistMmsAtoms.getIncomingMms(existingMmsIndex);
long updatedMmsCount = existingMms.getMmsCount() + 1;
long updatedAvgIntervalMillis =
(((existingMms.getAvgIntervalMillis() * existingMms.getMmsCount())
+ mms.getAvgIntervalMillis()) / updatedMmsCount);
existingMms = existingMms.toBuilder()
.setMmsCount(updatedMmsCount)
.setAvgIntervalMillis(updatedAvgIntervalMillis)
.build();
mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
.setIncomingMms(existingMmsIndex, existingMms)
.build();
} else {
// Insert new mms at random place.
List<IncomingMms> incomingMmsList = insertAtRandomPlace(
mPersistMmsAtoms.getIncomingMmsList(), mms, mMaxNumMms);
mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
.clearIncomingMms()
.addAllIncomingMms(incomingMmsList)
.build();
}
saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
}
/** Adds an OutgoingMms to the storage. */
public synchronized void addOutgoingMms(OutgoingMms mms) {
int existingMmsIndex = findIndex(mms);
if (existingMmsIndex != -1) {
// Update mmsCount and avgIntervalMillis of existingMms.
OutgoingMms existingMms = mPersistMmsAtoms.getOutgoingMms(existingMmsIndex);
long updatedMmsCount = existingMms.getMmsCount() + 1;
long updatedAvgIntervalMillis =
(((existingMms.getAvgIntervalMillis() * existingMms.getMmsCount())
+ mms.getAvgIntervalMillis()) / updatedMmsCount);
existingMms = existingMms.toBuilder()
.setMmsCount(updatedMmsCount)
.setAvgIntervalMillis(updatedAvgIntervalMillis)
.build();
mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
.setOutgoingMms(existingMmsIndex, existingMms)
.build();
} else {
// Insert new mms at random place.
List<OutgoingMms> outgoingMmsList = insertAtRandomPlace(
mPersistMmsAtoms.getOutgoingMmsList(), mms, mMaxNumMms);
mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
.clearOutgoingMms()
.addAllOutgoingMms(outgoingMmsList)
.build();
}
saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
}
/**
* Returns and clears the IncomingMms if last pulled longer than {@code minIntervalMillis} ago,
* otherwise returns {@code null}.
*/
@Nullable
public synchronized List<IncomingMms> getIncomingMms(long minIntervalMillis) {
if ((getWallTimeMillis() - mPersistMmsAtoms.getIncomingMmsPullTimestampMillis())
> minIntervalMillis) {
List<IncomingMms> previousIncomingMmsList = mPersistMmsAtoms.getIncomingMmsList();
mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
.setIncomingMmsPullTimestampMillis(getWallTimeMillis())
.clearIncomingMms()
.build();
saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
return previousIncomingMmsList;
} else {
return null;
}
}
/**
* Returns and clears the OutgoingMms if last pulled longer than {@code minIntervalMillis} ago,
* otherwise returns {@code null}.
*/
@Nullable
public synchronized List<OutgoingMms> getOutgoingMms(long minIntervalMillis) {
if ((getWallTimeMillis() - mPersistMmsAtoms.getOutgoingMmsPullTimestampMillis())
> minIntervalMillis) {
List<OutgoingMms> previousOutgoingMmsList = mPersistMmsAtoms.getOutgoingMmsList();
mPersistMmsAtoms = mPersistMmsAtoms.toBuilder()
.setOutgoingMmsPullTimestampMillis(getWallTimeMillis())
.clearOutgoingMms()
.build();
saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_GET_MILLIS);
return previousOutgoingMmsList;
} else {
return null;
}
}
/** Saves a pending {@link PersistMmsAtoms} to a file in private storage immediately. */
public void flushAtoms() {
if (mHandler.hasCallbacks(mSaveRunnable)) {
mHandler.removeCallbacks(mSaveRunnable);
saveAtomsToFileNow();
}
}
/** Returns an empty PersistMmsAtoms with pull timestamp set to current time. */
private PersistMmsAtoms makeNewPersistMmsAtoms() {
// allow pulling only after some time so data are sufficiently aggregated.
long currentTime = getWallTimeMillis();
PersistMmsAtoms atoms = PersistMmsAtoms.newBuilder()
.setBuildFingerprint(Build.FINGERPRINT)
.setIncomingMmsPullTimestampMillis(currentTime)
.setOutgoingMmsPullTimestampMillis(currentTime)
.build();
return atoms;
}
/**
* Posts message to save a copy of {@link PersistMmsAtoms} to a file after a delay.
*
* <p>The delay is introduced to avoid too frequent operations to disk, which would negatively
* impact the power consumption.
*/
private void saveAtomsToFile(int delayMillis) {
if (delayMillis > 0 && !mSaveImmediately) {
mHandler.removeCallbacks(mSaveRunnable);
if (mHandler.postDelayed(mSaveRunnable, delayMillis)) {
return;
}
}
// In case of error posting the event or if delay is 0, save immediately.
saveAtomsToFileNow();
}
/** Saves a copy of {@link PersistMmsAtoms} to a file in private storage. */
private synchronized void saveAtomsToFileNow() {
try (FileOutputStream stream = mContext.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
stream.write(mPersistMmsAtoms.toByteArray());
} catch (IOException e) {
Log.e(TAG, "[saveAtomsToFileNow]: Cannot save PersistMmsAtoms", e);
}
}
/**
* Inserts a new element in a random position.
*/
private static <T> List<T> insertAtRandomPlace(List<T> storage, T instance, int maxSize) {
final int storage_size = storage.size();
List<T> result = new ArrayList<>(storage);
if (storage_size == 0) {
result.add(instance);
} else if (storage_size == maxSize) {
// Index of the item suitable for eviction is chosen randomly when the array is full.
int insertAt = sRandom.nextInt(maxSize);
result.set(insertAt, instance);
} else {
// Insert at random place (by moving the item at the random place to the end).
int insertAt = sRandom.nextInt(storage_size);
result.add(result.get(insertAt));
result.set(insertAt, instance);
}
return result;
}
/**
* Returns IncomingMms atom index that has the same dimension values with the given one,
* or {@code -1} if it does not exist.
*/
private int findIndex(IncomingMms key) {
for (int i = 0; i < mPersistMmsAtoms.getIncomingMmsCount(); i++) {
IncomingMms mms = mPersistMmsAtoms.getIncomingMms(i);
if (mms.getRat() == key.getRat()
&& mms.getResult() == key.getResult()
&& mms.getRoaming() == key.getRoaming()
&& mms.getSimSlotIndex() == key.getSimSlotIndex()
&& mms.getIsMultiSim() == key.getIsMultiSim()
&& mms.getIsEsim() == key.getIsEsim()
&& mms.getCarrierId() == key.getCarrierId()
&& mms.getRetryId() == key.getRetryId()
&& mms.getHandledByCarrierApp() == key.getHandledByCarrierApp()) {
return i;
}
}
return -1;
}
/**
* Returns OutgoingMms atom index that has the same dimension values with the given one,
* or {@code -1} if it does not exist.
*/
private int findIndex(OutgoingMms key) {
for (int i = 0; i < mPersistMmsAtoms.getOutgoingMmsCount(); i++) {
OutgoingMms mms = mPersistMmsAtoms.getOutgoingMms(i);
if (mms.getRat() == key.getRat()
&& mms.getResult() == key.getResult()
&& mms.getRoaming() == key.getRoaming()
&& mms.getSimSlotIndex() == key.getSimSlotIndex()
&& mms.getIsMultiSim() == key.getIsMultiSim()
&& mms.getIsEsim() == key.getIsEsim()
&& mms.getCarrierId() == key.getCarrierId()
&& mms.getIsFromDefaultApp() == key.getIsFromDefaultApp()
&& mms.getRetryId() == key.getRetryId()
&& mms.getHandledByCarrierApp() == key.getHandledByCarrierApp()) {
return i;
}
}
return -1;
}
/** Sanitizes the loaded list of atoms to avoid null values. */
private <T> List<T> sanitizeAtoms(List<T> list) {
return list == null ? Collections.emptyList() : list;
}
/** Sanitizes the loaded list of atoms loaded to avoid null values and enforce max length. */
private <T> List<T> sanitizeAtoms(List<T> list, int maxSize) {
list = sanitizeAtoms(list);
if (list.size() > maxSize) {
return list.subList(0, maxSize);
}
return list;
}
/** Sanitizes the timestamp of the last pull loaded from persistent storage. */
private long sanitizeTimestamp(long timestamp) {
return timestamp <= 0L ? getWallTimeMillis() : timestamp;
}
@VisibleForTesting
protected long getWallTimeMillis() {
// Epoch time in UTC, preserved across reboots, but can be adjusted e.g. by the user or NTP.
return System.currentTimeMillis();
}
}