1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.telephony; 18 19 import android.annotation.NonNull; 20 import android.telephony.Rlog; 21 22 import com.android.internal.annotations.VisibleForTesting; 23 24 import java.nio.ByteBuffer; 25 import java.util.Arrays; 26 import java.util.HashMap; 27 import java.util.Iterator; 28 import java.util.LinkedHashMap; 29 import java.util.NoSuchElementException; 30 import java.util.concurrent.TimeUnit; 31 32 /** 33 * Caches WAP push PDU data for retrieval during MMS downloading. 34 * When on a satellite connection, the cached message size will be used to prevent downloading 35 * messages that exceed a threshold. 36 * 37 * The cache uses a circular buffer and will start invalidating the oldest entries after 250 38 * message sizes have been inserted. 39 * The cache also invalidates entries that have been in the cache for over 14 days. 40 */ 41 public class WapPushCache { 42 private static final String TAG = "WAP PUSH CACHE"; 43 44 // Because we store each size twice, this represents 250 messages. That limit is chosen so 45 // that the memory footprint of the cache stays reasonably small while still supporting what 46 // we guess will be the vast majority of real use cases. 47 private static final int MAX_CACHE_SIZE = 500; 48 49 // WAP push PDUs have an expiry property, but we can't be certain that it is set accurately 50 // by the carrier. We will use our own expiry for this cache to keep it small. One example 51 // carrier has an expiry of 7 days so 14 will give us room for those with longer times as well. 52 private static final long CACHE_EXPIRY_TIME = TimeUnit.DAYS.toMillis(14); 53 54 private static final HashMap<String, CacheEntry> sMessageSizes = new LinkedHashMap<>() { 55 @Override 56 protected boolean removeEldestEntry(Entry<String, CacheEntry> eldest) { 57 return size() > MAX_CACHE_SIZE; 58 } 59 }; 60 61 @VisibleForTesting 62 public static TelephonyFacade sTelephonyFacade = new TelephonyFacade(); 63 64 /** 65 * Puts a WAP push PDU's messageSize in the cache. 66 * 67 * The data is stored twice, once using just locationUrl as the key and once 68 * using transactionId appended to the locationUrl. For some carriers, xMS apps 69 * append the transactionId to the location and we need to support lookup using either the 70 * original location or one modified in this way. 71 72 * 73 * @param locationUrl location of the message used as part of the cache key. 74 * @param transactionId message transaction ID used as part of the cache key. 75 * @param messageSize size of the message to be stored in the cache. 76 */ putWapMessageSize( @onNull byte[] locationUrl, @NonNull byte[] transactionId, long messageSize )77 public static void putWapMessageSize( 78 @NonNull byte[] locationUrl, 79 @NonNull byte[] transactionId, 80 long messageSize 81 ) { 82 long expiry = sTelephonyFacade.getElapsedSinceBootMillis() + CACHE_EXPIRY_TIME; 83 if (messageSize <= 0) { 84 Rlog.e(TAG, "Invalid message size of " + messageSize + ". Not inserting."); 85 return; 86 } 87 synchronized (sMessageSizes) { 88 sMessageSizes.put(Arrays.toString(locationUrl), new CacheEntry(messageSize, expiry)); 89 90 // concatenate the locationUrl and transactionId 91 byte[] joinedKey = ByteBuffer 92 .allocate(locationUrl.length + transactionId.length) 93 .put(locationUrl) 94 .put(transactionId) 95 .array(); 96 sMessageSizes.put(Arrays.toString(joinedKey), new CacheEntry(messageSize, expiry)); 97 invalidateOldEntries(); 98 } 99 } 100 101 /** 102 * Remove entries from the cache that are older than CACHE_EXPIRY_TIME 103 */ invalidateOldEntries()104 private static void invalidateOldEntries() { 105 long currentTime = sTelephonyFacade.getElapsedSinceBootMillis(); 106 107 // We can just remove elements from the start until one is found that does not exceed the 108 // expiry since the elements are in order of insertion. 109 for (Iterator<CacheEntry> it = sMessageSizes.values().iterator(); it.hasNext(); ) { 110 CacheEntry entry = it.next(); 111 if (entry.mExpiry < currentTime) { 112 it.remove(); 113 } else { 114 break; 115 } 116 } 117 } 118 119 /** 120 * Gets the message size of a WAP from the cache. 121 * 122 * Because we stored the size both using the location+transactionId key and using the 123 * location only key, we should be able to find the size whether the xMS app modified 124 * the location or not. 125 * 126 * @param locationUrl the location to use as a key for looking up the size in the cache. 127 * 128 * @return long representing the message size of the WAP 129 * @throws NoSuchElementException if the WAP doesn't exist in the cache 130 * @throws IllegalArgumentException if the locationUrl is empty 131 */ getWapMessageSize(@onNull byte[] locationUrl)132 public static long getWapMessageSize(@NonNull byte[] locationUrl) { 133 if (locationUrl.length == 0) { 134 throw new IllegalArgumentException("Found empty locationUrl"); 135 } 136 CacheEntry entry = sMessageSizes.get(Arrays.toString(locationUrl)); 137 if (entry == null) { 138 throw new NoSuchElementException( 139 "No cached WAP size for locationUrl " + Arrays.toString(locationUrl) 140 ); 141 } 142 return entry.mSize; 143 } 144 145 /** 146 * Clears all elements from the cache 147 */ 148 @VisibleForTesting clear()149 public static void clear() { 150 sMessageSizes.clear(); 151 } 152 153 /** 154 * Returns a count of the number of elements in the cache 155 * @return count of elements 156 */ 157 @VisibleForTesting size()158 public static int size() { 159 return sMessageSizes.size(); 160 } 161 162 163 164 private static class CacheEntry { CacheEntry(long size, long expiry)165 CacheEntry(long size, long expiry) { 166 mSize = size; 167 mExpiry = expiry; 168 } 169 private final long mSize; 170 private final long mExpiry; 171 } 172 } 173