summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2022-12-09 10:30:37 -0800
committerXin Li <delphij@google.com>2022-12-09 10:30:37 -0800
commita3e8a55fccabc67390dcad357316a3f8abc1d04f (patch)
treea04d268b0a3a7239f969c13f5e1c23f4a90352ae
parentb4c34c3c85f8c42c6bd582417e891b75ca634635 (diff)
parent2b92f4683318dfb7c6441bdc4284c82d0682724a (diff)
downloadMms-main-16k-with-phones.tar.gz
Merge Android 13 QPR1main-16k-with-phones
Bug: 261731544 Merged-In: I956efc3881df9b3a6ce8ddfd717fe4954c681a71 Change-Id: I35ac7e0346b5a97249240d2d559ae509d77020fc
-rw-r--r--Android.bp27
-rw-r--r--proto/Android.bp29
-rw-r--r--proto/src/persist_mms_atoms.proto68
-rw-r--r--src/com/android/mms/service/DownloadRequest.java6
-rw-r--r--src/com/android/mms/service/MmsRequest.java17
-rw-r--r--src/com/android/mms/service/MmsService.java55
-rw-r--r--src/com/android/mms/service/SendRequest.java6
-rw-r--r--src/com/android/mms/service/metrics/MmsMetricsCollector.java164
-rw-r--r--src/com/android/mms/service/metrics/MmsStats.java213
-rw-r--r--src/com/android/mms/service/metrics/PersistMmsAtomsStorage.java370
-rw-r--r--tests/unittests/Android.bp27
-rw-r--r--tests/unittests/AndroidManifest.xml31
-rw-r--r--tests/unittests/AndroidTest.xml29
-rw-r--r--tests/unittests/src/com/android/mms/service/metrics/MmsMetricsCollectorTest.java142
-rw-r--r--tests/unittests/src/com/android/mms/service/metrics/MmsStatsTest.java129
-rw-r--r--tests/unittests/src/com/android/mms/service/metrics/PersistMmsAtomsStorageTest.java734
16 files changed, 2024 insertions, 23 deletions
diff --git a/Android.bp b/Android.bp
index e7e78ae..2917cfc 100644
--- a/Android.bp
+++ b/Android.bp
@@ -19,6 +19,21 @@ package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
+genrule {
+ name: "statslog-mms-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module mms"
+ + " --javaPackage com.android.mms --javaClass MmsStatsLog",
+ out: ["com/android/mms/MmsStatsLog.java"],
+}
+
+java_library {
+ name: "mms-statsd",
+ srcs: [
+ ":statslog-mms-java-gen",
+ ],
+}
+
android_app {
name: "MmsService",
platform_apis: true,
@@ -30,4 +45,16 @@ android_app {
proguard_flags_files: ["proguard.flags"],
},
certificate: "platform",
+ static_libs: [
+ "mms-protos-lite",
+ "mms-statsd",
+ "androidx.annotation_annotation",
+ ],
}
+
+filegroup {
+ name: "mms-metrics-srcs",
+ srcs: [
+ "src/com/android/mms/service/metrics/*.java",
+ ],
+} \ No newline at end of file
diff --git a/proto/Android.bp b/proto/Android.bp
new file mode 100644
index 0000000..b55e987
--- /dev/null
+++ b/proto/Android.bp
@@ -0,0 +1,29 @@
+/*
+ * 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ }
+
+ java_library_static {
+ name: "mms-protos-lite",
+ proto: {
+ type: "lite",
+ },
+ sdk_version: "system_current",
+ min_sdk_version: "33",
+ srcs: ["src/persist_mms_atoms.proto"],
+ } \ No newline at end of file
diff --git a/proto/src/persist_mms_atoms.proto b/proto/src/persist_mms_atoms.proto
new file mode 100644
index 0000000..c562246
--- /dev/null
+++ b/proto/src/persist_mms_atoms.proto
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.mms;
+option java_package = "com.android.mms";
+option java_outer_classname = "PersistMmsAtomsProto";
+option java_multiple_files = true;
+
+message PersistMmsAtoms {
+ /* Last Android build fingerprint. This usually changes after system OTA. */
+ optional string build_fingerprint = 1;
+
+ /* Incoming MMS statistics and information. */
+ repeated IncomingMms incoming_mms = 2;
+
+ /* Timestamp of last incoming_mms pull. */
+ optional int64 incoming_mms_pull_timestamp_millis = 3;
+
+ /* Outgoing MMS statistics and information. */
+ repeated OutgoingMms outgoing_mms = 4;
+
+ /* Timestamp of last outgoing_mms pull. */
+ optional int64 outgoing_mms_pull_timestamp_millis = 5;
+}
+
+message IncomingMms {
+ optional int32 rat = 1;
+ optional int32 result = 2;
+ optional int32 roaming = 3;
+ optional int32 sim_slot_index = 4;
+ optional bool is_multi_sim = 5;
+ optional bool is_esim = 6;
+ optional int32 carrier_id = 7;
+ optional int64 avg_interval_millis = 8;
+ optional int64 mms_count = 9;
+ optional int32 retry_id = 10;
+ optional bool handled_by_carrier_app = 11;
+}
+
+message OutgoingMms {
+ optional int32 rat = 1;
+ optional int32 result = 2;
+ optional int32 roaming = 3;
+ optional int32 sim_slot_index = 4;
+ optional bool is_multi_sim = 5;
+ optional bool is_esim = 6;
+ optional int32 carrier_id = 7;
+ optional int64 avg_interval_millis = 8;
+ optional int64 mms_count = 9;
+ optional bool is_from_default_app = 10;
+ optional int32 retry_id = 11;
+ optional bool handled_by_carrier_app = 12;
+} \ No newline at end of file
diff --git a/src/com/android/mms/service/DownloadRequest.java b/src/com/android/mms/service/DownloadRequest.java
index 0f12415..d81b384 100644
--- a/src/com/android/mms/service/DownloadRequest.java
+++ b/src/com/android/mms/service/DownloadRequest.java
@@ -38,6 +38,8 @@ import android.telephony.SmsManager;
import android.text.TextUtils;
import com.android.mms.service.exception.MmsHttpException;
+import com.android.mms.service.metrics.MmsStats;
+
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.GenericPdu;
import com.google.android.mms.pdu.PduHeaders;
@@ -59,8 +61,8 @@ public class DownloadRequest extends MmsRequest {
public DownloadRequest(RequestManager manager, int subId, String locationUrl,
Uri contentUri, PendingIntent downloadedIntent, String creator,
- Bundle configOverrides, Context context, long messageId) {
- super(manager, subId, creator, configOverrides, context, messageId);
+ Bundle configOverrides, Context context, long messageId, MmsStats mmsStats) {
+ super(manager, subId, creator, configOverrides, context, messageId, mmsStats);
mLocationUrl = locationUrl;
mDownloadedIntent = downloadedIntent;
mContentUri = contentUri;
diff --git a/src/com/android/mms/service/MmsRequest.java b/src/com/android/mms/service/MmsRequest.java
index dfef1cc..32e4c6e 100644
--- a/src/com/android/mms/service/MmsRequest.java
+++ b/src/com/android/mms/service/MmsRequest.java
@@ -39,6 +39,7 @@ import android.telephony.ims.stub.ImsRegistrationImplBase;
import com.android.mms.service.exception.ApnException;
import com.android.mms.service.exception.MmsHttpException;
import com.android.mms.service.exception.MmsNetworkException;
+import com.android.mms.service.metrics.MmsStats;
import java.util.UUID;
@@ -101,6 +102,7 @@ public abstract class MmsRequest {
protected Context mContext;
protected long mMessageId;
protected int mLastConnectionFailure;
+ private MmsStats mMmsStats;
class MonitorTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.PreciseDataConnectionStateListener {
@@ -121,13 +123,14 @@ public abstract class MmsRequest {
}
public MmsRequest(RequestManager requestManager, int subId, String creator,
- Bundle mmsConfig, Context context, long messageId) {
+ Bundle mmsConfig, Context context, long messageId, MmsStats mmsStats) {
mRequestManager = requestManager;
mSubId = subId;
mCreator = creator;
mMmsConfig = mmsConfig;
mContext = context;
mMessageId = messageId;
+ mMmsStats = mmsStats;
}
public int getSubId() {
@@ -146,6 +149,7 @@ public abstract class MmsRequest {
int result = SmsManager.MMS_ERROR_UNSPECIFIED;
int httpStatusCode = 0;
byte[] response = null;
+ int retryId = 0;
// TODO: add mms data channel check back to fast fail if no way to send mms,
// when telephony provides such API.
if (!prepareForHttpRequest()) { // Prepare request, like reading pdu data from user
@@ -154,7 +158,7 @@ public abstract class MmsRequest {
} else { // Execute
long retryDelaySecs = 2;
// Try multiple times of MMS HTTP request, depending on the error.
- for (int i = 0; i < RETRY_TIMES; i++) {
+ for (retryId = 0; retryId < RETRY_TIMES; retryId++) {
httpStatusCode = 0; // Clear for retry.
MonitorTelephonyCallback connectionStateCallback = new MonitorTelephonyCallback();
try {
@@ -212,7 +216,8 @@ public abstract class MmsRequest {
retryDelaySecs <<= 1;
}
}
- processResult(context, result, response, httpStatusCode, /* handledByCarrierApp= */ false);
+ processResult(context, result, response, httpStatusCode, /* handledByCarrierApp= */ false,
+ retryId);
}
private void listenToDataConnectionState(MonitorTelephonyCallback connectionStateCallback) {
@@ -240,6 +245,11 @@ public abstract class MmsRequest {
*/
public void processResult(Context context, int result, byte[] response, int httpStatusCode,
boolean handledByCarrierApp) {
+ processResult(context, result, response, httpStatusCode, handledByCarrierApp, 0);
+ }
+
+ private void processResult(Context context, int result, byte[] response, int httpStatusCode,
+ boolean handledByCarrierApp, int retryId) {
final Uri messageUri = persistIfRequired(context, result, response);
final String requestId = this.getRequestId();
@@ -276,6 +286,7 @@ public abstract class MmsRequest {
}
reportPossibleAnomaly(result, httpStatusCode);
pendingIntent.send(context, result, fillIn);
+ mMmsStats.addAtomToStorage(result, retryId, handledByCarrierApp);
} catch (PendingIntent.CanceledException e) {
LogUtil.e(requestId, "Sending pending intent canceled", e);
}
diff --git a/src/com/android/mms/service/MmsService.java b/src/com/android/mms/service/MmsService.java
index d227b3c..2f3b3aa 100644
--- a/src/com/android/mms/service/MmsService.java
+++ b/src/com/android/mms/service/MmsService.java
@@ -51,6 +51,8 @@ import android.util.EventLog;
import android.util.SparseArray;
import com.android.internal.telephony.IMms;
+import com.android.mms.service.metrics.MmsMetricsCollector;
+import com.android.mms.service.metrics.MmsStats;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.DeliveryInd;
@@ -138,6 +140,8 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
// 1: download queue
private final ExecutorService[] mRunningRequestExecutors = new ExecutorService[2];
+ private static MmsMetricsCollector mMmsMetricsCollector;
+
private MmsNetworkManager getNetworkManager(int subId) {
synchronized (mNetworkManagerCache) {
MmsNetworkManager manager = mNetworkManagerCache.get(subId);
@@ -211,19 +215,24 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
LogUtil.d("sendMessage " + formatCrossStackMessageId(messageId));
enforceSystemUid();
+ MmsStats mmsStats = new MmsStats(MmsService.this,
+ mMmsMetricsCollector.getAtomsStorage(), subId, getTelephonyManager(subId),
+ callingPkg, false);
+
// Make sure the subId is correct
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
LogUtil.e("Invalid subId " + subId);
- sendErrorInPendingIntent(sentIntent, SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID);
+ handleError(sentIntent, SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID, mmsStats);
return;
}
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
subId = SubscriptionManager.getDefaultSmsSubscriptionId();
+ mmsStats.updateSubId(subId, getTelephonyManager(subId));
}
// Make sure the subId is active
if (!isActiveSubId(subId)) {
- sendErrorInPendingIntent(sentIntent, SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION);
+ handleError(sentIntent, SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION, mmsStats);
return;
}
@@ -231,7 +240,7 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
Bundle mmsConfig = loadMmsConfig(subId);
if (mmsConfig == null) {
LogUtil.e("MMS config is not loaded yet for subId " + subId);
- sendErrorInPendingIntent(sentIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR);
+ handleError(sentIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
return;
}
@@ -243,12 +252,13 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
// Make sure MMS is enabled
if (!mmsConfig.getBoolean(SmsManager.MMS_CONFIG_MMS_ENABLED)) {
LogUtil.e("MMS is not enabled for subId " + subId);
- sendErrorInPendingIntent(sentIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR);
+ handleError(sentIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
return;
}
final SendRequest request = new SendRequest(MmsService.this, subId, contentUri,
- locationUrl, sentIntent, callingPkg, mmsConfig, MmsService.this, messageId);
+ locationUrl, sentIntent, callingPkg, mmsConfig, MmsService.this,
+ messageId, mmsStats);
final String carrierMessagingServicePackage =
getCarrierMessagingServicePackageIfExists(subId);
@@ -269,7 +279,7 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
// AcknowledgeInd and NotifyRespInd are parts of downloading sequence.
// TODO: Should consider ReadRecInd(Read Report)?
sendSettingsIntentForFailedMms(!isRawPduSendReq(contentUri), subId);
- sendErrorInPendingIntent(sentIntent, SmsManager.MMS_ERROR_NO_DATA_NETWORK);
+ handleError(sentIntent, SmsManager.MMS_ERROR_NO_DATA_NETWORK, mmsStats);
return;
}
@@ -288,22 +298,27 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
enforceSystemUid();
+ MmsStats mmsStats = new MmsStats(MmsService.this,
+ mMmsMetricsCollector.getAtomsStorage(), subId, getTelephonyManager(subId),
+ callingPkg, true);
+
// Make sure the subId is correct
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
LogUtil.e("Invalid subId " + subId);
- sendErrorInPendingIntent(downloadedIntent,
- SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID);
+ handleError(downloadedIntent, SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID,
+ mmsStats);
return;
}
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
subId = SubscriptionManager.getDefaultSmsSubscriptionId();
+ mmsStats.updateSubId(subId, getTelephonyManager(subId));
}
if (!isActiveSubId(subId)) {
List<SubscriptionInfo> activeSubList = getActiveSubscriptionsInGroup(subId);
if (activeSubList.isEmpty()) {
- sendErrorInPendingIntent(downloadedIntent,
- SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION);
+ handleError(downloadedIntent, SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION,
+ mmsStats);
return;
}
@@ -317,13 +332,13 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
}
}
}
+ mmsStats.updateSubId(subId, getTelephonyManager(subId));
// Load MMS config
Bundle mmsConfig = loadMmsConfig(subId);
if (mmsConfig == null) {
LogUtil.e("MMS config is not loaded yet for subId " + subId);
- sendErrorInPendingIntent(
- downloadedIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR);
+ handleError(downloadedIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
return;
}
@@ -335,14 +350,13 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
// Make sure MMS is enabled
if (!mmsConfig.getBoolean(SmsManager.MMS_CONFIG_MMS_ENABLED)) {
LogUtil.e("MMS is not enabled for subId " + subId);
- sendErrorInPendingIntent(
- downloadedIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR);
+ handleError(downloadedIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
return;
}
final DownloadRequest request = new DownloadRequest(MmsService.this, subId, locationUrl,
contentUri, downloadedIntent, callingPkg, mmsConfig, MmsService.this,
- messageId);
+ messageId, mmsStats);
final String carrierMessagingServicePackage =
getCarrierMessagingServicePackageIfExists(subId);
@@ -357,7 +371,7 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
// Make sure subId has MMS data
if (!getTelephonyManager(subId).isDataEnabledForApn(ApnSetting.TYPE_MMS)) {
sendSettingsIntentForFailedMms(/*isIncoming=*/ true, subId);
- sendErrorInPendingIntent(downloadedIntent, SmsManager.MMS_ERROR_DATA_DISABLED);
+ handleError(downloadedIntent, SmsManager.MMS_ERROR_DATA_DISABLED, mmsStats);
return;
}
@@ -577,6 +591,12 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
}
return false;
}
+
+ private void handleError(@Nullable PendingIntent pendingIntent, int resultCode,
+ MmsStats mmsStats) {
+ sendErrorInPendingIntent(pendingIntent, resultCode);
+ mmsStats.addAtomToStorage(resultCode);
+ }
};
@Override
@@ -703,6 +723,9 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(true);
+ // Registers statsd pullers
+ mMmsMetricsCollector = new MmsMetricsCollector(this);
+
// Initialize running request state
for (int i = 0; i < mRunningRequestExecutors.length; i++) {
mRunningRequestExecutors[i] = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
diff --git a/src/com/android/mms/service/SendRequest.java b/src/com/android/mms/service/SendRequest.java
index 67c368d..7e078a6 100644
--- a/src/com/android/mms/service/SendRequest.java
+++ b/src/com/android/mms/service/SendRequest.java
@@ -36,6 +36,8 @@ import android.text.TextUtils;
import com.android.internal.telephony.SmsApplication;
import com.android.internal.telephony.SmsNumberUtils;
import com.android.mms.service.exception.MmsHttpException;
+import com.android.mms.service.metrics.MmsStats;
+
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.pdu.GenericPdu;
@@ -58,8 +60,8 @@ public class SendRequest extends MmsRequest {
public SendRequest(RequestManager manager, int subId, Uri contentUri, String locationUrl,
PendingIntent sentIntent, String creator, Bundle configOverrides, Context context,
- long messageId) {
- super(manager, subId, creator, configOverrides, context, messageId);
+ long messageId, MmsStats mmsStats) {
+ super(manager, subId, creator, configOverrides, context, messageId, mmsStats);
mPduUri = contentUri;
mPduData = null;
mLocationUrl = locationUrl;
diff --git a/src/com/android/mms/service/metrics/MmsMetricsCollector.java b/src/com/android/mms/service/metrics/MmsMetricsCollector.java
new file mode 100644
index 0000000..1bec4ae
--- /dev/null
+++ b/src/com/android/mms/service/metrics/MmsMetricsCollector.java
@@ -0,0 +1,164 @@
+/*
+ * 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 static com.android.mms.MmsStatsLog.INCOMING_MMS;
+import static com.android.mms.MmsStatsLog.OUTGOING_MMS;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.util.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.mms.IncomingMms;
+import com.android.mms.MmsStatsLog;
+import com.android.mms.OutgoingMms;
+import com.android.internal.util.ConcurrentUtils;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements statsd pullers for Mms.
+ *
+ * <p>This class registers pullers to statsd, which will be called once a day to obtain mms
+ * statistics that cannot be sent to statsd in real time.
+ */
+public class MmsMetricsCollector implements StatsManager.StatsPullAtomCallback {
+ private static final String TAG = MmsMetricsCollector.class.getSimpleName();
+ /** Disables various restrictions to ease debugging during development. */
+ private static final boolean DBG = false; // STOPSHIP if true
+ private static final long MILLIS_PER_HOUR = Duration.ofHours(1).toMillis();
+ private static final long MILLIS_PER_SECOND = Duration.ofSeconds(1).toMillis();
+ /**
+ * Sets atom pull cool down to 23 hours to help enforcing privacy requirement.
+ *
+ * <p>Applies to certain atoms. The interval of 23 hours leaves some margin for pull operations
+ * that occur once a day.
+ */
+ private static final long MIN_COOLDOWN_MILLIS =
+ DBG ? 10L * MILLIS_PER_SECOND : 23L * MILLIS_PER_HOUR;
+ private static final StatsManager.PullAtomMetadata POLICY_PULL_DAILY =
+ new StatsManager.PullAtomMetadata.Builder()
+ .setCoolDownMillis(MIN_COOLDOWN_MILLIS)
+ .build();
+ private final PersistMmsAtomsStorage mStorage;
+ private final StatsManager mStatsManager;
+
+
+ public MmsMetricsCollector(Context context) {
+ this(context, new PersistMmsAtomsStorage(context));
+ }
+
+ @VisibleForTesting
+ public MmsMetricsCollector(Context context, PersistMmsAtomsStorage storage) {
+ mStorage = storage;
+ mStatsManager = context.getSystemService(StatsManager.class);
+ if (mStatsManager != null) {
+ registerAtom(INCOMING_MMS, POLICY_PULL_DAILY);
+ registerAtom(OUTGOING_MMS, POLICY_PULL_DAILY);
+ Log.d(TAG, "[MmsMetricsCollector]: registered atoms");
+ } else {
+ Log.e(TAG, "[MmsMetricsCollector]: could not get StatsManager, "
+ + "atoms not registered");
+ }
+ }
+
+ private static StatsEvent buildStatsEvent(IncomingMms mms) {
+ return MmsStatsLog.buildStatsEvent(
+ INCOMING_MMS,
+ mms.getRat(),
+ mms.getResult(),
+ mms.getRoaming(),
+ mms.getSimSlotIndex(),
+ mms.getIsMultiSim(),
+ mms.getIsEsim(),
+ mms.getCarrierId(),
+ mms.getAvgIntervalMillis(),
+ mms.getMmsCount(),
+ mms.getRetryId(),
+ mms.getHandledByCarrierApp());
+ }
+
+ private static StatsEvent buildStatsEvent(OutgoingMms mms) {
+ return MmsStatsLog.buildStatsEvent(
+ OUTGOING_MMS,
+ mms.getRat(),
+ mms.getResult(),
+ mms.getRoaming(),
+ mms.getSimSlotIndex(),
+ mms.getIsMultiSim(),
+ mms.getIsEsim(),
+ mms.getCarrierId(),
+ mms.getAvgIntervalMillis(),
+ mms.getMmsCount(),
+ mms.getIsFromDefaultApp(),
+ mms.getRetryId(),
+ mms.getHandledByCarrierApp());
+ }
+
+ @Override
+ public int onPullAtom(int atomTag, List<StatsEvent> data) {
+ switch (atomTag) {
+ case INCOMING_MMS:
+ return pullIncomingMms(data);
+ case OUTGOING_MMS:
+ return pullOutgoingMms(data);
+ default:
+ Log.e(TAG, String.format("unexpected atom ID %d", atomTag));
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ private int pullIncomingMms(List<StatsEvent> data) {
+ List<IncomingMms> incomingMmsList = mStorage.getIncomingMms(MIN_COOLDOWN_MILLIS);
+ if (incomingMmsList != null) {
+ // MMS List is already shuffled when MMS were inserted.
+ incomingMmsList.forEach(mms -> data.add(buildStatsEvent(mms)));
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ Log.w(TAG, "INCOMING_MMS pull too frequent, skipping");
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ private int pullOutgoingMms(List<StatsEvent> data) {
+ List<OutgoingMms> outgoingMmsList = mStorage.getOutgoingMms(MIN_COOLDOWN_MILLIS);
+ if (outgoingMmsList != null) {
+ // MMS List is already shuffled when MMS were inserted.
+ outgoingMmsList.forEach(mms -> data.add(buildStatsEvent(mms)));
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ Log.w(TAG, "OUTGOING_MMS pull too frequent, skipping");
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ /** Registers a pulled atom ID {@code atomId} with optional {@code policy} for pulling. */
+ private void registerAtom(int atomId, @Nullable StatsManager.PullAtomMetadata policy) {
+ mStatsManager.setPullAtomCallback(atomId, policy, ConcurrentUtils.DIRECT_EXECUTOR, this);
+ }
+
+ /** Returns the {@link PersistMmsAtomsStorage} backing the puller. */
+ public PersistMmsAtomsStorage getAtomsStorage() {
+ return mStorage;
+ }
+}
diff --git a/src/com/android/mms/service/metrics/MmsStats.java b/src/com/android/mms/service/metrics/MmsStats.java
new file mode 100644
index 0000000..26e8967
--- /dev/null
+++ b/src/com/android/mms/service/metrics/MmsStats.java
@@ -0,0 +1,213 @@
+/*
+ * 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 static com.android.mms.MmsStatsLog.INCOMING_MMS__RESULT__MMS_RESULT_ERROR_UNSPECIFIED;
+import static com.android.mms.MmsStatsLog.INCOMING_MMS__RESULT__MMS_RESULT_SUCCESS;
+import static com.android.mms.MmsStatsLog.OUTGOING_MMS__RESULT__MMS_RESULT_ERROR_UNSPECIFIED;
+import static com.android.mms.MmsStatsLog.OUTGOING_MMS__RESULT__MMS_RESULT_SUCCESS;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Binder;
+import android.os.SystemClock;
+import android.telephony.ServiceState;
+import android.telephony.SmsManager;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccCardInfo;
+
+import com.android.internal.telephony.SmsApplication;
+import com.android.mms.IncomingMms;
+import com.android.mms.OutgoingMms;
+
+import java.util.List;
+
+/** Collects mms events for the pulled atom. */
+public class MmsStats {
+ private static final String TAG = MmsStats.class.getSimpleName();
+
+ private final Context mContext;
+ private final PersistMmsAtomsStorage mPersistMmsAtomsStorage;
+ private final String mCallingPkg;
+ private final boolean mIsIncomingMms;
+ private final long mTimestamp;
+ private int mSubId;
+ private TelephonyManager mTelephonyManager;
+
+ public MmsStats(Context context, PersistMmsAtomsStorage persistMmsAtomsStorage, int subId,
+ TelephonyManager telephonyManager, String callingPkg, boolean isIncomingMms) {
+ mContext = context;
+ mPersistMmsAtomsStorage = persistMmsAtomsStorage;
+ mSubId = subId;
+ mTelephonyManager = telephonyManager;
+ mCallingPkg = callingPkg;
+ mIsIncomingMms = isIncomingMms;
+ mTimestamp = SystemClock.elapsedRealtime();
+ }
+
+ /** Updates subId and corresponding telephonyManager. */
+ public void updateSubId(int subId, TelephonyManager telephonyManager) {
+ mSubId = subId;
+ mTelephonyManager = telephonyManager;
+ }
+
+ /** Adds incoming or outgoing mms atom to storage. */
+ public void addAtomToStorage(int result) {
+ addAtomToStorage(result, 0, false);
+ }
+
+ /** Adds incoming or outgoing mms atom to storage. */
+ public void addAtomToStorage(int result, int retryId, boolean handledByCarrierApp) {
+ long identity = Binder.clearCallingIdentity();
+ try {
+ if (mIsIncomingMms) {
+ onIncomingMms(result, retryId, handledByCarrierApp);
+ } else {
+ onOutgoingMms(result, retryId, handledByCarrierApp);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /** Creates a new atom when MMS is received. */
+ private void onIncomingMms(int result, int retryId, boolean handledByCarrierApp) {
+ IncomingMms incomingMms = IncomingMms.newBuilder()
+ .setRat(getDataNetworkType())
+ .setResult(getIncomingMmsResult(result))
+ .setRoaming(getDataRoamingType())
+ .setSimSlotIndex(getSlotIndex())
+ .setIsMultiSim(getIsMultiSim())
+ .setIsEsim(getIsEuicc())
+ .setCarrierId(getSimCarrierId())
+ .setAvgIntervalMillis(getInterval())
+ .setMmsCount(1)
+ .setRetryId(retryId)
+ .setHandledByCarrierApp(handledByCarrierApp)
+ .build();
+ mPersistMmsAtomsStorage.addIncomingMms(incomingMms);
+ }
+
+ /** Creates a new atom when MMS is sent. */
+ private void onOutgoingMms(int result, int retryId, boolean handledByCarrierApp) {
+ OutgoingMms outgoingMms = OutgoingMms.newBuilder()
+ .setRat(getDataNetworkType())
+ .setResult(getOutgoingMmsResult(result))
+ .setRoaming(getDataRoamingType())
+ .setSimSlotIndex(getSlotIndex())
+ .setIsMultiSim(getIsMultiSim())
+ .setIsEsim(getIsEuicc())
+ .setCarrierId(getSimCarrierId())
+ .setAvgIntervalMillis(getInterval())
+ .setMmsCount(1)
+ .setIsFromDefaultApp(isDefaultMmsApp())
+ .setRetryId(retryId)
+ .setHandledByCarrierApp(handledByCarrierApp)
+ .build();
+ mPersistMmsAtomsStorage.addOutgoingMms(outgoingMms);
+ }
+
+ /** Returns data network type of current subscription. */
+ private int getDataNetworkType() {
+ return mTelephonyManager.getDataNetworkType();
+ }
+
+ /** Returns incoming mms result. */
+ private int getIncomingMmsResult(int result) {
+ switch (result) {
+ case SmsManager.MMS_ERROR_UNSPECIFIED:
+ // SmsManager.MMS_ERROR_UNSPECIFIED(1) -> MMS_RESULT_ERROR_UNSPECIFIED(0)
+ return INCOMING_MMS__RESULT__MMS_RESULT_ERROR_UNSPECIFIED;
+ case Activity.RESULT_OK:
+ // Activity.RESULT_OK -> MMS_RESULT_SUCCESS(1)
+ return INCOMING_MMS__RESULT__MMS_RESULT_SUCCESS;
+ default:
+ // Int value of other SmsManager.MMS_ERROR matches MMS_RESULT_ERROR
+ return result;
+ }
+ }
+
+ /** Returns outgoing mms result. */
+ private int getOutgoingMmsResult(int result) {
+ switch (result) {
+ case SmsManager.MMS_ERROR_UNSPECIFIED:
+ // SmsManager.MMS_ERROR_UNSPECIFIED(1) -> MMS_RESULT_ERROR_UNSPECIFIED(0)
+ return OUTGOING_MMS__RESULT__MMS_RESULT_ERROR_UNSPECIFIED;
+ case Activity.RESULT_OK:
+ // Activity.RESULT_OK -> MMS_RESULT_SUCCESS(1)
+ return OUTGOING_MMS__RESULT__MMS_RESULT_SUCCESS;
+ default:
+ // Int value of other SmsManager.MMS_ERROR matches MMS_RESULT_ERROR
+ return result;
+ }
+ }
+
+ /** Returns data network roaming type of current subscription. */
+ private int getDataRoamingType() {
+ ServiceState serviceState = mTelephonyManager.getServiceState();
+ return (serviceState != null) ? serviceState.getDataRoamingType() :
+ ServiceState.ROAMING_TYPE_NOT_ROAMING;
+ }
+
+ /** Returns slot index associated with the subscription. */
+ private int getSlotIndex() {
+ return SubscriptionManager.getSlotIndex(mSubId);
+ }
+
+ /** Returns whether the device has multiple active SIM profiles. */
+ private boolean getIsMultiSim() {
+ SubscriptionManager subManager = mContext.getSystemService(SubscriptionManager.class);
+ if(subManager == null) {
+ return false;
+ }
+
+ List<SubscriptionInfo> activeSubscriptionInfo = subManager.getActiveSubscriptionInfoList();
+ return (activeSubscriptionInfo.size() > 1);
+ }
+
+ /** Returns if current subscription is embedded subscription. */
+ private boolean getIsEuicc() {
+ List<UiccCardInfo> uiccCardInfoList = mTelephonyManager.getUiccCardsInfo();
+ for (UiccCardInfo card : uiccCardInfoList) {
+ if (card.getPhysicalSlotIndex() == getSlotIndex()) {
+ return card.isEuicc();
+ }
+ }
+ return false;
+ }
+
+ /** Returns carrier id of the current subscription used by MMS. */
+ private int getSimCarrierId() {
+ return mTelephonyManager.getSimCarrierId();
+ }
+
+ /** Returns if the MMS was originated from the default MMS application. */
+ private boolean isDefaultMmsApp() {
+ return SmsApplication.isDefaultMmsApplication(mContext, mCallingPkg);
+ }
+
+ /**
+ * Returns the interval in milliseconds between sending/receiving MMS message and current time.
+ * Calculates the time taken to send message to the network
+ * or download message from the network.
+ */
+ private long getInterval() {
+ return (SystemClock.elapsedRealtime() - mTimestamp);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/mms/service/metrics/PersistMmsAtomsStorage.java b/src/com/android/mms/service/metrics/PersistMmsAtomsStorage.java
new file mode 100644
index 0000000..4e9ffc7
--- /dev/null
+++ b/src/com/android/mms/service/metrics/PersistMmsAtomsStorage.java
@@ -0,0 +1,370 @@
+/*
+ * 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();
+ }
+} \ No newline at end of file
diff --git a/tests/unittests/Android.bp b/tests/unittests/Android.bp
new file mode 100644
index 0000000..92371c6
--- /dev/null
+++ b/tests/unittests/Android.bp
@@ -0,0 +1,27 @@
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "MmsServiceTests",
+ static_libs: [
+ "mms-protos-lite",
+ "mms-statsd",
+ "androidx.annotation_annotation",
+ "mockito-target",
+ "compatibility-device-util-axt",
+ "androidx.test.rules",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+ srcs: ["src/**/*.java", ":mms-metrics-srcs"],
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ certificate: "platform",
+ instrumentation_for: "MmsService",
+} \ No newline at end of file
diff --git a/tests/unittests/AndroidManifest.xml b/tests/unittests/AndroidManifest.xml
new file mode 100644
index 0000000..3b4b582
--- /dev/null
+++ b/tests/unittests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.mms.service.tests"
+ android:debuggable="true"
+ android:sharedUserId="android.uid.phone">
+
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.mms.service.tests"
+ android:label="MmsServiceTests"
+ android:debuggable="true">
+ </instrumentation>
+</manifest> \ No newline at end of file
diff --git a/tests/unittests/AndroidTest.xml b/tests/unittests/AndroidTest.xml
new file mode 100644
index 0000000..61b3f5d
--- /dev/null
+++ b/tests/unittests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<configuration description="Run MmsServiceTests.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="MmsServiceTests.apk"/>
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct"/>
+ <option name="test-tag" value="MmsServiceTests"/>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.mms.service.tests"/>
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration> \ No newline at end of file
diff --git a/tests/unittests/src/com/android/mms/service/metrics/MmsMetricsCollectorTest.java b/tests/unittests/src/com/android/mms/service/metrics/MmsMetricsCollectorTest.java
new file mode 100644
index 0000000..8d93739
--- /dev/null
+++ b/tests/unittests/src/com/android/mms/service/metrics/MmsMetricsCollectorTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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 static com.android.mms.MmsStatsLog.INCOMING_MMS;
+import static com.android.mms.MmsStatsLog.OUTGOING_MMS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.util.StatsEvent;
+
+import com.android.mms.IncomingMms;
+import com.android.mms.OutgoingMms;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MmsMetricsCollectorTest {
+ private static final long MIN_COOLDOWN_MILLIS = 23L * 3600L * 1000L;
+ Context mContext;
+ private PersistMmsAtomsStorage mPersistMmsAtomsStorage;
+ private MmsMetricsCollector mMmsMetricsCollector;
+
+ @Before
+ public void setUp() {
+ mContext = mock(Context.class);
+ mPersistMmsAtomsStorage = mock(PersistMmsAtomsStorage.class);
+ mMmsMetricsCollector = new MmsMetricsCollector(mContext, mPersistMmsAtomsStorage);
+ }
+
+ @After
+ public void tearDown() {
+ mContext = null;
+ mPersistMmsAtomsStorage = null;
+ mMmsMetricsCollector = null;
+ }
+
+ @Test
+ public void onPullAtom_incomingMms_empty() {
+ doReturn(new ArrayList<>()).when(mPersistMmsAtomsStorage).getIncomingMms(anyLong());
+ List<StatsEvent> actualAtoms = new ArrayList<>();
+
+ int result = mMmsMetricsCollector.onPullAtom(INCOMING_MMS, actualAtoms);
+
+ assertThat(actualAtoms).hasSize(0);
+ assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+ }
+
+ @Test
+ public void onPullAtom_incomingMms_tooFrequent() {
+ doReturn(null).when(mPersistMmsAtomsStorage).getIncomingMms(anyLong());
+ List<StatsEvent> actualAtoms = new ArrayList<>();
+
+ int result = mMmsMetricsCollector.onPullAtom(INCOMING_MMS, actualAtoms);
+
+ assertThat(actualAtoms).hasSize(0);
+ assertThat(result).isEqualTo(StatsManager.PULL_SKIP);
+ verify(mPersistMmsAtomsStorage, times(1))
+ .getIncomingMms(eq(MIN_COOLDOWN_MILLIS));
+ verifyNoMoreInteractions(mPersistMmsAtomsStorage);
+ }
+
+ @Test
+ public void onPullAtom_incomingMms_multipleMms() {
+ IncomingMms incomingMms = IncomingMms.newBuilder().build();
+ doReturn(Arrays.asList(incomingMms, incomingMms, incomingMms, incomingMms))
+ .when(mPersistMmsAtomsStorage).getIncomingMms(anyLong());
+ List<StatsEvent> actualAtoms = new ArrayList<>();
+
+ int result = mMmsMetricsCollector.onPullAtom(INCOMING_MMS, actualAtoms);
+
+ assertThat(actualAtoms).hasSize(4);
+ assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+ }
+
+ @Test
+ public void onPullAtom_outgoingMms_empty() {
+ doReturn(new ArrayList<>()).when(mPersistMmsAtomsStorage).getOutgoingMms(anyLong());
+ List<StatsEvent> actualAtoms = new ArrayList<>();
+
+ int result = mMmsMetricsCollector.onPullAtom(OUTGOING_MMS, actualAtoms);
+
+ assertThat(actualAtoms).hasSize(0);
+ assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+ }
+
+ @Test
+ public void onPullAtom_outgoingMms_tooFrequent() {
+ doReturn(null).when(mPersistMmsAtomsStorage).getOutgoingMms(anyLong());
+ List<StatsEvent> actualAtoms = new ArrayList<>();
+
+ int result = mMmsMetricsCollector.onPullAtom(OUTGOING_MMS, actualAtoms);
+
+ assertThat(actualAtoms).hasSize(0);
+ assertThat(result).isEqualTo(StatsManager.PULL_SKIP);
+ verify(mPersistMmsAtomsStorage, times(1))
+ .getOutgoingMms(eq(MIN_COOLDOWN_MILLIS));
+ verifyNoMoreInteractions(mPersistMmsAtomsStorage);
+ }
+
+ @Test
+ public void onPullAtom_outgoingMms_multipleMms() {
+ OutgoingMms outgoingMms = OutgoingMms.newBuilder().build();
+ doReturn(Arrays.asList(outgoingMms, outgoingMms, outgoingMms, outgoingMms))
+ .when(mPersistMmsAtomsStorage).getOutgoingMms(anyLong());
+ List<StatsEvent> actualAtoms = new ArrayList<>();
+
+ int result = mMmsMetricsCollector.onPullAtom(OUTGOING_MMS, actualAtoms);
+
+ assertThat(actualAtoms).hasSize(4);
+ assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+ }
+} \ No newline at end of file
diff --git a/tests/unittests/src/com/android/mms/service/metrics/MmsStatsTest.java b/tests/unittests/src/com/android/mms/service/metrics/MmsStatsTest.java
new file mode 100644
index 0000000..f176b6b
--- /dev/null
+++ b/tests/unittests/src/com/android/mms/service/metrics/MmsStatsTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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 static com.android.mms.MmsStatsLog.INCOMING_MMS__RESULT__MMS_RESULT_SUCCESS;
+import static com.android.mms.MmsStatsLog.OUTGOING_MMS__RESULT__MMS_RESULT_SUCCESS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.android.mms.IncomingMms;
+import com.android.mms.OutgoingMms;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class MmsStatsTest {
+ // Mocked classes
+ private Context mContext;
+ private PersistMmsAtomsStorage mPersistMmsAtomsStorage;
+ private TelephonyManager mTelephonyManager;
+
+ @Before
+ public void setUp() {
+ mContext = mock(Context.class);
+ mPersistMmsAtomsStorage = mock(PersistMmsAtomsStorage.class);
+ mTelephonyManager = mock(TelephonyManager.class);
+ }
+
+ @After
+ public void tearDown() {
+ mContext = null;
+ mPersistMmsAtomsStorage = null;
+ mTelephonyManager = null;
+ }
+
+ @Test
+ public void addAtomToStorage_incomingMms_default() {
+ doReturn(null).when(mTelephonyManager).getServiceState();
+ doReturn(TelephonyManager.UNKNOWN_CARRIER_ID).when(mTelephonyManager).getSimCarrierId();
+ int inactiveSubId = 123;
+ MmsStats mmsStats = new MmsStats(mContext, mPersistMmsAtomsStorage, inactiveSubId,
+ mTelephonyManager, null, true);
+ mmsStats.addAtomToStorage(Activity.RESULT_OK);
+
+ ArgumentCaptor<IncomingMms> incomingMmsCaptor = ArgumentCaptor.forClass(IncomingMms.class);
+ verify(mPersistMmsAtomsStorage).addIncomingMms(incomingMmsCaptor.capture());
+ IncomingMms incomingMms = incomingMmsCaptor.getValue();
+ assertThat(incomingMms.getRat()).isEqualTo(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertThat(incomingMms.getResult()).isEqualTo(INCOMING_MMS__RESULT__MMS_RESULT_SUCCESS);
+ assertThat(incomingMms.getRoaming()).isEqualTo(ServiceState.ROAMING_TYPE_NOT_ROAMING);
+ assertThat(incomingMms.getSimSlotIndex()).isEqualTo(
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ assertThat(incomingMms.getIsMultiSim()).isEqualTo(false);
+ assertThat(incomingMms.getIsEsim()).isEqualTo(false);
+ assertThat(incomingMms.getCarrierId()).isEqualTo(TelephonyManager.UNKNOWN_CARRIER_ID);
+ assertThat(incomingMms.getMmsCount()).isEqualTo(1);
+ assertThat(incomingMms.getRetryId()).isEqualTo(0);
+ assertThat(incomingMms.getHandledByCarrierApp()).isEqualTo(false);
+ verifyNoMoreInteractions(mPersistMmsAtomsStorage);
+ }
+
+ @Test
+ public void addAtomToStorage_outgoingMms_default() {
+ doReturn(null).when(mTelephonyManager).getServiceState();
+ doReturn(TelephonyManager.UNKNOWN_CARRIER_ID).when(mTelephonyManager).getSimCarrierId();
+ int inactiveSubId = 123;
+ MmsStats mmsStats = new MmsStats(mContext, mPersistMmsAtomsStorage, inactiveSubId,
+ mTelephonyManager, null, false);
+ mmsStats.addAtomToStorage(Activity.RESULT_OK);
+
+ ArgumentCaptor<OutgoingMms> outgoingMmsCaptor = ArgumentCaptor.forClass(OutgoingMms.class);
+ verify(mPersistMmsAtomsStorage).addOutgoingMms(outgoingMmsCaptor.capture());
+ OutgoingMms outgoingMms = outgoingMmsCaptor.getValue();
+ assertThat(outgoingMms.getRat()).isEqualTo(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertThat(outgoingMms.getResult()).isEqualTo(OUTGOING_MMS__RESULT__MMS_RESULT_SUCCESS);
+ assertThat(outgoingMms.getRoaming()).isEqualTo(ServiceState.ROAMING_TYPE_NOT_ROAMING);
+ assertThat(outgoingMms.getSimSlotIndex()).isEqualTo(
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ assertThat(outgoingMms.getIsMultiSim()).isEqualTo(false);
+ assertThat(outgoingMms.getIsEsim()).isEqualTo(false);
+ assertThat(outgoingMms.getCarrierId()).isEqualTo(TelephonyManager.UNKNOWN_CARRIER_ID);
+ assertThat(outgoingMms.getMmsCount()).isEqualTo(1);
+ assertThat(outgoingMms.getRetryId()).isEqualTo(0);
+ assertThat(outgoingMms.getHandledByCarrierApp()).isEqualTo(false);
+ assertThat(outgoingMms.getIsFromDefaultApp()).isEqualTo(false);
+ verifyNoMoreInteractions(mPersistMmsAtomsStorage);
+ }
+
+ @Test
+ public void getDataRoamingType_serviceState_notNull() {
+ ServiceState serviceState = mock(ServiceState.class);
+ doReturn(serviceState).when(mTelephonyManager).getServiceState();
+ MmsStats mmsStats = new MmsStats(mContext, mPersistMmsAtomsStorage, 1,
+ mTelephonyManager, null, true);
+ mmsStats.addAtomToStorage(Activity.RESULT_OK);
+
+ ArgumentCaptor<IncomingMms> incomingMmsCaptor = ArgumentCaptor.forClass(IncomingMms.class);
+ verify(mPersistMmsAtomsStorage).addIncomingMms(incomingMmsCaptor.capture());
+ IncomingMms incomingMms = incomingMmsCaptor.getValue();
+ assertThat(incomingMms.getRoaming()).isEqualTo(ServiceState.ROAMING_TYPE_NOT_ROAMING);
+ }
+} \ No newline at end of file
diff --git a/tests/unittests/src/com/android/mms/service/metrics/PersistMmsAtomsStorageTest.java b/tests/unittests/src/com/android/mms/service/metrics/PersistMmsAtomsStorageTest.java
new file mode 100644
index 0000000..7f604bc
--- /dev/null
+++ b/tests/unittests/src/com/android/mms/service/metrics/PersistMmsAtomsStorageTest.java
@@ -0,0 +1,734 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import androidx.annotation.Nullable;
+
+import com.android.mms.IncomingMms;
+import com.android.mms.OutgoingMms;
+import com.android.mms.PersistMmsAtoms;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class PersistMmsAtomsStorageTest {
+ private static final String TEST_FILE = "PersistMmsAtomsStorageTest.pb";
+ @Rule
+ public TemporaryFolder mFolder = new TemporaryFolder();
+ private File mTestFile;
+ private static final long START_TIME_MILLIS = 2000L;
+ private static final int CARRIER1_ID = 1435;
+ private static final int CARRIER2_ID = 1187;
+ private TestablePersistMmsAtomsStorage mTestablePersistMmsAtomsStorage;
+ // IncomingMms
+ private List<IncomingMms> mIncomingMmsList;
+ private IncomingMms mIncomingMms1Proto;
+ private IncomingMms mIncomingMms2Proto;
+ // OutgoingMms
+ private List<OutgoingMms> mOutgoingMmsList;
+ private OutgoingMms mOutgoingMms1Proto;
+ private OutgoingMms mOutgoingMms2Proto;
+ // Mocked classes
+ private Context mContext;
+ private PackageManager mPackageManager;
+ private FileOutputStream mTestFileOutputStream;
+ // Comparator to compare proto objects
+ private static final Comparator<Object> sProtoComparator =
+ new Comparator<Object>() {
+ @Override
+ public int compare(Object o1, Object o2) {
+ if (o1 == o2) {
+ return 0;
+ }
+ if (o1 == null) {
+ return -1;
+ }
+ if (o2 == null) {
+ return 1;
+ }
+ assertEquals(o1.getClass(), o2.getClass());
+ return o1.toString().compareTo(o2.toString());
+ }
+ };
+
+
+ @Before
+ public void setUp() throws Exception {
+ mTestFileOutputStream = mock(FileOutputStream.class);
+ mContext = mock(Context.class);
+ mPackageManager = mock(PackageManager.class);
+ makeTestData();
+
+ // By default, test loading with real file IO and saving with mocks.
+ mTestFile = mFolder.newFile(TEST_FILE);
+ doReturn(false).when(mPackageManager).
+ hasSystemFeature(PackageManager.FEATURE_RAM_LOW);
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(mTestFileOutputStream).when(mContext).openFileOutput(anyString(), anyInt());
+ doReturn(mTestFile).when(mContext).getFileStreamPath(anyString());
+ }
+
+ @After
+ public void tearDown() {
+ mTestFile.delete();
+ mTestFile = null;
+ mFolder = null;
+ mIncomingMmsList = null;
+ mIncomingMms1Proto = null;
+ mIncomingMms2Proto = null;
+ mOutgoingMmsList = null;
+ mOutgoingMms1Proto = null;
+ mOutgoingMms2Proto = null;
+ mTestablePersistMmsAtomsStorage = null;
+ mTestFileOutputStream = null;
+ mPackageManager = null;
+ mContext = null;
+ }
+
+ @Test
+ public void loadAtoms_fileNotExist() {
+ mTestFile.delete();
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // No exception should be thrown, storage should be empty, pull time should be start time.
+ assertAllPullTimestampEquals(START_TIME_MILLIS);
+ assertStorageIsEmptyForAllAtoms();
+ }
+
+ @Test
+ public void loadAtoms_unreadable() throws Exception {
+ createEmptyTestFile();
+ mTestFile.setReadable(false);
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // No exception should be thrown, storage should be empty, pull time should be start time.
+ assertAllPullTimestampEquals(START_TIME_MILLIS);
+ assertStorageIsEmptyForAllAtoms();
+ }
+
+ @Test
+ public void loadAtoms_emptyProto() throws Exception {
+ createEmptyTestFile();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // No exception should be thrown, storage should be empty, pull time should be start time.
+ assertAllPullTimestampEquals(START_TIME_MILLIS);
+ assertStorageIsEmptyForAllAtoms();
+ }
+
+ @Test
+ public void loadAtoms_malformedFile() throws Exception {
+ FileOutputStream stream = new FileOutputStream(mTestFile);
+ stream.write("This is not a proto file.".getBytes(StandardCharsets.UTF_8));
+ stream.close();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // No exception should be thrown, storage should be empty, pull time should be start time.
+ assertAllPullTimestampEquals(START_TIME_MILLIS);
+ assertStorageIsEmptyForAllAtoms();
+ }
+
+ @Test
+ public void loadAtoms_pullTimeMissing() throws Exception {
+ // Create test file with lastPullTimeMillis = 0L, i.e. default/unknown.
+ createTestFile(0L);
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // No exception should be thrown, storage should be match, pull time should be start time.
+ assertAllPullTimestampEquals(START_TIME_MILLIS);
+ assertProtoListEqualsIgnoringOrder(mIncomingMmsList,
+ mTestablePersistMmsAtomsStorage.getIncomingMms(0L));
+ assertProtoListEqualsIgnoringOrder(mOutgoingMmsList,
+ mTestablePersistMmsAtomsStorage.getOutgoingMms(0L));
+ }
+
+ @Test
+ public void loadAtoms_validContents() throws Exception {
+ createTestFile(100L);
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+
+ // No exception should be thrown, storage and pull time should match.
+ assertAllPullTimestampEquals(100L);
+ assertProtoListEqualsIgnoringOrder(mIncomingMmsList,
+ mTestablePersistMmsAtomsStorage.getIncomingMms(0L));
+ assertProtoListEqualsIgnoringOrder(mOutgoingMmsList,
+ mTestablePersistMmsAtomsStorage.getOutgoingMms(0L));
+ }
+
+ @Test
+ public void addIncomingMms_emptyProto() throws Exception {
+ createEmptyTestFile();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.addIncomingMms(mIncomingMms1Proto);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // IncomingMms should be added successfully, there should not be any OutgoingMms,
+ // changes should be saved.
+ verifyCurrentStateSavedToFileOnce();
+ assertProtoListIsEmpty(mTestablePersistMmsAtomsStorage.getOutgoingMms(0L));
+ List<IncomingMms> expectedIncomingMmsList = new ArrayList<>();
+ expectedIncomingMmsList.add(mIncomingMms1Proto);
+ assertProtoListEquals(expectedIncomingMmsList,
+ mTestablePersistMmsAtomsStorage.getIncomingMms(0L));
+ }
+
+ @Test
+ public void addIncomingMms_withExistingEntries() throws Exception {
+ createEmptyTestFile();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.addIncomingMms(mIncomingMms1Proto);
+ mTestablePersistMmsAtomsStorage.addIncomingMms(mIncomingMms2Proto);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // IncomingMms should be added successfully.
+ verifyCurrentStateSavedToFileOnce();
+ List<IncomingMms> expectedIncomingMmsList = Arrays.asList(mIncomingMms1Proto,
+ mIncomingMms2Proto);
+ assertProtoListEqualsIgnoringOrder(expectedIncomingMmsList,
+ mTestablePersistMmsAtomsStorage.getIncomingMms(0L));
+ }
+
+ @Test
+ public void addIncomingMms_updateExistingEntries() throws Exception {
+ createTestFile(START_TIME_MILLIS);
+
+ // Add copy of mIncomingMms1Proto.
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.addIncomingMms(copyOf(mIncomingMms1Proto));
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // mIncomingMms1Proto's mms count should be increased by 1 and avgIntervalMillis
+ // should be updated correctly.
+ verifyCurrentStateSavedToFileOnce();
+ IncomingMms newIncomingMm1Proto = copyOf(mIncomingMms1Proto);
+ newIncomingMm1Proto = newIncomingMm1Proto.toBuilder()
+ .setMmsCount(2)
+ .setAvgIntervalMillis(mIncomingMms1Proto.getAvgIntervalMillis())
+ .build();
+ List<IncomingMms> expectedIncomingMmsList = Arrays.asList(newIncomingMm1Proto,
+ mIncomingMms2Proto);
+ assertProtoListEqualsIgnoringOrder(expectedIncomingMmsList,
+ mTestablePersistMmsAtomsStorage.getIncomingMms(0L));
+ }
+
+ @Test
+ public void addIncomingMms_tooManyEntries() throws Exception {
+ createEmptyTestFile();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ // Add 26 mms whereas max size is 25.
+ IncomingMms mms = IncomingMms.newBuilder()
+ .setRoaming(ServiceState.ROAMING_TYPE_DOMESTIC)
+ .setSimSlotIndex(0)
+ .setIsMultiSim(false)
+ .setIsEsim(false)
+ .setCarrierId(CARRIER1_ID)
+ .setMmsCount(1)
+ .setAvgIntervalMillis(500L)
+ .setRetryId(0)
+ .setHandledByCarrierApp(false)
+ .build();
+ for (int ratType = 0; ratType < 5; ratType++) {
+ for (int resultType = 0; resultType < 5; resultType++) {
+ mms = mms.toBuilder().setRat(ratType).setResult(resultType).build();
+ mTestablePersistMmsAtomsStorage.addIncomingMms(mms);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ }
+ }
+
+ // Add 26th mms 5 times
+ IncomingMms lastMms = copyOf(mms);
+ lastMms = lastMms.toBuilder().setRat(6).setResult(6).build();
+ for (int i = 0; i < 5; i++) {
+ mTestablePersistMmsAtomsStorage.addIncomingMms(lastMms);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ }
+
+ // Last mms should be present in storage.
+ assertHasMmsAndCountAvg(mTestablePersistMmsAtomsStorage.getIncomingMms(0L),
+ lastMms, 5L, lastMms.getAvgIntervalMillis());
+ }
+
+ @Test
+ public void getIncomingMms_tooFrequent() throws Exception {
+ createTestFile(START_TIME_MILLIS);
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ // Pull interval less than minimum.
+ mTestablePersistMmsAtomsStorage.incTimeMillis(50L);
+
+ List<IncomingMms> incomingMmsList = mTestablePersistMmsAtomsStorage
+ .getIncomingMms(100L);
+ // Should be denied.
+ assertNull(incomingMmsList);
+ }
+
+ @Test
+ public void getIncomingMms_withSavedAtoms() throws Exception {
+ createTestFile(START_TIME_MILLIS);
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ List<IncomingMms> incomingMmsList1 = mTestablePersistMmsAtomsStorage
+ .getIncomingMms(50L);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ List<IncomingMms> incomingMmsList2 = mTestablePersistMmsAtomsStorage
+ .getIncomingMms(50L);
+
+ // First set of results should be equal to file contents.
+ List<IncomingMms> expectedIncomingMmsList = Arrays.asList(mIncomingMms1Proto,
+ mIncomingMms2Proto);
+ assertProtoListEqualsIgnoringOrder(expectedIncomingMmsList, incomingMmsList1);
+ // Second set of results should be empty.
+ expectedIncomingMmsList = new ArrayList<>();
+ assertProtoListEqualsIgnoringOrder(expectedIncomingMmsList, incomingMmsList2);
+ // Corresponding pull timestamp should be updated and saved.
+ assertEquals(START_TIME_MILLIS + 200L, mTestablePersistMmsAtomsStorage
+ .getAtomsProto().getIncomingMmsPullTimestampMillis());
+ InOrder inOrder = inOrder(mTestFileOutputStream);
+ assertEquals(START_TIME_MILLIS + 100L,
+ getAtomsWritten(inOrder).getIncomingMmsPullTimestampMillis());
+ assertEquals(START_TIME_MILLIS + 200L,
+ getAtomsWritten(inOrder).getIncomingMmsPullTimestampMillis());
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void addOutgoingMms_emptyProto() throws Exception {
+ createEmptyTestFile();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.addOutgoingMms(mOutgoingMms1Proto);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // OutgoingMms should be added successfully, there should not be any IncomingMms,
+ // changes should be saved.
+ verifyCurrentStateSavedToFileOnce();
+ assertProtoListIsEmpty(mTestablePersistMmsAtomsStorage.getIncomingMms(0L));
+ List<OutgoingMms> expectedOutgoingMmsList = new ArrayList<>();
+ expectedOutgoingMmsList.add(mOutgoingMms1Proto);
+ assertProtoListEquals(expectedOutgoingMmsList,
+ mTestablePersistMmsAtomsStorage.getOutgoingMms(0L));
+ }
+
+ @Test
+ public void addOutgoingMms_withExistingEntries() throws Exception {
+ createEmptyTestFile();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.addOutgoingMms(mOutgoingMms1Proto);
+ mTestablePersistMmsAtomsStorage.addOutgoingMms(mOutgoingMms2Proto);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // OutgoingMms should be added successfully
+ verifyCurrentStateSavedToFileOnce();
+ List<OutgoingMms> expectedOutgoingMmsList = Arrays.asList(mOutgoingMms1Proto,
+ mOutgoingMms2Proto);
+ assertProtoListEqualsIgnoringOrder(expectedOutgoingMmsList,
+ mTestablePersistMmsAtomsStorage.getOutgoingMms(0L));
+ }
+
+ @Test
+ public void addOutgoingMms_updateExistingEntries() throws Exception {
+ createTestFile(START_TIME_MILLIS);
+
+ // Add copy of mOutgoingMms1Proto
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.addOutgoingMms(copyOf(mOutgoingMms1Proto));
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+
+ // mOutgoingMms1Proto's mms count should be increased by 1 and avgIntervalMillis
+ // should be updated correctly.
+ verifyCurrentStateSavedToFileOnce();
+ OutgoingMms newOutgoingMm1Proto = copyOf(mOutgoingMms1Proto);
+ newOutgoingMm1Proto = newOutgoingMm1Proto.toBuilder()
+ .setMmsCount(2)
+ .setAvgIntervalMillis(mOutgoingMms1Proto.getAvgIntervalMillis())
+ .build();
+ List<OutgoingMms> expectedOutgoingMmsList = Arrays.asList(newOutgoingMm1Proto,
+ mOutgoingMms2Proto);
+ assertProtoListEqualsIgnoringOrder(expectedOutgoingMmsList,
+ mTestablePersistMmsAtomsStorage.getOutgoingMms(0L));
+ }
+
+ @Test
+ public void addOutgoingMms_tooManyEntries() throws Exception {
+ createEmptyTestFile();
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ // Add 26 mms whereas max size is 25.
+ OutgoingMms mms = OutgoingMms.newBuilder()
+ .setRoaming(ServiceState.ROAMING_TYPE_DOMESTIC)
+ .setSimSlotIndex(0)
+ .setIsMultiSim(false)
+ .setIsEsim(false)
+ .setCarrierId(CARRIER1_ID)
+ .setMmsCount(1)
+ .setAvgIntervalMillis(500L)
+ .setIsFromDefaultApp(true)
+ .setHandledByCarrierApp(false)
+ .setRetryId(0)
+ .build();
+ for (int ratType = 0; ratType < 5; ratType++) {
+ for (int resultType = 0; resultType < 5; resultType++) {
+ mms = mms.toBuilder().setRat(ratType).setResult(resultType).build();
+ mTestablePersistMmsAtomsStorage.addOutgoingMms(mms);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ }
+ }
+
+ // Add 26th mms 5 times
+ OutgoingMms lastMms = copyOf(mms);
+ lastMms = lastMms.toBuilder().setRat(6).setResult(6).build();
+ for (int i = 0; i < 5; i++) {
+ mTestablePersistMmsAtomsStorage.addOutgoingMms(lastMms);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ }
+
+ // Last mms should be present in storage.
+ assertHasMmsAndCountAvg(mTestablePersistMmsAtomsStorage.getOutgoingMms(0L),
+ lastMms, 5L, lastMms.getAvgIntervalMillis());
+ }
+
+ @Test
+ public void getOutgoingMms_tooFrequent() throws Exception {
+ createTestFile(START_TIME_MILLIS);
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ // Pull interval less than minimum.
+ mTestablePersistMmsAtomsStorage.incTimeMillis(50L);
+
+ List<OutgoingMms> outgoingMmsList = mTestablePersistMmsAtomsStorage
+ .getOutgoingMms(100L);
+ // Should be denied.
+ assertNull(outgoingMmsList);
+ }
+
+ @Test
+ public void getOutgoingMms_withSavedAtoms() throws Exception {
+ createTestFile(START_TIME_MILLIS);
+
+ mTestablePersistMmsAtomsStorage = new TestablePersistMmsAtomsStorage(mContext);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ List<OutgoingMms> outgoingMmsList1 = mTestablePersistMmsAtomsStorage
+ .getOutgoingMms(50L);
+ mTestablePersistMmsAtomsStorage.incTimeMillis(100L);
+ List<OutgoingMms> outgoingMmsList2 = mTestablePersistMmsAtomsStorage
+ .getOutgoingMms(50L);
+
+ // First set of results should be equal to file contents.
+ List<OutgoingMms> expectedOutgoingMmsList = Arrays.asList(mOutgoingMms1Proto,
+ mOutgoingMms2Proto);
+ assertProtoListEqualsIgnoringOrder(expectedOutgoingMmsList, outgoingMmsList1);
+ // Second set of results should be empty.
+ expectedOutgoingMmsList = new ArrayList<>();
+ assertProtoListEqualsIgnoringOrder(expectedOutgoingMmsList, outgoingMmsList2);
+ // Corresponding pull timestamp should be updated and saved.
+ assertEquals(START_TIME_MILLIS + 200L, mTestablePersistMmsAtomsStorage
+ .getAtomsProto().getOutgoingMmsPullTimestampMillis());
+ InOrder inOrder = inOrder(mTestFileOutputStream);
+ assertEquals(START_TIME_MILLIS + 100L,
+ getAtomsWritten(inOrder).getOutgoingMmsPullTimestampMillis());
+ assertEquals(START_TIME_MILLIS + 200L,
+ getAtomsWritten(inOrder).getOutgoingMmsPullTimestampMillis());
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ /** Utilities */
+
+ private void assertAllPullTimestampEquals(long timestamp) {
+ assertEquals(timestamp, mTestablePersistMmsAtomsStorage.getAtomsProto()
+ .getIncomingMmsPullTimestampMillis());
+ assertEquals(timestamp, mTestablePersistMmsAtomsStorage.getAtomsProto()
+ .getOutgoingMmsPullTimestampMillis());
+ }
+
+ private void assertStorageIsEmptyForAllAtoms() {
+ assertProtoListIsEmpty(mTestablePersistMmsAtomsStorage.getIncomingMms(0L));
+ assertProtoListIsEmpty(mTestablePersistMmsAtomsStorage.getOutgoingMms(0L));
+ }
+
+ private static <T> void assertProtoListIsEmpty(@Nullable List<T> list) {
+ assertNotNull(list);
+ assertEquals(0, list.size());
+ }
+
+ private static <T> void assertProtoListEquals(@Nullable List<T> expected,
+ @Nullable List<T> actual) {
+ assertNotNull(expected);
+ assertNotNull(actual);
+ String message =
+ "Expected:\n" + expected.stream().map(Object::toString).collect(
+ Collectors.joining(", "))
+ + "\nGot:\n" + actual.stream().map(Object::toString).collect(
+ Collectors.joining(", "));
+ assertEquals(message, expected.size(), actual.size());
+ for (int i = 0; i < expected.size(); i++) {
+ assertTrue(message, expected.get(i).equals(actual.get(i)));
+ }
+ }
+
+ private static <T> void assertProtoListEqualsIgnoringOrder(@Nullable List<T> expected,
+ @Nullable List<T> actual) {
+ assertNotNull(expected);
+ assertNotNull(actual);
+ expected = new ArrayList<>(expected);
+ actual = new ArrayList<>(actual);
+ Collections.sort(expected, sProtoComparator);
+ Collections.sort(actual, sProtoComparator);
+ assertProtoListEquals(expected, actual);
+ }
+
+ private static void assertHasMmsAndCountAvg(@Nullable List<IncomingMms> incomingMmsList,
+ @Nullable IncomingMms expectedMms, long expectedCount, long expectedAvg) {
+ assertNotNull(incomingMmsList);
+ assertNotNull(expectedMms);
+ long actualCount = -1;
+ long actualAvg = -1;
+ for (IncomingMms mms : incomingMmsList) {
+ if (mms.getRat() == expectedMms.getRat()
+ && mms.getResult() == expectedMms.getResult()
+ && mms.getRoaming() == expectedMms.getRoaming()
+ && mms.getSimSlotIndex() == expectedMms.getSimSlotIndex()
+ && mms.getIsMultiSim() == expectedMms.getIsMultiSim()
+ && mms.getIsEsim() == expectedMms.getIsEsim()
+ && mms.getCarrierId() == expectedMms.getCarrierId()
+ && mms.getRetryId() == expectedMms.getRetryId()
+ && mms.getHandledByCarrierApp() == expectedMms.getHandledByCarrierApp()) {
+ actualCount = mms.getMmsCount();
+ actualAvg = mms.getAvgIntervalMillis();
+ }
+ }
+
+ assertEquals(expectedCount, actualCount);
+ assertEquals(expectedAvg, actualAvg);
+ }
+
+ private static void assertHasMmsAndCountAvg(@Nullable List<OutgoingMms> outgoingMmsList,
+ @Nullable OutgoingMms expectedMms, long expectedCount, long expectedAvg) {
+ assertNotNull(outgoingMmsList);
+ assertNotNull(expectedMms);
+ long actualCount = -1;
+ long actualAvg = -1;
+ for (OutgoingMms mms : outgoingMmsList) {
+ if (mms.getRat() == expectedMms.getRat()
+ && mms.getResult() == expectedMms.getResult()
+ && mms.getRoaming() == expectedMms.getRoaming()
+ && mms.getSimSlotIndex() == expectedMms.getSimSlotIndex()
+ && mms.getIsMultiSim() == expectedMms.getIsMultiSim()
+ && mms.getIsEsim() == expectedMms.getIsEsim()
+ && mms.getCarrierId() == expectedMms.getCarrierId()
+ && mms.getIsFromDefaultApp() == expectedMms.getIsFromDefaultApp()
+ && mms.getRetryId() == expectedMms.getRetryId()
+ && mms.getHandledByCarrierApp() == expectedMms.getHandledByCarrierApp()) {
+ actualCount = mms.getMmsCount();
+ actualAvg = mms.getAvgIntervalMillis();
+ }
+ }
+
+ assertEquals(expectedCount, actualCount);
+ assertEquals(expectedAvg, actualAvg);
+ }
+
+ private void verifyCurrentStateSavedToFileOnce() throws Exception {
+ InOrder inOrder = inOrder(mTestFileOutputStream);
+ inOrder.verify(mTestFileOutputStream, times(1))
+ .write(eq(mTestablePersistMmsAtomsStorage.getAtomsProto().toByteArray()));
+ inOrder.verify(mTestFileOutputStream, times(1)).close();
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ private PersistMmsAtoms getAtomsWritten(@Nullable InOrder inOrder) throws Exception {
+ if (inOrder == null) {
+ inOrder = inOrder(mTestFileOutputStream);
+ }
+ ArgumentCaptor bytesCaptor = ArgumentCaptor.forClass(Object.class);
+ inOrder.verify(mTestFileOutputStream, times(1))
+ .write((byte[]) bytesCaptor.capture());
+ PersistMmsAtoms savedAtoms = PersistMmsAtoms.parseFrom((byte[]) bytesCaptor.getValue());
+ inOrder.verify(mTestFileOutputStream, times(1)).close();
+ return savedAtoms;
+ }
+
+ private static IncomingMms copyOf(IncomingMms source) {
+ return source.toBuilder().build();
+ }
+
+ private static OutgoingMms copyOf(OutgoingMms source) {
+ return source.toBuilder().build();
+ }
+
+ private void makeTestData() {
+ mIncomingMms1Proto = IncomingMms.newBuilder()
+ .setRat(TelephonyManager.NETWORK_TYPE_LTE)
+ .setResult(1)
+ .setRoaming(ServiceState.ROAMING_TYPE_NOT_ROAMING)
+ .setSimSlotIndex(0)
+ .setIsMultiSim(true)
+ .setIsEsim(false)
+ .setCarrierId(CARRIER1_ID)
+ .setAvgIntervalMillis(500L)
+ .setMmsCount(1)
+ .setRetryId(0)
+ .setHandledByCarrierApp(false)
+ .build();
+
+ mIncomingMms2Proto = IncomingMms.newBuilder()
+ .setRat(TelephonyManager.NETWORK_TYPE_LTE)
+ .setResult(1)
+ .setRoaming(ServiceState.ROAMING_TYPE_NOT_ROAMING)
+ .setSimSlotIndex(1)
+ .setIsMultiSim(false)
+ .setIsEsim(false)
+ .setCarrierId(CARRIER2_ID)
+ .setAvgIntervalMillis(500L)
+ .setMmsCount(1)
+ .setRetryId(0)
+ .setHandledByCarrierApp(false)
+ .build();
+
+ mIncomingMmsList = new ArrayList<>();
+ mIncomingMmsList.add(mIncomingMms1Proto);
+ mIncomingMmsList.add(mIncomingMms2Proto);
+
+ mOutgoingMms1Proto = OutgoingMms.newBuilder()
+ .setRat(0)
+ .setResult(1)
+ .setRoaming(0)
+ .setSimSlotIndex(0)
+ .setIsMultiSim(true)
+ .setIsEsim(false)
+ .setCarrierId(CARRIER1_ID)
+ .setAvgIntervalMillis(500L)
+ .setMmsCount(1)
+ .setIsFromDefaultApp(true)
+ .setRetryId(0)
+ .setHandledByCarrierApp(false)
+ .build();
+
+ mOutgoingMms2Proto = OutgoingMms.newBuilder()
+ .setRat(0)
+ .setResult(1)
+ .setRoaming(0)
+ .setSimSlotIndex(0)
+ .setIsMultiSim(false)
+ .setIsEsim(false)
+ .setCarrierId(CARRIER2_ID)
+ .setAvgIntervalMillis(500L)
+ .setMmsCount(1)
+ .setIsFromDefaultApp(true)
+ .setRetryId(0)
+ .setHandledByCarrierApp(false)
+ .build();
+
+ mOutgoingMmsList = new ArrayList<>();
+ mOutgoingMmsList.add(mOutgoingMms1Proto);
+ mOutgoingMmsList.add(mOutgoingMms2Proto);
+ }
+
+ private void createEmptyTestFile() throws Exception {
+ PersistMmsAtoms atoms = PersistMmsAtoms.newBuilder().build();
+ FileOutputStream stream = new FileOutputStream(mTestFile);
+ stream.write(atoms.toByteArray());
+ stream.close();
+ }
+
+ private void createTestFile(long lastPullTimeMillis) throws Exception {
+ PersistMmsAtoms atoms = PersistMmsAtoms.newBuilder()
+ .setBuildFingerprint(Build.FINGERPRINT)
+ .setIncomingMmsPullTimestampMillis(lastPullTimeMillis)
+ .setOutgoingMmsPullTimestampMillis(lastPullTimeMillis)
+ .addAllIncomingMms(mIncomingMmsList)
+ .addAllOutgoingMms(mOutgoingMmsList)
+ .build();
+
+ FileOutputStream stream = new FileOutputStream(mTestFile);
+ stream.write(atoms.toByteArray());
+ stream.close();
+ }
+
+ private static class TestablePersistMmsAtomsStorage extends PersistMmsAtomsStorage {
+ private long mTimeMillis = START_TIME_MILLIS;
+
+ TestablePersistMmsAtomsStorage(Context context) {
+ super(context);
+ // Remove delay for saving to persistent storage during tests.
+ mSaveImmediately = true;
+ }
+
+ @Override
+ protected long getWallTimeMillis() {
+ // NOTE: super class constructor will be executed before private field is set, which
+ // gives the wrong start time (mTimeMillis will have its default value of 0L).
+ return mTimeMillis == 0L ? START_TIME_MILLIS : mTimeMillis;
+ }
+
+ private void incTimeMillis(long timeMillis) {
+ mTimeMillis += timeMillis;
+ }
+
+ private PersistMmsAtoms getAtomsProto() {
+ // NOTE: unlike other methods in PersistAtomsStorage, this is not synchronized, but
+ // should be fine since the test is single-threaded.
+ return mPersistMmsAtoms;
+ }
+ }
+} \ No newline at end of file