Browse Source

[New]新增文件搜索 & 文件保存相关

zhipeng 1 year ago
parent
commit
7f34826e4a

+ 5 - 2
app/src/main/AndroidManifest.xml

@@ -7,7 +7,10 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission
+        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
 
     <application
         android:name=".App"
@@ -22,7 +25,7 @@
         android:theme="@style/Theme.DataRecover"
         tools:ignore="LockedOrientationActivity"
         tools:replace="android:allowBackup"
-        tools:targetApi="31">
+        tools:targetApi="32">
 
         <activity
             android:name=".module.splash.SplashActivity"

+ 233 - 0
app/src/main/java/com/datarecovery/master/utils/FilesSearch.java

@@ -0,0 +1,233 @@
+package com.datarecovery.master.utils;
+
+import static android.content.Context.POWER_SERVICE;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.PowerManager;
+import android.text.TextUtils;
+
+import com.datarecovery.master.utils.xfile.XFile;
+import com.datarecovery.master.utils.xfile.XFileSearch;
+
+import java.io.InputStream;
+import java.util.HashSet;
+
+import atmob.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import atmob.reactivex.rxjava3.core.BackpressureStrategy;
+import atmob.reactivex.rxjava3.core.Flowable;
+import atmob.reactivex.rxjava3.core.FlowableOnSubscribe;
+
+public class FilesSearch {
+
+    public static Flowable<DocumentFile> search(Context context, int... acceptCategory) {
+        if (acceptCategory == null || acceptCategory.length == 0) {
+            return Flowable.empty();
+        }
+        HashSet<Integer> categories = new HashSet<>();
+        for (int category : acceptCategory) {
+            categories.add(category);
+        }
+        return Flowable.create((FlowableOnSubscribe<XFile>) emitter -> {
+                    try {
+                        CancellationSignal cancellationSignal = XFileSearch.searchExternalStorageAsync(context, new XFileSearch.FileFilter() {
+                            @Override
+                            public boolean acceptFile(XFile file) {
+                                return isAcceptFile(file);
+                            }
+
+                            @Override
+                            public boolean acceptDirectory(XFile file) {
+                                return false;
+                            }
+                        }, new XFileSearch.FileSearchCallback() {
+                            @Override
+                            public void onStart() {
+
+                            }
+
+                            @Override
+                            public void onEachFile(XFile file) {
+                                emitter.onNext(file);
+                            }
+
+                            @Override
+                            public void onFinish() {
+                                emitter.onComplete();
+                            }
+                        });
+                        emitter.setCancellable(cancellationSignal::cancel);
+                    } catch (Exception e) {
+                        emitter.onError(e);
+                    }
+                }, BackpressureStrategy.BUFFER)
+                .filter(xFile -> xFile.getTag() != null)
+                .filter(xFile -> {
+                    Integer tag = (Integer) xFile.getTag();
+                    return categories.contains(tag);
+                })
+                .map(xFile -> new DocumentFile(xFile, (Integer) xFile.getTag()))
+                .doOnSubscribe(subscription -> {
+                    subscription.request(Long.MAX_VALUE);
+
+                    acquireWakeLock(context);
+                })
+                .doOnCancel(() -> releaseWakeLock(context))
+                .doOnTerminate(() -> releaseWakeLock(context))
+                .subscribeOn(AndroidSchedulers.mainThread());
+    }
+
+    private static void releaseWakeLock(Context context) {
+        PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
+        PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                "DocumentSearch::search");
+        if (wakeLock.isHeld()) {
+            wakeLock.release();
+        }
+    }
+
+    private static void acquireWakeLock(Context context) {
+        PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
+        PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                "DocumentSearch::search");
+        wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/);
+    }
+
+    private static boolean isAcceptFile(XFile file) {
+        if (file == null) {
+            return false;
+        }
+        try {
+            String name = file.getName();
+            if (TextUtils.isEmpty(name)) {
+                return false;
+            }
+            if (isWordFile(name)) {
+                file.setTag(DocumentFile.WORD);
+                return true;
+            }
+            if (isExcelFile(name)) {
+                file.setTag(DocumentFile.EXCEL);
+                return true;
+            }
+            if (isPPTFile(name)) {
+                file.setTag(DocumentFile.PPT);
+                return true;
+            }
+            if (isPDFFile(name)) {
+                file.setTag(DocumentFile.PDF);
+                return true;
+            }
+            if (isVideoFile(name)) {
+                file.setTag(DocumentFile.VIDEO);
+                return true;
+            }
+            if (isAudioFile(name)) {
+                file.setTag(DocumentFile.AUDIO);
+                return true;
+            }
+        } catch (Exception ignore) {
+        }
+        return false;
+    }
+
+    private static boolean isAudioFile(String name) {
+        return name.endsWith(".mp3") || name.endsWith(".wav") || name.endsWith(".wma") ||
+                name.endsWith(".ogg") || name.endsWith(".ape") || name.endsWith(".flac") ||
+                name.endsWith(".aac") || name.endsWith(".m4a") || name.endsWith(".ac3") ||
+                name.endsWith(".mmf") || name.endsWith(".amr") || name.endsWith(".m4r") ||
+                name.endsWith(".m4b") || name.endsWith(".midi") || name.endsWith(".mp2") ||
+                name.endsWith(".mka") || name.endsWith(".mpa") || name.endsWith(".mpc") ||
+                name.endsWith(".ra") || name.endsWith(".rm") || name.endsWith(".tta") ||
+                name.endsWith(".wv") || name.endsWith(".opus");
+    }
+
+    private static boolean isVideoFile(String name) {
+        return name.endsWith(".mp4") || name.endsWith(".avi") || name.endsWith(".mov") ||
+                name.endsWith(".wmv") || name.endsWith(".flv") || name.endsWith(".mkv") ||
+                name.endsWith(".rmvb") || name.endsWith(".rm") || name.endsWith(".3gp") ||
+                name.endsWith(".mpeg") || name.endsWith(".mpg") || name.endsWith(".ts") ||
+                name.endsWith(".webm") || name.endsWith(".vob") || name.endsWith(".swf");
+    }
+
+    private static boolean isPDFFile(String name) {
+        return name.endsWith(".pdf");
+    }
+
+    private static boolean isPPTFile(String name) {
+        return name.endsWith(".ppt") || name.endsWith(".pptx") || name.endsWith(".dps");
+    }
+
+    private static boolean isExcelFile(String name) {
+        return name.endsWith(".xls") || name.endsWith(".xlsx") || name.endsWith(".et");
+    }
+
+    private static boolean isWordFile(String name) {
+        return name.endsWith(".doc") || name.endsWith(".docx") || name.endsWith(".wps");
+    }
+
+    public static class DocumentFile {
+        public static final int WORD = 1;
+        public static final int EXCEL = 2;
+        public static final int PPT = 3;
+        public static final int PDF = 4;
+        public static final int VIDEO = 5;
+        public static final int AUDIO = 6;
+        private final XFile xFile;
+        private final int category;
+        private String name;
+        private long size;
+        private Uri uri;
+        private String path;
+
+        public DocumentFile(XFile xFile, int category) {
+            this.xFile = xFile;
+            this.category = category;
+            try {
+                this.name = xFile.getName();
+            } catch (Exception ignore) {
+            }
+            try {
+                this.size = xFile.length();
+            } catch (Exception ignore) {
+            }
+            try {
+                this.uri = xFile.getUri();
+            } catch (Exception ignore) {
+            }
+            try {
+                this.path = xFile.getPath();
+            } catch (Exception ignore) {
+            }
+        }
+
+        public int getCategory() {
+            return category;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public long getSize() {
+            return size;
+        }
+
+        public Uri getUri() {
+            return uri;
+        }
+
+        public String getPath() {
+            return path;
+        }
+
+        public InputStream newInputStream() throws Exception {
+            return xFile.newInputStream();
+        }
+
+        public boolean delete() throws Exception {
+            return xFile.delete();
+        }
+    }
+}

