diff --git a/app/build.gradle b/app/build.gradle index c688e60..3732692 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -502,6 +502,12 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2' + implementation 'androidx.lifecycle:lifecycle-livedata:2.6.2' + implementation 'androidx.lifecycle:lifecycle-runtime:2.6.2' + // 可选:RxJava3 Observable → LiveData 的桥接 + implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.6.2' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' @@ -533,6 +539,8 @@ dependencies { annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2' //磁盘缓存 implementation 'com.jakewharton:disklrucache:2.0.2' + //图表 + implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' //Aria implementation 'com.arialyy.aria:core:3.8.15' annotationProcessor 'com.arialyy.aria:compiler:3.8.15' diff --git a/app/src/main/java/com/fuying/sn/disklrucache/CacheHelper.java b/app/src/main/java/com/fuying/sn/disklrucache/CacheHelper.java index 69bb124..1d2d141 100644 --- a/app/src/main/java/com/fuying/sn/disklrucache/CacheHelper.java +++ b/app/src/main/java/com/fuying/sn/disklrucache/CacheHelper.java @@ -6,9 +6,7 @@ import android.graphics.drawable.Drawable; import android.os.Environment; import android.util.Log; -import com.fuying.sn.config.CommonConfig; import com.jakewharton.disklrucache.DiskLruCache; -import com.tencent.mmkv.MMKV; import org.json.JSONArray; import org.json.JSONException; @@ -19,20 +17,21 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Serializable; +import java.nio.charset.StandardCharsets; /** * 磁盘缓存帮助类 + * https://www.cnblogs.com/aademeng/articles/6817058.html */ public class CacheHelper { private static final String TAG = "DiskLruCacheHelper"; - private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); - private static final String DIR_NAME = "diskCache"; private static final int MAX_COUNT = 1024 * 1024 * 1024; private static final int DEFAULT_APP_VERSION = 1; @@ -72,9 +71,8 @@ public class CacheHelper { int appVersion = context == null ? DEFAULT_APP_VERSION : Utils.getAppVersion(context); - DiskLruCache diskLruCache = null; try { - diskLruCache = DiskLruCache.open( + return DiskLruCache.open( dir, appVersion, DEFAULT_APP_VERSION, @@ -82,13 +80,12 @@ public class CacheHelper { } catch (IOException e) { e.printStackTrace(); } - return diskLruCache; + return null; } private DiskLruCache generateCache(Context context, String dirName, int maxCount) { - DiskLruCache diskLruCache = null; try { - diskLruCache = DiskLruCache.open( + return DiskLruCache.open( getDiskCacheDir(context, dirName), Utils.getAppVersion(context), DEFAULT_APP_VERSION, @@ -96,7 +93,7 @@ public class CacheHelper { } catch (IOException e) { e.printStackTrace(); } - return diskLruCache; + return null; } // ======================================= @@ -104,9 +101,7 @@ public class CacheHelper { // ======================================= public void put(String key, String value) { -// Log.e(TAG, "put: key = " + key + " value = " + value); - mMMKV.encode(key + "_time", System.currentTimeMillis()); - mMMKV.encode(key + "_mmkv", value); + Log.e(TAG, "put: key = " + key + " value = " + value); DiskLruCache.Editor edit = null; BufferedWriter bw = null; @@ -116,15 +111,16 @@ public class CacheHelper { return; } OutputStream os = edit.newOutputStream(0); - bw = new BufferedWriter(new OutputStreamWriter(os)); + bw = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8)); bw.write(value); edit.commit();//write CLEAN } catch (IOException e) { e.printStackTrace(); Log.e(TAG, "put: " + e.getMessage()); try { - //s - edit.abort();//write REMOVE + if (edit != null) { + edit.abort();//write REMOVE + } } catch (IOException e1) { e1.printStackTrace(); Log.e(TAG, "put: " + e1.getMessage()); @@ -142,30 +138,30 @@ public class CacheHelper { } public String getAsString(String key) { -// Log.e(TAG, "getAsString: " + key); + Log.e(TAG, "getAsString: " + key); InputStream inputStream = null; try { - //write READ inputStream = get(key); if (inputStream == null) { - return mMMKV.decodeString(key + "_mmkv", null); + return null; } StringBuilder sb = new StringBuilder(); - int len = 0; - byte[] buf = new byte[128]; - while ((len = inputStream.read(buf)) != -1) { - sb.append(new String(buf, 0, len)); + InputStreamReader isr = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + char[] buf = new char[1024]; + int len; + while ((len = isr.read(buf)) != -1) { + sb.append(buf, 0, len); } return sb.toString(); } catch (IOException e) { e.printStackTrace(); Log.e(TAG, "getAsString: " + e.getMessage()); + } finally { if (inputStream != null) { try { inputStream.close(); - } catch (IOException e1) { - e1.printStackTrace(); - Log.e(TAG, "getAsString: " + e1.getMessage()); + } catch (IOException e) { + e.printStackTrace(); } } } @@ -199,9 +195,9 @@ public class CacheHelper { public JSONArray getAsJSONArray(String key) { String JSONString = getAsString(key); + if (JSONString == null) return null; try { - JSONArray obj = new JSONArray(JSONString); - return obj; + return new JSONArray(JSONString); } catch (Exception e) { e.printStackTrace(); return null; @@ -233,7 +229,9 @@ public class CacheHelper { } catch (Exception e) { e.printStackTrace(); try { - editor.abort();//write REMOVE + if (editor != null) { + editor.abort();//write REMOVE + } } catch (IOException e1) { e1.printStackTrace(); } @@ -256,8 +254,8 @@ public class CacheHelper { if (is == null) { return null; } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[256]; int len = 0; while ((len = is.read(buf)) != -1) { @@ -266,6 +264,12 @@ public class CacheHelper { res = baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); + } finally { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } } return res; } @@ -289,7 +293,9 @@ public class CacheHelper { } catch (IOException e) { e.printStackTrace(); try { - editor.abort(); + if (editor != null) { + editor.abort(); + } } catch (IOException e1) { e1.printStackTrace(); } @@ -365,8 +371,8 @@ public class CacheHelper { // ======================================= public boolean remove(String key) { try { - key = Utils.hashKeyForDisk(key); - return mDiskLruCache.remove(key); + String hashKey = Utils.hashKeyForDisk(key); + return mDiskLruCache.remove(hashKey); } catch (IOException e) { e.printStackTrace(); } @@ -379,7 +385,6 @@ public class CacheHelper { public void delete() throws IOException { mDiskLruCache.delete(); - mMMKV.clearAll(); } public void flush() throws IOException { @@ -413,12 +418,12 @@ public class CacheHelper { //basic editor public DiskLruCache.Editor editor(String key) { try { - key = Utils.hashKeyForDisk(key); + String hashKey = Utils.hashKeyForDisk(key); //wirte DIRTY - DiskLruCache.Editor edit = mDiskLruCache.edit(key); + DiskLruCache.Editor edit = mDiskLruCache.edit(hashKey); //edit maybe null :the entry is editing if (edit == null) { - Log.w(TAG, "the entry spcified key:" + key + " is editing by other . "); + Log.w(TAG, "the entry spcified key:" + hashKey + " is editing by other . "); } return edit; } catch (IOException e) { @@ -436,7 +441,7 @@ public class CacheHelper { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(Utils.hashKeyForDisk(key)); if (snapshot == null) //not find entry , or entry.readable = false { -// Log.e(TAG, "not find entry , or entry.readable = false"); + Log.e(TAG, "not find entry , or entry.readable = false"); return null; } //write READ @@ -455,20 +460,17 @@ public class CacheHelper { // ======================================= private File getDiskCacheDir(Context context, String uniqueName) { - String cachePath; + File cacheDir; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { - if (context.getExternalCacheDir() != null) { - cachePath = context.getExternalCacheDir().getPath(); - } else if (context.getExternalFilesDir("cache") != null) { - cachePath = context.getExternalFilesDir("cache").getPath(); - } else { - cachePath = context.getCacheDir().getPath(); + cacheDir = context.getExternalCacheDir(); + if (cacheDir == null) { + cacheDir = context.getCacheDir(); } } else { - cachePath = context.getCacheDir().getPath(); + cacheDir = context.getCacheDir(); } - return new File(cachePath + File.separator + uniqueName); + return new File(cacheDir, uniqueName); } } diff --git a/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java b/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java index 46a40d7..bc7ee38 100644 --- a/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java +++ b/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java @@ -67,6 +67,8 @@ import com.fuying.sn.network.api.SnControlApi; import com.fuying.sn.network.api.SnInfoApi; import com.fuying.sn.network.api.SnLogApi; import com.fuying.sn.network.api.newly.ControlApi; +import com.fuying.sn.network.api.newly.StatisticsApi; +import com.fuying.sn.network.cache.RxCacheManager; import com.fuying.sn.network.interceptor.RepeatRequestInterceptor; import com.fuying.sn.service.LogcatService; import com.fuying.sn.utils.ApkUtils; @@ -125,19 +127,39 @@ import retrofit2.converter.gson.GsonConverterFactory; public class NetInterfaceManager { private static final String TAG = "NetInterfaceManager"; + private static final RxCacheManager CACHE_MANAGER = RxCacheManager.getInstance(); + + public static void clearAllCache() { + Log.e(TAG, "clearAllCache: "); + CACHE_MANAGER.clearAllCache(); + } + + public static void removeCache(String key) { + Log.e(TAG, "removeCache: key = " + key); + CACHE_MANAGER.removeCache(key); + } + + //缓存过期时间1秒 + private static final long CACHE_EXPIRE_ONE_SECOND = 1000; + //默认 缓存过期时间10秒 + private static final long DEFAULT_CACHE_EXPIRE = 10 * CACHE_EXPIRE_ONE_SECOND; + //缓存过期时间1分钟 + private static final long CACHE_EXPIRE_ONE_MINUTE = 60 * CACHE_EXPIRE_ONE_SECOND; + //超时时间 + private static final int TIME_OUT = 30; + // 缓存文件最大限制大小20M + private static final long CACHE_SIZE = 1024 * 1024 * 64; + + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + @SuppressLint("StaticFieldLeak") private static NetInterfaceManager INSTANCE; private Context mContext; private ContentResolver crv; - private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); - private Retrofit mRetrofit; - private OkHttpClient okHttpClient; private CacheHelper mCacheHelper; - //超时时间 - private static final int timeOut = 15; - // 缓存文件最大限制大小20M - private static final long cacheSize = 1024 * 1024 * 64; + private Retrofit mRetrofit; + private OkHttpClient mOkHttpClient; private NetInterfaceManager(Context context) { if (context == null) { @@ -148,23 +170,23 @@ public class NetInterfaceManager { this.mCacheHelper = new CacheHelper(mContext); if (null == mRetrofit) { - if (okHttpClient == null) { + if (mOkHttpClient == null) { //如果无法生存缓存文件目录,检测权限使用已经加上,检测手机是否把文件读写权限禁止了 OkHttpClient.Builder builder = new OkHttpClient.Builder(); - builder.connectTimeout(timeOut, TimeUnit.SECONDS); // 设置连接超时时间 - builder.writeTimeout(timeOut, TimeUnit.SECONDS);// 设置写入超时时间 - builder.readTimeout(timeOut, TimeUnit.SECONDS);// 设置读取数据超时时间 + builder.connectTimeout(TIME_OUT, TimeUnit.SECONDS); // 设置连接超时时间 + builder.writeTimeout(TIME_OUT, TimeUnit.SECONDS);// 设置写入超时时间 + builder.readTimeout(TIME_OUT, TimeUnit.SECONDS);// 设置读取数据超时时间 builder.retryOnConnectionFailure(true);// 设置进行连接失败重试 builder.addInterceptor(new RepeatRequestInterceptor()); // 设置缓存文件路径 String cacheDirectory = getCacheDir() + "/OkHttpCache"; - Cache cache = new Cache(new File(cacheDirectory), cacheSize); + Cache cache = new Cache(new File(cacheDirectory), CACHE_SIZE); builder.cache(cache);// 设置缓存 - okHttpClient = builder.build(); + mOkHttpClient = builder.build(); } mRetrofit = new Retrofit.Builder() - .client(okHttpClient) + .client(mOkHttpClient) .baseUrl(BuildConfig.ROOT_URL) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) @@ -206,10 +228,132 @@ public class NetInterfaceManager { } public OkHttpClient getOkHttpClient() { - return okHttpClient; + return mOkHttpClient; } + /** + * 极简调用:仅需 网络请求Observable + 缓存过期时间 + * + * @param networkObservable 原始网络请求 + * @param expireTimeMillis 缓存有效期(毫秒) + * @return 带自动缓存的Observable + */ + public static Observable withCache(Observable networkObservable, String cacheKey, long expireTimeMillis) { + return Observable.defer(() -> { +// Log.e(TAG, "withCache: cacheKey = " + cacheKey); + // 1. 读取缓存 + T cacheData = CACHE_MANAGER.getCache(cacheKey); + if (cacheData != null) { + Log.e(TAG, "withCache: getCache " + cacheKey); + return Observable.just(cacheData); + } + + Log.e(TAG, "withCache: request " + cacheKey); + // 2. 无缓存则发起网络请求,成功后缓存 + return networkObservable + .subscribeOn(Schedulers.io()) + .doOnNext(data -> CACHE_MANAGER.saveCache(cacheKey, data, expireTimeMillis)); + }) + .subscribeOn(Schedulers.io()) // 缓存IO切到子线程 + .observeOn(AndroidSchedulers.mainThread()); + } + + /** + * 通用缓存策略:优先从内存缓存获取,其次从磁盘缓存获取,最后请求网络 + * 统一使用 cacheKey 同时作为内存和磁盘缓存的键,且支持 Stale-While-Revalidate 策略 + * + * @param networkObservable 网络请求Observable + * @param cacheKey 缓存键(内存与磁盘通用) + * @param type 数据类型(通过 TypeToken 获取,支持泛型) + * @param expireTimeMillis 内存缓存过期时间(毫秒) + * @return 带三级缓存策略的Observable,可能会发射两次(缓存 + 最新网络数据) + */ + public static Observable> withMultiLevelCache( + Observable> networkObservable, + String cacheKey, + Type type, + long expireTimeMillis) { + + return Observable.>create(emitter -> { + Log.e(TAG, "withMultiLevelCache: cacheKey = " + cacheKey); + + // 1. 优先从内存缓存获取 + BaseResponse memoryCache = CACHE_MANAGER.getCache(cacheKey); + if (memoryCache != null) { + Log.e(TAG, "withMultiLevelCache: hit memory cache"); + emitter.onNext(memoryCache); + } else { + // 2. 内存未命中,从磁盘缓存获取 + try { + String diskCacheJson = INSTANCE.mCacheHelper.getAsString(cacheKey); + if (!TextUtils.isEmpty(diskCacheJson)) { + Log.e(TAG, "withMultiLevelCache: hit disk cache"); + BaseResponse diskCache = GsonUtils.toJavaObject(diskCacheJson, type); + if (diskCache != null) { + emitter.onNext(diskCache); + } + } + } catch (Exception e) { + Log.e(TAG, "withMultiLevelCache: read disk cache error", e); + } + } + + // 3. 始终请求网络数据以同步更新 + Log.e(TAG, "withMultiLevelCache: request from network"); + networkObservable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + emitter.setDisposable(d); + } + + @Override + public void onNext(BaseResponse response) { + Log.e(TAG, "withMultiLevelCache: network success code = " + (response != null ? response.code : "null")); + if (response != null && response.code == 200) { + // 保存到内存和磁盘缓存 + CACHE_MANAGER.saveCache(cacheKey, response, expireTimeMillis); + try { + String json = GsonUtils.toJSONString(response); + INSTANCE.mCacheHelper.put(cacheKey, json); + } catch (Exception e) { + Log.e(TAG, "withMultiLevelCache: save disk cache error", e); + } + } else { + // 状态码不为 200,清除缓存 + Log.e(TAG, "withMultiLevelCache: code != 200, clear cache"); + CACHE_MANAGER.removeCache(cacheKey); + INSTANCE.mCacheHelper.remove(cacheKey); + } + emitter.onNext(response); + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e(TAG, "withMultiLevelCache: network error", e); + // 网络错误,清除缓存 + CACHE_MANAGER.removeCache(cacheKey); + INSTANCE.mCacheHelper.remove(cacheKey); + if (!emitter.isDisposed()) { + emitter.onError(e); + } + } + + @Override + public void onComplete() { + if (!emitter.isDisposed()) { + emitter.onComplete(); + } + } + }); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + /* * * API @@ -257,6 +401,10 @@ public class NetInterfaceManager { return mRetrofit.create(ControlApi.class); } + public StatisticsApi getStatisticsApi() { + return mRetrofit.create(StatisticsApi.class); + } + /* * diff --git a/app/src/main/java/com/fuying/sn/network/UrlAddress.java b/app/src/main/java/com/fuying/sn/network/UrlAddress.java index 430867a..5b0c452 100644 --- a/app/src/main/java/com/fuying/sn/network/UrlAddress.java +++ b/app/src/main/java/com/fuying/sn/network/UrlAddress.java @@ -91,6 +91,7 @@ public class UrlAddress { public final static String DEVICE_INFO = "device/info"; /*检查设备守护更新*/ public final static String CHECK_UPDATE = "device/check-update"; + /*使用统计*/ /*获取整机控制配置*/ public final static String CONTROL_STATUS = "statistics/control-status"; diff --git a/app/src/main/java/com/fuying/sn/network/cache/CacheEntry.java b/app/src/main/java/com/fuying/sn/network/cache/CacheEntry.java new file mode 100644 index 0000000..034de2f --- /dev/null +++ b/app/src/main/java/com/fuying/sn/network/cache/CacheEntry.java @@ -0,0 +1,30 @@ +package com.fuying.sn.network.cache; + +/** + * 缓存实体:存储请求结果、创建时间、有效期 + * + * @param 接口返回数据类型 + */ +public class CacheEntry { + // 缓存的成功数据 + private final T data; + // 缓存创建时间(毫秒) + private final long createTime; + // 缓存有效期(毫秒) + private final long expireTimeMillis; + + public CacheEntry(T data, long createTime, long expireTimeMillis) { + this.data = data; + this.createTime = createTime; + this.expireTimeMillis = expireTimeMillis; + } + + // 判断缓存是否过期 + public boolean isExpired() { + return System.currentTimeMillis() - createTime > expireTimeMillis; + } + + public T getData() { + return data; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fuying/sn/network/cache/RxCacheManager.java b/app/src/main/java/com/fuying/sn/network/cache/RxCacheManager.java new file mode 100644 index 0000000..b02390b --- /dev/null +++ b/app/src/main/java/com/fuying/sn/network/cache/RxCacheManager.java @@ -0,0 +1,63 @@ +package com.fuying.sn.network.cache; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * 全局缓存管理器:线程安全,存储所有接口缓存 + */ +public class RxCacheManager { + // 单例 + private static volatile RxCacheManager INSTANCE; + // 缓存容器:key=请求唯一标识,value=缓存实体 + private final ConcurrentHashMap> cacheMap; + + private RxCacheManager() { + cacheMap = new ConcurrentHashMap<>(); + } + + public static RxCacheManager getInstance() { + if (INSTANCE == null) { + synchronized (RxCacheManager.class) { + if (INSTANCE == null) { + INSTANCE = new RxCacheManager(); + } + } + } + return INSTANCE; + } + + /** + * 保存缓存 + */ + public void saveCache(String key, T data, long expireTimeMillis) { + if (key == null || data == null) return; + cacheMap.put(key, new CacheEntry<>(data, System.currentTimeMillis(), expireTimeMillis)); + } + + /** + * 获取缓存(自动判断过期) + */ + public T getCache(String key) { + CacheEntry entry = cacheMap.get(key); + if (entry == null || entry.isExpired()) { + // 缓存不存在/已过期,移除并返回null + cacheMap.remove(key); + return null; + } + return (T) entry.getData(); + } + + /** + * 清空所有缓存 + */ + public void clearAllCache() { + cacheMap.clear(); + } + + /** + * 删除指定缓存 + */ + public void removeCache(String key) { + cacheMap.remove(key); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fuying/sn/network/cache/RxCacheRequest.java b/app/src/main/java/com/fuying/sn/network/cache/RxCacheRequest.java new file mode 100644 index 0000000..10e0a06 --- /dev/null +++ b/app/src/main/java/com/fuying/sn/network/cache/RxCacheRequest.java @@ -0,0 +1,38 @@ +package com.fuying.sn.network.cache; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * 优化后:无需手动传CacheKey,自动从请求获取 + */ +public class RxCacheRequest { + private static final RxCacheManager CACHE_MANAGER = RxCacheManager.getInstance(); + + /** + * 极简调用:仅需 网络请求Observable + 缓存过期时间 + * + * @param networkObservable 原始网络请求 + * @param expireTimeMillis 缓存有效期(毫秒) + * @return 带自动缓存的Observable + */ + public static Observable withCache(Observable networkObservable, String cacheKey, long expireTimeMillis) { + return Observable.defer(() -> { + + // 2. 读取缓存 + T cacheData = CACHE_MANAGER.getCache(cacheKey); + if (cacheData != null) { + return Observable.just(cacheData); + } + + // 3. 无缓存则发起网络请求,成功后缓存 + return networkObservable + .subscribeOn(Schedulers.io()) + .doOnNext(data -> CACHE_MANAGER.saveCache(cacheKey, data, expireTimeMillis)); + }) + .subscribeOn(Schedulers.io()) // 缓存IO切到子线程 + .observeOn(AndroidSchedulers.mainThread()); + } + +} diff --git a/app/src/main/java/com/fuying/sn/network/cache/RxTimeCacheManager.java b/app/src/main/java/com/fuying/sn/network/cache/RxTimeCacheManager.java new file mode 100644 index 0000000..d81414e --- /dev/null +++ b/app/src/main/java/com/fuying/sn/network/cache/RxTimeCacheManager.java @@ -0,0 +1,66 @@ +package com.fuying.sn.network.cache; + +import java.util.concurrent.ConcurrentHashMap; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.ObservableTransformer; + +public class RxTimeCacheManager { + + // Thread-safe memory cache to hold our data and its timestamp + private static final ConcurrentHashMap> cacheMap = new ConcurrentHashMap<>(); + + /** + * Industry Standard RxJava Cache Transformer. + * * @param cacheKey Unique key for this specific request (e.g., URL or Method Name) + * + * @param validTimeMillis How long the data is valid before a new network request is allowed + */ + public static ObservableTransformer applyCache(final String cacheKey, final long validTimeMillis) { + return new ObservableTransformer() { + @Override + public ObservableSource apply(Observable upstream) { + // defer() ensures this logic runs EVERY time someone subscribes, not at initialization + return Observable.defer(() -> { + CacheEntry entry = cacheMap.get(cacheKey); + long currentTime = System.currentTimeMillis(); + + // 1. Check if cache exists and is within the time limit + if (entry != null && (currentTime - entry.timestamp) < validTimeMillis) { + // Cache HIT -> Return cached data immediately, do not trigger network + @SuppressWarnings("unchecked") + T cachedData = (T) entry.data; + return Observable.just(cachedData); + } + + // 2. Cache MISS or EXPIRED -> Trigger upstream (network) and save result + return upstream.doOnNext(data -> { + // Save the fresh data and current timestamp back to the cache + cacheMap.put(cacheKey, new CacheEntry<>(data, System.currentTimeMillis())); + }); + }); + } + }; + } + + /** + * Clears a specific cache key if you need to force a refresh manually. + */ + public static void clearCache(String cacheKey) { + cacheMap.remove(cacheKey); + } + + /** + * Internal wrapper to hold the payload and the time it was fetched. + */ + private static class CacheEntry { + final T data; + final long timestamp; + + CacheEntry(T data, long timestamp) { + this.data = data; + this.timestamp = timestamp; + } + } +} \ No newline at end of file