godot/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java

/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.vending.expansion.downloader;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.os.SystemClock;
import android.util.Log;

// -- GODOT start --
//import com.android.vending.expansion.downloader.R;
import org.godotengine.godot.R;
// -- GODOT end --

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Some helper functions for the download manager
 */
public class Helpers {

    public static Random sRandom = new Random(SystemClock.uptimeMillis());

    /** Regex used to parse content-disposition headers */
    private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
            .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");

    private Helpers() {
    }

    /*
     * Parse the Content-Disposition HTTP Header. The format of the header is defined here:
     * https://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
     * content that is going to be downloaded to the file system. We only support the attachment
     * type.
     */
    static String parseContentDisposition(String contentDisposition) {
        try {
            Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
            if (m.find()) {
                return m.group(1);
            }
        } catch (IllegalStateException ex) {
            // This function is defined as returning null when it can't parse
            // the header
        }
        return null;
    }

    /**
     * @return the root of the filesystem containing the given path
     */
    public static File getFilesystemRoot(String path) {
        File cache = Environment.getDownloadCacheDirectory();
        if (path.startsWith(cache.getPath())) {
            return cache;
        }
        File external = Environment.getExternalStorageDirectory();
        if (path.startsWith(external.getPath())) {
            return external;
        }
        throw new IllegalArgumentException(
                "Cannot determine filesystem root for " + path);
    }

    public static boolean isExternalMediaMounted() {
        if (!Environment.getExternalStorageState().equals(
                Environment.MEDIA_MOUNTED)) {
            // No SD card found.
            if (Constants.LOGVV) {
                Log.d(Constants.TAG, "no external storage");
            }
            return false;
        }
        return true;
    }

    /**
     * @return the number of bytes available on the filesystem rooted at the given File
     */
    public static long getAvailableBytes(File root) {
        StatFs stat = new StatFs(root.getPath());
        // put a bit of margin (in case creating the file grows the system by a
        // few blocks)
        long availableBlocks = (long) stat.getAvailableBlocks() - 4;
        return stat.getBlockSize() * availableBlocks;
    }

    /**
     * Checks whether the filename looks legitimate
     */
    public static boolean isFilenameValid(String filename) {
        filename = filename.replaceFirst("/+", "/"); // normalize leading
                                                     // slashes
        return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
                || filename.startsWith(Environment.getExternalStorageDirectory().toString());
    }

    /*
     * Delete the given file from device
     */
    /* package */static void deleteFile(String path) {
        try {
            File file = new File(path);
            file.delete();
        } catch (Exception e) {
            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
        }
    }

    /**
     * Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total
     * file size, but given what we know about the expected ranges of file sizes for APK expansion
     * files, it's probably not necessary.
     *
     * @param overallProgress
     * @param overallTotal
     * @return
     */

    static public String getDownloadProgressString(long overallProgress, long overallTotal) {
        if (overallTotal == 0) {
            if (Constants.LOGVV) {
                Log.e(Constants.TAG, "Notification called when total is zero");
            }
            return "";
        }
        // -- GODOT start --
        return String.format(Locale.ENGLISH, "%.2f",
                (float) overallProgress / (1024.0f * 1024.0f))
                + "MB /" +
                String.format(Locale.ENGLISH, "%.2f", (float) overallTotal /
                        (1024.0f * 1024.0f))
                + "MB";
        // -- GODOT end --
    }

    /**
     * Adds a percentile to getDownloadProgressString.
     *
     * @param overallProgress
     * @param overallTotal
     * @return
     */
    static public String getDownloadProgressStringNotification(long overallProgress,
            long overallTotal) {
        if (overallTotal == 0) {
            if (Constants.LOGVV) {
                Log.e(Constants.TAG, "Notification called when total is zero");
            }
            return "";
        }
        return getDownloadProgressString(overallProgress, overallTotal) + " (" +
                getDownloadProgressPercent(overallProgress, overallTotal) + ")";
    }

