diff options
author | Xin Li <delphij@google.com> | 2024-01-17 22:14:36 -0800 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2024-01-17 22:14:36 -0800 |
commit | ec6e77186041fd3f82b0783821c0820c0bcfc42c (patch) | |
tree | d08fb2c72478e26d6012603806ec40154e3bfea1 | |
parent | 2480e24a21d207a4cf34ad76b477169b322947f1 (diff) | |
parent | 2f625059ce181774f1cbfc983453d10d351e0a87 (diff) | |
download | Mms-ec6e77186041fd3f82b0783821c0820c0bcfc42c.tar.gz |
Merge Android 24Q1 Release (ab/11220357)temp_319669529
Bug: 319669529
Merged-In: I0f15493f505c4cb99f829e9d6765a2a9eb152e44
Change-Id: I793afaf1d5a7e16f7f6badd5c16d24472ef83072
-rw-r--r-- | PREUPLOAD.cfg | 2 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsConfigManager.java | 5 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsHttpClient.java | 48 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsNetworkManager.java | 76 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsRequest.java | 142 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsService.java | 21 | ||||
-rw-r--r-- | src/com/android/mms/service/exception/VoluntaryDisconnectMmsHttpException.java | 27 | ||||
-rw-r--r-- | src/com/android/mms/service/metrics/MmsStats.java | 5 | ||||
-rw-r--r-- | tests/robotests/src/com/android/mms/service/MmsNetworkManagerTest.java | 36 | ||||
-rw-r--r-- | tests/unittests/src/com/android/mms/service/MmsHttpClientTest.java | 57 |
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"); + }); + } } |