From 1f243db999a7f8a0769fa5666c609ed20c7e198b Mon Sep 17 00:00:00 2001 From: Der_Googler <54764558+DerGoogler@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:28:31 +0200 Subject: [PATCH] add downloader --- .../com/dergoogler/plugin/DownloadPlugin.java | 120 ++++++++++++++++++ app/src/main/res/xml/config.xml | 4 + src/activitys/ModuleViewActivity/index.tsx | 61 ++++++++- src/locales/en.ts | 4 +- src/native/Download.ts | 50 ++++++++ src/native/Environment.ts | 2 +- src/typings/global.d.ts | 1 + web/cordova/cordova_plugins.js | 4 + web/cordova/plugins/download.js | 9 ++ 9 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/dergoogler/plugin/DownloadPlugin.java create mode 100644 src/native/Download.ts create mode 100644 web/cordova/plugins/download.js diff --git a/app/src/main/java/com/dergoogler/plugin/DownloadPlugin.java b/app/src/main/java/com/dergoogler/plugin/DownloadPlugin.java new file mode 100644 index 00000000..28fffc49 --- /dev/null +++ b/app/src/main/java/com/dergoogler/plugin/DownloadPlugin.java @@ -0,0 +1,120 @@ +package com.dergoogler.plugin; + +import android.os.Handler; +import android.os.Looper; + +import com.topjohnwu.superuser.io.SuFileOutputStream; + +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class DownloadPlugin extends CordovaPlugin { + private static final String TAG = "DownloadPlugin"; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final Handler handler = new Handler(Looper.getMainLooper()); + private CallbackContext downloadCallbackContext = null; + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + if (action.equals("start")) { + String url = args.getString(0); + String dest = args.getString(1); + this.downloadCallbackContext = callbackContext; + executor.execute(() -> downloadFile(url, dest, callbackContext)); + return true; + } + return false; + } + + private void downloadFile(String fileUrl, String fileDest, CallbackContext callbackContext) { + InputStream input = null; + OutputStream output = null; + HttpURLConnection connection = null; + try { + URL url = new URL(fileUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.connect(); + + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + String error = "Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage(); + handler.post(() -> updateError(error)); + return; + } + + int fileLength = connection.getContentLength(); + + input = new BufferedInputStream(connection.getInputStream()); + output = SuFileOutputStream.open(fileDest); + + byte[] data = new byte[4096]; + long total = 0; + int count; + while ((count = input.read(data)) != -1) { + total += count; + if (fileLength > 0) { + int progress = (int) (total * 100 / fileLength); + JSONObject progressObj = new JSONObject(); + progressObj.put("type", "downloading"); + progressObj.put("state", progress); + handler.post(() -> updateResult(progressObj)); + } + output.write(data, 0, count); + } + JSONObject progressObj = new JSONObject(); + progressObj.put("type", "finished"); + progressObj.put("state", null); + handler.post(() -> updateResult(progressObj)); + } catch (Exception e) { + handler.post(() -> updateError(e.toString())); + } finally { + try { + if (output != null) { + output.close(); + } + if (input != null) { + input.close(); + } + } catch (Exception ignored) { + } + if (connection != null) { + connection.disconnect(); + } + } + } + + private void updateError(String err) { + sendUpdate(PluginResult.Status.ERROR, err); + } + + private void updateResult(JSONObject line) { + sendUpdate(PluginResult.Status.OK, line); + } + + private void sendUpdate(PluginResult.Status status, String res) { + if (this.downloadCallbackContext != null) { + PluginResult result = new PluginResult(status, res); + result.setKeepCallback(true); + this.downloadCallbackContext.sendPluginResult(result); + } + } + + private void sendUpdate(PluginResult.Status status, JSONObject res) { + if (this.downloadCallbackContext != null) { + PluginResult result = new PluginResult(status, res); + result.setKeepCallback(true); + this.downloadCallbackContext.sendPluginResult(result); + } + } +} diff --git a/app/src/main/res/xml/config.xml b/app/src/main/res/xml/config.xml index 08196830..9901950b 100644 --- a/app/src/main/res/xml/config.xml +++ b/app/src/main/res/xml/config.xml @@ -34,4 +34,8 @@ + + + + \ No newline at end of file diff --git a/src/activitys/ModuleViewActivity/index.tsx b/src/activitys/ModuleViewActivity/index.tsx index 8aac0919..91251361 100644 --- a/src/activitys/ModuleViewActivity/index.tsx +++ b/src/activitys/ModuleViewActivity/index.tsx @@ -31,6 +31,9 @@ import { VersionsTab } from "./tabs/VersionsTab"; import { DropdownButton } from "@Components/DropdownButton"; import { useModuleInfo } from "@Hooks/useModuleInfo"; import { useFormatBytes } from "@Hooks/useFormatBytes"; +import LinearProgress from "@mui/material/LinearProgress"; +import { Download } from "@Native/Download"; +import { Environment } from "@Native/Environment"; function a11yProps(index: number) { return { @@ -132,6 +135,10 @@ const ModuleViewActivity = () => { setValue(newValue); }; + const cconfirm = useConfirm(); + + const [downloadProgress, setDownloadProgress] = React.useState(0); + return ( { )} - + + {downloadProgress !== 0 && ( + + + + + + {`${Math.round(downloadProgress)}%`} + + + )} + { children: strings("download"), disabled: !latestVersion.zipUrl, onClick: () => { - os.open(latestVersion.zipUrl, { - target: "_blank", - features: { - color: theme.palette.primary.main, - }, - }); + const lasSeg = new URL(latestVersion.zipUrl).pathname.split("/").pop(); + const dlPath = Environment.getPublicDir(Environment.DIRECTORY_DOWNLOADS) + "/" + lasSeg; + const dl = new Download(latestVersion.zipUrl, dlPath); + + dl.onChange = (obj) => { + switch (obj.type) { + case "downloading": + setDownloadProgress(obj.state); + break; + case "finished": + setDownloadProgress(0); + cconfirm({ + title: strings("download"), + description: strings("file_downloaded", { path: dlPath }), + }) + .then(() => {}) + .catch(() => {}); + + break; + } + }; + + dl.onError = (err) => { + setDownloadProgress(0); + os.toast("finsish: " + err, Toast.LENGTH_SHORT); + }; + + dl.start(); + + // os.open(latestVersion.zipUrl, { + // target: "_blank", + // features: { + // color: theme.palette.primary.main, + // }, + // }); }, }, { diff --git a/src/locales/en.ts b/src/locales/en.ts index 5f5fac10..23009c32 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -153,5 +153,7 @@ export const en = { we_hit_a_brick: "We hit a brick!", open_settings: "Open settings", open_logcat: "Open Logcat", - try_again: "Try again" + try_again: "Try again", + + file_downloaded: "File has been downloaded to {path}", }; diff --git a/src/native/Download.ts b/src/native/Download.ts new file mode 100644 index 00000000..c4aa4454 --- /dev/null +++ b/src/native/Download.ts @@ -0,0 +1,50 @@ +import { Native } from "./Native"; + +type DownloadState = { type: "downloading"; state: number } | { type: "finished"; state: null }; + +interface DownloadStart { + url: string; + dest: string; + onChange: ((s: DownloadState) => void) | undefined; + onError?: ((err: string) => void) | null; +} + +interface DownloadNative { + start(options: DownloadStart): void; +} + +class Download extends Native { + private _onError: ((err: string) => void) | null | undefined; + private _onChange: ((s: DownloadState) => void) | undefined; + private _dest: string; + private _url: string; + + public constructor(url: string, dest: string) { + super(window.__download__); + this._url = url; + this._dest = dest; + } + + public set onChange(func: DownloadStart["onChange"]) { + this._onChange = func; + } + + public set onError(func: DownloadStart["onError"]) { + this._onError = func; + } + + public start(): void { + if (this.isAndroid) { + if (typeof this._onChange !== "function") throw new TypeError("Download 'onChange' is not a function"); + + this.interface.start({ + url: this._url, + dest: this._dest, + onChange: this._onChange, + onError: this._onError, + }); + } + } +} + +export { Download }; diff --git a/src/native/Environment.ts b/src/native/Environment.ts index 4542ed9a..611e61d9 100644 --- a/src/native/Environment.ts +++ b/src/native/Environment.ts @@ -46,4 +46,4 @@ class EnvironmentClass extends Native { } } -const Environment: EnvironmentClass = new EnvironmentClass(); +export const Environment: EnvironmentClass = new EnvironmentClass(); diff --git a/src/typings/global.d.ts b/src/typings/global.d.ts index 82eb3dd1..82dd0cf3 100644 --- a/src/typings/global.d.ts +++ b/src/typings/global.d.ts @@ -95,6 +95,7 @@ declare global { readonly __terminal__: I; readonly __chooser__: I; + readonly __download__: I; } export type MMRLTheme = Theme & { diff --git a/web/cordova/cordova_plugins.js b/web/cordova/cordova_plugins.js index 11678bb2..8603460e 100644 --- a/web/cordova/cordova_plugins.js +++ b/web/cordova/cordova_plugins.js @@ -12,6 +12,10 @@ cordova.define("cordova/plugin_list", function (require, exports, module) { id: "com.dergoogler.plugin.terminal", clobbers: ["__terminal__"], }, + { + id: "com.dergoogler.plugin.download", + clobbers: ["__download__"], + }, ]; module.exports = plugins.map((plugin) => ({ file: "bundle/c-plugins.js", ...plugin })); diff --git a/web/cordova/plugins/download.js b/web/cordova/plugins/download.js new file mode 100644 index 00000000..ad548483 --- /dev/null +++ b/web/cordova/plugins/download.js @@ -0,0 +1,9 @@ +cordova.define("com.dergoogler.plugin.download", function (require, exports, module) { + const exec = require("cordova/exec"); + + module.exports = { + start: function (opt) { + exec(opt.onChange, opt.onError, "Download", "start", [opt.url, opt.dest]); + }, + }; +});