2014-03-14 02:57:24 +01:00
|
|
|
/*
|
|
|
|
* 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;
|
|
|
|
|
2018-12-03 22:46:43 +01:00
|
|
|
import android.annotation.TargetApi;
|
2014-03-14 02:57:24 +01:00
|
|
|
import android.content.Context;
|
Supporting Android API 23 (Android 6.0)
If we update build gradle to use ``compileSdkVersion 23``,
``org.apache.http`` package causes error. (issue #4711)
We need to use ``useLibrary 'org.apache.http.legacy'`` to solve this problem.
To use ``useLibrary``, we need to use latest gradle also.
And now, we faced another problem with ``APK Expansion`` java sources.
```
/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
137 : mCurrentNotification.setLatestEventInfo(mContext, mCurrentTitle, mCurrentText, mContentIntent); // causes error
```
So, some of APK Expansion java sources are updated by referencing commits from https://github.com/danikula/Google-Play-Expansion-File
And dropped V3CustomNotification.java which was for android 3.0, since godot supports android 14 (4.0) above officially.
Unfortunately, another problem, The 'MissingTranslation' error was occurred.
So, build.gradle is updated to use ``disable 'MissingTranslation'``
Additionally, I updated ``buildToolsVersion``, ``targetSdkVersion`` to latest version.
I tested APK Expansion funtionality on Android 6.0 (Nexus 9, Nexus 6p) and Android 4.4 (Galaxy Note 2) with Google Developer console.
2016-05-20 15:57:49 +02:00
|
|
|
import android.os.Build;
|
2014-03-14 02:57:24 +01:00
|
|
|
import android.os.Environment;
|
|
|
|
import android.os.StatFs;
|
|
|
|
import android.os.SystemClock;
|
|
|
|
import android.util.Log;
|
|
|
|
|
2019-08-27 14:30:14 +02:00
|
|
|
// -- GODOT start --
|
|
|
|
//import com.android.vending.expansion.downloader.R;
|
2019-09-03 02:31:51 +02:00
|
|
|
import org.godotengine.godot.R;
|
2019-08-27 14:30:14 +02:00
|
|
|
// -- GODOT end --
|
2018-12-03 22:46:43 +01:00
|
|
|
|
2014-03-14 02:57:24 +01:00
|
|
|
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 {
|
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
public static Random sRandom = new Random(SystemClock.uptimeMillis());
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/** Regex used to parse content-disposition headers */
|
|
|
|
private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
|
|
|
|
.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
private Helpers() {
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/*
|
2018-12-03 22:46:43 +01:00
|
|
|
* Parse the Content-Disposition HTTP Header. The format of the header is defined here:
|
2021-08-22 03:56:25 +02:00
|
|
|
* https://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
|
2018-12-03 22:46:43 +01:00
|
|
|
* content that is going to be downloaded to the file system. We only support the attachment
|
|
|
|
* type.
|
2014-03-14 02:57:24 +01:00
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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;
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2014-03-14 02:57:24 +01:00
|
|
|
* @return the root of the filesystem containing the given path
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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);
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
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;
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* @return the number of bytes available on the filesystem rooted at the given File
|
2014-03-14 02:57:24 +01:00
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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;
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2014-03-14 02:57:24 +01:00
|
|
|
* Checks whether the filename looks legitimate
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
public static boolean isFilenameValid(String filename) {
|
|
|
|
filename = filename.replaceFirst("/+", "/"); // normalize leading
|
|
|
|
// slashes
|
|
|
|
return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
|
|
|
|
|| filename.startsWith(Environment.getExternalStorageDirectory().toString());
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/*
|
2014-03-14 02:57:24 +01:00
|
|
|
* Delete the given file from device
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
/* 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);
|
|
|
|
}
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* 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.
|
|
|
|
*
|
2014-03-14 02:57:24 +01:00
|
|
|
* @param overallProgress
|
|
|
|
* @param overallTotal
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
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 "";
|
|
|
|
}
|
2019-08-27 14:30:14 +02:00
|
|
|
// -- GODOT start --
|
|
|
|
return String.format(Locale.ENGLISH, "%.2f",
|
2019-08-27 14:07:17 +02:00
|
|
|
(float) overallProgress / (1024.0f * 1024.0f))
|
|
|
|
+ "MB /" +
|
2019-08-27 14:30:14 +02:00
|
|
|
String.format(Locale.ENGLISH, "%.2f", (float) overallTotal /
|
2019-08-27 14:07:17 +02:00
|
|
|
(1024.0f * 1024.0f))
|
|
|
|
+ "MB";
|
2019-08-27 14:30:14 +02:00
|
|
|
// -- GODOT end --
|
2019-08-27 14:07:17 +02:00
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2014-03-14 02:57:24 +01:00
|
|
|
* Adds a percentile to getDownloadProgressString.
|
2018-12-03 22:46:43 +01:00
|
|
|
*
|
2014-03-14 02:57:24 +01:00
|
|
|
* @param overallProgress
|
|
|
|
* @param overallTotal
|
|
|
|
* @return
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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) + ")";
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
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) + "%";
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
public static String getSpeedString(float bytesPerMillisecond) {
|
2019-08-27 14:30:14 +02:00
|
|
|
// -- GODOT start --
|
|
|
|
return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
|
|
|
|
// -- GODOT end --
|
2019-08-27 14:07:17 +02:00
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
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()));
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* Returns the file name (without full path) for an Expansion APK file from the given context.
|
|
|
|
*
|
2014-03-14 02:57:24 +01:00
|
|
|
* @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
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
|
|
|
|
return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* Returns the filename (where the file should be saved) from info about a download
|
2014-03-14 02:57:24 +01:00
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
static public String generateSaveFileName(Context c, String fileName) {
|
|
|
|
String path = getSaveFilePath(c)
|
|
|
|
+ File.separator + fileName;
|
|
|
|
return path;
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
@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;
|
|
|
|
}
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* Helper function to ascertain the existence of a file and return true/false appropriately
|
|
|
|
*
|
2014-03-14 02:57:24 +01:00
|
|
|
* @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
|
2018-12-03 22:46:43 +01:00
|
|
|
* @param deleteFileOnMismatch if the file sizes do not match, delete the file
|
|
|
|
* @return true if it does exist, false otherwise
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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;
|
|
|
|
}
|
2018-12-03 22:46:43 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
public static final int FS_READABLE = 0;
|
|
|
|
public static final int FS_DOES_NOT_EXIST = 1;
|
|
|
|
public static final int FS_CANNOT_READ = 2;
|
2018-12-03 22:46:43 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* 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
|
2014-03-14 02:57:24 +01:00
|
|
|
* @return true if it does exist, false otherwise
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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;
|
|
|
|
}
|
2018-12-03 22:46:43 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* 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
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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;
|
|
|
|
}
|
2014-03-14 02:57:24 +01:00
|
|
|
|
2019-08-27 14:07:17 +02:00
|
|
|
/**
|
2018-12-03 22:46:43 +01:00
|
|
|
* 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.
|
|
|
|
*
|
2014-03-14 02:57:24 +01:00
|
|
|
* @param state One of the STATE_* constants from {@link IDownloaderClient}.
|
|
|
|
* @return string resource ID for the corresponding string.
|
|
|
|
*/
|
2019-08-27 14:07:17 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-14 02:57:24 +01:00
|
|
|
}
|