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