+ 29 - 1
app/src/main/java/com/datarecovery/master/utils/ImageDeepDetector.java

@@ -1,8 +1,11 @@
 package com.datarecovery.master.utils;
 
+import static android.content.Context.POWER_SERVICE;
+
 import android.content.Context;
 import android.net.Uri;
 import android.os.CancellationSignal;
+import android.os.PowerManager;
 import android.text.TextUtils;
 
 import com.atmob.common.crypto.CryptoUtils;
@@ -28,6 +31,7 @@ import java.util.List;
 import java.util.UUID;
 import java.util.zip.Adler32;
 
+import atmob.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
 import atmob.reactivex.rxjava3.annotations.NonNull;
 import atmob.reactivex.rxjava3.core.BackpressureStrategy;
 import atmob.reactivex.rxjava3.core.Flowable;
@@ -103,7 +107,31 @@ public class ImageDeepDetector {
                         default:
                             return Flowable.empty();
                     }
-                });
+                })
+                .doOnSubscribe(subscription -> {
+                    subscription.request(Long.MAX_VALUE);
+
+                    acquireWakeLock(context);
+                })
+                .doOnCancel(() -> releaseWakeLock(context))
+                .doOnTerminate(() -> releaseWakeLock(context))
+                .subscribeOn(AndroidSchedulers.mainThread());
+    }
+
+    private static void releaseWakeLock(Context context) {
+        PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
+        PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                "ImageDeepDetector::detect");
+        if (wakeLock.isHeld()) {
+            wakeLock.release();
+        }
+    }
+
+    private static void acquireWakeLock(Context context) {
+        PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
+        PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                "ImageDeepDetector::detect");
+        wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/);
     }
 
     private static Flowable<ImageFile> detectMeizuGalleryCache(Context context, XFile xFile) {

+ 239 - 0
app/src/main/java/com/datarecovery/master/utils/MediaStoreHelper.java

@@ -0,0 +1,239 @@
+package com.datarecovery.master.utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+
+import com.atmob.common.runtime.ContextUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * 用于保存文件到公共存储空间
+ */
+public class MediaStoreHelper {
+
+    public static final int TYPE_IMAGE = 1;
+
+    public static final int TYPE_VIDEO = 2;
+
+    public static final int TYPE_AUDIO = 3;
+
+    public static final int TYPE_FILES = 4;
+
+    public static final int TYPE_DOWNLOADS = 5;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({TYPE_IMAGE, TYPE_VIDEO, TYPE_AUDIO, TYPE_FILES, TYPE_DOWNLOADS})
+    @interface MediaType {
+    }
+
+    public static void saveToSharedStorage(@MediaType int mediaType, File file, String fileName) throws Exception {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            saveToSharedStorage(mediaType, java.nio.file.Files.newInputStream(file.toPath()), fileName);
+        } else {
+            saveToSharedStorage(mediaType, new FileInputStream(file), fileName);
+        }
+    }
+
+    public static void saveToSharedStorage(@MediaType int mediaType, InputStream inputStream, String fileName) throws Exception {
+        // Add a media item that other apps don't see until the item is
+        // fully written to the media store.
+        Context applicationContext = ContextUtil.getContext().getApplicationContext();
+
+        ContentResolver resolver = applicationContext.getContentResolver();
+
+        Media targetMedia = getTargetMedia(mediaType);
+
+        // Find all media files on the primary external storage device.
+        Uri mediaCollection = targetMedia.getMediaCollection();
+
+        ContentValues mediaDetails = new ContentValues();
+        mediaDetails.put(targetMedia.displayName(), fileName);
+
+        // lock media file until it's fully written.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            mediaDetails.put(targetMedia.isPending(), 1);
+        }
+
+        Uri mediaContentUri = resolver.insert(mediaCollection, mediaDetails);
+
+        if (mediaContentUri == null) {
+            throw new IllegalStateException("Failed to create new media item.");
+        }
+
+        // "w" for write.
+        try (ParcelFileDescriptor pfd =
+                     resolver.openFileDescriptor(mediaContentUri, "w", null);
+             InputStream is = inputStream;
+             FileOutputStream fos = pfd == null ? null : new FileOutputStream(pfd.getFileDescriptor())
+        ) {
+            if (fos == null) {
+                throw new IllegalStateException("Failed to open new media item.");
+            }
+            // Write data into the pending media file.
+            byte[] buf = new byte[8192];
+            int len;
+            while ((len = is.read(buf)) > 0) {
+                fos.write(buf, 0, len);
+            }
+            fos.flush();
+        } finally {
+            // finished, release the "pending" status.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                mediaDetails.clear();
+                mediaDetails.put(targetMedia.isPending(), 0);
+                resolver.update(mediaContentUri, mediaDetails, null, null);
+            }
+        }
+    }
+
+    private static Media getTargetMedia(int mediaType) {
+        switch (mediaType) {
+            case TYPE_IMAGE:
+                return new Image();
+            case TYPE_VIDEO:
+                return new Video();
+            case TYPE_AUDIO:
+                return new Audio();
+            case TYPE_FILES:
+                return new Files();
+            case TYPE_DOWNLOADS:
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                    return new Downloads();
+                }
+                return new Files();
+            default:
+                throw new IllegalArgumentException("Unknown media type: " + mediaType);
+        }
+    }
+
+    private static class Image implements Media {
+        @Override
+        public Uri getMediaCollection() {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                return MediaStore.Images.Media
+                        .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+            } else {
+                return MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+            }
+        }
+
+        @Override
+        public String displayName() {
+            return MediaStore.Images.Media.DISPLAY_NAME;
+        }
+
+        @Override
+        public String isPending() {
+            return MediaStore.Images.Media.IS_PENDING;
+        }
+    }
+
+    private static class Video implements Media {
+
+        @Override
+        public Uri getMediaCollection() {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                return MediaStore.Video.Media
+                        .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+            } else {
+                return MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+            }
+        }
+
+        @Override
+        public String displayName() {
+            return MediaStore.Video.Media.DISPLAY_NAME;
+        }
+
+        @Override
+        public String isPending() {
+            return MediaStore.Video.Media.IS_PENDING;
+        }
+    }
+
+    private static class Audio implements Media {
+
+        @Override
+        public Uri getMediaCollection() {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                return MediaStore.Audio.Media
+                        .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+            } else {
+                return MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+            }
+        }
+
+        @Override
+        public String displayName() {
+            return MediaStore.Audio.Media.DISPLAY_NAME;
+        }
+
+        @Override
+        public String isPending() {
+            return MediaStore.Audio.Media.IS_PENDING;
+        }
+    }
+
+    private static class Files implements Media {
+
+        @Override
+        public Uri getMediaCollection() {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                return MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+            } else {
+                return MediaStore.Files.getContentUri("external");
+            }
+        }
+
+        @Override
+        public String displayName() {
+            return MediaStore.Files.FileColumns.DISPLAY_NAME;
+        }
+
+        @Override
+        public String isPending() {
+            return MediaStore.Files.FileColumns.IS_PENDING;
+        }
+    }
+
+    private static class Downloads implements Media {
+
+        @RequiresApi(api = Build.VERSION_CODES.Q)
+        @Override
+        public Uri getMediaCollection() {
+            return MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        }
+
+        @Override
+        public String displayName() {
+            return MediaStore.Downloads.DISPLAY_NAME;
+        }
+
+        @Override
+        public String isPending() {
+            return MediaStore.Downloads.IS_PENDING;
+        }
+    }
+
+    private interface Media {
+        Uri getMediaCollection();
+
+        String displayName();
+
+        String isPending();
+    }
+}

+ 1 - 1
build.gradle

@@ -4,7 +4,7 @@ buildscript {
         compileSdkVersion = 33
         applicationId = "com.datarecovery.my.master"
         minSdkVersion = 21
-        targetSdkVersion = 31
+        targetSdkVersion = 32
 
         versionCode = 100
         versionName = "1.0.0"