6.7.0 - Alpha19 - 修复检查更新中止下载过程后, 网络数据包依然持续后台获取的问题 (试修)
This commit is contained in:
@@ -2,11 +2,9 @@ package org.autojs.autojs.network.download;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.ProgressBar;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.afollestad.materialdialogs.DialogAction;
|
||||
@@ -17,18 +15,18 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import io.reactivex.subjects.Subject;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.autojs.autojs.app.DialogUtils;
|
||||
import org.autojs.autojs.concurrent.VolatileBox;
|
||||
import org.autojs.autojs.network.UpdateChecker;
|
||||
import org.autojs.autojs.network.api.DownloadApi;
|
||||
import org.autojs.autojs.network.entity.VersionInfo;
|
||||
import org.autojs.autojs.pio.PFiles;
|
||||
import org.autojs.autojs.tool.SimpleObserver;
|
||||
import org.autojs.autojs.util.IntentUtils;
|
||||
import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder;
|
||||
import org.autojs.autojs.util.StreamUtils;
|
||||
@@ -37,40 +35,69 @@ import org.autojs.autojs6.R;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private static DownloadManager sInstance;
|
||||
|
||||
private final int mRetryCount = 3;
|
||||
|
||||
private final Handler mHandler;
|
||||
private final DownloadApi mDownloadApi;
|
||||
private final ConcurrentHashMap<String, VolatileBox<Boolean>> mDownloadStatuses = new ConcurrentHashMap<>();
|
||||
private final OkHttpClient mOkHttpClient;
|
||||
|
||||
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() {
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mOkHttpClient = getOkHttpClient();
|
||||
|
||||
Retrofit mRetrofit = new Retrofit.Builder()
|
||||
.baseUrl(UpdateChecker.URL_BASE_GITHUB_RAW)
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(mOkHttpClient)
|
||||
.build();
|
||||
|
||||
mDownloadApi = mRetrofit.create(DownloadApi.class);
|
||||
}
|
||||
|
||||
@@ -82,6 +109,11 @@ public class DownloadManager {
|
||||
int tryCount = 0;
|
||||
while (!response.isSuccessful() && tryCount < mRetryCount) {
|
||||
tryCount++;
|
||||
try {
|
||||
response.close();
|
||||
} catch (Throwable ignored) {
|
||||
/* Ignored. */
|
||||
}
|
||||
response = chain.proceed(request);
|
||||
}
|
||||
return response;
|
||||
@@ -98,10 +130,13 @@ public class DownloadManager {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void disposeIfNeeded() {
|
||||
if (mDisposable != null) {
|
||||
mDisposable.dispose();
|
||||
}
|
||||
public void setPartialFilePolicy(@NonNull PartialFilePolicy policy) {
|
||||
mPartialFilePolicy = policy;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public PartialFilePolicy getPartialFilePolicy() {
|
||||
return mPartialFilePolicy;
|
||||
}
|
||||
|
||||
@SuppressWarnings("CharsetObjectCanBeUsed")
|
||||
@@ -165,12 +200,10 @@ public class DownloadManager {
|
||||
if (contentText != null) {
|
||||
mProgressDialog.setContent(contentText);
|
||||
}
|
||||
|
||||
if (versionInfo != null) {
|
||||
long size = versionInfo.getSize();
|
||||
DialogUtils.setProgressNumberFormatByBytes(mProgressDialog, 0, size, context.getString(R.string.text_half_ellipsis));
|
||||
}
|
||||
|
||||
DialogUtils.applyProgressThemeColorTintLists(mProgressDialog);
|
||||
}
|
||||
|
||||
@@ -182,55 +215,144 @@ public class DownloadManager {
|
||||
initProgressDialog(context, versionInfo.getDownloadUrl(), versionInfo, null);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private Observable<File> download(String url, String path) {
|
||||
|
||||
PublishSubject<File> downloadSubject = PublishSubject.create();
|
||||
|
||||
DownloadTask task = new DownloadTask(url, path);
|
||||
PublishSubject<ProgressInfo> progressSubject = task.progress();
|
||||
|
||||
mDownloadApi.download(url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(task::start, progressSubject::onError);
|
||||
|
||||
progressSubject
|
||||
.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;
|
||||
/**
|
||||
* Dismiss progress dialog safely to avoid races with async progress updates.
|
||||
* zh-CN: 安全关闭进度对话框, 以避免与异步进度更新产生竞态.
|
||||
*/
|
||||
private void dismissProgressDialogSafely() {
|
||||
try {
|
||||
MaterialDialog dialog = mProgressDialog;
|
||||
if (dialog != null && dialog.isShowing()) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
/* Ignored. */
|
||||
} finally {
|
||||
mProgressDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
VolatileBox<Boolean> status = mDownloadStatuses.get(url);
|
||||
if (status != null) {
|
||||
status.set(false);
|
||||
DownloadTask task = mActiveTasks.get(url);
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
} else {
|
||||
cancelOkHttpCallsByUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,91 +360,166 @@ public class DownloadManager {
|
||||
|
||||
private final String mUrl;
|
||||
private final String mPath;
|
||||
private final VolatileBox<Boolean> mStatus;
|
||||
|
||||
private InputStream mInputStream;
|
||||
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) {
|
||||
mUrl = url;
|
||||
mPath = path;
|
||||
mStatus = new VolatileBox<>(true);
|
||||
VolatileBox<Boolean> previous = mDownloadStatuses.put(mUrl, mStatus);
|
||||
if (previous != null) {
|
||||
previous.set(false);
|
||||
}
|
||||
mProgress = PublishSubject.create();
|
||||
mProgress = PublishSubject.<ProgressInfo>create().toSerialized();
|
||||
}
|
||||
|
||||
private void startImpl(ResponseBody body) throws Exception {
|
||||
byte[] buffer = new byte[4096];
|
||||
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 setNetworkDisposable(@Nullable Disposable disposable) {
|
||||
mNetworkDisposable = disposable;
|
||||
}
|
||||
|
||||
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);
|
||||
mHandler.post(this::activeProgressDialogButton);
|
||||
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() {
|
||||
MDButton button = mProgressDialog.getActionButton(DialogAction.POSITIVE);
|
||||
button.setTextColor(mProgressDialog.getContext().getColor(R.color.dialog_button_caution));
|
||||
MaterialDialog dialog = mProgressDialog;
|
||||
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 -> {
|
||||
mProgressDialog.dismiss();
|
||||
dismissProgressDialogSafely();
|
||||
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() {
|
||||
recycle();
|
||||
// TODO by Stardust on Dec 6, 2017.
|
||||
// ! 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() {
|
||||
// FIXME by SuperMonster003 on May 31, 2022.
|
||||
// ! 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);
|
||||
StreamUtils.closeSilently(mInputStream, mBufferedOutputStream, mFileOutputStream);
|
||||
}
|
||||
|
||||
public PublishSubject<ProgressInfo> progress() {
|
||||
return mProgress;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package org.autojs.autojs.util;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by SuperMonster003 on Jun 3, 2022.
|
||||
* Modified by SuperMonster003 as of Feb 2, 2026.
|
||||
*/
|
||||
public class StreamUtils {
|
||||
|
||||
@@ -15,7 +15,7 @@ public class StreamUtils {
|
||||
for (Closeable c : closeable) {
|
||||
try {
|
||||
c.close();
|
||||
} catch (IOException ignored) {
|
||||
} catch (Throwable ignored) {
|
||||
/* Ignored. */
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user