Merge "Create WAP cache and API for retrieving WAP message sizes." into main
diff --git a/src/java/com/android/internal/telephony/SmsController.java b/src/java/com/android/internal/telephony/SmsController.java
index 777357d..251a1fd 100644
--- a/src/java/com/android/internal/telephony/SmsController.java
+++ b/src/java/com/android/internal/telephony/SmsController.java
@@ -1117,4 +1117,20 @@
// Check if smscAddr is present in FDN list
return FdnUtils.isNumberBlockedByFDN(phoneId, smscAddr, defaultCountryIso);
}
+
+ /**
+ * Gets the message size of WAP from the cache.
+ *
+ * @param locationUrl the location to use as a key for looking up the size in the cache.
+ * The locationUrl may or may not have the transactionId appended to the url.
+ *
+ * @return long representing the message size
+ * @throws java.util.NoSuchElementException if the WAP push doesn't exist in the cache
+ * @throws IllegalArgumentException if the locationUrl is empty
+ */
+ @Override
+ public long getWapMessageSize(@NonNull String locationUrl) {
+ byte[] bytes = locationUrl.getBytes(StandardCharsets.ISO_8859_1);
+ return WapPushCache.getWapMessageSize(bytes);
+ }
}
\ No newline at end of file
diff --git a/src/java/com/android/internal/telephony/WapPushCache.java b/src/java/com/android/internal/telephony/WapPushCache.java
new file mode 100644
index 0000000..70d12e2
--- /dev/null
+++ b/src/java/com/android/internal/telephony/WapPushCache.java
@@ -0,0 +1,172 @@
+/*
+ * 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.internal.telephony;
+
+import android.annotation.NonNull;
+import android.telephony.Rlog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.NoSuchElementException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Caches WAP push PDU data for retrieval during MMS downloading.
+ * When on a satellite connection, the cached message size will be used to prevent downloading
+ * messages that exceed a threshold.
+ *
+ * The cache uses a circular buffer and will start invalidating the oldest entries after 250
+ * message sizes have been inserted.
+ * The cache also invalidates entries that have been in the cache for over 14 days.
+ */
+public class WapPushCache {
+ private static final String TAG = "WAP PUSH CACHE";
+
+ // Because we store each size twice, this represents 250 messages. That limit is chosen so
+ // that the memory footprint of the cache stays reasonably small while still supporting what
+ // we guess will be the vast majority of real use cases.
+ private static final int MAX_CACHE_SIZE = 500;
+
+ // WAP push PDUs have an expiry property, but we can't be certain that it is set accurately
+ // by the carrier. We will use our own expiry for this cache to keep it small. One example
+ // carrier has an expiry of 7 days so 14 will give us room for those with longer times as well.
+ private static final long CACHE_EXPIRY_TIME = TimeUnit.DAYS.toMillis(14);
+
+ private static final HashMap<String, CacheEntry> sMessageSizes = new LinkedHashMap<>() {
+ @Override
+ protected boolean removeEldestEntry(Entry<String, CacheEntry> eldest) {
+ return size() > MAX_CACHE_SIZE;
+ }
+ };
+
+ @VisibleForTesting
+ public static TelephonyFacade sTelephonyFacade = new TelephonyFacade();
+
+ /**
+ * Puts a WAP push PDU's messageSize in the cache.
+ *
+ * The data is stored twice, once using just locationUrl as the key and once
+ * using transactionId appended to the locationUrl. For some carriers, xMS apps
+ * append the transactionId to the location and we need to support lookup using either the
+ * original location or one modified in this way.
+
+ *
+ * @param locationUrl location of the message used as part of the cache key.
+ * @param transactionId message transaction ID used as part of the cache key.
+ * @param messageSize size of the message to be stored in the cache.
+ */
+ public static void putWapMessageSize(
+ @NonNull byte[] locationUrl,
+ @NonNull byte[] transactionId,
+ long messageSize
+ ) {
+ long expiry = sTelephonyFacade.getElapsedSinceBootMillis() + CACHE_EXPIRY_TIME;
+ if (messageSize <= 0) {
+ Rlog.e(TAG, "Invalid message size of " + messageSize + ". Not inserting.");
+ return;
+ }
+ synchronized (sMessageSizes) {
+ sMessageSizes.put(Arrays.toString(locationUrl), new CacheEntry(messageSize, expiry));
+
+ // concatenate the locationUrl and transactionId
+ byte[] joinedKey = ByteBuffer
+ .allocate(locationUrl.length + transactionId.length)
+ .put(locationUrl)
+ .put(transactionId)
+ .array();
+ sMessageSizes.put(Arrays.toString(joinedKey), new CacheEntry(messageSize, expiry));
+ invalidateOldEntries();
+ }
+ }
+
+ /**
+ * Remove entries from the cache that are older than CACHE_EXPIRY_TIME
+ */
+ private static void invalidateOldEntries() {
+ long currentTime = sTelephonyFacade.getElapsedSinceBootMillis();
+
+ // We can just remove elements from the start until one is found that does not exceed the
+ // expiry since the elements are in order of insertion.
+ for (Iterator<CacheEntry> it = sMessageSizes.values().iterator(); it.hasNext(); ) {
+ CacheEntry entry = it.next();
+ if (entry.mExpiry < currentTime) {
+ it.remove();
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Gets the message size of a WAP from the cache.
+ *
+ * Because we stored the size both using the location+transactionId key and using the
+ * location only key, we should be able to find the size whether the xMS app modified
+ * the location or not.
+ *
+ * @param locationUrl the location to use as a key for looking up the size in the cache.
+ *
+ * @return long representing the message size of the WAP
+ * @throws NoSuchElementException if the WAP doesn't exist in the cache
+ * @throws IllegalArgumentException if the locationUrl is empty
+ */
+ public static long getWapMessageSize(@NonNull byte[] locationUrl) {
+ if (locationUrl.length == 0) {
+ throw new IllegalArgumentException("Found empty locationUrl");
+ }
+ CacheEntry entry = sMessageSizes.get(Arrays.toString(locationUrl));
+ if (entry == null) {
+ throw new NoSuchElementException(
+ "No cached WAP size for locationUrl " + Arrays.toString(locationUrl)
+ );
+ }
+ return entry.mSize;
+ }
+
+ /**
+ * Clears all elements from the cache
+ */
+ @VisibleForTesting
+ public static void clear() {
+ sMessageSizes.clear();
+ }
+
+ /**
+ * Returns a count of the number of elements in the cache
+ * @return count of elements
+ */
+ @VisibleForTesting
+ public static int size() {
+ return sMessageSizes.size();
+ }
+
+
+
+ private static class CacheEntry {
+ CacheEntry(long size, long expiry) {
+ mSize = size;
+ mExpiry = expiry;
+ }
+ private final long mSize;
+ private final long mExpiry;
+ }
+}
diff --git a/src/java/com/android/internal/telephony/WapPushOverSms.java b/src/java/com/android/internal/telephony/WapPushOverSms.java
index d6ea4ad..7669411 100644
--- a/src/java/com/android/internal/telephony/WapPushOverSms.java
+++ b/src/java/com/android/internal/telephony/WapPushOverSms.java
@@ -262,6 +262,13 @@
if (parsedPdu != null && parsedPdu.getMessageType() == MESSAGE_TYPE_NOTIFICATION_IND) {
final NotificationInd nInd = (NotificationInd) parsedPdu;
+ // save the WAP push message size so that if a download request is made for it
+ // while on a satellite connection we can check if the size is under the threshold
+ WapPushCache.putWapMessageSize(
+ nInd.getContentLocation(),
+ nInd.getTransactionId(),
+ nInd.getMessageSize()
+ );
if (nInd.getFrom() != null
&& BlockChecker.isBlocked(mContext, nInd.getFrom().getString(), null)) {
result.statusCode = Intents.RESULT_SMS_HANDLED;
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SmsControllerTest.java b/tests/telephonytests/src/com/android/internal/telephony/SmsControllerTest.java
index 8180bc0..972df5d 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/SmsControllerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/SmsControllerTest.java
@@ -16,7 +16,10 @@
package com.android.internal.telephony;
+import static junit.framework.Assert.assertEquals;
+
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.anyInt;
@@ -40,7 +43,9 @@
import org.junit.runner.RunWith;
import org.mockito.Mockito;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.NoSuchElementException;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -65,6 +70,7 @@
@After
public void tearDown() throws Exception {
mAdnRecordCache = null;
+ WapPushCache.clear();
super.tearDown();
}
@@ -239,4 +245,40 @@
verify(mIccSmsInterfaceManager, Mockito.times(0))
.sendText(mCallingPackage, "1234", null, "text", null, null, false, 0L, true);
}
+
+ @Test
+ public void testGetWapMessageSize() {
+ long expectedSize = 100L;
+ String location = "content://mms";
+ byte[] locationBytes = location.getBytes(StandardCharsets.ISO_8859_1);
+ byte[] transactionId = "123".getBytes(StandardCharsets.ISO_8859_1);
+
+ WapPushCache.putWapMessageSize(locationBytes, transactionId, expectedSize);
+ long size = mSmsControllerUT.getWapMessageSize(location);
+
+ assertEquals(expectedSize, size);
+ }
+
+ @Test
+ public void testGetWapMessageSize_withTransactionIdAppended() {
+ long expectedSize = 100L;
+ byte[] location = "content://mms".getBytes(StandardCharsets.ISO_8859_1);
+ byte[] transactionId = "123".getBytes(StandardCharsets.ISO_8859_1);
+ byte[] joinedKey = new byte[location.length + transactionId.length];
+ System.arraycopy(location, 0, joinedKey, 0, location.length);
+ System.arraycopy(transactionId, 0, joinedKey, location.length, transactionId.length);
+ String joinedKeyString = new String(joinedKey, StandardCharsets.ISO_8859_1);
+
+ WapPushCache.putWapMessageSize(location, transactionId, expectedSize);
+ long size = mSmsControllerUT.getWapMessageSize(joinedKeyString);
+
+ assertEquals(expectedSize, size);
+ }
+
+ @Test
+ public void testGetWapMessageSize_nonexistentThrows() {
+ assertThrows(NoSuchElementException.class, () ->
+ mSmsControllerUT.getWapMessageSize("content://mms")
+ );
+ }
}
\ No newline at end of file
diff --git a/tests/telephonytests/src/com/android/internal/telephony/WapPushCacheTest.java b/tests/telephonytests/src/com/android/internal/telephony/WapPushCacheTest.java
new file mode 100644
index 0000000..f572c08
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/WapPushCacheTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.internal.telephony;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.Clock;
+import java.util.NoSuchElementException;
+import java.util.concurrent.TimeUnit;
+
+public class WapPushCacheTest extends TelephonyTest {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp(getClass().getSimpleName());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ WapPushCache.clear();
+ WapPushCache.sTelephonyFacade = new TelephonyFacade();
+ super.tearDown();
+ }
+
+ @Test
+ public void testGetWapMessageSize() {
+ long expectedSize = 100L;
+ byte[] location = "content://mms".getBytes();
+ byte[] transactionId = "123".getBytes();
+
+ WapPushCache.putWapMessageSize(location, transactionId, expectedSize);
+ long size = WapPushCache.getWapMessageSize(location);
+
+ assertEquals(expectedSize, size);
+ }
+
+ @Test
+ public void testGetWapMessageSize_withTransactionIdAppended() {
+ long expectedSize = 100L;
+ byte[] location = "content://mms".getBytes();
+ byte[] transactionId = "123".getBytes();
+ byte[] joinedKey = new byte[location.length + transactionId.length];
+ System.arraycopy(location, 0, joinedKey, 0, location.length);
+ System.arraycopy(transactionId, 0, joinedKey, location.length, transactionId.length);
+
+ WapPushCache.putWapMessageSize(location, transactionId, expectedSize);
+ long size = WapPushCache.getWapMessageSize(joinedKey);
+
+ assertEquals(expectedSize, size);
+ }
+
+ @Test
+ public void testGetWapMessageSize_nonexistentThrows() {
+ assertThrows(NoSuchElementException.class, () ->
+ WapPushCache.getWapMessageSize("content://mms".getBytes())
+ );
+ }
+ @Test
+ public void testGetWapMessageSize_emptyLocationUrlThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
+ WapPushCache.getWapMessageSize(new byte[0])
+ );
+ }
+
+ @Test
+ public void testPutWapMessageSize_invalidValuePreventsInsert() {
+ long expectedSize = 0L;
+ byte[] location = "content://mms".getBytes();
+ byte[] transactionId = "123".getBytes();
+
+ WapPushCache.putWapMessageSize(location, transactionId, expectedSize);
+
+ assertEquals(0, WapPushCache.size());
+ }
+
+ @Test
+ public void testPutWapMessageSize_sizeLimitExceeded_oldestEntryRemoved() {
+ long expectedSize = 100L;
+ for (int i = 0; i < 251; i++) {
+ byte[] location = ("" + i).getBytes();
+ byte[] transactionId = "abc".getBytes();
+ WapPushCache.putWapMessageSize(location, transactionId, expectedSize);
+ }
+
+ // assert one of the entries inserted above has been removed
+ assertEquals(500, WapPushCache.size());
+ // assert last entry added exists
+ assertEquals(expectedSize, WapPushCache.getWapMessageSize("250".getBytes()));
+ // assert the first entry added was removed
+ assertThrows(NoSuchElementException.class, () ->
+ WapPushCache.getWapMessageSize("0".getBytes())
+ );
+ }
+
+ @Test
+ public void testPutWapMessageSize_expiryExceeded_entryRemoved() {
+ long currentTime = Clock.systemUTC().millis();
+ TelephonyFacade facade = mock(TelephonyFacade.class);
+ when(facade.getElapsedSinceBootMillis()).thenReturn(currentTime);
+ WapPushCache.sTelephonyFacade = facade;
+
+ long expectedSize = 100L;
+ byte[] transactionId = "abc".getBytes();
+ byte[] location1 = "old".getBytes();
+ byte[] location2 = "new".getBytes();
+
+ WapPushCache.putWapMessageSize(location1, transactionId, expectedSize);
+ assertEquals(2, WapPushCache.size());
+
+ // advance time
+ when(facade.getElapsedSinceBootMillis())
+ .thenReturn(currentTime + TimeUnit.DAYS.toMillis(14) + 1);
+
+ WapPushCache.putWapMessageSize(location2, transactionId, expectedSize);
+
+ assertEquals(2, WapPushCache.size());
+ assertEquals(expectedSize, WapPushCache.getWapMessageSize(location2));
+ assertThrows(NoSuchElementException.class, () ->
+ WapPushCache.getWapMessageSize(location1)
+ );
+ }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/WapPushOverSmsTest.java b/tests/telephonytests/src/com/android/internal/telephony/WapPushOverSmsTest.java
index 8e40271..f5d4e95 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/WapPushOverSmsTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/WapPushOverSmsTest.java
@@ -62,6 +62,7 @@
@After
public void tearDown() throws Exception {
+ WapPushCache.clear();
mWapPushOverSmsUT = null;
super.tearDown();
}
@@ -150,4 +151,29 @@
any(UserHandle.class),
anyInt());
}
+
+ @Test @SmallTest
+ public void testDispatchWapPdu_notificationIndInsertedToCache() throws Exception {
+ assertEquals(0, WapPushCache.size());
+ when(mISmsStub.getCarrierConfigValuesForSubscriber(anyInt())).thenReturn(new Bundle());
+
+ doReturn(true).when(mWspTypeDecoder).decodeUintvarInteger(anyInt());
+ doReturn(true).when(mWspTypeDecoder).decodeContentType(anyInt());
+ doReturn((long) 2).when(mWspTypeDecoder).getValue32();
+ doReturn(2).when(mWspTypeDecoder).getDecodedDataLength();
+ doReturn(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO).when(mWspTypeDecoder).getValueString();
+
+ byte[] pdu = {1, 6, 0, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 47,
+ 118, 110, 100, 46, 119, 97, 112, 46, 109, 109, 115, 45, 109, 101, 115, 115,
+ 97, 103, 101, 0, -116, -126, -104, 77, 109, 115, 84, 114, 97, 110, 115, 97,
+ 99, 116, 105, 111, 110, 73, 68, 0, -115, 18, -119, 8, -128, 49, 54, 49, 55,
+ 56, 50, 54, 57, 49, 54, 56, 47, 84, 89, 80, 69, 61, 80, 76, 77, 78, 0, -118,
+ -128, -114, 2, 3, -24, -120, 3, -127, 3, 3, -12, -128, -106, 84, 101, 115,
+ 116, 32, 77, 109, 115, 32, 83, 117, 98, 106, 101, 99, 116, 0, -125, 104, 116,
+ 116, 112, 58, 47, 47, 119, 119, 119, 46, 103, 111, 111, 103, 108, 101, 46, 99,
+ 111, 109, 47, 115, 97, 100, 102, 100, 100, 0};
+
+ mWapPushOverSmsUT.dispatchWapPdu(pdu, null, mInboundSmsHandler, null, 0, 0L);
+ assertEquals(2, WapPushCache.size());
+ }
}