package de.robv.android.xposed.installer.util;
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.v4.content.ContextCompat;
import android.support.v4.os.EnvironmentCompat;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.MaterialDialog.SingleButtonCallback;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.robv.android.xposed.installer.R;
import de.robv.android.xposed.installer.XposedApp;
import de.robv.android.xposed.installer.repo.Module;
import de.robv.android.xposed.installer.repo.ModuleVersion;
import de.robv.android.xposed.installer.repo.ReleaseType;
public class DownloadsUtil {
public static final String MIME_TYPE_APK = "application/vnd.android.package-archive";
public static final String MIME_TYPE_ZIP = "application/zip";
private static final Map<String, DownloadFinishedCallback> mCallbacks = new HashMap<>();
private static final XposedApp mApp = XposedApp.getInstance();
private static final SharedPreferences mPref = mApp
.getSharedPreferences("download_cache", Context.MODE_PRIVATE);
public static class Builder {
private final Context mContext;
private String mTitle = null;
private String mUrl = null;
private DownloadFinishedCallback mCallback = null;
private MIME_TYPES mMimeType = MIME_TYPES.APK;
private boolean mSave = false;
private File mDestination = null;
private boolean mModule = false;
private boolean mDialog = false;
public Builder(Context context) {
mContext = context;
}
public Builder setTitle(String title) {
mTitle = title;
return this;
}
public Builder setUrl(String url) {
mUrl = url;
return this;
}
public Builder setCallback(DownloadFinishedCallback callback) {
mCallback = callback;
return this;
}
public Builder setMimeType(MIME_TYPES mimeType) {
mMimeType = mimeType;
return this;
}
public Builder setSave(boolean save) {
mSave = save;
return this;
}
public Builder setDestination(File file) {
mDestination = file;
return this;
}
public Builder setDestinationFromUrl(String subDir) {
if (mUrl == null) {
throw new IllegalStateException("URL must be set first");
}
return setDestination(getDownloadTargetForUrl(subDir, mUrl));
}
public Builder setModule(boolean module) {
mModule = module;
return this;
}
public Builder setDialog(boolean dialog) {
mDialog = dialog;
return this;
}
public DownloadInfo download() {
return add(this);
}
}
public static String DOWNLOAD_FRAMEWORK = "framework";
public static String DOWNLOAD_MODULES = "modules";
public static File[] getDownloadDirs(String subDir) {
Context context = XposedApp.getInstance();
ArrayList<File> dirs = new ArrayList<>(2);
for (File dir : ContextCompat.getExternalCacheDirs(context)) {
if (dir != null && EnvironmentCompat.getStorageState(dir).equals(Environment.MEDIA_MOUNTED)) {
dirs.add(new File(new File(dir, "downloads"), subDir));
}
}
dirs.add(new File(new File(context.getCacheDir(), "downloads"), subDir));
return dirs.toArray(new File[dirs.size()]);
}
public static File getDownloadTarget(String subDir, String filename) {
return new File(getDownloadDirs(subDir)[0], filename);
}
public static File getDownloadTargetForUrl(String subDir, String url) {
return getDownloadTarget(subDir, Uri.parse(url).getLastPathSegment());
}
@Deprecated
public static DownloadInfo add(Context context, String title, String url, DownloadFinishedCallback callback, MIME_TYPES mimeType) {
return add(context, title, url, callback, mimeType, false, false);
}
@Deprecated
public static DownloadInfo add(Context context, String title, String url, DownloadFinishedCallback callback, MIME_TYPES mimeType, boolean save, boolean module) {
return new Builder(context)
.setTitle(title)
.setUrl(url)
.setCallback(callback)
.setMimeType(mimeType)
.setSave(save)
.setModule(module)
.download();
}
private static DownloadInfo add(Builder b) {
Context context = b.mContext;
removeAllForUrl(context, b.mUrl);
if (!b.mDialog) {
synchronized (mCallbacks) {
mCallbacks.put(b.mUrl, b.mCallback);
}
}
String savePath = "XposedInstaller";
if (b.mModule) {
savePath = XposedApp.getDownloadPath().replace(Environment.getExternalStorageDirectory() + "", "");
}
Request request = new Request(Uri.parse(b.mUrl));
request.setTitle(b.mTitle);
request.setMimeType(b.mMimeType.toString());
if (b.mDestination != null) {
b.mDestination.getParentFile().mkdirs();
removeAllForLocalFile(context, b.mDestination);
request.setDestinationUri(Uri.fromFile(b.mDestination));
} else if (b.mSave) {
try {
request.setDestinationInExternalPublicDir(savePath, b.mTitle + b.mMimeType.getExtension());
} catch (IllegalStateException e) {
Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE);
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
long id = dm.enqueue(request);
if (b.mDialog) {
showDownloadDialog(b, id);
}
return getById(context, id);
}
private static void showDownloadDialog(final Builder b, final long id) {
final Context context = b.mContext;
final DownloadDialog dialog = new DownloadDialog(new MaterialDialog.Builder(context)
.title(b.mTitle)
.content(R.string.download_view_waiting)
.progress(false, 0, true)
.progressNumberFormat(context.getString(R.string.download_progress))
.canceledOnTouchOutside(false)
.negativeText(R.string.download_view_cancel)
.onNegative(new SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
dialog.cancel();
}
})
.cancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
removeById(context, id);
}
})
);
dialog.setShowProcess(false);
dialog.show();
new Thread("DownloadDialog") {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return;
}
final DownloadInfo info = getById(context, id);
if (info == null) {
dialog.cancel();
return;
} else if (info.status == DownloadManager.STATUS_FAILED) {
dialog.cancel();
XposedApp.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context,
context.getString(R.string.download_view_failed, info.reason),
Toast.LENGTH_LONG).show();
}
});
return;
} else if (info.status == DownloadManager.STATUS_SUCCESSFUL) {
dialog.dismiss();
// Hack to reset stat information.
new File(info.localFilename).setExecutable(false);
if (b.mCallback != null) {
b.mCallback.onDownloadFinished(context, info);
}
return;
}
XposedApp.runOnUiThread(new Runnable() {
@Override
public void run() {
if (info.totalSize <= 0 || info.status != DownloadManager.STATUS_RUNNING) {
dialog.setContent(R.string.download_view_waiting);
dialog.setShowProcess(false);
} else {
dialog.setContent(R.string.download_running);
dialog.setProgress(info.bytesDownloaded / 1024);
dialog.setMaxProgress(info.totalSize / 1024);
dialog.setShowProcess(true);
}
}
});
}
}
}.start();
}
private static class DownloadDialog extends MaterialDialog {
public DownloadDialog(Builder builder) {
super(builder);
}
@UiThread
public void setShowProcess(boolean show) {
int visibility = show ? View.VISIBLE : View.GONE;
mProgress.setVisibility(visibility);
mProgressLabel.setVisibility(visibility);
mProgressMinMax.setVisibility(visibility);
}
}
public static ModuleVersion getStableVersion(Module m) {
for (int i = 0; i < m.versions.size(); i++) {
ModuleVersion mvTemp = m.versions.get(i);
if (mvTemp.relType == ReleaseType.STABLE) {
return mvTemp;
}
}
return null;
}
public static DownloadInfo getById(Context context, long id) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query().setFilterById(id));
if (!c.moveToFirst()) {
c.close();
return null;
}
int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE);
int columnLastMod = c.getColumnIndexOrThrow(
DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
int columnFilename = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME);
int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
int status = c.getInt(columnStatus);
String localFilename = c.getString(columnFilename);
if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) {
dm.remove(id);
c.close();
return null;
}
DownloadInfo info = new DownloadInfo(id, c.getString(columnUri),
c.getString(columnTitle), c.getLong(columnLastMod),
localFilename, status,
c.getInt(columnTotalSize), c.getInt(columnBytesDownloaded),
c.getInt(columnReason));
c.close();
return info;
}
public static DownloadInfo getLatestForUrl(Context context, String url) {
List<DownloadInfo> all = getAllForUrl(context, url);
return all.isEmpty() ? null : all.get(0);
}
public static List<DownloadInfo> getAllForUrl(Context context, String url) {
DownloadManager dm = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query());
int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE);
int columnLastMod = c.getColumnIndexOrThrow(
DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
int columnFilename = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME);
int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
List<DownloadInfo> downloads = new ArrayList<>();
while (c.moveToNext()) {
if (!url.equals(c.getString(columnUri)))
continue;
int status = c.getInt(columnStatus);
String localFilename = c.getString(columnFilename);
if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) {
dm.remove(c.getLong(columnId));
continue;
}
downloads.add(new DownloadInfo(c.getLong(columnId),
c.getString(columnUri), c.getString(columnTitle),
c.getLong(columnLastMod), localFilename,
status, c.getInt(columnTotalSize),
c.getInt(columnBytesDownloaded), c.getInt(columnReason)));
}
c.close();
Collections.sort(downloads);
return downloads;
}
public static void removeById(Context context, long id) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
dm.remove(id);
}
public static void removeAllForUrl(Context context, String url) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query());
int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI);
List<Long> idsList = new ArrayList<>(1);
while (c.moveToNext()) {
if (url.equals(c.getString(columnUri)))
idsList.add(c.getLong(columnId));
}
c.close();
if (idsList.isEmpty())
return;
long ids[] = new long[idsList.size()];
for (int i = 0; i < ids.length; i++)
ids[i] = idsList.get(i);
dm.remove(ids);
}
public static void removeAllForLocalFile(Context context, File file) {
file.delete();
String filename;
try {
filename = file.getCanonicalPath();
} catch (IOException e) {
Log.w(XposedApp.TAG, "Could not resolve path for " + file.getAbsolutePath(), e);
return;
}
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query());
int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
int columnFilename = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME);
List<Long> idsList = new ArrayList<>(1);
while (c.moveToNext()) {
String itemFilename = c.getString(columnFilename);
if (itemFilename != null) {
if (filename.equals(itemFilename)) {
idsList.add(c.getLong(columnId));
} else {
try {
if (filename.equals(new File(itemFilename).getCanonicalPath())) {
idsList.add(c.getLong(columnId));
}
} catch (IOException ignored) {}
}
}
}
c.close();
if (idsList.isEmpty())
return;
long ids[] = new long[idsList.size()];
for (int i = 0; i < ids.length; i++)
ids[i] = idsList.get(i);
dm.remove(ids);
}
public static void removeOutdated(Context context, long cutoff) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new Query());
int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
int columnLastMod = c.getColumnIndexOrThrow(
DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
List<Long> idsList = new ArrayList<>();
while (c.moveToNext()) {
if (c.getLong(columnLastMod) < cutoff)
idsList.add(c.getLong(columnId));
}
c.close();
if (idsList.isEmpty())
return;
long ids[] = new long[idsList.size()];
for (int i = 0; i < ids.length; i++)
ids[i] = idsList.get(0);
dm.remove(ids);
}
public static void triggerDownloadFinishedCallback(Context context, long id) {
DownloadInfo info = getById(context, id);
if (info == null || info.status != DownloadManager.STATUS_SUCCESSFUL)
return;
DownloadFinishedCallback callback;
synchronized (mCallbacks) {
callback = mCallbacks.get(info.url);
}
if (callback == null)
return;
// Hack to reset stat information.
new File(info.localFilename).setExecutable(false);
callback.onDownloadFinished(context, info);
}
public static SyncDownloadInfo downloadSynchronously(String url, File target) {
final boolean useNotModifiedTags = target.exists();
URLConnection connection = null;
InputStream in = null;
FileOutputStream out = null;
try {
connection = new URL(url).openConnection();
connection.setDoOutput(false);
connection.setConnectTimeout(30000);
connection.setReadTimeout(30000);
if (connection instanceof HttpURLConnection) {
// Disable transparent gzip encoding for gzipped files
if (url.endsWith(".gz")) {
connection.addRequestProperty("Accept-Encoding", "identity");
}
if (useNotModifiedTags) {
String modified = mPref.getString("download_" + url + "_modified", null);
String etag = mPref.getString("download_" + url + "_etag", null);
if (modified != null) {
connection.addRequestProperty("If-Modified-Since", modified);
}
if (etag != null) {
connection.addRequestProperty("If-None-Match", etag);
}
}
}
connection.connect();
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection) connection;
int responseCode = httpConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
return new SyncDownloadInfo(SyncDownloadInfo.STATUS_NOT_MODIFIED, null);
} else if (responseCode < 200 || responseCode >= 300) {
return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED,
mApp.getString(R.string.repo_download_failed_http,
url, responseCode,
httpConnection.getResponseMessage()));
}
}
in = connection.getInputStream();
out = new FileOutputStream(target);
byte buf[] = new byte[1024];
int read;
while ((read = in.read(buf)) != -1) {
out.write(buf, 0, read);
}
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection) connection;
String modified = httpConnection.getHeaderField("Last-Modified");
String etag = httpConnection.getHeaderField("ETag");
mPref.edit()
.putString("download_" + url + "_modified", modified)
.putString("download_" + url + "_etag", etag).apply();
}
return new SyncDownloadInfo(SyncDownloadInfo.STATUS_SUCCESS, null);
} catch (Throwable t) {
return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED,
mApp.getString(R.string.repo_download_failed, url,
t.getMessage()));
} finally {
if (connection != null && connection instanceof HttpURLConnection)
((HttpURLConnection) connection).disconnect();
if (in != null)
try {
in.close();
} catch (IOException ignored) {
}
if (out != null)
try {
out.close();
} catch (IOException ignored) {
}
}
}
public static void clearCache(String url) {
if (url != null) {
mPref.edit().remove("download_" + url + "_modified")
.remove("download_" + url + "_etag").apply();
} else {
mPref.edit().clear().apply();
}
}
public enum MIME_TYPES {
APK {
public String toString() {
return MIME_TYPE_APK;
}
public String getExtension() {
return ".apk";
}
},
ZIP {
public String toString() {
return MIME_TYPE_ZIP;
}
public String getExtension() {
return ".zip";
}
};
public String getExtension() {
return null;
}
}
public interface DownloadFinishedCallback {
void onDownloadFinished(Context context, DownloadInfo info);
}
public static class DownloadInfo implements Comparable<DownloadInfo> {
public final long id;
public final String url;
public final String title;
public final long lastModification;
public final String localFilename;
public final int status;
public final int totalSize;
public final int bytesDownloaded;
public final int reason;
private DownloadInfo(long id, String url, String title, long lastModification, String localFilename, int status, int totalSize, int bytesDownloaded, int reason) {
this.id = id;
this.url = url;
this.title = title;
this.lastModification = lastModification;
this.localFilename = localFilename;
this.status = status;
this.totalSize = totalSize;
this.bytesDownloaded = bytesDownloaded;
this.reason = reason;
}
@Override
public int compareTo(@NonNull DownloadInfo another) {
int compare = (int) (another.lastModification
- this.lastModification);
if (compare != 0)
return compare;
return this.url.compareTo(another.url);
}
}
public static class SyncDownloadInfo {
public static final int STATUS_SUCCESS = 0;
public static final int STATUS_NOT_MODIFIED = 1;
public static final int STATUS_FAILED = 2;
public final int status;
public final String errorMessage;
private SyncDownloadInfo(int status, String errorMessage) {
this.status = status;
this.errorMessage = errorMessage;
}
}
}