6.7.0 - Alpha19 - 修复检查更新中止下载过程后, 网络数据包依然持续后台获取的问题 (试修)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$data": {
|
"$data": {
|
||||||
"v6.7.0": {
|
"v6.7.0": {
|
||||||
"released_date": "2026/02/02",
|
"released_date": "2026/02/03",
|
||||||
"feature": [
|
"feature": [
|
||||||
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
|
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
|
||||||
"Paddle OCR (PP-OCRv5) 插件, 用于光学字符识别",
|
"Paddle OCR (PP-OCRv5) 插件, 用于光学字符识别",
|
||||||
@@ -110,6 +110,7 @@
|
|||||||
"浮动按钮 \"运行脚本\" 对话框后台操作文件时可能导致应用崩溃的问题",
|
"浮动按钮 \"运行脚本\" 对话框后台操作文件时可能导致应用崩溃的问题",
|
||||||
"主页活动页面生命周期结束后重新进入主页时, 浮动按钮状态可能被重置的问题",
|
"主页活动页面生命周期结束后重新进入主页时, 浮动按钮状态可能被重置的问题",
|
||||||
"小米设备 \"显示在其他应用上层\" 开关可能跳转到错误设置页面的问题",
|
"小米设备 \"显示在其他应用上层\" 开关可能跳转到错误设置页面的问题",
|
||||||
|
"检查更新中止下载过程后, 网络数据包依然持续后台获取的问题 (试修)",
|
||||||
"构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题"
|
"构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题"
|
||||||
],
|
],
|
||||||
"improvement": [
|
"improvement": [
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package org.autojs.autojs.network.download;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.ColorStateList;
|
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.afollestad.materialdialogs.DialogAction;
|
import com.afollestad.materialdialogs.DialogAction;
|
||||||
@@ -17,18 +15,18 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
|||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
import io.reactivex.subjects.PublishSubject;
|
||||||
|
import io.reactivex.subjects.Subject;
|
||||||
|
import okhttp3.Call;
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
import org.autojs.autojs.app.DialogUtils;
|
import org.autojs.autojs.app.DialogUtils;
|
||||||
import org.autojs.autojs.concurrent.VolatileBox;
|
|
||||||
import org.autojs.autojs.network.UpdateChecker;
|
import org.autojs.autojs.network.UpdateChecker;
|
||||||
import org.autojs.autojs.network.api.DownloadApi;
|
import org.autojs.autojs.network.api.DownloadApi;
|
||||||
import org.autojs.autojs.network.entity.VersionInfo;
|
import org.autojs.autojs.network.entity.VersionInfo;
|
||||||
import org.autojs.autojs.pio.PFiles;
|
import org.autojs.autojs.pio.PFiles;
|
||||||
import org.autojs.autojs.tool.SimpleObserver;
|
|
||||||
import org.autojs.autojs.util.IntentUtils;
|
import org.autojs.autojs.util.IntentUtils;
|
||||||
import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder;
|
import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder;
|
||||||
import org.autojs.autojs.util.StreamUtils;
|
import org.autojs.autojs.util.StreamUtils;
|
||||||
@@ -37,40 +35,69 @@ import org.autojs.autojs6.R;
|
|||||||
import retrofit2.Retrofit;
|
import retrofit2.Retrofit;
|
||||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Stardust on Oct 20, 2017.
|
* Created by Stardust on Oct 20, 2017.
|
||||||
|
* Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 2, 2026.
|
||||||
|
* Modified by OpenAI ChatGPT (GPT-5.2 Thinking) as of Feb 2, 2026.
|
||||||
|
* Modified by SuperMonster003 as of Feb 3, 2026.
|
||||||
*/
|
*/
|
||||||
public class DownloadManager {
|
public class DownloadManager {
|
||||||
|
|
||||||
private static DownloadManager sInstance;
|
private static DownloadManager sInstance;
|
||||||
|
|
||||||
private final int mRetryCount = 3;
|
private final int mRetryCount = 3;
|
||||||
|
|
||||||
private final Handler mHandler;
|
private final Handler mHandler;
|
||||||
private final DownloadApi mDownloadApi;
|
private final DownloadApi mDownloadApi;
|
||||||
private final ConcurrentHashMap<String, VolatileBox<Boolean>> mDownloadStatuses = new ConcurrentHashMap<>();
|
|
||||||
private final OkHttpClient mOkHttpClient;
|
private final OkHttpClient mOkHttpClient;
|
||||||
|
|
||||||
private MaterialDialog mProgressDialog;
|
private MaterialDialog mProgressDialog;
|
||||||
private Disposable mDisposable;
|
|
||||||
|
// Track active tasks so cancel can close streams to break blocking reads.
|
||||||
|
// zh-CN: 跟踪活动任务, 以便 cancel 时可关闭流从而打断阻塞 read().
|
||||||
|
private final ConcurrentHashMap<String, DownloadTask> mActiveTasks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partial file policy for cancellations/failures.
|
||||||
|
* zh-CN: 用于取消/失败场景的未完成文件处理策略.
|
||||||
|
*/
|
||||||
|
public enum PartialFilePolicy {
|
||||||
|
DELETE_ALWAYS,
|
||||||
|
DELETE_APK_ZIP_ONLY,
|
||||||
|
KEEP_ALWAYS
|
||||||
|
}
|
||||||
|
|
||||||
|
private volatile PartialFilePolicy mPartialFilePolicy = PartialFilePolicy.DELETE_ALWAYS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dedicated exception type for user cancellations.
|
||||||
|
* zh-CN: 专用于用户取消的异常类型.
|
||||||
|
*/
|
||||||
|
private static final class DownloadCancelledException extends CancellationException {
|
||||||
|
DownloadCancelledException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public DownloadManager() {
|
public DownloadManager() {
|
||||||
mHandler = new Handler(Looper.getMainLooper());
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
mOkHttpClient = getOkHttpClient();
|
mOkHttpClient = getOkHttpClient();
|
||||||
|
|
||||||
Retrofit mRetrofit = new Retrofit.Builder()
|
Retrofit mRetrofit = new Retrofit.Builder()
|
||||||
.baseUrl(UpdateChecker.URL_BASE_GITHUB_RAW)
|
.baseUrl(UpdateChecker.URL_BASE_GITHUB_RAW)
|
||||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||||
.client(mOkHttpClient)
|
.client(mOkHttpClient)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
mDownloadApi = mRetrofit.create(DownloadApi.class);
|
mDownloadApi = mRetrofit.create(DownloadApi.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +109,11 @@ public class DownloadManager {
|
|||||||
int tryCount = 0;
|
int tryCount = 0;
|
||||||
while (!response.isSuccessful() && tryCount < mRetryCount) {
|
while (!response.isSuccessful() && tryCount < mRetryCount) {
|
||||||
tryCount++;
|
tryCount++;
|
||||||
|
try {
|
||||||
|
response.close();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
/* Ignored. */
|
||||||
|
}
|
||||||
response = chain.proceed(request);
|
response = chain.proceed(request);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
@@ -98,10 +130,13 @@ public class DownloadManager {
|
|||||||
return sInstance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disposeIfNeeded() {
|
public void setPartialFilePolicy(@NonNull PartialFilePolicy policy) {
|
||||||
if (mDisposable != null) {
|
mPartialFilePolicy = policy;
|
||||||
mDisposable.dispose();
|
}
|
||||||
}
|
|
||||||
|
@NonNull
|
||||||
|
public PartialFilePolicy getPartialFilePolicy() {
|
||||||
|
return mPartialFilePolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("CharsetObjectCanBeUsed")
|
@SuppressWarnings("CharsetObjectCanBeUsed")
|
||||||
@@ -165,12 +200,10 @@ public class DownloadManager {
|
|||||||
if (contentText != null) {
|
if (contentText != null) {
|
||||||
mProgressDialog.setContent(contentText);
|
mProgressDialog.setContent(contentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (versionInfo != null) {
|
if (versionInfo != null) {
|
||||||
long size = versionInfo.getSize();
|
long size = versionInfo.getSize();
|
||||||
DialogUtils.setProgressNumberFormatByBytes(mProgressDialog, 0, size, context.getString(R.string.text_half_ellipsis));
|
DialogUtils.setProgressNumberFormatByBytes(mProgressDialog, 0, size, context.getString(R.string.text_half_ellipsis));
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogUtils.applyProgressThemeColorTintLists(mProgressDialog);
|
DialogUtils.applyProgressThemeColorTintLists(mProgressDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,55 +215,144 @@ public class DownloadManager {
|
|||||||
initProgressDialog(context, versionInfo.getDownloadUrl(), versionInfo, null);
|
initProgressDialog(context, versionInfo.getDownloadUrl(), versionInfo, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
/**
|
||||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
* Dismiss progress dialog safely to avoid races with async progress updates.
|
||||||
private Observable<File> download(String url, String path) {
|
* zh-CN: 安全关闭进度对话框, 以避免与异步进度更新产生竞态.
|
||||||
|
*/
|
||||||
PublishSubject<File> downloadSubject = PublishSubject.create();
|
private void dismissProgressDialogSafely() {
|
||||||
|
try {
|
||||||
DownloadTask task = new DownloadTask(url, path);
|
MaterialDialog dialog = mProgressDialog;
|
||||||
PublishSubject<ProgressInfo> progressSubject = task.progress();
|
if (dialog != null && dialog.isShowing()) {
|
||||||
|
dialog.dismiss();
|
||||||
mDownloadApi.download(url)
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
} catch (Throwable ignored) {
|
||||||
.subscribe(task::start, progressSubject::onError);
|
/* Ignored. */
|
||||||
|
} finally {
|
||||||
progressSubject
|
mProgressDialog = null;
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}
|
||||||
.doOnNext(o -> {
|
|
||||||
DialogUtils.setProgressNumberFormatByBytes(mProgressDialog, o.getReadBytes(), o.getTotalBytes());
|
|
||||||
mProgressDialog.setProgress(o.getProgress());
|
|
||||||
})
|
|
||||||
.subscribe(new SimpleObserver<>() {
|
|
||||||
@Override
|
|
||||||
public void onSubscribe(Disposable disposable) {
|
|
||||||
mDisposable = disposable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
mProgressDialog.dismiss();
|
|
||||||
mProgressDialog = null;
|
|
||||||
downloadSubject.onNext(new File(path));
|
|
||||||
downloadSubject.onComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Throwable error) {
|
|
||||||
mProgressDialog.dismiss();
|
|
||||||
mProgressDialog = null;
|
|
||||||
disposeIfNeeded();
|
|
||||||
mOkHttpClient.dispatcher().cancelAll();
|
|
||||||
downloadSubject.onError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return downloadSubject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel only OkHttp calls matching the given url instead of cancelAll().
|
||||||
|
* zh-CN: 仅取消与 url 匹配的 OkHttp 请求, 而不是使用 cancelAll().
|
||||||
|
*/
|
||||||
|
private void cancelOkHttpCallsByUrl(@NonNull String url) {
|
||||||
|
try {
|
||||||
|
for (Call call : mOkHttpClient.dispatcher().queuedCalls()) {
|
||||||
|
if (url.equals(call.request().url().toString())) {
|
||||||
|
call.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Call call : mOkHttpClient.dispatcher().runningCalls()) {
|
||||||
|
if (url.equals(call.request().url().toString())) {
|
||||||
|
call.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
/* Ignored. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldDeletePartialFile(@NonNull String path) {
|
||||||
|
PartialFilePolicy policy = getPartialFilePolicy();
|
||||||
|
if (policy == PartialFilePolicy.KEEP_ALWAYS) return false;
|
||||||
|
if (policy == PartialFilePolicy.DELETE_ALWAYS) return true;
|
||||||
|
|
||||||
|
String name = new File(path).getName().toLowerCase(Locale.US);
|
||||||
|
return name.endsWith(".apk") || name.endsWith(".zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteFileSilentlyAsync(@NonNull String path) {
|
||||||
|
Schedulers.io().scheduleDirect(() -> {
|
||||||
|
try {
|
||||||
|
File f = new File(path);
|
||||||
|
if (f.exists()) {
|
||||||
|
// noinspection ResultOfMethodCallIgnored
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
/* Ignored. */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified single-source Observable without PublishSubject<File>.
|
||||||
|
* zh-CN: 通过 Observable.create() 精简结构, 不再使用 PublishSubject<File>.
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private Observable<File> download(String url, String path) {
|
||||||
|
return Observable.create(emitter -> {
|
||||||
|
|
||||||
|
DownloadTask task = new DownloadTask(url, path);
|
||||||
|
|
||||||
|
DownloadTask previous = mActiveTasks.put(url, task);
|
||||||
|
if (previous != null) {
|
||||||
|
previous.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downstream dispose should behave like user cancellation.
|
||||||
|
// zh-CN: 下游 dispose() 应与用户取消行为一致.
|
||||||
|
emitter.setCancellable(() -> cancelDownload(url));
|
||||||
|
|
||||||
|
Disposable progressDisposable = task.progress()
|
||||||
|
.sample(50, TimeUnit.MILLISECONDS, true)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
o -> {
|
||||||
|
MaterialDialog dialog = mProgressDialog;
|
||||||
|
if (dialog == null || !dialog.isShowing()) return;
|
||||||
|
DialogUtils.setProgressNumberFormatByBytes(dialog, o.getReadBytes(), o.getTotalBytes());
|
||||||
|
dialog.setProgress(o.getProgress());
|
||||||
|
},
|
||||||
|
error -> {
|
||||||
|
mActiveTasks.remove(url);
|
||||||
|
dismissProgressDialogSafely();
|
||||||
|
|
||||||
|
if (error instanceof DownloadCancelledException || error instanceof CancellationException) {
|
||||||
|
// Cancellation is expected; cancel only the matching call.
|
||||||
|
// zh-CN: 取消属于预期行为, 仅取消匹配的请求.
|
||||||
|
cancelOkHttpCallsByUrl(url);
|
||||||
|
} else {
|
||||||
|
cancelOkHttpCallsByUrl(url);
|
||||||
|
if (shouldDeletePartialFile(path)) {
|
||||||
|
deleteFileSilentlyAsync(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emitter.isDisposed()) {
|
||||||
|
emitter.onError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
mActiveTasks.remove(url);
|
||||||
|
dismissProgressDialogSafely();
|
||||||
|
if (!emitter.isDisposed()) {
|
||||||
|
emitter.onNext(new File(path));
|
||||||
|
emitter.onComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Disposable networkDisposable = mDownloadApi.download(url)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe(task::start, task::fail);
|
||||||
|
|
||||||
|
task.setNetworkDisposable(networkDisposable);
|
||||||
|
task.setProgressDisposable(progressDisposable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel by url. Button click and downstream dispose both arrive here.
|
||||||
|
* zh-CN: 按 url 取消, 按钮点击与下游 dispose 都会走到这里.
|
||||||
|
*/
|
||||||
public void cancelDownload(String url) {
|
public void cancelDownload(String url) {
|
||||||
VolatileBox<Boolean> status = mDownloadStatuses.get(url);
|
DownloadTask task = mActiveTasks.get(url);
|
||||||
if (status != null) {
|
if (task != null) {
|
||||||
status.set(false);
|
task.cancel();
|
||||||
|
} else {
|
||||||
|
cancelOkHttpCallsByUrl(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,91 +360,166 @@ public class DownloadManager {
|
|||||||
|
|
||||||
private final String mUrl;
|
private final String mUrl;
|
||||||
private final String mPath;
|
private final String mPath;
|
||||||
private final VolatileBox<Boolean> mStatus;
|
|
||||||
private InputStream mInputStream;
|
private InputStream mInputStream;
|
||||||
private FileOutputStream mFileOutputStream;
|
private FileOutputStream mFileOutputStream;
|
||||||
private final PublishSubject<ProgressInfo> mProgress;
|
private BufferedOutputStream mBufferedOutputStream;
|
||||||
|
|
||||||
|
private final Subject<ProgressInfo> mProgress;
|
||||||
|
|
||||||
|
private Disposable mNetworkDisposable;
|
||||||
|
private Disposable mProgressDisposable;
|
||||||
|
|
||||||
|
private final AtomicBoolean mCancelled = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean mTerminated = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
// Larger IO buffer for higher throughput.
|
||||||
|
// zh-CN: 更大的 IO 缓冲区, 用于提升吞吐.
|
||||||
|
private static final int IO_BUFFER_SIZE = 256 * 1024;
|
||||||
|
|
||||||
|
// Minimum interval between progress emissions to avoid Rx/UI overhead.
|
||||||
|
// zh-CN: 进度上报的最小间隔, 用于避免 Rx/UI 开销过大.
|
||||||
|
private static final long PROGRESS_EMIT_MIN_INTERVAL_MS = 50L;
|
||||||
|
|
||||||
public DownloadTask(String url, String path) {
|
public DownloadTask(String url, String path) {
|
||||||
mUrl = url;
|
mUrl = url;
|
||||||
mPath = path;
|
mPath = path;
|
||||||
mStatus = new VolatileBox<>(true);
|
mProgress = PublishSubject.<ProgressInfo>create().toSerialized();
|
||||||
VolatileBox<Boolean> previous = mDownloadStatuses.put(mUrl, mStatus);
|
|
||||||
if (previous != null) {
|
|
||||||
previous.set(false);
|
|
||||||
}
|
|
||||||
mProgress = PublishSubject.create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startImpl(ResponseBody body) throws Exception {
|
public void setNetworkDisposable(@Nullable Disposable disposable) {
|
||||||
byte[] buffer = new byte[4096];
|
mNetworkDisposable = disposable;
|
||||||
mFileOutputStream = new FileOutputStream(mPath);
|
|
||||||
mInputStream = body.byteStream();
|
|
||||||
long total = body.contentLength();
|
|
||||||
|
|
||||||
ProgressInfo o = new ProgressInfo(total);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (!mStatus.get()) {
|
|
||||||
onCancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int len = mInputStream.read(buffer);
|
|
||||||
if (len == -1) {
|
|
||||||
mProgress.onComplete();
|
|
||||||
recycle();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
o.incrementRead(len);
|
|
||||||
mFileOutputStream.write(buffer, 0, len);
|
|
||||||
if (o.getTotalBytes() > 0) {
|
|
||||||
mProgress.onNext(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start(ResponseBody body) throws Exception {
|
public void setProgressDisposable(@Nullable Disposable disposable) {
|
||||||
|
mProgressDisposable = disposable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Observable<ProgressInfo> progress() {
|
||||||
|
return mProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fail(Throwable error) {
|
||||||
|
terminate(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(ResponseBody body) {
|
||||||
PFiles.ensureDir(mPath);
|
PFiles.ensureDir(mPath);
|
||||||
mHandler.post(this::activeProgressDialogButton);
|
mHandler.post(this::activeProgressDialogButton);
|
||||||
startImpl(body);
|
startImpl(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startImpl(ResponseBody body) {
|
||||||
|
byte[] buffer = new byte[IO_BUFFER_SIZE];
|
||||||
|
long total = body.contentLength();
|
||||||
|
ProgressInfo o = new ProgressInfo(total);
|
||||||
|
long lastEmitAt = 0L;
|
||||||
|
|
||||||
|
try (ResponseBody ignoredBody = body) {
|
||||||
|
mFileOutputStream = new FileOutputStream(mPath);
|
||||||
|
mBufferedOutputStream = new BufferedOutputStream(mFileOutputStream, IO_BUFFER_SIZE);
|
||||||
|
mInputStream = body.byteStream();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (mCancelled.get()) {
|
||||||
|
cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int len = mInputStream.read(buffer);
|
||||||
|
if (len == -1) {
|
||||||
|
mBufferedOutputStream.flush();
|
||||||
|
terminate(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
o.incrementRead(len);
|
||||||
|
mBufferedOutputStream.write(buffer, 0, len);
|
||||||
|
|
||||||
|
if (o.getTotalBytes() > 0) {
|
||||||
|
long now = android.os.SystemClock.uptimeMillis();
|
||||||
|
if (now - lastEmitAt >= PROGRESS_EMIT_MIN_INTERVAL_MS) {
|
||||||
|
lastEmitAt = now;
|
||||||
|
mProgress.onNext(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (mCancelled.get()) {
|
||||||
|
terminate(new DownloadCancelledException("Download cancelled: " + mUrl));
|
||||||
|
} else {
|
||||||
|
terminate(t);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
// Mark as cancelled first so read loop can stop ASAP.
|
||||||
|
// zh-CN: 先标记为已取消, 让 read 循环尽快停止.
|
||||||
|
if (!mCancelled.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close streams off the main thread; close() may trigger network I/O (HTTP/2 RST_STREAM, TLS write).
|
||||||
|
// zh-CN: 在非主线程关闭流, close() 可能触发网络 I/O (HTTP/2 RST_STREAM, TLS write).
|
||||||
|
final InputStream in = mInputStream;
|
||||||
|
final BufferedOutputStream bout = mBufferedOutputStream;
|
||||||
|
final FileOutputStream fos = mFileOutputStream;
|
||||||
|
Schedulers.io().scheduleDirect(() -> StreamUtils.closeSilently(in, bout, fos));
|
||||||
|
|
||||||
|
Disposable nd = mNetworkDisposable;
|
||||||
|
if (nd != null && !nd.isDisposed()) {
|
||||||
|
nd.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Disposable pd = mProgressDisposable;
|
||||||
|
if (pd != null && !pd.isDisposed()) {
|
||||||
|
pd.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelOkHttpCallsByUrl(mUrl);
|
||||||
|
|
||||||
|
onCancel();
|
||||||
|
|
||||||
|
terminate(new DownloadCancelledException("Download cancelled: " + mUrl));
|
||||||
|
}
|
||||||
|
|
||||||
private void activeProgressDialogButton() {
|
private void activeProgressDialogButton() {
|
||||||
MDButton button = mProgressDialog.getActionButton(DialogAction.POSITIVE);
|
MaterialDialog dialog = mProgressDialog;
|
||||||
button.setTextColor(mProgressDialog.getContext().getColor(R.color.dialog_button_caution));
|
if (dialog == null || !dialog.isShowing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MDButton button = dialog.getActionButton(DialogAction.POSITIVE);
|
||||||
|
button.setTextColor(dialog.getContext().getColor(R.color.dialog_button_caution));
|
||||||
button.setOnClickListener(view -> {
|
button.setOnClickListener(view -> {
|
||||||
mProgressDialog.dismiss();
|
dismissProgressDialogSafely();
|
||||||
DownloadManager.getInstance().cancelDownload(mUrl);
|
DownloadManager.getInstance().cancelDownload(mUrl);
|
||||||
ViewUtils.showToast(mProgressDialog.getContext(), R.string.text_download_cancelled);
|
ViewUtils.showToast(dialog.getContext(), R.string.text_download_cancelled);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCancel() {
|
private void onCancel() {
|
||||||
recycle();
|
|
||||||
// TODO by Stardust on Dec 6, 2017.
|
// TODO by Stardust on Dec 6, 2017.
|
||||||
// ! notify?
|
// ! notify?
|
||||||
|
|
||||||
|
if (shouldDeletePartialFile(mPath)) {
|
||||||
|
deleteFileSilentlyAsync(mPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void terminate(@Nullable Throwable error) {
|
||||||
|
if (!mTerminated.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error == null) {
|
||||||
|
mProgress.onComplete();
|
||||||
|
} else {
|
||||||
|
mProgress.onError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recycle() {
|
public void recycle() {
|
||||||
// FIXME by SuperMonster003 on May 31, 2022.
|
StreamUtils.closeSilently(mInputStream, mBufferedOutputStream, mFileOutputStream);
|
||||||
// ! Seems like none of the ways below could stop the downloading process.
|
|
||||||
// ! Even worse, progress may stuck at around 99% and suspend.
|
|
||||||
// ! zh-CN:
|
|
||||||
// ! 看起来下面几种方法均无法停止下载进程.
|
|
||||||
// ! 更糟的是, 下载进程可能在 99% 附近卡住.
|
|
||||||
// !
|
|
||||||
// # [1] disposeIfNeeded();
|
|
||||||
// # [2] getOkHttpClient().dispatcher().cancelAll();
|
|
||||||
// # [3] body.close();
|
|
||||||
|
|
||||||
mDownloadStatuses.remove(mUrl);
|
|
||||||
StreamUtils.closeSilently(mInputStream, mFileOutputStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PublishSubject<ProgressInfo> progress() {
|
|
||||||
return mProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,10 @@ package org.autojs.autojs.util;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by SuperMonster003 on Jun 3, 2022.
|
* Created by SuperMonster003 on Jun 3, 2022.
|
||||||
|
* Modified by SuperMonster003 as of Feb 2, 2026.
|
||||||
*/
|
*/
|
||||||
public class StreamUtils {
|
public class StreamUtils {
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ public class StreamUtils {
|
|||||||
for (Closeable c : closeable) {
|
for (Closeable c : closeable) {
|
||||||
try {
|
try {
|
||||||
c.close();
|
c.close();
|
||||||
} catch (IOException ignored) {
|
} catch (Throwable ignored) {
|
||||||
/* Ignored. */
|
/* Ignored. */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Mon Feb 02 21:31:36 CST 2026
|
#Tue Feb 03 01:13:39 CST 2026
|
||||||
BUILD_TIME=1770039096757
|
BUILD_TIME=1770052419058
|
||||||
COMPILE_SDK_VERSION=36
|
COMPILE_SDK_VERSION=36
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||||
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
|
|||||||
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=36
|
TARGET_SDK_VERSION=36
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3687
|
VERSION_BUILD=3688
|
||||||
VERSION_NAME=6.7.0 Alpha19
|
VERSION_NAME=6.7.0 Alpha19
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
VSCODE_EXT_REQUIRED_VERSION=1.0.13
|
||||||
|
|||||||
Reference in New Issue
Block a user