Add LRU cache with expiry class

This is a no-op refactoring which extracts an
LruCacheWithExpiry<K, V> superclass from
TrafficStatsRateLimitCache to provide an LRU
cache with an adjustable expiry duration and generic types,
allowing entries to be automatically removed from the cache
after a certain duration.

This is needed for follow-up changes to provide similar
functionality with other types without manually handling expiry.

Test: atest ConnectivityCoverageTests:android.net.connectivity.com.android.server.net.TrafficStatsRateLimitCacheTest
Bug: 343260158
Change-Id: Ia413b8c6e73c376dd821cd2bb676ef996cd85a91
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index ca97d07..667aad1 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -19,9 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.NetworkStats;
-import android.util.LruCache;
 
-import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.LruCacheWithExpiry;
 
 import java.time.Clock;
 import java.util.Objects;
@@ -31,9 +30,8 @@
  * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
  * with an adjustable expiry duration to manage data freshness.
  */
-class TrafficStatsRateLimitCache {
-    private final Clock mClock;
-    private final long mExpiryDurationMs;
+class TrafficStatsRateLimitCache extends
+        LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
 
     /**
      * Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
@@ -43,19 +41,17 @@
      * @param maxSize Maximum number of entries.
      */
     TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
-        mClock = clock;
-        mExpiryDurationMs = expiryDurationMs;
-        mMap = new LruCache<>(maxSize);
+        super(clock, expiryDurationMs, maxSize, it -> !it.isEmpty());
     }
 
-    private static class TrafficStatsCacheKey {
+    public static class TrafficStatsCacheKey {
         @Nullable
-        public final String iface;
-        public final int uid;
+        private final String mIface;
+        private final int mUid;
 
         TrafficStatsCacheKey(@Nullable String iface, int uid) {
-            this.iface = iface;
-            this.uid = uid;
+            this.mIface = iface;
+            this.mUid = uid;
         }
 
         @Override
@@ -63,29 +59,15 @@
             if (this == o) return true;
             if (!(o instanceof TrafficStatsCacheKey)) return false;
             TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
-            return uid == that.uid && Objects.equals(iface, that.iface);
+            return mUid == that.mUid && Objects.equals(mIface, that.mIface);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(iface, uid);
+            return Objects.hash(mIface, mUid);
         }
     }
 
-    private static class TrafficStatsCacheValue {
-        public final long timestamp;
-        @NonNull
-        public final NetworkStats.Entry entry;
-
-        TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
-            this.timestamp = timestamp;
-            this.entry = entry;
-        }
-    }
-
-    @GuardedBy("mMap")
-    private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
-
     /**
      * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
      *
@@ -95,16 +77,7 @@
      */
     @Nullable
     NetworkStats.Entry get(String iface, int uid) {
-        final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
-        synchronized (mMap) { // Synchronize for thread-safety
-            final TrafficStatsCacheValue value = mMap.get(key);
-            if (value != null && !isExpired(value.timestamp)) {
-                return value.entry;
-            } else {
-                mMap.remove(key); // Remove expired entries
-                return null;
-            }
-        }
+        return super.get(new TrafficStatsCacheKey(iface, uid));
     }
 
     /**
@@ -122,19 +95,7 @@
     @Nullable
     NetworkStats.Entry getOrCompute(String iface, int uid,
             @NonNull Supplier<NetworkStats.Entry> supplier) {
-        synchronized (mMap) {
-            final NetworkStats.Entry cachedValue = get(iface, uid);
-            if (cachedValue != null) {
-                return cachedValue;
-            }
-
-            // Entry not found or expired, compute it
-            final NetworkStats.Entry computedEntry = supplier.get();
-            if (computedEntry != null && !computedEntry.isEmpty()) {
-                put(iface, uid, computedEntry);
-            }
-            return computedEntry;
-        }
+        return super.getOrCompute(new TrafficStatsCacheKey(iface, uid), supplier);
     }
 
     /**
@@ -145,23 +106,7 @@
      * @param entry The {@link NetworkStats.Entry} to store in the cache.
      */
     void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
-        Objects.requireNonNull(entry);
-        final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
-        synchronized (mMap) { // Synchronize for thread-safety
-            mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
-        }
+        super.put(new TrafficStatsCacheKey(iface, uid), entry);
     }
 