    public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
        if (overallTotal == 0) {
            if (Constants.LOGVV) {
                Log.e(Constants.TAG, "Notification called when total is zero");
            }
            return "";
        }
        return Long.toString(overallProgress * 100 / overallTotal) + "%";
    }

    public static String getSpeedString(float bytesPerMillisecond) {
        // -- GODOT start --
        return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
        // -- GODOT end --
    }

    public static String getTimeRemaining(long durationInMilliseconds) {
        SimpleDateFormat sdf;
        if (durationInMilliseconds > 1000 * 60 * 60) {
            sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
        } else {
            sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
        }
        return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
    }

    /**
     * Returns the file name (without full path) for an Expansion APK file from the given context.
     *
     * @param c the context
     * @param mainFile true for main file, false for patch file
     * @param versionCode the version of the file
     * @return String the file name of the expansion file
     */
    public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
        return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
    }

    /**
     * Returns the filename (where the file should be saved) from info about a download
     */
    static public String generateSaveFileName(Context c, String fileName) {
        String path = getSaveFilePath(c)
                + File.separator + fileName;
        return path;
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    static public String getSaveFilePath(Context c) {
        // This technically existed since Honeycomb, but it is critical
        // on KitKat and greater versions since it will create the
        // directory if needed
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return c.getObbDir().toString();
        } else {
            File root = Environment.getExternalStorageDirectory();
            String path = root.toString() + Constants.EXP_PATH + c.getPackageName();
            return path;
        }
    }

    /**
     * Helper function to ascertain the existence of a file and return true/false appropriately
     *
     * @param c the app/activity/service context
     * @param fileName the name (sans path) of the file to query
     * @param fileSize the size that the file must match
     * @param deleteFileOnMismatch if the file sizes do not match, delete the file
     * @return true if it does exist, false otherwise
     */
    static public boolean doesFileExist(Context c, String fileName, long fileSize,
            boolean deleteFileOnMismatch) {
        // the file may have been delivered by Play --- let's make sure
        // it's the size we expect
        File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
        if (fileForNewFile.exists()) {
            if (fileForNewFile.length() == fileSize) {
                return true;
            }
            if (deleteFileOnMismatch) {
                // delete the file --- we won't be able to resume
                // because we cannot confirm the integrity of the file
                fileForNewFile.delete();
            }
        }
        return false;
    }

    public static final int FS_READABLE = 0;
    public static final int FS_DOES_NOT_EXIST = 1;
    public static final int FS_CANNOT_READ = 2;

    /**
     * Helper function to ascertain whether a file can be read.
     *
     * @param c the app/activity/service context
     * @param fileName the name (sans path) of the file to query
     * @return true if it does exist, false otherwise
     */
    static public int getFileStatus(Context c, String fileName) {
        // the file may have been delivered by Play --- let's make sure
        // it's the size we expect
        File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
        int returnValue;
        if (fileForNewFile.exists()) {
            if (fileForNewFile.canRead()) {
                returnValue = FS_READABLE;
            } else {
                returnValue = FS_CANNOT_READ;
            }
        } else {
            returnValue = FS_DOES_NOT_EXIST;
        }
        return returnValue;
    }

    /**
     * Helper function to ascertain whether the application has the correct access to the OBB
     * directory to allow an OBB file to be written.
     * 
     * @param c the app/activity/service context
     * @return true if the application can write an OBB file, false otherwise
     */
    static public boolean canWriteOBBFile(Context c) {
        String path = getSaveFilePath(c);
        File fileForNewFile = new File(path);
        boolean canWrite;
        if (fileForNewFile.exists()) {
            canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite();
        } else {
            canWrite = fileForNewFile.mkdirs();
        }
        return canWrite;
    }

    /**
     * Converts download states that are returned by the
     * {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful
     * if using the state strings built into the library to display user messages.
     * 
     * @param state One of the STATE_* constants from {@link IDownloaderClient}.
     * @return string resource ID for the corresponding string.
     */
    static public int getDownloaderStringResourceIDFromState(int state) {
        switch (state) {
            case IDownloaderClient.STATE_IDLE:
                return R.string.state_idle;
            case IDownloaderClient.STATE_FETCHING_URL:
                return R.string.state_fetching_url;
            case IDownloaderClient.STATE_CONNECTING:
                return R.string.state_connecting;
            case IDownloaderClient.STATE_DOWNLOADING:
                return R.string.state_downloading;
            case IDownloaderClient.STATE_COMPLETED:
                return R.string.state_completed;
            case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
                return R.string.state_paused_network_unavailable;
            case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
                return R.string.state_paused_by_request;
            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
                return R.string.state_paused_wifi_disabled;
            case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
                return R.string.state_paused_wifi_unavailable;
            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
                return R.string.state_paused_wifi_disabled;
            case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
                return R.string.state_paused_wifi_unavailable;
            case IDownloaderClient.STATE_PAUSED_ROAMING:
                return R.string.state_paused_roaming;
            case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
                return R.string.state_paused_network_setup_failure;
            case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
                return R.string.state_paused_sdcard_unavailable;
            case IDownloaderClient.STATE_FAILED_UNLICENSED:
                return R.string.state_failed_unlicensed;
            case IDownloaderClient.STATE_FAILED_FETCHING_URL:
                return R.string.state_failed_fetching_url;
            case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
                return R.string.state_failed_sdcard_full;
            case IDownloaderClient.STATE_FAILED_CANCELED:
                return R.string.state_failed_cancelled;
            default:
                return R.string.state_unknown;
        }
    }

}