|
|
@@ -0,0 +1,981 @@
|
|
|
+package com.datarecovery.master.utils.filedetect;
|
|
|
+
|
|
|
+import android.content.Context;
|
|
|
+import android.database.Cursor;
|
|
|
+import android.database.sqlite.SQLiteDatabase;
|
|
|
+import android.os.Environment;
|
|
|
+import android.text.TextUtils;
|
|
|
+
|
|
|
+import androidx.annotation.IntDef;
|
|
|
+import androidx.annotation.NonNull;
|
|
|
+
|
|
|
+import com.atmob.common.crypto.CryptoUtils;
|
|
|
+import com.datarecovery.master.utils.ImageDeepDetector;
|
|
|
+import com.datarecovery.master.utils.xfile.XFile;
|
|
|
+import com.datarecovery.master.utils.xfile.XPathFile;
|
|
|
+
|
|
|
+import org.reactivestreams.Publisher;
|
|
|
+import org.reactivestreams.Subscriber;
|
|
|
+
|
|
|
+import java.io.Closeable;
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileOutputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.io.RandomAccessFile;
|
|
|
+import java.lang.annotation.Retention;
|
|
|
+import java.lang.annotation.RetentionPolicy;
|
|
|
+import java.nio.ByteOrder;
|
|
|
+import java.nio.MappedByteBuffer;
|
|
|
+import java.nio.channels.FileChannel;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.UUID;
|
|
|
+import java.util.zip.Adler32;
|
|
|
+
|
|
|
+import atmob.reactivex.rxjava3.core.Flowable;
|
|
|
+import atmob.reactivex.rxjava3.schedulers.Schedulers;
|
|
|
+
|
|
|
+public class ImageCacheUtil {
|
|
|
+
|
|
|
+ public static boolean isImageSuffix(String name) {
|
|
|
+ if (TextUtils.isEmpty(name)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png")
|
|
|
+ || name.endsWith(".gif") || name.endsWith(".bmp") || name.endsWith(".webp")
|
|
|
+ || name.endsWith(".tiff") || name.endsWith(".psd") || name.endsWith(".svg")
|
|
|
+ || name.endsWith(".raw") || name.endsWith(".heif") || name.endsWith(".indd");
|
|
|
+ }
|
|
|
+
|
|
|
+ public static boolean isHuaweiGalleryCacheFile(String path) {
|
|
|
+ if (TextUtils.isEmpty(path)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!path.contains("com.huawei.photos%2Ffiles%2Fthumbdb") &&
|
|
|
+ !path.contains("com.huawei.photos/files/thumbdb")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return path.endsWith("photoshare.db");
|
|
|
+ }
|
|
|
+
|
|
|
+ public static boolean isMeizuGalleryCacheFile(String path) {
|
|
|
+ if (TextUtils.isEmpty(path)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!path.contains("com.meizu.media.gallery%2Fcache") &&
|
|
|
+ !path.contains("com.meizu.media.gallery/cache")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return path.contains("bestPhotoCache") || path.contains("face_thumbnails")
|
|
|
+ || path.contains("uri_thumbnail_cache");
|
|
|
+ }
|
|
|
+
|
|
|
+ public static boolean isXiaomiGalleryCacheFile(String path) {
|
|
|
+ if (TextUtils.isEmpty(path)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!path.contains("com.miui.gallery%2Ffiles%2Fgallery_disk_cache") &&
|
|
|
+ !path.contains("com.miui.gallery/files/gallery_disk_cache")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return path.contains("full_size") || path.contains("small_size");
|
|
|
+ }
|
|
|
+
|
|
|
+ public static boolean isVivoGalleryCacheFile(String path) {
|
|
|
+ if (TextUtils.isEmpty(path)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!path.contains("com.vivo.gallery%2Fcache") &&
|
|
|
+ !path.contains("com.vivo.gallery/cache")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return path.contains("imgcache") || path.contains("trackthumbnail_cache");
|
|
|
+ }
|
|
|
+
|
|
|
+ public static boolean isOppoGalleryCacheFile(String path) {
|
|
|
+ if (TextUtils.isEmpty(path)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!path.contains("com.coloros.gallery3d%2Fcache") &&
|
|
|
+ !path.contains("com.coloros.gallery3d/cache")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return path.contains("imgcache") || path.contains("screennailcache")
|
|
|
+ || path.contains("tilecache");
|
|
|
+ }
|
|
|
+
|
|
|
+ public static boolean hasImgMagic(XFile file) {
|
|
|
+ try (InputStream inputStream = file.newInputStream()) {
|
|
|
+ byte[] bytes = new byte[8];
|
|
|
+ if (inputStream.read(bytes) != 8) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50 && bytes[2] == (byte) 0x4E && bytes[3] == (byte) 0x47
|
|
|
+ && bytes[4] == (byte) 0x0D && bytes[5] == (byte) 0x0A && bytes[6] == (byte) 0x1A && bytes[7] == (byte) 0x0A) {
|
|
|
+ // png
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean hasHeaderMagic = bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF; // jpg header
|
|
|
+ if (!hasHeaderMagic) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ long skip = inputStream.available() - 2;
|
|
|
+ if (inputStream.skip(skip) != skip) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (inputStream.read(bytes) != 2) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD9;
|
|
|
+ } catch (Exception ignore) {
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static Flowable<DetectFile> detectOppoGalleryCache(Context context, XFile xFile) {
|
|
|
+ return new GenericImgCollectionDetector(context, xFile, FileType.IMAGE_GALLERY)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .onErrorComplete();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static Flowable<DetectFile> detectGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
|
|
|
+ return new GalleryCacheDetector(context, xFile)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .onErrorComplete();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static Publisher<DetectFile> detectWechatCache(@NonNull Context context, @NonNull XFile xFile) {
|
|
|
+ return new WechatCacheDetector(context, xFile)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .onErrorComplete();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static Publisher<DetectFile> detectVivoGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
|
|
|
+ return new GenericImgCollectionDetector(context, xFile, FileType.IMAGE_GALLERY)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .onErrorComplete();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static Publisher<DetectFile> detectMeizuGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
|
|
|
+ return new GenericImgCollectionDetector(context, xFile, FileType.IMAGE_GALLERY)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .onErrorComplete();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static Publisher<DetectFile> detectHuaweiGalleryCache(@NonNull Context context, @NonNull XFile xFile) {
|
|
|
+ return new HuaweiGalleryCacheDetector(context, xFile)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .onErrorComplete();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class HuaweiGalleryCacheDetector extends Flowable<DetectFile> {
|
|
|
+
|
|
|
+ private static final String CACHE_DOMAIN = "huawei_gallery_cache_detector";
|
|
|
+ private final Context context;
|
|
|
+ private final XFile dbFile;
|
|
|
+
|
|
|
+ public HuaweiGalleryCacheDetector(Context context, XFile dbFile) {
|
|
|
+ this.context = context;
|
|
|
+ this.dbFile = dbFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
|
|
|
+ long lastModified;
|
|
|
+ try {
|
|
|
+ lastModified = dbFile.lastModified();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (checkDetectedCache(context, lastModified, subscriber)) {
|
|
|
+ subscriber.onComplete();
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ clearDetectedCache(context, CACHE_DOMAIN);
|
|
|
+ }
|
|
|
+
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ if (!detectedCacheDir.exists()) {
|
|
|
+ detectedCacheDir.mkdirs();
|
|
|
+ }
|
|
|
+
|
|
|
+ File dbTempFile;
|
|
|
+ try {
|
|
|
+ dbTempFile = createDbTempFile(detectedCacheDir);
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try (SQLiteDatabase sqLiteDatabase = SQLiteDatabase.openDatabase(dbTempFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
|
|
+ Cursor cursor = sqLiteDatabase.rawQuery("select * from general_kv", null)
|
|
|
+ ) {
|
|
|
+ int vIndex = cursor.getColumnIndex("v");
|
|
|
+ if (vIndex == -1) {
|
|
|
+ subscriber.onComplete();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ while (cursor.moveToNext()) {
|
|
|
+ byte[] data = cursor.getBlob(vIndex);
|
|
|
+ if (data == null || data.length == 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
|
|
|
+ if (cache.createNewFile() && bytes2File(data, cache)) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_GALLERY));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ subscriber.onComplete();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ } finally {
|
|
|
+ if (dbTempFile != null && dbTempFile.exists()) {
|
|
|
+ dbTempFile.delete();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private File createDbTempFile(File detectedCacheDir) throws Exception {
|
|
|
+ File dbTempFile = new File(detectedCacheDir, "huawei_gallery_cache_detector.db");
|
|
|
+ if (dbTempFile.exists()) {
|
|
|
+ dbTempFile.delete();
|
|
|
+ }
|
|
|
+ try (InputStream inputStream = dbFile.newInputStream();
|
|
|
+ OutputStream outputStream = new FileOutputStream(dbTempFile)
|
|
|
+ ) {
|
|
|
+ byte[] buffer = new byte[2048];
|
|
|
+ int read;
|
|
|
+ while ((read = inputStream.read(buffer)) != -1) {
|
|
|
+ outputStream.write(buffer, 0, read);
|
|
|
+ }
|
|
|
+ outputStream.flush();
|
|
|
+ return dbTempFile;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
|
|
|
+ if (files != null && files.length > 0) {
|
|
|
+ for (File file : files) {
|
|
|
+ if (file.getName().endsWith(".db")) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(this.context, file), FileType.IMAGE_GALLERY));
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class GenericImgCollectionDetector extends Flowable<DetectFile> {
|
|
|
+
|
|
|
+ private final int category;
|
|
|
+ private String CACHE_DOMAIN = "generic_img_collection_detector";
|
|
|
+ private final Context context;
|
|
|
+ private final XFile xFile;
|
|
|
+
|
|
|
+ public GenericImgCollectionDetector(Context context, XFile xFile, @FileType int category) {
|
|
|
+ this.context = context;
|
|
|
+ this.xFile = xFile;
|
|
|
+ this.category = category;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
|
|
|
+ long lastModified;
|
|
|
+ try {
|
|
|
+ lastModified = xFile.lastModified();
|
|
|
+ CACHE_DOMAIN += xFile.getName();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (checkDetectedCache(context, lastModified, subscriber)) {
|
|
|
+ subscriber.onComplete();
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ clearDetectedCache(context, CACHE_DOMAIN);
|
|
|
+ }
|
|
|
+
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ if (!detectedCacheDir.exists()) {
|
|
|
+ detectedCacheDir.mkdirs();
|
|
|
+ }
|
|
|
+
|
|
|
+ try (InputStream inputStream = xFile.newInputStream()) {
|
|
|
+ ArrayList<Byte> imageBytes = new ArrayList<>();
|
|
|
+ byte[] buffer = new byte[2048];
|
|
|
+ int read;
|
|
|
+ while ((read = inputStream.read(buffer)) != -1) {
|
|
|
+ for (int i = 0; i < read; i++) {
|
|
|
+ byte b = buffer[i];
|
|
|
+ imageBytes.add(b);
|
|
|
+ if (imageBytes.size() < 3) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (imageBytes.size() == 3) {
|
|
|
+ if (imageBytes.get(0) != (byte) 0xFF || imageBytes.get(1) != (byte) 0xD8 || imageBytes.get(2) != (byte) 0xFF) {
|
|
|
+ imageBytes.remove(0);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (i == read - 1 && inputStream.available() == 0) {
|
|
|
+ if (imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF && imageBytes.get(imageBytes.size() - 1) == (byte) 0xD9) {
|
|
|
+ File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
|
|
|
+ if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(context, cache), category));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ imageBytes.clear();
|
|
|
+ } else if (imageBytes.size() >= 5) {
|
|
|
+ if (imageBytes.get(imageBytes.size() - 1) == (byte) 0xD9
|
|
|
+ && imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF
|
|
|
+ ) {
|
|
|
+ File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
|
|
|
+ if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(context, cache), category));
|
|
|
+ }
|
|
|
+ imageBytes.clear();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ subscriber.onComplete();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
|
|
|
+ if (files != null && files.length > 0) {
|
|
|
+ for (File file : files) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(this.context, file), category));
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class GalleryCacheDetector extends Flowable<DetectFile> {
|
|
|
+
|
|
|
+ private static final String CACHE_DOMAIN = "gallery_cache_detector";
|
|
|
+ private static final int MAGIC_INDEX_FILE = 0xB3273030;
|
|
|
+ private static final int MAGIC_DATA_FILE = 0xBD248510;
|
|
|
+
|
|
|
+ // index header offset
|
|
|
+ private static final int IH_MAGIC = 0;
|
|
|
+ private static final int IH_MAX_ENTRIES = 4;
|
|
|
+ private static final int IH_MAX_BYTES = 8;
|
|
|
+ private static final int IH_ACTIVE_REGION = 12;
|
|
|
+ private static final int IH_ACTIVE_ENTRIES = 16;
|
|
|
+ private static final int IH_ACTIVE_BYTES = 20;
|
|
|
+ private static final int IH_CHECKSUM = 28;
|
|
|
+ private static final int INDEX_HEADER_SIZE = 32;
|
|
|
+
|
|
|
+ private static final int DATA_HEADER_SIZE = 4;
|
|
|
+
|
|
|
+ // blob header offset
|
|
|
+ private static final int BH_KEY = 0;
|
|
|
+ private static final int BH_CHECKSUM = 8;
|
|
|
+ private static final int BH_OFFSET = 12;
|
|
|
+ private static final int BH_LENGTH = 16;
|
|
|
+ private static final int BLOB_HEADER_SIZE = 20;
|
|
|
+ private final XFile galleryCacheDir;
|
|
|
+ private final byte[] indexHeader;
|
|
|
+ private final byte[] blobHeader;
|
|
|
+ private final Adler32 mAdler32 = new Adler32();
|
|
|
+ private final Context context;
|
|
|
+
|
|
|
+ private int mMaxEntries;
|
|
|
+ private int mMaxBytes;
|
|
|
+ private int mActiveRegion;
|
|
|
+ private int mActiveBytes;
|
|
|
+
|
|
|
+ private FileChannel mIndexChannel;
|
|
|
+ private MappedByteBuffer mIndexBuffer;
|
|
|
+ private RandomAccessFile mIndexFile;
|
|
|
+ private RandomAccessFile mDataFile0;
|
|
|
+ private RandomAccessFile mDataFile1;
|
|
|
+
|
|
|
+ private RandomAccessFile mActiveDataFile;
|
|
|
+ private int mActiveHashStart;
|
|
|
+ private File indexTemp;
|
|
|
+ private File data0Temp;
|
|
|
+ private File data1Temp;
|
|
|
+
|
|
|
+ public GalleryCacheDetector(Context context, XFile galleryCacheDir) {
|
|
|
+ this.context = context;
|
|
|
+ this.galleryCacheDir = galleryCacheDir;
|
|
|
+ this.indexHeader = new byte[INDEX_HEADER_SIZE];
|
|
|
+ this.blobHeader = new byte[BLOB_HEADER_SIZE];
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
|
|
|
+ XFile[] xFiles;
|
|
|
+ try {
|
|
|
+ xFiles = galleryCacheDir.listFiles();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (xFiles == null || xFiles.length == 0) {
|
|
|
+ subscriber.onComplete();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ XFile indexFile = null;
|
|
|
+ XFile dataFile0 = null;
|
|
|
+ XFile dataFile1 = null;
|
|
|
+ for (XFile xFile : xFiles) {
|
|
|
+ try {
|
|
|
+ String name = xFile.getName();
|
|
|
+ if (name.endsWith(".idx")) {
|
|
|
+ indexFile = xFile;
|
|
|
+ } else if (name.endsWith(".0")) {
|
|
|
+ dataFile0 = xFile;
|
|
|
+ } else if (name.endsWith(".1")) {
|
|
|
+ dataFile1 = xFile;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (indexFile == null || dataFile0 == null || dataFile1 == null) {
|
|
|
+ subscriber.onComplete();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ doDetect(indexFile, dataFile0, dataFile1, subscriber);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void doDetect(XFile indexFile, XFile dataFile0, XFile dataFile1, Subscriber<? super DetectFile> subscriber) {
|
|
|
+ try {
|
|
|
+ long lastModified;
|
|
|
+ try {
|
|
|
+ lastModified = indexFile.lastModified();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (checkDetectedCache(context, lastModified, subscriber)) {
|
|
|
+ subscriber.onComplete();
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ clearDetectedCache(context, CACHE_DOMAIN);
|
|
|
+ }
|
|
|
+
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ if (!detectedCacheDir.exists()) {
|
|
|
+ detectedCacheDir.mkdirs();
|
|
|
+ }
|
|
|
+
|
|
|
+ loadIndex(indexFile, dataFile0, dataFile1);
|
|
|
+ for (int i = 0; i < mMaxEntries; i++) {
|
|
|
+ int offset = mActiveHashStart + i * 12;
|
|
|
+ long candidateKey = mIndexBuffer.getLong(offset);
|
|
|
+ try {
|
|
|
+ GalleryCacheDetector.LookupRequest lookupRequest = new GalleryCacheDetector.LookupRequest(candidateKey);
|
|
|
+ if (!lookup(lookupRequest)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ byte[] lookup = lookupRequest.buffer;
|
|
|
+ if (lookup == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ byte[] cropData = cropLookup(lookup);
|
|
|
+ if (cropData == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
|
|
|
+ if (cache.createNewFile() && bytes2File(cropData, cache)) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_GALLERY));
|
|
|
+ }
|
|
|
+ } catch (Exception ignore) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ subscriber.onComplete();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ } finally {
|
|
|
+ closeAll();
|
|
|
+ deleteTempFiles();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
|
|
|
+ if (files != null && files.length > 0) {
|
|
|
+ for (File file : files) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(this.context, file), FileType.IMAGE_GALLERY));
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] cropLookup(byte[] lookup) {
|
|
|
+ for (int i = 0; i < lookup.length; i++) {
|
|
|
+ if (lookup[i] == (byte) 0xFF && i + 1 < lookup.length && lookup[i + 1] == (byte) 0xD8) {
|
|
|
+ return crop(lookup, i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] crop(byte[] lookup, int i) {
|
|
|
+ byte[] crop = new byte[lookup.length - i];
|
|
|
+ System.arraycopy(lookup, i, crop, 0, crop.length);
|
|
|
+ return crop;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void loadIndex(XFile indexFile, XFile dataFile0, XFile dataFile1) throws Exception {
|
|
|
+ checkFileValid(indexFile, dataFile0, dataFile1);
|
|
|
+
|
|
|
+ try (InputStream idxIs = indexFile.newInputStream();
|
|
|
+ InputStream data0Is = dataFile0.newInputStream();
|
|
|
+ InputStream data1Is = dataFile1.newInputStream()
|
|
|
+ ) {
|
|
|
+ indexTemp = createTempFile("index.temp", idxIs);
|
|
|
+ mIndexFile = new RandomAccessFile(indexTemp, "rw");
|
|
|
+
|
|
|
+ data0Temp = createTempFile("data0.temp", data0Is);
|
|
|
+ mDataFile0 = new RandomAccessFile(data0Temp, "rw");
|
|
|
+
|
|
|
+ data1Temp = createTempFile("data1.temp", data1Is);
|
|
|
+ mDataFile1 = new RandomAccessFile(data1Temp, "rw");
|
|
|
+
|
|
|
+ // Map index file to memory
|
|
|
+ mIndexChannel = mIndexFile.getChannel();
|
|
|
+ mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE,
|
|
|
+ 0, mIndexFile.length());
|
|
|
+ mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
|
+
|
|
|
+ setActiveVariables();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void setActiveVariables() throws Exception {
|
|
|
+ mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
|
|
|
+ mActiveDataFile.setLength(mActiveBytes);
|
|
|
+ mActiveDataFile.seek(mActiveBytes);
|
|
|
+
|
|
|
+ mActiveHashStart = INDEX_HEADER_SIZE;
|
|
|
+
|
|
|
+ if (mActiveRegion != 0) {
|
|
|
+ mActiveHashStart += mMaxEntries * 12;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void checkFileValid(XFile indexFile, XFile dataFile0, XFile dataFile1) throws Exception {
|
|
|
+ byte[] buf = indexHeader;
|
|
|
+ try (InputStream inputStream = indexFile.newInputStream()) {
|
|
|
+ if (inputStream.read(buf) != INDEX_HEADER_SIZE) {
|
|
|
+ throw new Exception("cannot read header");
|
|
|
+ }
|
|
|
+ if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) {
|
|
|
+ throw new Exception("cannot read header magic");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mMaxEntries = readInt(buf, IH_MAX_ENTRIES);
|
|
|
+ mMaxBytes = readInt(buf, IH_MAX_BYTES);
|
|
|
+ mActiveRegion = readInt(buf, IH_ACTIVE_REGION);
|
|
|
+ int mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES);
|
|
|
+ mActiveBytes = readInt(buf, IH_ACTIVE_BYTES);
|
|
|
+
|
|
|
+ int sum = readInt(buf, IH_CHECKSUM);
|
|
|
+ if (checkSum(buf, 0, IH_CHECKSUM) != sum) {
|
|
|
+ throw new Exception("header checksum does not match");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sanity check
|
|
|
+ if (mMaxEntries <= 0) {
|
|
|
+ throw new Exception("invalid max entries");
|
|
|
+ }
|
|
|
+ if (mMaxBytes <= 0) {
|
|
|
+ throw new Exception("invalid max bytes");
|
|
|
+ }
|
|
|
+ if (mActiveRegion != 0 && mActiveRegion != 1) {
|
|
|
+ throw new Exception("invalid active region");
|
|
|
+ }
|
|
|
+ if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) {
|
|
|
+ throw new Exception("invalid active entries");
|
|
|
+ }
|
|
|
+ if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) {
|
|
|
+ throw new Exception("invalid active bytes");
|
|
|
+ }
|
|
|
+ if (indexFile.length() != INDEX_HEADER_SIZE + mMaxEntries * 12 * 2L) {
|
|
|
+ throw new Exception("invalid index file length");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Make sure data file has magic
|
|
|
+ byte[] magic = new byte[4];
|
|
|
+ try (InputStream data0Is = dataFile0.newInputStream()) {
|
|
|
+ if (data0Is.read(magic) != 4) {
|
|
|
+ throw new Exception("cannot read data file magic");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (readInt(magic, 0) != MAGIC_DATA_FILE) {
|
|
|
+ throw new Exception("invalid data file magic");
|
|
|
+ }
|
|
|
+ try (InputStream data1Is = dataFile1.newInputStream()) {
|
|
|
+ if (data1Is.read(magic) != 4) {
|
|
|
+ throw new Exception("cannot read data file magic");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (readInt(magic, 0) != MAGIC_DATA_FILE) {
|
|
|
+ throw new Exception("invalid data file magic");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private File createTempFile(String fileName, InputStream inputStream) throws Exception {
|
|
|
+ File tempFile = new File(context.getCacheDir(), fileName);
|
|
|
+ if (tempFile.exists()) {
|
|
|
+ tempFile.delete();
|
|
|
+ }
|
|
|
+ if (!tempFile.createNewFile()) {
|
|
|
+ throw new Exception("cannot create temp file");
|
|
|
+ }
|
|
|
+ try (OutputStream outputStream = new FileOutputStream(tempFile)) {
|
|
|
+ copyStream(inputStream, outputStream);
|
|
|
+ }
|
|
|
+ return tempFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean lookup(GalleryCacheDetector.LookupRequest req) throws IOException {
|
|
|
+ if (lookupInternal(req.key, mActiveHashStart)) {
|
|
|
+ return getBlob(mActiveDataFile, mFileOffset, req);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int mFileOffset;
|
|
|
+
|
|
|
+ private boolean lookupInternal(long key, int hashStart) {
|
|
|
+ int slot = (int) (key % mMaxEntries);
|
|
|
+ if (slot < 0) slot += mMaxEntries;
|
|
|
+ int slotBegin = slot;
|
|
|
+ while (true) {
|
|
|
+ int offset = hashStart + slot * 12;
|
|
|
+ long candidateKey = mIndexBuffer.getLong(offset);
|
|
|
+ int candidateOffset = mIndexBuffer.getInt(offset + 8);
|
|
|
+ if (candidateOffset == 0) {
|
|
|
+ return false;
|
|
|
+ } else if (candidateKey == key) {
|
|
|
+ mFileOffset = candidateOffset;
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ if (++slot >= mMaxEntries) {
|
|
|
+ slot = 0;
|
|
|
+ }
|
|
|
+ if (slot == slotBegin) {
|
|
|
+ mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean getBlob(RandomAccessFile file, int offset,
|
|
|
+ GalleryCacheDetector.LookupRequest req) throws IOException {
|
|
|
+ byte[] header = blobHeader;
|
|
|
+ long oldPosition = file.getFilePointer();
|
|
|
+ try {
|
|
|
+ file.seek(offset);
|
|
|
+ if (file.read(header) != BLOB_HEADER_SIZE) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ long blobKey = readLong(header, BH_KEY);
|
|
|
+ if (blobKey == 0) {
|
|
|
+ return false; // This entry has been cleared.
|
|
|
+ }
|
|
|
+ if (blobKey != req.key) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ int sum = readInt(header, BH_CHECKSUM);
|
|
|
+ int blobOffset = readInt(header, BH_OFFSET);
|
|
|
+ if (blobOffset != offset) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ int length = readInt(header, BH_LENGTH);
|
|
|
+ if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (req.buffer == null || req.buffer.length < length) {
|
|
|
+ req.buffer = new byte[length];
|
|
|
+ }
|
|
|
+
|
|
|
+ byte[] blob = req.buffer;
|
|
|
+ req.length = length;
|
|
|
+
|
|
|
+ if (file.read(blob, 0, length) != length) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return checkSum(blob, 0, length) == sum;
|
|
|
+ } catch (Throwable t) {
|
|
|
+ return false;
|
|
|
+ } finally {
|
|
|
+ file.seek(oldPosition);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static int readInt(byte[] buf, int offset) {
|
|
|
+ return (buf[offset] & 0xff)
|
|
|
+ | ((buf[offset + 1] & 0xff) << 8)
|
|
|
+ | ((buf[offset + 2] & 0xff) << 16)
|
|
|
+ | ((buf[offset + 3] & 0xff) << 24);
|
|
|
+ }
|
|
|
+
|
|
|
+ static long readLong(byte[] buf, int offset) {
|
|
|
+ long result = buf[offset + 7] & 0xff;
|
|
|
+ for (int i = 6; i >= 0; i--) {
|
|
|
+ result = (result << 8) | (buf[offset + i] & 0xff);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ int checkSum(byte[] data, int offset, int nbytes) {
|
|
|
+ mAdler32.reset();
|
|
|
+ mAdler32.update(data, offset, nbytes);
|
|
|
+ return (int) mAdler32.getValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void copyStream(InputStream is, OutputStream os) throws Exception {
|
|
|
+ byte[] buf = new byte[2048];
|
|
|
+ int n;
|
|
|
+ while ((n = is.read(buf)) > 0) {
|
|
|
+ os.write(buf, 0, n);
|
|
|
+ }
|
|
|
+ os.flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void closeAll() {
|
|
|
+ closeSilently(mIndexChannel);
|
|
|
+ closeSilently(mIndexFile);
|
|
|
+ closeSilently(mDataFile0);
|
|
|
+ closeSilently(mDataFile1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void closeSilently(Closeable c) {
|
|
|
+ if (c == null) return;
|
|
|
+ try {
|
|
|
+ c.close();
|
|
|
+ } catch (Throwable t) {
|
|
|
+ // do nothing
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void deleteTempFiles() {
|
|
|
+ deleteSilently(indexTemp);
|
|
|
+ deleteSilently(data0Temp);
|
|
|
+ deleteSilently(data1Temp);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void deleteSilently(File tempFile) {
|
|
|
+ if (tempFile == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ if (tempFile.exists()) {
|
|
|
+ tempFile.delete();
|
|
|
+ }
|
|
|
+ } catch (Throwable t) {
|
|
|
+ // do nothing
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static class LookupRequest {
|
|
|
+ public long key; // input: the key to find
|
|
|
+ public byte[] buffer; // input/output: the buffer to store the blob
|
|
|
+ public int length; // output: the length of the blob
|
|
|
+
|
|
|
+ public LookupRequest(long key) {
|
|
|
+ this.key = key;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class WechatCacheDetector extends Flowable<DetectFile> {
|
|
|
+ private static final String CACHE_DOMAIN = "wechat_cache_detector";
|
|
|
+ private final XFile xFile;
|
|
|
+ private final Context context;
|
|
|
+
|
|
|
+ public WechatCacheDetector(Context context, XFile xFile) {
|
|
|
+ this.context = context;
|
|
|
+ this.xFile = xFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void subscribeActual(@NonNull Subscriber<? super DetectFile> subscriber) {
|
|
|
+ long lastModified;
|
|
|
+ try {
|
|
|
+ lastModified = xFile.lastModified();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (checkDetectedCache(context, lastModified, subscriber)) {
|
|
|
+ subscriber.onComplete();
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ clearDetectedCache(context, CACHE_DOMAIN);
|
|
|
+ }
|
|
|
+
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ detectedCacheDir = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ if (!detectedCacheDir.exists()) {
|
|
|
+ detectedCacheDir.mkdirs();
|
|
|
+ }
|
|
|
+
|
|
|
+ try (InputStream inputStream = xFile.newInputStream()) {
|
|
|
+ ArrayList<Byte> imageBytes = new ArrayList<>();
|
|
|
+ byte[] buffer = new byte[2048];
|
|
|
+ int read;
|
|
|
+ while ((read = inputStream.read(buffer)) != -1) {
|
|
|
+ for (int i = 0; i < read; i++) {
|
|
|
+ byte b = buffer[i];
|
|
|
+ imageBytes.add(b);
|
|
|
+ if (imageBytes.size() < 2) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (imageBytes.size() == 2) {
|
|
|
+ if (imageBytes.get(0) != (byte) 0xFF || imageBytes.get(1) != (byte) 0xD8) {
|
|
|
+ imageBytes.remove(0);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (i == read - 1 && inputStream.available() == 0) {
|
|
|
+ if (imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF && imageBytes.get(imageBytes.size() - 1) == (byte) 0xD9) {
|
|
|
+ File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
|
|
|
+ if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_WEIXIN));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ imageBytes.clear();
|
|
|
+ } else if (imageBytes.size() >= 6) {
|
|
|
+ if (imageBytes.get(imageBytes.size() - 1) == (byte) 0xD8
|
|
|
+ && imageBytes.get(imageBytes.size() - 2) == (byte) 0xFF
|
|
|
+ && imageBytes.get(imageBytes.size() - 3) == (byte) 0xD9
|
|
|
+ && imageBytes.get(imageBytes.size() - 4) == (byte) 0xFF
|
|
|
+ ) {
|
|
|
+ imageBytes.remove(imageBytes.size() - 1);
|
|
|
+ imageBytes.remove(imageBytes.size() - 1);
|
|
|
+ File cache = new File(detectedCacheDir, UUID.randomUUID().toString());
|
|
|
+ if (cache.createNewFile() && bytes2File(imageBytes, cache)) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(context, cache), FileType.IMAGE_WEIXIN));
|
|
|
+ }
|
|
|
+ imageBytes.clear();
|
|
|
+ imageBytes.add((byte) 0xFF);
|
|
|
+ imageBytes.add((byte) 0xD8);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ subscriber.onComplete();
|
|
|
+ } catch (Exception e) {
|
|
|
+ subscriber.onError(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean checkDetectedCache(Context context, long lastModified, Subscriber<? super DetectFile> subscriber) {
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, CACHE_DOMAIN);
|
|
|
+ File targetCaches = new File(detectedCacheDir, CryptoUtils.HASH.md5(String.valueOf(lastModified)));
|
|
|
+ File[] files = targetCaches.exists() && targetCaches.isDirectory() ? targetCaches.listFiles() : null;
|
|
|
+ if (files != null && files.length > 0) {
|
|
|
+ for (File file : files) {
|
|
|
+ subscriber.onNext(new DetectFile(new XPathFile(this.context, file), FileType.IMAGE_WEIXIN));
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void clearDetectedCache(Context context, String domain) {
|
|
|
+ File detectedCacheDir = getDetectedCacheDir(context, domain);
|
|
|
+ try {
|
|
|
+ clearDir(detectedCacheDir);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean bytes2File(byte[] bytes, File file) {
|
|
|
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
|
|
|
+ fileOutputStream.write(bytes);
|
|
|
+ fileOutputStream.flush();
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean bytes2File(List<Byte> bytes, File file) {
|
|
|
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
|
|
|
+ for (Byte aByte : bytes) {
|
|
|
+ fileOutputStream.write(aByte);
|
|
|
+ }
|
|
|
+ fileOutputStream.flush();
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void clearDir(File dir) {
|
|
|
+ if (dir == null || !dir.exists()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ File[] files = dir.listFiles();
|
|
|
+ if (files == null || files.length == 0) {
|
|
|
+ dir.delete();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (File file : files) {
|
|
|
+ if (file.isDirectory()) {
|
|
|
+ clearDir(file);
|
|
|
+ } else {
|
|
|
+ file.delete();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ dir.delete();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static File getDetectedCacheDir(Context context, String domain) {
|
|
|
+ File cacheDir;
|
|
|
+ if (Objects.equals(Environment.getExternalStorageState(), Environment.MEDIA_MOUNTED)
|
|
|
+ && Environment.getExternalStorageDirectory().canWrite()) {
|
|
|
+ cacheDir = context.getExternalCacheDir();
|
|
|
+ } else {
|
|
|
+ cacheDir = context.getCacheDir();
|
|
|
+ }
|
|
|
+ File detectedCacheDir = new File(cacheDir, CryptoUtils.HASH.md5(domain));
|
|
|
+ if (!detectedCacheDir.exists()) {
|
|
|
+ detectedCacheDir.mkdirs();
|
|
|
+ }
|
|
|
+ return detectedCacheDir;
|
|
|
+ }
|
|
|
+}
|