summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2024-01-17 22:14:36 -0800
committerXin Li <delphij@google.com>2024-01-17 22:14:36 -0800
commitec6e77186041fd3f82b0783821c0820c0bcfc42c (patch)
treed08fb2c72478e26d6012603806ec40154e3bfea1
parent2480e24a21d207a4cf34ad76b477169b322947f1 (diff)
parent2f625059ce181774f1cbfc983453d10d351e0a87 (diff)
downloadMms-ec6e77186041fd3f82b0783821c0820c0bcfc42c.tar.gz
Merge Android 24Q1 Release (ab/11220357)temp_319669529
Bug: 319669529 Merged-In: I0f15493f505c4cb99f829e9d6765a2a9eb152e44 Change-Id: I793afaf1d5a7e16f7f6badd5c16d24472ef83072
-rw-r--r--PREUPLOAD.cfg2
-rw-r--r--src/com/android/mms/service/MmsConfigManager.java5
-rw-r--r--src/com/android/mms/service/MmsHttpClient.java48
-rw-r--r--src/com/android/mms/service/MmsNetworkManager.java76
-rw-r--r--src/com/android/mms/service/MmsRequest.java142
-rw-r--r--src/com/android/mms/service/MmsService.java21
-rw-r--r--src/com/android/mms/service/exception/VoluntaryDisconnectMmsHttpException.java27
-rw-r--r--src/com/android/mms/service/metrics/MmsStats.java5
-rw-r--r--tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java36
-rw-r--r--tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java57
10 files changed, 347 insertions, 72 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..e5f1877
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,2 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} \ No newline at end of file
diff --git a/src/com/android/mms/service/MmsConfigManager.java b/src/com/android/mms/service/MmsConfigManager.java
index 3606ed8..ce27d9f 100644
--- a/src/com/android/mms/service/MmsConfigManager.java
+++ b/src/com/android/mms/service/MmsConfigManager.java
@@ -27,6 +27,8 @@ import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.util.ArrayMap;
+import com.android.internal.telephony.flags.Flags;
+
import java.util.List;
import java.util.Map;
@@ -60,6 +62,9 @@ public class MmsConfigManager {
public void init(final Context context) {
mContext = context;
mSubscriptionManager = SubscriptionManager.from(context);
+ if (Flags.workProfileApiSplit()) {
+ mSubscriptionManager = mSubscriptionManager.createForAllUserProfiles();
+ }
context.registerReceiver(
mReceiver, new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
LogUtil.i("MmsConfigManager loads mms config in init()");
diff --git a/src/com/android/mms/service/MmsHttpClient.java b/src/com/android/mms/service/MmsHttpClient.java
index b54b1aa..7978a71 100644
--- a/src/com/android/mms/service/MmsHttpClient.java
+++ b/src/com/android/mms/service/MmsHttpClient.java
@@ -29,7 +29,9 @@ import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.mms.service.exception.MmsHttpException;
+import com.android.mms.service.exception.VoluntaryDisconnectMmsHttpException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -49,11 +51,12 @@ import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
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
*/
@@ -87,6 +90,11 @@ public class MmsHttpClient {
private final Network mNetwork;
private final ConnectivityManager mConnectivityManager;
+ /** Store all currently open connections, for potential voluntarily early disconnect. */
+ private final Set<HttpURLConnection> mAllUrlConnections = ConcurrentHashMap.newKeySet();
+ /** Flag indicating whether a disconnection is voluntary. */
+ private final AtomicBoolean mVoluntarilyDisconnectingConnections = new AtomicBoolean(false);
+
/**
* Constructor
*
@@ -136,6 +144,7 @@ public class MmsHttpClient {
maybeWaitForIpv4(requestId, url);
// Now get the connection
connection = (HttpURLConnection) mNetwork.openConnection(url, proxy);
+ if (connection != null) mAllUrlConnections.add(connection);
connection.setDoInput(true);
connection.setConnectTimeout(
mmsConfig.getInt(SmsManager.MMS_CONFIG_HTTP_SOCKET_TIMEOUT));
@@ -238,15 +247,46 @@ public class MmsHttpClient {
LogUtil.e(requestId, "HTTP: invalid URL protocol " + redactedUrl, e);
throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + redactedUrl, e);
} catch (IOException e) {
- LogUtil.e(requestId, "HTTP: IO failure", e);
- throw new MmsHttpException(0/*statusCode*/, e);
+ if (mVoluntarilyDisconnectingConnections.get()) {
+ // If in the process of voluntarily disconnecting all connections, the exception
+ // is casted as VoluntaryDisconnectMmsHttpException to indicate this attempt is
+ // cancelled rather than failure.
+ LogUtil.d(requestId,
+ "HTTP voluntarily disconnected due to WLAN network available");
+ throw new VoluntaryDisconnectMmsHttpException(0/*statusCode*/,
+ "Expected disconnection due to WLAN network available");
+ } else {
+ LogUtil.e(requestId, "HTTP: IO failure ", e);
+ throw new MmsHttpException(0/*statusCode*/, e);
+ }
} finally {
if (connection != null) {
connection.disconnect();
+ mAllUrlConnections.remove(connection);
+ // If all connections are done disconnected, flag voluntary disconnection done if
+ // applicable.
+ if (mAllUrlConnections.isEmpty() && mVoluntarilyDisconnectingConnections
+ .compareAndSet(/*expectedValue*/true, /*newValue*/false)) {
+ LogUtil.d("All voluntarily disconnected connections are removed.");
+ }
}
}
}
+ /**
+ * Voluntarily disconnect all Http URL connections. This will trigger
+ * {@link VoluntaryDisconnectMmsHttpException} to be thrown, to indicate voluntary disconnection
+ */
+ public void disconnectAllUrlConnections() {
+ LogUtil.d("Disconnecting all Url connections, size = " + mAllUrlConnections.size());
+ if (mAllUrlConnections.isEmpty()) return;
+ mVoluntarilyDisconnectingConnections.set(true);
+ for (HttpURLConnection connection : mAllUrlConnections) {
+ // TODO: An improvement is to check the writing/reading progress before disconnect.
+ connection.disconnect();
+ }
+ }
+
private void maybeWaitForIpv4(final String requestId, final URL url) {
// If it's a literal IPv4 address and we're on an IPv6-only network,
// wait until IPv4 is available.
diff --git a/src/com/android/mms/service/MmsNetworkManager.java b/src/com/android/mms/service/MmsNetworkManager.java
index f21e510..1f872e0 100644
--- a/src/com/android/mms/service/MmsNetworkManager.java
+++ b/src/com/android/mms/service/MmsNetworkManager.java
@@ -16,6 +16,7 @@
package com.android.mms.service;
+import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -26,7 +27,6 @@ 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;
@@ -35,7 +35,6 @@ 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;
@@ -45,8 +44,10 @@ import com.android.mms.service.exception.MmsNetworkException;
* Manages the MMS network connectivity
*/
public class MmsNetworkManager {
+ /** Device Config Keys */
private static final String MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS =
"mms_service_network_request_timeout_millis";
+ private static final String MMS_ENHANCEMENT_ENABLED = "mms_enhancement_enabled";
// Default timeout used to call ConnectivityManager.requestNetwork if the
// MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS flag is not set.
@@ -59,6 +60,8 @@ public class MmsNetworkManager {
/* Event created when receiving ACTION_CARRIER_CONFIG_CHANGED */
private static final int EVENT_CARRIER_CONFIG_CHANGED = 1;
+ /** Event when a WLAN network newly available despite of the existing available one. */
+ private static final int EVENT_IWLAN_NETWORK_NEWLY_AVAILABLE = 2;
private final Context mContext;
@@ -66,6 +69,8 @@ public class MmsNetworkManager {
// We need this when we unbind from it. This is also used to indicate if the
// MMS network is available.
private Network mNetwork;
+ /** Whether an Iwlan MMS network is available to use. */
+ private boolean mIsLastAvailableNetworkIwlan;
// The current count of MMS requests that require the MMS network
// If mMmsRequestCount is 0, we should release the MMS network.
private int mMmsRequestCount;
@@ -116,6 +121,9 @@ public class MmsNetworkManager {
// Reload mNetworkReleaseTimeoutMillis from CarrierConfigManager.
handleCarrierConfigChanged();
break;
+ case EVENT_IWLAN_NETWORK_NEWLY_AVAILABLE:
+ onIwlanNetworkNewlyAvailable();
+ break;
default:
LogUtil.e("MmsNetworkManager: ignoring message of unexpected type " + msg.what);
}
@@ -187,6 +195,17 @@ public class MmsNetworkManager {
}
};
+ /**
+ * Called when a WLAN network newly available. This new WLAN network should replace the
+ * existing network and retry sending traffic on this network.
+ */
+ private void onIwlanNetworkNewlyAvailable() {
+ if (mMmsHttpClient == null || mNetwork == null) return;
+ LogUtil.d("onIwlanNetworkNewlyAvailable net " + mNetwork.getNetId());
+ mMmsHttpClient.disconnectAllUrlConnections();
+ populateHttpClientWithCurrentNetwork();
+ }
+
private void handleCarrierConfigChanged() {
final CarrierConfigManager configManager =
(CarrierConfigManager)
@@ -230,8 +249,13 @@ public class MmsNetworkManager {
// onAvailable will always immediately be followed by a onCapabilitiesChanged. Check
// network status here is enough.
super.onCapabilitiesChanged(network, nc);
+ final NetworkInfo networkInfo = getConnectivityManager().getNetworkInfo(network);
+ // wlan network is preferred over wwan network, because its existence meaning it's
+ // recommended by QualifiedNetworksService.
+ final boolean isWlan = networkInfo != null
+ && networkInfo.getSubtype() == TelephonyManager.NETWORK_TYPE_IWLAN;
LogUtil.w("NetworkCallbackListener.onCapabilitiesChanged: network="
- + network + ", nc=" + nc);
+ + network + ", isWlan=" + isWlan + ", nc=" + nc);
synchronized (MmsNetworkManager.this) {
final boolean isAvailable =
nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
@@ -244,10 +268,18 @@ public class MmsNetworkManager {
return;
}
- // New available network
- if (mNetwork == null && isAvailable) {
- mNetwork = network;
- MmsNetworkManager.this.notifyAll();
+ // Use new available network
+ if (isAvailable) {
+ if (mNetwork == null) {
+ mNetwork = network;
+ MmsNetworkManager.this.notifyAll();
+ } else if (mDeps.isMmsEnhancementEnabled()
+ // Iwlan network newly available, try send MMS over the new network.
+ && !mIsLastAvailableNetworkIwlan && isWlan) {
+ mNetwork = network;
+ mEventHandler.sendEmptyMessage(EVENT_IWLAN_NETWORK_NEWLY_AVAILABLE);
+ }
+ mIsLastAvailableNetworkIwlan = isWlan;
}
}
}
@@ -271,6 +303,11 @@ public class MmsNetworkManager {
DEFAULT_MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS);
}
+ public boolean isMmsEnhancementEnabled() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_TELEPHONY, MMS_ENHANCEMENT_ENABLED, true);
+ }
+
public int getAdditionalNetworkAcquireTimeoutMillis() {
return ADDITIONAL_NETWORK_ACQUIRE_TIMEOUT_MILLIS;
}
@@ -395,17 +432,22 @@ public class MmsNetworkManager {
* Release the MMS network when nobody is holding on to it.
*
* @param requestId request ID for logging.
+ * @param canRelease whether the request can be released. An early release of a request
+ * can result in unexpected network torn down, as that network is used
+ * for immediate retry.
* @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) {
+ public void releaseNetwork(final String requestId, final boolean canRelease,
+ final boolean shouldDelayRelease) {
synchronized (this) {
if (mMmsRequestCount > 0) {
mMmsRequestCount -= 1;
- LogUtil.d(requestId, "MmsNetworkManager: release, count=" + mMmsRequestCount);
- if (mMmsRequestCount < 1) {
+ LogUtil.d(requestId, "MmsNetworkManager: release, count=" + mMmsRequestCount
+ + " canRelease=" + canRelease);
+ if (mMmsRequestCount < 1 && canRelease) {
if (shouldDelayRelease) {
// remove previously posted task and post a delayed task on the release
// handler to release the network
@@ -464,7 +506,7 @@ public class MmsNetworkManager {
mMmsHttpClient = null;
}
- private ConnectivityManager getConnectivityManager() {
+ private @NonNull ConnectivityManager getConnectivityManager() {
if (mConnectivityManager == null) {
mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
Context.CONNECTIVITY_SERVICE);
@@ -480,15 +522,19 @@ public class MmsNetworkManager {
public MmsHttpClient getOrCreateHttpClient() {
synchronized (this) {
if (mMmsHttpClient == null) {
- if (mNetwork != null) {
- // Create new MmsHttpClient for the current Network
- mMmsHttpClient = new MmsHttpClient(mContext, mNetwork, mConnectivityManager);
- }
+ populateHttpClientWithCurrentNetwork();
}
return mMmsHttpClient;
}
}
+ // Create new MmsHttpClient for the current Network
+ private void populateHttpClientWithCurrentNetwork() {
+ if (mNetwork != null) {
+ mMmsHttpClient = new MmsHttpClient(mContext, mNetwork, mConnectivityManager);
+ }
+ }
+
/**
* Get the APN name for the active network
*
diff --git a/src/com/android/mms/service/MmsRequest.java b/src/com/android/mms/service/MmsRequest.java
index dca77df..8e51896 100644
--- a/src/com/android/mms/service/MmsRequest.java
+++ b/src/com/android/mms/service/MmsRequest.java
@@ -16,6 +16,7 @@
package com.android.mms.service;
+import android.annotation.NonNull;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
@@ -39,9 +40,12 @@ 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.exception.VoluntaryDisconnectMmsHttpException;
import com.android.mms.service.metrics.MmsStats;
import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
/**
* Base class for MMS requests. This has the common logic of sending/downloading MMS.
@@ -101,6 +105,20 @@ public abstract class MmsRequest {
protected long mMessageId;
protected int mLastConnectionFailure;
private MmsStats mMmsStats;
+ private int result;
+ private int httpStatusCode;
+
+ protected enum MmsRequestState {
+ Unknown,
+ Created,
+ PrepareForHttpRequest,
+ AcquiringNetwork,
+ LoadingApn,
+ DoingHttp,
+ Success,
+ Failure
+ };
+ protected MmsRequestState currentState = MmsRequestState.Unknown;
class MonitorTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.PreciseDataConnectionStateListener {
@@ -122,6 +140,7 @@ public abstract class MmsRequest {
public MmsRequest(RequestManager requestManager, int subId, String creator,
Bundle mmsConfig, Context context, long messageId, MmsStats mmsStats) {
+ currentState = MmsRequestState.Created;
mRequestManager = requestManager;
mSubId = subId;
mCreator = creator;
@@ -144,50 +163,47 @@ public abstract class MmsRequest {
public void execute(Context context, MmsNetworkManager networkManager) {
final String requestId = this.getRequestId();
LogUtil.i(requestId, "Executing...");
- int result = SmsManager.MMS_ERROR_UNSPECIFIED;
- int httpStatusCode = 0;
+ result = SmsManager.MMS_ERROR_UNSPECIFIED;
+ 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.
+ currentState = MmsRequestState.PrepareForHttpRequest;
+ int attemptedTimes = 0;
if (!prepareForHttpRequest()) { // Prepare request, like reading pdu data from user
LogUtil.e(requestId, "Failed to prepare for request");
result = SmsManager.MMS_ERROR_IO_ERROR;
} else { // Execute
long retryDelaySecs = 2;
// Try multiple times of MMS HTTP request, depending on the error.
- for (retryId = 0; retryId < RETRY_TIMES; retryId++) {
+ while (retryId < RETRY_TIMES) {
httpStatusCode = 0; // Clear for retry.
MonitorTelephonyCallback connectionStateCallback = new MonitorTelephonyCallback();
try {
listenToDataConnectionState(connectionStateCallback);
+ currentState = MmsRequestState.AcquiringNetwork;
networkManager.acquireNetwork(requestId);
final String apnName = networkManager.getApnName();
LogUtil.d(requestId, "APN name is " + apnName);
+ ApnSettings apn = null;
+ currentState = MmsRequestState.LoadingApn;
try {
- ApnSettings apn = null;
- try {
- apn = ApnSettings.load(context, apnName, mSubId, requestId);
- } catch (ApnException e) {
- // If no APN could be found, fall back to trying without the APN name
- if (apnName == null) {
- // If the APN name was already null then don't need to retry
- throw (e);
- }
- LogUtil.i(requestId, "No match with APN name: "
- + apnName + ", try with no name");
- apn = ApnSettings.load(context, null, mSubId, requestId);
+ apn = ApnSettings.load(context, apnName, mSubId, requestId);
+ } catch (ApnException e) {
+ // If no APN could be found, fall back to trying without the APN name
+ if (apnName == null) {
+ // If the APN name was already null then don't need to retry
+ throw (e);
}
- LogUtil.i(requestId, "Using " + apn.toString());
- response = doHttp(context, networkManager, apn);
- result = Activity.RESULT_OK;
- // Success
- break;
- } finally {
- // Release the MMS network immediately except successful DownloadRequest.
- networkManager.releaseNetwork(requestId,
- this instanceof DownloadRequest && result == Activity.RESULT_OK);
+ LogUtil.i(requestId, "No match with APN name: "
+ + apnName + ", try with no name");
+ apn = ApnSettings.load(context, null, mSubId, requestId);
}
+ LogUtil.i(requestId, "Using " + apn.toString());
+ currentState = MmsRequestState.DoingHttp;
+ response = doHttp(context, networkManager, apn);
+ result = Activity.RESULT_OK;
+ // Success
+ break;
} catch (ApnException e) {
LogUtil.e(requestId, "APN failure", e);
result = SmsManager.MMS_ERROR_INVALID_APN;
@@ -197,8 +213,12 @@ public abstract class MmsRequest {
result = SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS;
break;
} catch (MmsHttpException e) {
- LogUtil.e(requestId, "HTTP or network I/O failure", e);
- result = SmsManager.MMS_ERROR_HTTP_FAILURE;
+ if (e instanceof VoluntaryDisconnectMmsHttpException) {
+ result = Activity.RESULT_CANCELED;
+ } else {
+ LogUtil.e(requestId, "HTTP or network I/O failure", e);
+ result = SmsManager.MMS_ERROR_HTTP_FAILURE;
+ }
httpStatusCode = e.getStatusCode();
// Retry
} catch (Exception e) {
@@ -206,12 +226,43 @@ public abstract class MmsRequest {
result = SmsManager.MMS_ERROR_UNSPECIFIED;
break;
} finally {
+ // Don't release the MMS network if the last attempt was voluntarily
+ // cancelled (due to better network available), because releasing the request
+ // could result that network being torn down as it's thought to be useless.
+ boolean canRelease = false;
+ if (result != Activity.RESULT_CANCELED) {
+ retryId++;
+ canRelease = true;
+ }
+ // Otherwise, delay the release for successful download request.
+ networkManager.releaseNetwork(requestId, canRelease,
+ this instanceof DownloadRequest && result == Activity.RESULT_OK);
+
stopListeningToDataConnectionState(connectionStateCallback);
}
- try {
- Thread.sleep(retryDelaySecs * 1000, 0/*nano*/);
- } catch (InterruptedException e) {}
- retryDelaySecs <<= 1;
+
+ // THEORETICALLY WOULDN'T OCCUR - PUTTING HERE AS A SAFETY NET.
+ // TODO: REMOVE WITH FLAG mms_enhancement_enabled after soaking enough time, V-QPR.
+ // Only possible if network kept disconnecting due to Activity.RESULT_CANCELED,
+ // causing retryId doesn't increase and thus stuck in the infinite loop.
+ // However, it's theoretically impossible because RESULT_CANCELED is only triggered
+ // when a WLAN network becomes newly available in addition to an existing network.
+ // Therefore, the WLAN network's own death cannot be triggered by RESULT_CANCELED,
+ // and thus must result in retryId++.
+ if (++attemptedTimes > RETRY_TIMES * 2) {
+ LogUtil.e(requestId, "Retry is performed too many times");
+ reportAnomaly("MMS retried too many times",
+ UUID.fromString("038c9155-5daa-4515-86ae-aafdd33c1435"));
+ break;
+ }
+
+ if (result != Activity.RESULT_CANCELED) {
+ try { // Cool down retry if the previous attempt wasn't voluntarily cancelled.
+ new CountDownLatch(1).await(retryDelaySecs, TimeUnit.SECONDS);
+ } catch (InterruptedException e) { }
+ // Double the cool down time if the next try fails again.
+ retryDelaySecs <<= 1;
+ }
}
}
processResult(context, result, response, httpStatusCode, /* handledByCarrierApp= */ false,
@@ -251,6 +302,8 @@ public abstract class MmsRequest {
final Uri messageUri = persistIfRequired(context, result, response);
final String requestId = this.getRequestId();
+ currentState = result == Activity.RESULT_OK ? MmsRequestState.Success
+ : MmsRequestState.Failure;
// As noted in the @param comment above, the httpStatusCode is only set when there's
// an http failure. On success, such as an http code of 200, the value here will be 0.
// "httpStatusCode: xxx" is now reported for an http failure only.
@@ -307,19 +360,25 @@ public abstract class MmsRequest {
String message = "MMS failed";
LogUtil.i(this.toString(),
message + " with error: " + result + " httpStatus:" + httpStatusCode);
- TelephonyManager telephonyManager =
- mContext.getSystemService(TelephonyManager.class)
- .createForSubscriptionId(mSubId);
- AnomalyReporter.reportAnomaly(
- generateUUID(result, httpStatusCode),
- message,
- telephonyManager.getSimCarrierId());
+ reportAnomaly(message, generateUUID(result, httpStatusCode));
break;
default:
break;
}
}
+ private void reportAnomaly(@NonNull String anomalyMsg, @NonNull UUID uuid) {
+ TelephonyManager telephonyManager =
+ mContext.getSystemService(TelephonyManager.class)
+ .createForSubscriptionId(mSubId);
+ if (telephonyManager != null) {
+ AnomalyReporter.reportAnomaly(
+ uuid,
+ anomalyMsg,
+ telephonyManager.getSimCarrierId());
+ }
+ }
+
private UUID generateUUID(int result, int httpStatusCode) {
long lresult = result;
long lhttpStatusCode = httpStatusCode;
@@ -409,7 +468,10 @@ public abstract class MmsRequest {
@Override
public String toString() {
return getClass().getSimpleName() + '@' + Integer.toHexString(hashCode())
- + " " + MmsService.formatCrossStackMessageId(mMessageId);
+ + " " + MmsService.formatCrossStackMessageId(mMessageId)
+ + " subId: " + mSubId
+ + " currentState: \"" + currentState.name() + "\""
+ + " result: " + result;
}
protected String getRequestId() {
diff --git a/src/com/android/mms/service/MmsService.java b/src/com/android/mms/service/MmsService.java
index ee88f09..6e7f1a1 100644
--- a/src/com/android/mms/service/MmsService.java
+++ b/src/com/android/mms/service/MmsService.java
@@ -16,6 +16,8 @@
package com.android.mms.service;
+import static android.telephony.SmsManager.MMS_ERROR_MMS_DISABLED_BY_CARRIER;
+
import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE;
import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_SEND_REQ;
@@ -52,6 +54,7 @@ import android.util.EventLog;
import android.util.SparseArray;
import com.android.internal.telephony.IMms;
+import com.android.internal.telephony.flags.Flags;
import com.android.mms.service.metrics.MmsMetricsCollector;
import com.android.mms.service.metrics.MmsStats;
@@ -68,7 +71,6 @@ import com.google.android.mms.util.SqliteWrapper;
import java.io.IOException;
import java.util.ArrayDeque;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
@@ -80,7 +82,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* System service to process MMS API requests
@@ -185,7 +186,7 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
if (carrierPackages == null || carrierPackages.size() != 1) {
LogUtil.d("getCarrierMessagingServicePackageIfExists - multiple ("
- + carrierPackages.size() + ") carrier apps installed, not using any.");
+ + carrierPackages.size() + ") or no carrier apps installed, not using any.");
return null;
} else {
return carrierPackages.get(0);
@@ -255,7 +256,9 @@ 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);
- handleError(sentIntent, SmsManager.MMS_ERROR_CONFIGURATION_ERROR, mmsStats);
+ int resultCode = Flags.mmsDisabledError() ? MMS_ERROR_MMS_DISABLED_BY_CARRIER
+ : SmsManager.MMS_ERROR_CONFIGURATION_ERROR;
+ handleError(sentIntent, resultCode, mmsStats);
return;
}
@@ -283,7 +286,10 @@ 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);
- handleError(sentIntent, SmsManager.MMS_ERROR_NO_DATA_NETWORK, mmsStats);
+
+ int resultCode = Flags.mmsDisabledError() ? SmsManager.MMS_ERROR_DATA_DISABLED
+ : SmsManager.MMS_ERROR_NO_DATA_NETWORK;
+ handleError(sentIntent, resultCode, mmsStats);
return;
}
@@ -634,9 +640,14 @@ public class MmsService extends Service implements MmsRequest.RequestManager {
+ " current subId=" + mCurrentSubId);
addToRunningRequestQueueSynchronized(request);
}
+ dumpRequestQueue();
}
}
+ private void dumpRequestQueue() {
+ LogUtil.d("request queue dump [size: " + mPendingSimRequestQueue.size() + "]:");
+ mPendingSimRequestQueue.forEach(request -> LogUtil.d(request.toString()));
+ }
private void sendSettingsIntentForFailedMms(boolean isIncoming, int subId) {
LogUtil.w("Subscription with id: " + subId
+ " cannot " + (isIncoming ? "download" : "send")
diff --git a/src/com/android/mms/service/exception/VoluntaryDisconnectMmsHttpException.java b/src/com/android/mms/service/exception/VoluntaryDisconnectMmsHttpException.java
new file mode 100644
index 0000000..2b6840a
--- /dev/null
+++ b/src/com/android/mms/service/exception/VoluntaryDisconnectMmsHttpException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.exception;
+
+/**
+ * Thrown when voluntarily disconnect an MMS http connection to trigger immediate retry. This
+ * exception indicates the connection is voluntarily cancelled, instead of a failure.
+ */
+public class VoluntaryDisconnectMmsHttpException extends MmsHttpException{
+ public VoluntaryDisconnectMmsHttpException(int statusCode, String message) {
+ super(statusCode, message);
+ }
+}
diff --git a/src/com/android/mms/service/metrics/MmsStats.java b/src/com/android/mms/service/metrics/MmsStats.java
index 7e98b0b..fd45a5d 100644
--- a/src/com/android/mms/service/metrics/MmsStats.java
+++ b/src/com/android/mms/service/metrics/MmsStats.java
@@ -35,6 +35,7 @@ import android.telephony.TelephonyManager;
import android.telephony.UiccCardInfo;
import com.android.internal.telephony.SmsApplication;
+import com.android.internal.telephony.flags.Flags;
import com.android.mms.IncomingMms;
import com.android.mms.OutgoingMms;
@@ -190,7 +191,9 @@ public class MmsStats {
if(subManager == null) {
return false;
}
-
+ if (Flags.workProfileApiSplit()) {
+ subManager = subManager.createForAllUserProfiles();
+ }
List<SubscriptionInfo> activeSubscriptionInfo = subManager.getActiveSubscriptionInfoList();
return (activeSubscriptionInfo.size() > 1);
}
diff --git a/tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java b/tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java
index dff2bab..70b1a0a 100644
--- a/tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java
+++ b/tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java
@@ -22,13 +22,16 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.fail;
+import static org.junit.Assert.assertNotSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.getMasterScheduler;
import android.content.Context;
import android.net.ConnectivityManager;
@@ -38,6 +41,7 @@ import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
import org.junit.Before;
import org.junit.Test;
@@ -47,6 +51,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
+import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -150,6 +155,29 @@ public final class MmsNetworkManagerTest {
}
@Test
+ public void testAvailableNetwork_wwanNetworkReplacedByWlanNetwork() throws Exception {
+ MmsHttpClient mockMmsHttpClient = mock(MmsHttpClient.class);
+ doReturn(true).when(mDeps).isMmsEnhancementEnabled();
+
+ // WWAN network is always available, whereas WLAN network needs extra time to be set up.
+ doReturn(TelephonyManager.NETWORK_TYPE_LTE).when(mNetworkInfo).getSubtype();
+ doReturn(TelephonyManager.NETWORK_TYPE_IWLAN).when(mNetworkInfo2).getSubtype();
+
+ final NetworkCallback callback = acquireAvailableNetworkAndGetCallback(
+ mTestNetwork /* expectNetwork */, MMS_APN /* expectApn */);
+ replaceInstance(MmsNetworkManager.class, "mMmsHttpClient", mMnm,
+ mockMmsHttpClient);
+
+ // The WLAN network become available.
+ callback.onCapabilitiesChanged(mTestNetwork2, USABLE_NC);
+ getMasterScheduler().advanceToLastPostedRunnable();
+
+ // Verify current connections disconnect, then the client is replaced with a new network.
+ verify(mockMmsHttpClient).disconnectAllUrlConnections();
+ assertNotSame(mMnm.getOrCreateHttpClient(), mockMmsHttpClient);
+ }
+
+ @Test
public void testAvailableNetwork_networkBecomeSuspend() throws Exception {
final NetworkCallback callback = acquireAvailableNetworkAndGetCallback(
mTestNetwork /* expectNetwork */, MMS_APN /* expectApn */);
@@ -256,4 +284,12 @@ public final class MmsNetworkManagerTest {
return future;
}
+
+ /** Helper to replace instance field with reflection. */
+ private void replaceInstance(final Class c, final String instanceName,
+ final Object obj, final Object newValue) throws Exception {
+ Field field = c.getDeclaredField(instanceName);
+ field.setAccessible(true);
+ field.set(obj, newValue);
+ }
}
diff --git a/tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java b/tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java
index dd126e8..e144ef7 100644
--- a/tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java
+++ b/tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java
@@ -18,9 +18,14 @@ package com.android.mms.service;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -29,22 +34,25 @@ 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 com.android.mms.service.exception.VoluntaryDisconnectMmsHttpException;
+
import org.junit.After;
import org.junit.Before;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
import org.junit.Test;
+import org.mockito.MockitoAnnotations;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.reset;
-
-import android.util.Log;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.SocketException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
public class MmsHttpClientTest {
// Mocked classes
@@ -145,4 +153,39 @@ public class MmsHttpClientTest {
assertThat(phoneNo).contains(subscriberPhoneNumber);
verify(mSubscriptionManager).getPhoneNumber(subId);
}
+
+ @Test
+ public void testDisconnectAllUrlConnections() throws IOException {
+ Network mockNetwork = mock(Network.class);
+ HttpURLConnection mockConnection = mock(HttpURLConnection.class);
+ doReturn(mockConnection).when(mockNetwork).openConnection(any(), any());
+ doReturn(mockNetwork).when(mockNetwork).getPrivateDnsBypassingCopy();
+ ConnectivityManager mockCm = mock(ConnectivityManager.class);
+ Bundle config = new Bundle();
+
+ // The external thread that voluntarily silently close the socket.
+ CountDownLatch latch = new CountDownLatch(1);
+ final ExecutorService externalThread = Executors.newSingleThreadExecutor();
+ doAnswer(invok -> {
+ latch.countDown();
+ return null;
+ }).when(mockConnection).disconnect();
+
+ MmsHttpClient clientUT = new MmsHttpClient(mContext, mockNetwork, mockCm);
+ doAnswer(invok -> {
+ externalThread.execute(clientUT::disconnectAllUrlConnections);
+ // connection.disconnect is silent, but it will trigger SocketException thrown from the
+ // connect thread.
+ if (latch.await(1, TimeUnit.SECONDS)) {
+ throw new SocketException("Socket Closed");
+ }
+ return null;
+ }).when(mockConnection).getResponseCode();
+
+ // Verify SocketException is transformed into VoluntaryDisconnectMmsHttpException
+ assertThrows(VoluntaryDisconnectMmsHttpException.class, () -> {
+ clientUT.execute("http://test", new byte[0], "GET", false,
+ "", 0, config, 1, "requestId");
+ });
+ }
}