diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 341fd5356..887478f9c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,5 +95,15 @@ android:name=".session.SessionService" android:exported="false" android:stopWithTask="false" /> + + + + diff --git a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt index b02e367b8..78e97c304 100644 --- a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt @@ -32,6 +32,7 @@ import com.steamdeck.launcher.ui.DesktopAppsDialog import com.steamdeck.launcher.ui.PackageRow import com.steamdeck.launcher.session.OfflineMode import com.steamdeck.launcher.session.ProtonExtras +import com.steamdeck.launcher.session.SessionLogShare import com.steamdeck.launcher.session.SessionPrefs import com.steamdeck.launcher.ui.ProtonDialog import com.steamdeck.launcher.ui.ProtonRow @@ -240,6 +241,15 @@ class MainActivity : ComponentActivity() { SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this)) logsEnabled = SessionPrefs.logsEnabled(this) }, + onShareLogs = { + Thread({ + val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull() + ui.post { + if (zip == null) android.widget.Toast.makeText(this, "No session logs yet: run a session first.", android.widget.Toast.LENGTH_LONG).show() + else startActivity(SessionLogShare.shareIntent(this, zip)) + } + }, "share-logs").start() + }, onOffline = { OfflineMode.setEnabled(this, !OfflineMode.enabled(this)) offline = OfflineMode.enabled(this) diff --git a/app/src/main/java/com/steamdeck/launcher/session/SessionLogShare.kt b/app/src/main/java/com/steamdeck/launcher/session/SessionLogShare.kt new file mode 100644 index 000000000..4ed4fc347 --- /dev/null +++ b/app/src/main/java/com/steamdeck/launcher/session/SessionLogShare.kt @@ -0,0 +1,49 @@ +package com.steamdeck.launcher.session + +import android.content.ClipData +import android.content.Context +import android.content.Intent +import androidx.core.content.FileProvider +import com.steamdeck.launcher.runtime.LinuxRuntime +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * Zips the most recent session's log folder and hands it to Android's share sheet. Works where + * the folder could not be written to Downloads (no storage permission, or a device that refuses + * it) and landed in the app's private files instead, which a user cannot otherwise reach. + */ +object SessionLogShare { + /** The newest session folder in either place logs are written, or null if there is none. */ + fun latest(context: Context): File? = + listOf(LinuxRuntime.debugLogDir(), File(context.filesDir, "logs")) + .flatMap { dir -> dir.listFiles { f -> f.isDirectory && f.name.startsWith("session-") }?.toList() ?: emptyList() } + .maxByOrNull { it.name } + + /** Builds the zip (blocking). Returns null when there is no session to share. */ + fun zipLatest(context: Context): File? { + val folder = latest(context) ?: return null + val out = File(context.cacheDir, "shared-logs").apply { deleteRecursively(); mkdirs() } + val zip = File(out, "SteamDeck-${folder.name}.zip") + ZipOutputStream(zip.outputStream().buffered()).use { z -> + folder.walkTopDown().filter { it.isFile }.forEach { f -> + z.putNextEntry(ZipEntry(folder.name + "/" + f.relativeTo(folder).path)) + f.inputStream().use { it.copyTo(z) } + z.closeEntry() + } + } + return zip + } + + fun shareIntent(context: Context, zip: File): Intent { + val uri = FileProvider.getUriForFile(context, context.packageName + ".logs", zip) + val send = Intent(Intent.ACTION_SEND) + .setType("application/zip") + .putExtra(Intent.EXTRA_STREAM, uri) + .putExtra(Intent.EXTRA_SUBJECT, zip.nameWithoutExtension) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + send.clipData = ClipData.newRawUri(zip.name, uri) + return Intent.createChooser(send, "Share session logs").addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } +} diff --git a/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt index e7294af2b..806c56160 100644 --- a/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt @@ -150,6 +150,7 @@ class FrontEndActions( val onRoms: () -> Unit, val onFiles: () -> Unit, val onLogs: () -> Unit, + val onShareLogs: () -> Unit = {}, val onOffline: () -> Unit, val onEmulatorHelp: () -> Unit, val onCredits: () -> Unit, @@ -363,6 +364,7 @@ private fun Rail( ) { MenuItem("On", checked = s.logsEnabled) { if (!s.logsEnabled) a.onLogs(); menus.open = null } MenuItem("Off", checked = !s.logsEnabled) { if (s.logsEnabled) a.onLogs(); menus.open = null } + MenuItem("Share latest logs", checked = false) { a.onShareLogs(); menus.open = null } } } Box { diff --git a/app/src/main/res/xml/log_share_paths.xml b/app/src/main/res/xml/log_share_paths.xml new file mode 100644 index 000000000..cc98eee1e --- /dev/null +++ b/app/src/main/res/xml/log_share_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tools/aaudio-sink/module-directaudio-sink.c b/tools/aaudio-sink/module-directaudio-sink.c index eca10b307..533ff82f8 100644 --- a/tools/aaudio-sink/module-directaudio-sink.c +++ b/tools/aaudio-sink/module-directaudio-sink.c @@ -63,6 +63,10 @@ PA_MODULE_USAGE( #define STATS_EVERY_USEC (30 * PA_USEC_PER_SEC) /* Never queue more than this ahead of the relay's target, whatever it asks for. */ #define MAX_AHEAD_MS 250 +/* Always keep at least this much queued. The relay starts its target at two bursts (8 ms), which + * a producer inside proot misses while the client is loading: two underruns in the first seconds + * on the FIT, steady stutter on a slower phone. 40 ms of slack costs 40 ms of latency at most. */ +#define MIN_AHEAD_MS 40 static const char* const valid_modargs[] = { "socket", "sink_name", "sink_properties", "volume", "performance_mode", "adaptive", NULL @@ -239,6 +243,8 @@ static int service_ring(struct userdata *u) { return -1; } target = (uint32_t) (r->target_frames > 0 ? r->target_frames : 2 * u->burst); + if (target < u->ss.rate / 1000 * MIN_AHEAD_MS) + target = u->ss.rate / 1000 * MIN_AHEAD_MS; if (target > (uint32_t) (u->ss.rate / 1000 * MAX_AHEAD_MS)) target = (uint32_t) (u->ss.rate / 1000 * MAX_AHEAD_MS); avail = da_ring_avail(r);