-    /**
-     * Clear the cache.
-     */
-    void clear() {
-        synchronized (mMap) {
-            mMap.evictAll();
-        }
-    }
-
-    private boolean isExpired(long timestamp) {
-        return mClock.millis() > timestamp + mExpiryDurationMs;
-    }
 }
diff --git a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
new file mode 100644
index 0000000..80088b9
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * An LRU cache that stores key-value pairs with an expiry time.
+ *
+ * <p>This cache uses an {@link LruCache} to store entries and evicts the least
+ * recently used entries when the cache reaches its maximum capacity. It also
+ * supports an expiry time for each entry, allowing entries to be automatically
+ * removed from the cache after a certain duration.
+ *
+ * @param <K> The type of keys used to identify cached entries.
+ * @param <V> The type of values stored in the cache.
+ *
+ * @hide
+ */
+public class LruCacheWithExpiry<K, V> {
+    private final Clock mClock;
+    private final long mExpiryDurationMs;
+    @GuardedBy("mMap")
+    private final LruCache<K, CacheValue<V>> mMap;
+    private final Predicate<V> mShouldCacheValue;
+
+    /**
+     * Constructs a new {@link LruCacheWithExpiry} with the specified parameters.
+     *
+     * @param clock            The {@link Clock} to use for determining timestamps.
+     * @param expiryDurationMs The expiry duration for cached entries in milliseconds.
+     * @param maxSize          The maximum number of entries to hold in the cache.
+     * @param shouldCacheValue A {@link Predicate} that determines whether a given value should be
+     *                         cached. This can be used to filter out certain values from being
+     *                         stored in the cache.
+     */
+    public LruCacheWithExpiry(@NonNull Clock clock, long expiryDurationMs, int maxSize,
+            Predicate<V> shouldCacheValue) {
+        mClock = clock;
+        mExpiryDurationMs = expiryDurationMs;
+        mMap = new LruCache<>(maxSize);
+        mShouldCacheValue = shouldCacheValue;
+    }
+
+    /**
+     * Retrieves a value from the cache, associated with the given key.
+     *
+     * @param key The key to look up in the cache.
+     * @return The cached value, or {@code null} if not found or expired.
+     */
+    @Nullable
+    public V get(@NonNull K key) {
+        synchronized (mMap) {
+            final CacheValue<V> value = mMap.get(key);
+            if (value != null && !isExpired(value.timestamp)) {
+                return value.entry;
+            } else {
+                mMap.remove(key); // Remove expired entries
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Retrieves a value from the cache, associated with the given key.
+     * If the entry is not found in the cache or has expired, computes it using the provided
+     * {@code supplier} and stores the result in the cache.
+     *
+     * @param key      The key to look up in the cache.
+     * @param supplier The {@link Supplier} to compute the value if not found or expired.
+     * @return The cached or computed value, or {@code null} if the {@code supplier} returns null.
+     */
+    @Nullable
+    public V getOrCompute(@NonNull K key, @NonNull Supplier<V> supplier) {
+        synchronized (mMap) {
+            final V cachedValue = get(key);
+            if (cachedValue != null) {
+                return cachedValue;
+            }
+
+            // Entry not found or expired, compute it
+            final V computedValue = supplier.get();
+            if (computedValue != null && mShouldCacheValue.test(computedValue)) {
+                put(key, computedValue);
+            }
+            return computedValue;
+        }
+    }
+
+    /**
+     * Stores a value in the cache, associated with the given key.
+     *
+     * @param key   The key to associate with the value.
+     * @param value The value to store in the cache.
+     */
+    public void put(@NonNull K key, @NonNull V value) {
+        Objects.requireNonNull(value);
+        synchronized (mMap) {
+            mMap.put(key, new CacheValue<>(mClock.millis(), value));
+        }
+    }
+
+    /**
+     * Clear the cache.
+     */
+    public void clear() {
+        synchronized (mMap) {
+            mMap.evictAll();
+        }
+    }
+
+    private boolean isExpired(long timestamp) {
+        return mClock.millis() > timestamp + mExpiryDurationMs;
+    }
+
+    private static class CacheValue<V> {
+        public final long timestamp;
+        @NonNull
+        public final V entry;
+
+        CacheValue(long timestamp, V entry) {
+            this.timestamp = timestamp;
+            this.entry = entry;
+        }
+    }
+}