com.github.chenxiaolong.dualbootpatcher.FileUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.github.chenxiaolong.dualbootpatcher.FileUtils.java

Source

/*
 * Copyright (C) 2014  Andrew Gunnerson <andrewgunnerson@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.github.chenxiaolong.dualbootpatcher;

import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.v4.provider.DocumentFile;
import android.util.Log;

import com.github.chenxiaolong.dualbootpatcher.nativelib.LibMiniZip.MiniZipEntry;
import com.github.chenxiaolong.dualbootpatcher.nativelib.LibMiniZip.MiniZipInputFile;
import com.github.chenxiaolong.dualbootpatcher.pathchooser.PathChooserActivity;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
import org.apache.commons.lang3.text.translate.LookupTranslator;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class FileUtils {
    private static final String TAG = FileUtils.class.getSimpleName();

    public static final String FILE_SCHEME = "file";
    public static final String SAF_SCHEME = "content";
    public static final String SAF_AUTHORITY = "com.android.externalstorage.documents";

    private static final boolean FORCE_PATH_CHOOSER = false;
    private static final boolean FORCE_GET_CONTENT = false;

    private static final String PROC_MOUNTS = "/proc/mounts";

    // According to the getmntent(3) manpage and the glibc source code, these are the only escaped
    // values
    private static final CharSequenceTranslator UNESCAPE_MOUNT_ENTRY = new LookupTranslator(new String[][] {
            { "\\040", " " }, { "\\011", "\t" }, { "\\012", "\n" }, { "\\134", "\\" }, { "\\\\", "\\" } });

    private static boolean canHandleIntent(PackageManager pm, Intent intent) {
        List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
        return list.size() > 0;
    }

    private static boolean shouldHaveNativeSaf() {
        return !FORCE_PATH_CHOOSER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    }

    private static boolean isOxygenOS(Context context) {
        String value = SystemPropertiesProxy.get(context, "ro.build.version.ota");
        if (value == null) {
            value = SystemPropertiesProxy.get(context, "ro.build.ota.versionname");
        }
        return value != null && value.contains("OnePlus") && value.contains("Oxygen");
    }

    private static boolean isOpenDocumentBroken(Context context) {
        return FORCE_GET_CONTENT || isOxygenOS(context);
    }

    private static void setCommonNativeSafOptions(Intent intent) {
        // Avoid cloud providers since we need the C file descriptor for the file
        intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
        // Hack to show internal/external storage by default
        intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
    }

    private static void setCommonOpenOptions(Intent intent, String mimeType) {
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType(mimeType);
    }

    private static void setCommonSaveOptions(Intent intent, String mimeType, String defaultName) {
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType(mimeType);
        intent.putExtra(Intent.EXTRA_TITLE, defaultName);
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    private static Intent buildSafOpenDocumentIntent(String mimeType) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        setCommonNativeSafOptions(intent);
        setCommonOpenOptions(intent, mimeType);
        return intent;
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private static Intent buildSafOpenDocumentTreeIntent() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
        setCommonNativeSafOptions(intent);
        return intent;
    }

    private static Intent buildSafGetContentIntent(String mimeType) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        setCommonNativeSafOptions(intent);
        setCommonOpenOptions(intent, mimeType);
        return intent;
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    private static Intent buildSafCreateDocumentIntent(String mimeType, String defaultName) {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        setCommonNativeSafOptions(intent);
        setCommonSaveOptions(intent, mimeType, defaultName);
        return intent;
    }

    private static Intent buildPathChooserOpenFileIntent(Context context, String mimeType) {
        Intent intent = new Intent(context, PathChooserActivity.class);
        intent.setAction(PathChooserActivity.ACTION_OPEN_FILE);
        setCommonOpenOptions(intent, mimeType);
        return intent;
    }

    private static Intent buildPathChooserOpenDirectoryIntent(Context context) {
        Intent intent = new Intent(context, PathChooserActivity.class);
        intent.setAction(PathChooserActivity.ACTION_OPEN_DIRECTORY);
        return intent;
    }

    private static Intent buildPathChooserSaveFileIntent(Context context, String mimeType, String defaultName) {
        Intent intent = new Intent(context, PathChooserActivity.class);
        intent.setAction(PathChooserActivity.ACTION_SAVE_FILE);
        setCommonSaveOptions(intent, mimeType, defaultName);
        return intent;
    }

    @NonNull
    public static Intent getFileOpenIntent(Context context, String mimeType) {
        PackageManager pm = context.getPackageManager();
        Intent intent = null;

        if (shouldHaveNativeSaf()) {
            // Avoid ACTION_OPEN_DOCUMENT on OxygenOS since it browses zips instead of
            // selecting them
            if (isOpenDocumentBroken(context)) {
                Log.d(TAG, "Can't use ACTION_OPEN_DOCUMENT due to OxygenOS bug");
            } else {
                intent = buildSafOpenDocumentIntent(mimeType);
            }

            // Use ACTION_GET_CONTENT if this is 4.4+, but DocumentsUI is missing (wtf) or
            // ACTION_OPEN_DOCUMENT won't work for some reason
            if (intent == null || !canHandleIntent(pm, intent)) {
                Log.w(TAG, "ACTION_OPEN_DOCUMENT cannot be handled on 4.4+ device");
                intent = buildSafGetContentIntent(mimeType);
            }

            // If neither ACTION_OPEN_DOCUMENT nor ACTION_GET_CONTENT can be handled, fall back to
            // PathChooserActivity
            if (!canHandleIntent(pm, intent)) {
                Log.w(TAG, "Neither ACTION_OPEN_DOCUMENT nor ACTION_GET_CONTENT can be handled");
                intent = null;
            }
        }

        // Fall back to PathChooserActivity for all other scenarios
        if (intent == null) {
            intent = buildPathChooserOpenFileIntent(context, mimeType);
        }

        return intent;
    }

    @NonNull
    public static Intent getFileTreeOpenIntent(Context context) {
        PackageManager pm = context.getPackageManager();
        Intent intent = null;

        if (shouldHaveNativeSaf()) {
            intent = buildSafOpenDocumentTreeIntent();

            if (!canHandleIntent(pm, intent)) {
                Log.w(TAG, "ACTION_OPEN_DOCUMENT_TREE cannot be handled");
                intent = null;
            }
        }

        // Fall back to PathChooserActivity
        if (intent == null) {
            intent = buildPathChooserOpenDirectoryIntent(context);
        }

        return intent;
    }

    @NonNull
    public static Intent getFileSaveIntent(Context context, String mimeType, String defaultName) {
        Intent intent = null;

        if (shouldHaveNativeSaf()) {
            // Try ACTION_CREATE_DOCUMENT
            intent = buildSafCreateDocumentIntent(mimeType, defaultName);

            // If DocumentsUI is missing, fall back to NeuteredSaf
            if (!canHandleIntent(context.getPackageManager(), intent)) {
                Log.w(TAG, "ACTION_CREATE_DOCUMENT cannot be handled on 4.4+ device");
                intent = null;
            }
        }

        // Fall back to PathChooserActivity for all other scenarios
        if (intent == null) {
            intent = buildPathChooserSaveFileIntent(context, mimeType, defaultName);
        }

        return intent;
    }

    public static void extractAsset(Context context, String src, File dest) throws IOException {
        InputStream i = null;
        FileOutputStream o = null;
        try {
            i = context.getAssets().open(src);
            o = new FileOutputStream(dest);

            IOUtils.copy(i, o);
        } finally {
            IOUtils.closeQuietly(i);
            IOUtils.closeQuietly(o);
        }
    }

    public static void zipExtractFile(@NonNull Context context, @NonNull Uri uri, @NonNull String filename,
            @NonNull String destFile) throws IOException {
        ParcelFileDescriptor pfd = null;
        MiniZipInputFile mzif = null;
        FileOutputStream fos = null;

        try {
            pfd = context.getContentResolver().openFileDescriptor(uri, "r");
            if (pfd == null) {
                throw new IOException("Failed to open URI: " + uri);
            }

            mzif = new MiniZipInputFile("/proc/self/fd/" + pfd.getFd());

            MiniZipEntry entry;
            while ((entry = mzif.nextEntry()) != null) {
                if (entry.getName().equals(filename)) {
                    fos = new FileOutputStream(destFile);
                    IOUtils.copy(mzif.getInputStream(), fos);
                    return;
                }
            }
        } finally {
            IOUtils.closeQuietly(mzif);
            IOUtils.closeQuietly(fos);
            IOUtils.closeQuietly(pfd);
        }
    }

    private static class Base2Abbrev {
        long factor;
        int stringResId;

        public Base2Abbrev(long factor, int stringResId) {
            this.factor = factor;
            this.stringResId = stringResId;
        }
    }

    private static final Base2Abbrev[] BASE2_ABBREVS = new Base2Abbrev[] {
            new Base2Abbrev(1L << 60, R.string.format_exbibytes),
            new Base2Abbrev(1L << 50, R.string.format_pebibytes),
            new Base2Abbrev(1L << 40, R.string.format_tebibytes),
            new Base2Abbrev(1L << 30, R.string.format_gibibytes),
            new Base2Abbrev(1L << 20, R.string.format_mebibytes),
            new Base2Abbrev(1L << 10, R.string.format_kibibytes), new Base2Abbrev(1L, R.string.format_bytes) };

    @NonNull
    public static String toHumanReadableSize(Context context, long size, long precision) {
        Base2Abbrev abbrev = null;

        for (Base2Abbrev b2a : BASE2_ABBREVS) {
            if (size >= b2a.factor) {
                abbrev = b2a;
                break;
            }
        }

        if (abbrev == null) {
            return context.getString(R.string.format_bytes, size);
        }

        String decimal = String.format("%." + precision + "f", (double) size / abbrev.factor);
        return context.getString(abbrev.stringResId, decimal);
    }

    public static class UriMetadata {
        public Uri uri;
        public String displayName;
        public long size;
        public String mimeType;
    }

    public static UriMetadata[] queryUriMetadata(ContentResolver cr, Uri... uris) {
        ThreadUtils.enforceExecutionOnNonMainThread();

        UriMetadata[] metadatas = new UriMetadata[uris.length];
        for (int i = 0; i < metadatas.length; i++) {
            UriMetadata metadata = new UriMetadata();
            metadatas[i] = metadata;
            metadata.uri = uris[i];
            metadata.mimeType = cr.getType(metadata.uri);

            if (SAF_SCHEME.equals(metadata.uri.getScheme())) {
                Cursor cursor = cr.query(metadata.uri, null, null, null, null, null);
                try {
                    if (cursor != null && cursor.moveToFirst()) {
                        int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
                        int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);

                        metadata.displayName = cursor.getString(nameIndex);
                        if (cursor.isNull(sizeIndex)) {
                            metadata.size = -1;
                        } else {
                            metadata.size = cursor.getLong(sizeIndex);
                        }
                    }
                } finally {
                    IOUtils.closeQuietly(cursor);
                }
            } else if (FILE_SCHEME.equals(metadata.uri.getScheme())) {
                metadata.displayName = metadata.uri.getLastPathSegment();
                metadata.size = new File(metadata.uri.getPath()).length();
            } else {
                throw new IllegalArgumentException("Cannot handle URI: " + metadata.uri);
            }
        }

        return metadatas;
    }

    @NonNull
    public static DocumentFile getDocumentFile(@NonNull Context context, @NonNull Uri uri) {
        DocumentFile df = null;

        if (FILE_SCHEME.equals(uri.getScheme())) {
            df = DocumentFile.fromFile(new File(uri.getPath()));
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && SAF_SCHEME.equals(uri.getScheme())
                && SAF_AUTHORITY.equals(uri.getAuthority())) {
            if (DocumentsContract.isDocumentUri(context, uri)) {
                df = DocumentFile.fromSingleUri(context, uri);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && DocumentsContract.isTreeUri(uri)) {
                df = DocumentFile.fromTreeUri(context, uri);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                // Best guess is that it's a tree...
                df = DocumentFile.fromTreeUri(context, uri);
            }
        }

        if (df == null) {
            throw new IllegalArgumentException("Invalid URI: " + uri);
        }

        return df;
    }

    public static class MountEntry {
        public String mnt_fsname;
        public String mnt_dir;
        public String mnt_type;
        public String mnt_opts;
        public int mnt_freq;
        public int mnt_passno;
    }

    @NonNull
    public static MountEntry[] getMounts() throws IOException {
        ArrayList<MountEntry> entries = new ArrayList<>();
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(PROC_MOUNTS));

            for (String line; (line = br.readLine()) != null;) {
                String[] pieces = line.split("[ \t]");
                if (pieces.length != 6) {
                    throw new IOException("Illegal mount entry: " + line);
                }

                MountEntry entry = new MountEntry();
                entry.mnt_fsname = UNESCAPE_MOUNT_ENTRY.translate(pieces[0]);
                entry.mnt_dir = UNESCAPE_MOUNT_ENTRY.translate(pieces[1]);
                entry.mnt_type = UNESCAPE_MOUNT_ENTRY.translate(pieces[2]);
                entry.mnt_opts = UNESCAPE_MOUNT_ENTRY.translate(pieces[3]);
                try {
                    entry.mnt_freq = Integer.parseInt(pieces[4]);
                } catch (NumberFormatException e) {
                    throw new IOException("Illegal mnt_freq value: " + pieces[4], e);
                }
                try {
                    entry.mnt_passno = Integer.parseInt(pieces[5]);
                } catch (NumberFormatException e) {
                    throw new IOException("Illegal mnt_passno value: " + pieces[5], e);
                }

                entries.add(entry);
            }

            return entries.toArray(new MountEntry[entries.size()]);
        } finally {
            IOUtils.closeQuietly(br);
        }
    }
}