diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:22:17 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:22:17 +0000 |
commit | 3466fd4eecbc152728a0f3ad3e2dacba45fd7a82 (patch) | |
tree | e03dc9d843034a476a34f3e1fb0d0a7accf4a17a | |
parent | 65e30106a579db014c76b7d3a5cf0d2d3f349c63 (diff) | |
parent | 0a1ab1245ef3f64593d270f8cd5217a823e51c0c (diff) | |
download | Mms-android14-mainline-uwb-release.tar.gz |
Snap for 10453563 from 0a1ab1245ef3f64593d270f8cd5217a823e51c0c to mainline-uwb-releaseaml_uwb_341513070aml_uwb_341511050aml_uwb_341310300aml_uwb_341310030aml_uwb_341111010aml_uwb_341011000android14-mainline-uwb-release
Change-Id: Idd342b326565dc5018697a07333e93b14c8b29dd
22 files changed, 2419 insertions, 59 deletions
@@ -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-service-srcs", + srcs: [ + "src/com/android/mms/service/**/*.java", + ], } diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 87e7947..a19a11a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -26,6 +26,10 @@ <uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"/> <uses-permission android:name="android.permission.BIND_CARRIER_SERVICES"/> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/> + <!-- Needed to check if subscription is active. --> + <uses-permission android:name="android.permission.READ_PHONE_STATE"/> + <!-- Needed to query user associated with a subscription. --> + <uses-permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION"/> <protected-broadcast android:name="android.settings.ENABLE_MMS_DATA_REQUEST"/> 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..bde1cb9 --- /dev/null +++ b/proto/src/persist_mms_atoms.proto @@ -0,0 +1,70 @@ +/* + * 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; + optional bool is_managed_profile = 12; +} + +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; + optional bool is_managed_profile = 13; +} diff --git a/src/com/android/mms/service/DownloadRequest.java b/src/com/android/mms/service/DownloadRequest.java index 0f12415..62fa9e6 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; @@ -296,10 +298,12 @@ public class DownloadRequest extends MmsRequest { if (mCarrierMessagingServiceWrapper.bindToCarrierMessagingService( context, carrierMessagingServicePackage, Runnable::run, ()->onServiceReady())) { - LogUtil.v("bindService() for carrier messaging service succeeded. " + LogUtil.v("bindService() for carrier messaging service: " + + carrierMessagingServicePackage + " succeeded. " + MmsService.formatCrossStackMessageId(mMessageId)); } else { - LogUtil.e("bindService() for carrier messaging service failed. " + LogUtil.e("bindService() for carrier messaging service: " + + carrierMessagingServicePackage + " failed. " + MmsService.formatCrossStackMessageId(mMessageId)); carrierDownloadCallback.onDownloadMmsComplete( CarrierMessagingService.DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK); diff --git a/src/com/android/mms/service/MmsConstants.java b/src/com/android/mms/service/MmsConstants.java new file mode 100644 index 0000000..57ef5df --- /dev/null +++ b/src/com/android/mms/service/MmsConstants.java @@ -0,0 +1,25 @@ +/* + * 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; + +import java.util.UUID; + +public class MmsConstants { + // MMS anomaly uuid + public static final UUID MMS_ANOMALY_UUID = UUID.fromString( + "e4330975-17be-43b7-87d6-d9f281d33278"); +} diff --git a/src/com/android/mms/service/MmsHttpClient.java b/src/com/android/mms/service/MmsHttpClient.java index 5cec66f..b54b1aa 100644 --- a/src/com/android/mms/service/MmsHttpClient.java +++ b/src/com/android/mms/service/MmsHttpClient.java @@ -52,6 +52,8 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.android.internal.annotations.VisibleForTesting; + /** * MMS HTTP client for sending and downloading MMS messages */ @@ -452,6 +454,21 @@ public class MmsHttpClient { return sb.toString(); } + private static String getPhoneNumberForMacroLine1(TelephonyManager telephonyManager, + Context context, int subId) { + String phoneNo = telephonyManager.getLine1Number(); + if (TextUtils.isEmpty(phoneNo)) { + SubscriptionManager subscriptionManager = context.getSystemService( + SubscriptionManager.class); + if (subscriptionManager != null) { + phoneNo = subscriptionManager.getPhoneNumber(subId); + } else { + LogUtil.e("subscriptionManager is null"); + } + } + return phoneNo; + } + /* * Macro names */ @@ -471,15 +488,16 @@ public class MmsHttpClient { * @param subId The subscription ID used to get line number, etc. * @return The value of the defined macro */ - private static String getMacroValue(Context context, String macro, Bundle mmsConfig, - int subId) { + @VisibleForTesting + public static String getMacroValue(Context context, String macro, Bundle mmsConfig, + int subId) { final TelephonyManager telephonyManager = ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE)).createForSubscriptionId(subId); if (MACRO_LINE1.equals(macro)) { - return telephonyManager.getLine1Number(); + return getPhoneNumberForMacroLine1(telephonyManager, context, subId); } else if (MACRO_LINE1NOCOUNTRYCODE.equals(macro)) { return PhoneUtils.getNationalNumber(telephonyManager, - telephonyManager.getLine1Number()); + getPhoneNumberForMacroLine1(telephonyManager, context, subId)); } else if (MACRO_NAI.equals(macro)) { return getNai(telephonyManager, mmsConfig); } diff --git a/src/com/android/mms/service/MmsNetworkManager.java b/src/com/android/mms/service/MmsNetworkManager.java index 6799d29..f21e510 100644 --- a/src/com/android/mms/service/MmsNetworkManager.java +++ b/src/com/android/mms/service/MmsNetworkManager.java @@ -26,11 +26,16 @@ import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.TelephonyNetworkSpecifier; +import android.os.Binder; import android.os.Handler; import android.os.Looper; +import android.os.Message; +import android.os.PersistableBundle; import android.provider.DeviceConfig; +import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.PhoneConstants; @@ -52,9 +57,8 @@ public class MmsNetworkManager { // timeout to make sure we don't bail prematurely. private static final int ADDITIONAL_NETWORK_ACQUIRE_TIMEOUT_MILLIS = (5 * 1000); - // Waiting time used before releasing a network prematurely. This allows the MMS download - // acknowledgement messages to be sent using the same network that was used to download the data - private static final int NETWORK_RELEASE_TIMEOUT_MILLIS = 5 * 1000; + /* Event created when receiving ACTION_CARRIER_CONFIG_CHANGED */ + private static final int EVENT_CARRIER_CONFIG_CHANGED = 1; private final Context mContext; @@ -88,17 +92,43 @@ public class MmsNetworkManager { private int mPhoneId; // If ACTION_SIM_CARD_STATE_CHANGED intent receiver is registered - private boolean mReceiverRegistered; + private boolean mSimCardStateChangedReceiverRegistered; private final Dependencies mDeps; + private int mNetworkReleaseTimeoutMillis; + private EventHandler mEventHandler; + + private final class EventHandler extends Handler { + EventHandler() { + super(Looper.getMainLooper()); + } + + /** + * Handles events coming from the phone stack. Overridden from handler. + * + * @param msg the message to handle + */ + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_CARRIER_CONFIG_CHANGED: + // Reload mNetworkReleaseTimeoutMillis from CarrierConfigManager. + handleCarrierConfigChanged(); + break; + default: + LogUtil.e("MmsNetworkManager: ignoring message of unexpected type " + msg.what); + } + } + } + /** * This receiver listens to ACTION_SIM_CARD_STATE_CHANGED after starting a new NetworkRequest. * If ACTION_SIM_CARD_STATE_CHANGED with SIM_STATE_ABSENT for a SIM card corresponding to the * current NetworkRequest is received, it just releases the NetworkRequest without waiting for * timeout. */ - private final BroadcastReceiver mReceiver = + private final BroadcastReceiver mSimCardStateChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -140,6 +170,35 @@ public class MmsNetworkManager { } /** + * This receiver listens to ACTION_CARRIER_CONFIG_CHANGED. Whenever receiving this event, + * mNetworkReleaseTimeoutMillis needs to be reloaded from CarrierConfigManager. + */ + private final BroadcastReceiver mCarrierConfigChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action) + && mSubId == intent.getIntExtra( + CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)) { + mEventHandler.sendMessage(mEventHandler.obtainMessage( + EVENT_CARRIER_CONFIG_CHANGED)); + } + } + }; + + private void handleCarrierConfigChanged() { + final CarrierConfigManager configManager = + (CarrierConfigManager) + mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); + final PersistableBundle config = configManager.getConfigForSubId(mSubId); + mNetworkReleaseTimeoutMillis = + config.getInt(CarrierConfigManager.KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT); + LogUtil.d("MmsNetworkManager: handleCarrierConfigChanged() mNetworkReleaseTimeoutMillis " + + mNetworkReleaseTimeoutMillis); + } + + /** * Network callback for our network request */ private class NetworkRequestCallback extends ConnectivityManager.NetworkCallback { @@ -245,6 +304,13 @@ public class MmsNetworkManager { } } }; + + mEventHandler = new EventHandler(); + // Register a receiver to listen to ACTION_CARRIER_CONFIG_CHANGED + mContext.registerReceiver( + mCarrierConfigChangedReceiver, + new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)); + handleCarrierConfigChanged(); } public MmsNetworkManager(Context context, int subId) { @@ -270,7 +336,7 @@ public class MmsNetworkManager { return; } - if (!mReceiverRegistered) { + if (!mSimCardStateChangedReceiverRegistered) { mPhoneId = mDeps.getPhoneId(mSubId); if (mPhoneId == SubscriptionManager.INVALID_PHONE_INDEX || mPhoneId == SubscriptionManager.DEFAULT_PHONE_INDEX) { @@ -279,9 +345,9 @@ public class MmsNetworkManager { // Register a receiver to listen to ACTION_SIM_CARD_STATE_CHANGED mContext.registerReceiver( - mReceiver, + mSimCardStateChangedReceiver, new IntentFilter(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED)); - mReceiverRegistered = true; + mSimCardStateChangedReceiverRegistered = true; } // Not available, so start a new request if not done yet @@ -297,10 +363,10 @@ public class MmsNetworkManager { LogUtil.w(requestId, "MmsNetworkManager: acquire network wait interrupted"); } - if (mReceiverRegistered) { + if (mSimCardStateChangedReceiverRegistered) { // Unregister the receiver. - mContext.unregisterReceiver(mReceiver); - mReceiverRegistered = false; + mContext.unregisterReceiver(mSimCardStateChangedReceiver); + mSimCardStateChangedReceiverRegistered = false; } if (mNetwork != null) { @@ -328,10 +394,11 @@ public class MmsNetworkManager { /** * Release the MMS network when nobody is holding on to it. * - * @param requestId request ID for logging - * @param shouldDelayRelease whether the release should be delayed for 5 seconds, the regular - * use case is to delay this for DownloadRequests to use the network - * for sending an acknowledgement on the same network + * @param requestId request ID for logging. + * @param shouldDelayRelease whether the release should be delayed for a carrier-configured + * timeout (default 5 seconds), the regular use case is to delay this + * for DownloadRequests to use the network for sending an + * acknowledgement on the same network. */ public void releaseNetwork(final String requestId, final boolean shouldDelayRelease) { synchronized (this) { @@ -344,7 +411,7 @@ public class MmsNetworkManager { // handler to release the network mReleaseHandler.removeCallbacks(mNetworkReleaseTask); mReleaseHandler.postDelayed(mNetworkReleaseTask, - NETWORK_RELEASE_TIMEOUT_MILLIS); + mNetworkReleaseTimeoutMillis); } else { releaseRequestLocked(mNetworkCallback); } @@ -443,4 +510,9 @@ public class MmsNetworkManager { } return apnName; } + + @VisibleForTesting + protected int getNetworkReleaseTimeoutMillis() { + return mNetworkReleaseTimeoutMillis; + } } diff --git a/src/com/android/mms/service/MmsRequest.java b/src/com/android/mms/service/MmsRequest.java index dfef1cc..dca77df 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; @@ -49,8 +50,6 @@ public abstract class MmsRequest { private static final int RETRY_TIMES = 3; // Signal level threshold for both wifi and cellular private static final int SIGNAL_LEVEL_THRESHOLD = 2; - // MMS anomaly uuid - private final UUID mAnomalyUUID = UUID.fromString("e4330975-17be-43b7-87d6-d9f281d33278"); public static final String EXTRA_LAST_CONNECTION_FAILURE_CAUSE_CODE = "android.telephony.extra.LAST_CONNECTION_FAILURE_CAUSE_CODE"; public static final String EXTRA_HANDLED_BY_CARRIER_APP @@ -101,6 +100,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 +121,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 +147,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 +156,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 +214,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 +243,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 +284,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); } @@ -314,8 +323,9 @@ public abstract class MmsRequest { private UUID generateUUID(int result, int httpStatusCode) { long lresult = result; long lhttpStatusCode = httpStatusCode; - return new UUID(mAnomalyUUID.getMostSignificantBits(), - mAnomalyUUID.getLeastSignificantBits() + ((lhttpStatusCode << 32) + lresult)); + return new UUID(MmsConstants.MMS_ANOMALY_UUID.getMostSignificantBits(), + MmsConstants.MMS_ANOMALY_UUID.getLeastSignificantBits() + + ((lhttpStatusCode << 32) + lresult)); } private boolean isPoorSignal() { diff --git a/src/com/android/mms/service/MmsService.java b/src/com/android/mms/service/MmsService.java index d227b3c..ee88f09 100644 --- a/src/com/android/mms/service/MmsService.java +++ b/src/com/android/mms/service/MmsService.java @@ -41,6 +41,7 @@ import android.provider.Settings; import android.provider.Telephony; import android.security.NetworkSecurityPolicy; import android.service.carrier.CarrierMessagingService; +import android.telephony.AnomalyReporter; import android.telephony.SmsManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -51,6 +52,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 +141,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); @@ -179,6 +184,8 @@ public class MmsService extends Service implements MmsRequest.RequestManager { List<String> carrierPackages = telephonyManager.getCarrierPackageNamesForIntent(intent); if (carrierPackages == null || carrierPackages.size() != 1) { + LogUtil.d("getCarrierMessagingServicePackageIfExists - multiple (" + + carrierPackages.size() + ") carrier apps installed, not using any."); return null; } else { return carrierPackages.get(0); @@ -211,19 +218,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 +243,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,19 +255,21 @@ 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); if (carrierMessagingServicePackage != null) { - LogUtil.d(request.toString(), "sending message by carrier app " - + formatCrossStackMessageId(messageId)); + LogUtil.d(request.toString(), "sending message by carrier app: " + + carrierMessagingServicePackage + + " " + formatCrossStackMessageId(messageId)); request.trySendingByCarrierApp(MmsService.this, carrierMessagingServicePackage); return; } @@ -269,7 +283,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 +302,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 +336,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,21 +354,21 @@ 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); if (carrierMessagingServicePackage != null) { - LogUtil.d(request.toString(), "downloading message by carrier app " - + formatCrossStackMessageId(messageId)); + LogUtil.d(request.toString(), "downloading message by carrier app: " + + carrierMessagingServicePackage + + " " + formatCrossStackMessageId(messageId)); request.tryDownloadingByCarrierApp(MmsService.this, carrierMessagingServicePackage); return; } @@ -357,7 +376,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 +596,14 @@ 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); + String message = "MMS failed" + " with error " + resultCode; + AnomalyReporter.reportAnomaly(MmsConstants.MMS_ANOMALY_UUID, message); + } }; @Override @@ -703,6 +730,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..4f97d84 100644 --- a/src/com/android/mms/service/SendRequest.java +++ b/src/com/android/mms/service/SendRequest.java @@ -25,17 +25,21 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; +import android.os.UserHandle; import android.provider.BlockedNumberContract; import android.provider.Telephony; import android.service.carrier.CarrierMessagingService; import android.service.carrier.CarrierMessagingServiceWrapper; import android.telephony.SmsManager; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; 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 +62,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; @@ -172,10 +176,22 @@ public class SendRequest extends MmsRequest { @Override protected Uri persistIfRequired(Context context, int result, byte[] response) { final String requestId = getRequestId(); - if (!SmsApplication.shouldWriteMessageForPackage(mCreator, context)) { - // Not required to persist + + SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class); + UserHandle userHandle = null; + long identity = Binder.clearCallingIdentity(); + try { + if ((subManager != null) && (subManager.isActiveSubscriptionId(mSubId))) { + userHandle = subManager.getSubscriptionUserHandle(mSubId); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + + if (!SmsApplication.shouldWriteMessageForPackageAsUser(mCreator, context, userHandle)) { return null; } + LogUtil.d(requestId, "persistIfRequired. " + MmsService.formatCrossStackMessageId(mMessageId)); if (mPduData == null) { @@ -183,7 +199,7 @@ public class SendRequest extends MmsRequest { + MmsService.formatCrossStackMessageId(mMessageId)); return null; } - final long identity = Binder.clearCallingIdentity(); + identity = Binder.clearCallingIdentity(); try { final boolean supportContentDisposition = mMmsConfig.getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION); @@ -417,10 +433,12 @@ public class SendRequest extends MmsRequest { if (mCarrierMessagingServiceWrapper.bindToCarrierMessagingService( context, carrierMessagingServicePackage, Runnable::run, () -> onServiceReady())) { - LogUtil.v("bindService() for carrier messaging service succeeded. " + LogUtil.v("bindService() for carrier messaging service: " + + carrierMessagingServicePackage + " succeeded. " + MmsService.formatCrossStackMessageId(mMessageId)); } else { - LogUtil.e("bindService() for carrier messaging service failed. " + LogUtil.e("bindService() for carrier messaging service: " + + carrierMessagingServicePackage + " failed. " + MmsService.formatCrossStackMessageId(mMessageId)); carrierSendCompleteCallback.onSendMmsComplete( CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK, 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..8da61ba --- /dev/null +++ b/src/com/android/mms/service/metrics/MmsMetricsCollector.java @@ -0,0 +1,162 @@ +/* + * 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.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 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); + registerAtom(OUTGOING_MMS); + 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(), + mms.getIsManagedProfile()); + } + + 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(), + mms.getIsManagedProfile()); + } + + @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}. */ + private void registerAtom(int atomId) { + mStatsManager.setPullAtomCallback(atomId, /* metadata= */ null, + 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..7e98b0b --- /dev/null +++ b/src/com/android/mms/service/metrics/MmsStats.java @@ -0,0 +1,232 @@ +/* + * 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.os.UserHandle; +import android.os.UserManager; +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) + .setIsManagedProfile(isManagedProfile()) + .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) + .setIsManagedProfile(isManagedProfile()) + .build(); + mPersistMmsAtomsStorage.addOutgoingMms(outgoingMms); + } + + /** @return {@code true} if this SIM is dedicated to work profile */ + private boolean isManagedProfile() { + SubscriptionManager subManager = mContext.getSystemService(SubscriptionManager.class); + if (subManager == null || !subManager.isActiveSubscriptionId(mSubId)) return false; + UserHandle userHandle = subManager.getSubscriptionUserHandle(mSubId); + UserManager userManager = mContext.getSystemService(UserManager.class); + if (userHandle == null || userManager == null) return false; + return userManager.isManagedProfile(userHandle.getIdentifier()); + } + + /** 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() { + UserHandle userHandle = null; + SubscriptionManager subManager = mContext.getSystemService(SubscriptionManager.class); + if ((subManager != null) && (subManager.isActiveSubscriptionId(mSubId))) { + userHandle = subManager.getSubscriptionUserHandle(mSubId); + } + return SmsApplication.isDefaultMmsApplicationAsUser(mContext, mCallingPkg, userHandle); + } + + /** + * 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); + } +} 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/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java b/tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java index c2d3e05..dff2bab 100644 --- a/tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java +++ b/tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java @@ -36,6 +36,8 @@ import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; import org.junit.Before; import org.junit.Test; @@ -70,7 +72,8 @@ public final class MmsNetworkManagerTest { @Mock Context mCtx; @Mock ConnectivityManager mCm; @Mock MmsNetworkManager.Dependencies mDeps; - + @Mock CarrierConfigManager mCarrierConfigManager; + @Mock PersistableBundle mConfig; private MmsNetworkManager mMnm; private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); @@ -89,6 +92,8 @@ public final class MmsNetworkManagerTest { doReturn(MMS_APN2).when(mNetworkInfo2).getExtraInfo(); doReturn(NETWORK_ACQUIRE_TIMEOUT_MS).when(mDeps).getNetworkRequestTimeoutMillis(); doReturn(NETWORK_ACQUIRE_TIMEOUT_MS).when(mDeps).getAdditionalNetworkAcquireTimeoutMillis(); + doReturn(mCarrierConfigManager).when(mCtx).getSystemService(Context.CARRIER_CONFIG_SERVICE); + doReturn(mConfig).when(mCarrierConfigManager).getConfigForSubId(TEST_SUBID); mMnm = new MmsNetworkManager(mCtx, TEST_SUBID, mDeps); } @@ -201,6 +206,25 @@ public final class MmsNetworkManagerTest { assertEquals(null, mMnm.getApnName()); } + @Test + public void testHandleCarrierConfigChanged() throws Exception { + // Expect receiving default NETWORK_RELEASE_TIMEOUT of 5 seconds + int defaultNetworkReleaseTimeout = 5000; + doReturn(defaultNetworkReleaseTimeout).when(mConfig).getInt( + CarrierConfigManager.KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT); + MmsNetworkManager mmsNetworkManager = new MmsNetworkManager(mCtx, TEST_SUBID, mDeps); + assertEquals(defaultNetworkReleaseTimeout, + mmsNetworkManager.getNetworkReleaseTimeoutMillis()); + + // Expect receiving a carrier-configured value + int configuredNetworkReleaseTimeout = 10000; + doReturn(configuredNetworkReleaseTimeout).when(mConfig).getInt( + CarrierConfigManager.KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT); + mmsNetworkManager = new MmsNetworkManager(mCtx, TEST_SUBID, mDeps); + assertEquals(configuredNetworkReleaseTimeout, + mmsNetworkManager.getNetworkReleaseTimeoutMillis()); + } + private NetworkCallback acquireAvailableNetworkAndGetCallback( Network expectNetwork, String expectApn) throws Exception { final ArgumentCaptor<NetworkCallback> callbackCaptor = diff --git a/tests/unittests/Android.bp b/tests/unittests/Android.bp new file mode 100644 index 0000000..595b4be --- /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-service-srcs"], + platform_apis: true, + test_suites: ["device-tests"], + certificate: "platform", + instrumentation_for: "MmsService", +} 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/MmsHttpClientTest.java b/tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java new file mode 100644 index 0000000..dd126e8 --- /dev/null +++ b/tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2023 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; + +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.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.os.Bundle; +import android.telephony.ServiceState; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.After; +import org.junit.Before; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.reset; + +import android.util.Log; + +public class MmsHttpClientTest { + // Mocked classes + private Context mContext; + private TelephonyManager mTelephonyManager; + private SubscriptionManager mSubscriptionManager; + + // The raw phone number from TelephonyManager.getLine1Number + private static final String MACRO_LINE1 = "LINE1"; + // The phone number without country code + private static final String MACRO_LINE1NOCOUNTRYCODE = "LINE1NOCOUNTRYCODE"; + private String line1Number = "1234567890"; + private String subscriberPhoneNumber = "0987654321"; + private int subId = 1; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + mTelephonyManager = mock(TelephonyManager.class); + mSubscriptionManager = mock(SubscriptionManager.class); + + when(mContext.getSystemService(Context.TELEPHONY_SERVICE)) + .thenReturn(mTelephonyManager); + when(mTelephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(mTelephonyManager); + when(mContext.getSystemService(SubscriptionManager.class)) + .thenReturn(mSubscriptionManager); + } + + @After + public void tearDown() { + mContext = null; + mTelephonyManager = null; + mSubscriptionManager = null; + } + + @Test + public void getPhoneNumberForMacroLine1() { + String macro = MACRO_LINE1; + Bundle mmsConfig = new Bundle(); + String emptyStr = ""; + String phoneNo; + + /* when getLine1Number returns valid number */ + doReturn(line1Number).when(mTelephonyManager).getLine1Number(); + phoneNo = MmsHttpClient.getMacroValue(mContext, macro, mmsConfig, subId); + assertThat(phoneNo).isEqualTo(line1Number); + // getLine1NumberAPI should be called + verify(mTelephonyManager).getLine1Number(); + // getPhoneNumber should never be called + verify(mSubscriptionManager, never()).getPhoneNumber(subId); + + /* when getLine1Number returns empty string */ + doReturn(emptyStr).when(mTelephonyManager).getLine1Number(); + when(mSubscriptionManager.getPhoneNumber(subId)).thenReturn(subscriberPhoneNumber); + phoneNo = MmsHttpClient.getMacroValue(mContext, macro, mmsConfig, subId); + assertThat(phoneNo).isEqualTo(subscriberPhoneNumber); + verify(mSubscriptionManager).getPhoneNumber(subId); + + /* when getLine1Number returns null */ + reset(mSubscriptionManager); + when(mSubscriptionManager.getPhoneNumber(subId)).thenReturn(subscriberPhoneNumber); + doReturn(null).when(mTelephonyManager).getLine1Number(); + phoneNo = MmsHttpClient.getMacroValue(mContext, macro, mmsConfig, subId); + assertThat(phoneNo).isEqualTo(subscriberPhoneNumber); + verify(mSubscriptionManager).getPhoneNumber(subId); + } + + @Test + public void getPhoneNumberForMacroLine1CountryCode() throws Exception { + String macro = MACRO_LINE1NOCOUNTRYCODE; + String emptyStr = ""; + String phoneNo; + Bundle mmsConfig = new Bundle(); + + /* when getLine1Number returns valid number */ + doReturn(line1Number).when(mTelephonyManager).getLine1Number(); + phoneNo = MmsHttpClient.getMacroValue(mContext, macro, mmsConfig, subId); + assertThat(phoneNo).contains(line1Number); + // getLine1NumberAPI should be called + verify(mTelephonyManager).getLine1Number(); + // getPhoneNumber should never be called + verify(mSubscriptionManager, never()).getPhoneNumber(subId); + + /* when getLine1Number returns empty string */ + doReturn(emptyStr).when(mTelephonyManager).getLine1Number(); + when(mSubscriptionManager.getPhoneNumber(subId)).thenReturn(subscriberPhoneNumber); + phoneNo = MmsHttpClient.getMacroValue(mContext, macro, mmsConfig, subId); + assertThat(phoneNo).contains(subscriberPhoneNumber); + verify(mSubscriptionManager).getPhoneNumber(subId); + + /* when getLine1Number returns null */ + reset(mSubscriptionManager); + when(mSubscriptionManager.getPhoneNumber(subId)).thenReturn(subscriberPhoneNumber); + doReturn(null).when(mTelephonyManager).getLine1Number(); + phoneNo = MmsHttpClient.getMacroValue(mContext, macro, mmsConfig, subId); + assertThat(phoneNo).contains(subscriberPhoneNumber); + verify(mSubscriptionManager).getPhoneNumber(subId); + } +} 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..2b2cae5 --- /dev/null +++ b/tests/unittests/src/com/android/mms/service/metrics/MmsStatsTest.java @@ -0,0 +1,154 @@ +/* + * 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.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +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; + private SubscriptionManager mSubscriptionManager; + + @Before + public void setUp() { + mContext = mock(Context.class); + mPersistMmsAtomsStorage = mock(PersistMmsAtomsStorage.class); + mTelephonyManager = mock(TelephonyManager.class); + mSubscriptionManager = mock(SubscriptionManager.class); + + doReturn(mSubscriptionManager).when(mContext).getSystemService( + Context.TELEPHONY_SUBSCRIPTION_SERVICE); + } + + @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); + assertThat(incomingMms.getIsManagedProfile()).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); + assertThat(outgoingMms.getIsManagedProfile()).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); + } + + + @Test + public void isDefaultMmsApp_subId_inactive() { + int inactiveSubId = 123; + doReturn(false).when(mSubscriptionManager) + .isActiveSubscriptionId(eq(inactiveSubId)); + + MmsStats mmsStats = new MmsStats(mContext, mPersistMmsAtomsStorage, inactiveSubId, + mTelephonyManager, null, false); + mmsStats.addAtomToStorage(Activity.RESULT_OK); + + // getSubscriptionUserHandle should not be called if subID is inactive. + verify(mSubscriptionManager, never()).getSubscriptionUserHandle(eq(inactiveSubId)); + } +}
\ 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 |