mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-13 03:51:10 +00:00
fix: disconnect desktop download on session cancel
This commit is contained in:
1 parent
4e4a731221
commit
c02832ac59
6 files changed
+84
-7
No files matched your search
@@ -1436,9 +1436,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private fun installDesktop(): String? {
|
||||
uiHandler.post { loading.percent = -1; loading.say(getString(DESKTOP_LINES.downloading), DESKTOP_LINES.topic, readable = true) }
|
||||
val entry = com.droiddeck.launcher.runtime.DesktopCatalog.desktopEntry(this)
|
||||
val problem = com.droiddeck.launcher.runtime.DesktopCatalog.install(this, entry, { SessionState.stopRequested },
|
||||
progressFor(DESKTOP_LINES, entry.size / 1_000_000))
|
||||
return problem?.let { getString(R.string.session_desktop_install_failed, it) }
|
||||
val cancellation = com.droiddeck.launcher.core.Downloader.Cancellation { SessionState.stopRequested }
|
||||
SessionState.installCancellation = cancellation
|
||||
return try {
|
||||
val problem = com.droiddeck.launcher.runtime.DesktopCatalog.install(this, entry, cancellation,
|
||||
progressFor(DESKTOP_LINES, entry.size / 1_000_000))
|
||||
problem?.let { getString(R.string.session_desktop_install_failed, it) }
|
||||
} finally {
|
||||
if (SessionState.installCancellation === cancellation) SessionState.installCancellation = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun installProton(): String? {
|
||||
|
||||
@@ -15,6 +15,8 @@ import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class Downloader {
|
||||
private static final String TAG = "Downloader";
|
||||
@@ -24,6 +26,32 @@ public final class Downloader {
|
||||
|
||||
private Downloader() {}
|
||||
|
||||
public static final class Cancellation implements java.util.function.BooleanSupplier {
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||
private final AtomicReference<HttpURLConnection> connection = new AtomicReference<>();
|
||||
private final java.util.function.BooleanSupplier external;
|
||||
|
||||
public Cancellation() { this(null); }
|
||||
public Cancellation(java.util.function.BooleanSupplier external) { this.external = external; }
|
||||
|
||||
public void cancel() {
|
||||
cancelled.set(true);
|
||||
HttpURLConnection active = connection.get();
|
||||
if (active != null) active.disconnect();
|
||||
}
|
||||
|
||||
@Override public boolean getAsBoolean() {
|
||||
return cancelled.get() || (external != null && external.getAsBoolean());
|
||||
}
|
||||
|
||||
private void attach(HttpURLConnection active) {
|
||||
connection.set(active);
|
||||
if (getAsBoolean()) active.disconnect();
|
||||
}
|
||||
|
||||
private void detach(HttpURLConnection active) { connection.compareAndSet(active, null); }
|
||||
}
|
||||
|
||||
public static String downloadString(String url) {
|
||||
List<String> sources = DownloadSource.INSTANCE.candidates(url);
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
@@ -182,6 +210,7 @@ public final class Downloader {
|
||||
have = 0;
|
||||
}
|
||||
connection = open(url, STALL_TIMEOUT_MS);
|
||||
if (cancelled instanceof Cancellation) ((Cancellation) cancelled).attach(connection);
|
||||
if (have > 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + have + "-");
|
||||
connection.setRequestProperty("If-Range", identity.etag);
|
||||
@@ -245,7 +274,10 @@ public final class Downloader {
|
||||
Log.w(TAG, "download " + url, e);
|
||||
return Outcome.RETRY;
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
if (connection != null) {
|
||||
if (cancelled instanceof Cancellation) ((Cancellation) cancelled).detach(connection);
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,10 @@ object DesktopCatalog {
|
||||
install(context, entry, { false }, listener)
|
||||
|
||||
/** A cancel stops the download and is checked once more before anything on disk changes; extraction itself runs to the end. */
|
||||
fun install(context: Context, entry: Entry, cancelled: () -> Boolean, listener: LinuxRuntimeInstaller.ProgressListener?): String? {
|
||||
fun install(context: Context, entry: Entry, cancelled: () -> Boolean, listener: LinuxRuntimeInstaller.ProgressListener?): String? =
|
||||
install(context, entry, Downloader.Cancellation(cancelled), listener)
|
||||
|
||||
fun install(context: Context, entry: Entry, cancelled: Downloader.Cancellation, listener: LinuxRuntimeInstaller.ProgressListener?): String? {
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
if (!root.isDirectory) return context.getString(R.string.deskpkg_runtime_missing)
|
||||
if (entry.sha256.isEmpty()) return context.getString(R.string.deskpkg_no_checksum, entry.name)
|
||||
|
||||
@@ -1733,6 +1733,7 @@ class SessionService : Service() {
|
||||
fun stop(context: Context) {
|
||||
if (!SessionState.running) {
|
||||
SessionState.stopRequested = true
|
||||
SessionState.installCancellation?.cancel()
|
||||
return
|
||||
}
|
||||
context.startService(Intent(context, SessionService::class.java).setAction(ACTION_STOP))
|
||||
|
||||
@@ -36,6 +36,9 @@ object SessionState {
|
||||
@Volatile
|
||||
var installing: String? = null
|
||||
|
||||
@Volatile
|
||||
var installCancellation: com.droiddeck.launcher.core.Downloader.Cancellation? = null
|
||||
|
||||
@Volatile
|
||||
var stopRequested = false
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import java.net.Socket
|
||||
import java.nio.file.Files
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -29,6 +32,9 @@ class DownloaderResumeTest {
|
||||
@Volatile private var misbehave = false
|
||||
@Volatile private var etag: String? = "\"v1\""
|
||||
@Volatile private var corrupt = false
|
||||
@Volatile private var holdAfterFirstChunk = false
|
||||
private var bodyStarted = CountDownLatch(1)
|
||||
private var continueBody = CountDownLatch(1)
|
||||
|
||||
@Before fun setUp() {
|
||||
dir = Files.createTempDirectory("downloader-resume").toFile()
|
||||
@@ -70,8 +76,15 @@ class DownloaderResumeTest {
|
||||
val start = if (from == null) "200 OK" else "206 Partial Content"
|
||||
val extra = if (from == null) "" else "Content-Range: bytes 0-${data.size - 1}/${data.size}\r\n"
|
||||
out.write("HTTP/1.1 $start\r\n${extra}${validator}Content-Length: ${data.size}\r\nConnection: close\r\n\r\n".toByteArray())
|
||||
out.write(content)
|
||||
out.flush()
|
||||
if (holdAfterFirstChunk) {
|
||||
out.write(content, 0, 1024)
|
||||
out.flush()
|
||||
bodyStarted.countDown()
|
||||
continueBody.await(5, TimeUnit.SECONDS)
|
||||
} else {
|
||||
out.write(content)
|
||||
out.flush()
|
||||
}
|
||||
} else if (from == null) {
|
||||
out.write("HTTP/1.1 200 OK\r\n${validator}Content-Length: ${data.size}\r\nConnection: close\r\n\r\n".toByteArray())
|
||||
out.write(content, 0, cutAt)
|
||||
@@ -181,6 +194,25 @@ class DownloaderResumeTest {
|
||||
assertTrue("the transfer stopped early (kept=${target.length()})", target.length() in 1 until data.size)
|
||||
}
|
||||
|
||||
@Test fun cancellationDisconnectsAStalledTransfer() {
|
||||
val url = "http://127.0.0.1:${server.localPort}/first"
|
||||
val target = File(dir, "runtime.tar.zst")
|
||||
misbehave = true
|
||||
holdAfterFirstChunk = true
|
||||
val cancellation = Downloader.Cancellation()
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
try {
|
||||
val result = executor.submit<Boolean> { Downloader.downloadFile(url, target, true, null, cancellation, null) }
|
||||
assertTrue("server sent the first chunk", bodyStarted.await(2, TimeUnit.SECONDS))
|
||||
cancellation.cancel()
|
||||
assertFalse("cancelled download", result.get(2, TimeUnit.SECONDS))
|
||||
assertEquals(listOf<String?>(null), requests.toList())
|
||||
} finally {
|
||||
continueBody.countDown()
|
||||
executor.shutdownNow()
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun aCancelBeforeTheFirstRequestSendsNothing() {
|
||||
val url = "http://127.0.0.1:${server.localPort}/runtime.tar.zst"
|
||||
val target = File(dir, "runtime.tar.zst")
|
||||
|
||||
Reference in new issue
Block a user