Compare commits

...
Author SHA1 Message Date
The412Banner 7d51395ec7 feat: Performance boost switch, with Samsung's performance SDK on Samsung phones
One switch on the Performance page turns on the boost for a session:
- GameThreadHints (ADPF, kmnkv1 #682) now runs on every Android 12+ phone
  when the switch is on. It stays on by default only on the Odin 3, where
  it was measured; elsewhere it is off until turned on.
- On Samsung phones the switch also holds Samsung's CPU, GPU and memory-bus
  presets through the Galaxy Performance SDK while the game presents frames
  and falls behind the panel. It lets go when the game holds pace, stops
  presenting, the phone is severely hot or battery saver is on. The presets
  are renewed every 2 s with a 5 s timeout, so Samsung's service lets go on
  its own if the app dies.

Samsung's licence does not allow passing its SDK jar on, so DroidDeck ships
without it. A Samsung owner downloads the SDK zip from Samsung and imports
it once (the picker opens when the switch is turned on without it). The jar
is converted to dex on the phone with dx (dalvik-dx 16.0.1, Apache-2.0)
and loaded with DexClassLoader.
2026-10-30 23:13:06 +00:00
10 changed files with 339 additions and 8 deletions

No files matched your search

+2
View File
@@ -347,6 +347,8 @@ dependencies {
implementation 'com.github.luben:zstd-jni:1.5.7-4@aar'
implementation 'org.apache.commons:commons-compress:1.27.1'
implementation 'org.tukaani:xz:1.10'
// dx (Apache-2.0) turns the Samsung performance SDK a user imports into dex on the phone (SamsungPerf).
implementation 'com.jakewharton.android.repackaged:dalvik-dx:16.0.1'
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
@@ -46,6 +46,7 @@ import com.droiddeck.launcher.session.GameSaves
import com.droiddeck.launcher.session.SessionLogShare
import com.droiddeck.launcher.session.SessionPaths
import com.droiddeck.launcher.session.SessionPrefs
import com.droiddeck.launcher.session.SamsungPerf
import com.droiddeck.launcher.session.SteamRepair
import com.droiddeck.launcher.core.CpuCores
import com.droiddeck.launcher.core.DownloadSource
@@ -171,6 +172,8 @@ class FrontEndActivity : ComponentActivity() {
private var zramClient by mutableStateOf(true)
private var vramCapMb by mutableStateOf(0)
private var gpuClockPin by mutableStateOf(false)
private var performanceBoost by mutableStateOf(false)
private var samsungSdk by mutableStateOf<Boolean?>(null)
private var prootNoSeccomp by mutableStateOf(false)
private var prootFastPath by mutableStateOf(true)
private var cursorComposite by mutableStateOf(false)
@@ -254,6 +257,9 @@ class FrontEndActivity : ComponentActivity() {
private val pickLossless = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let { importLossless(it) }
}
private val pickSamsungSdk = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let { importSamsungSdk(it) }
}
private val pickSaveZip = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
val then = onSavePicked.also { onSavePicked = null } ?: return@registerForActivityResult
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let(then)
@@ -395,6 +401,27 @@ class FrontEndActivity : ComponentActivity() {
}, "lossless-import").start()
}
private fun importSamsungSdk(source: File) {
Thread({
val message = getString(
when (SamsungPerf.import(this, source)) {
SamsungPerf.ImportResult.IMPORTED -> R.string.perf_samsung_sdk_imported
SamsungPerf.ImportResult.INVALID -> R.string.perf_samsung_sdk_invalid
SamsungPerf.ImportResult.FAILED -> R.string.perf_samsung_sdk_failed
},
)
val imported = SamsungPerf.imported(this)
ui.post {
samsungSdk = imported
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show()
}
}, "samsung-sdk-import").start()
}
private fun pickSamsungSdk() {
pickSamsungSdk.launch(InAppFilePicker.buildIntent(this, listOf("zip", "jar"), getString(R.string.perf_samsung_sdk_pick)))
}
private fun importSaves(name: String, game: () -> GameSaves.Game) {
if (SessionState.running) {
android.widget.Toast.makeText(this, R.string.main_saves_close_session, android.widget.Toast.LENGTH_LONG).show()
@@ -2222,6 +2249,7 @@ class FrontEndActivity : ComponentActivity() {
fsyncFirst = fsyncFirst,
gamescopeRealtime = gamescopeRealtime,
gpuClockPin = gpuClockPin,
performanceBoost = performanceBoost, samsungSdk = samsungSdk,
ramCapMb = ramCapMb, vramCapMb = vramCapMb, zramClient = zramClient,
deviceRamMb = (android.app.ActivityManager.MemoryInfo()
.also((getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager)::getMemoryInfo).totalMem / (1024 * 1024)).toInt(),
@@ -2239,6 +2267,12 @@ class FrontEndActivity : ComponentActivity() {
onFsyncFirst = { on -> SessionPrefs.setFsyncFirst(this, on); fsyncFirst = on },
onGamescopeRealtime = { on -> SessionPrefs.setGamescopeRealtime(this, on); gamescopeRealtime = on },
onGpuClockPin = { on -> SessionPrefs.setGpuClockPin(this, on); gpuClockPin = on },
onPerformanceBoost = { on ->
SessionPrefs.setPerformanceBoost(this, on)
performanceBoost = on
if (on && samsungSdk == false) pickSamsungSdk()
},
onImportSamsungSdk = { pickSamsungSdk() },
onRamCapMb = { mb -> SessionPrefs.setRamCapMb(this, mb); ramCapMb = mb },
onZramClient = { on -> SessionPrefs.setZramClient(this, on); zramClient = on },
onVramCapMb = { mb -> SessionPrefs.setVramCapMb(this, mb); vramCapMb = mb },
@@ -2455,6 +2489,8 @@ class FrontEndActivity : ComponentActivity() {
zramClient = SessionPrefs.zramClient(this)
vramCapMb = SessionPrefs.vramCapMb(this)
gpuClockPin = SessionPrefs.gpuClockPin(this)
performanceBoost = SessionPrefs.performanceBoost(this)
samsungSdk = if (SamsungPerf.offered(this)) SamsungPerf.imported(this) else null
prootNoSeccomp = SessionPrefs.prootNoSeccomp(this)
prootFastPath = SessionPrefs.prootFastPath(this)
cursorComposite = SessionPrefs.cursorComposite(this)
@@ -36,6 +36,9 @@ import kotlin.math.abs
* under 90 fps against 2-7% unboosted. A game with no frames (Factorio's benchmark, a loading screen)
* or one falling behind the panel is boosted; one holding pace keeps what it has for [PACE_LEAVE_S]
* seconds and is then let go.
*
* Part of the Performance boost switch, on by default only where it was measured ([measuredHere]).
* Another phone's HAL may boost differently, or not at all.
*/
object GameThreadHints {
private const val TAG = "GameThreadHints"
@@ -46,21 +49,23 @@ object GameThreadHints {
private const val LEAVE = 0.15 // a capped game's main thread idles to ~30% on a prime core; under 0.3 it hopped back and forth
private const val MAX_THREADS = 2 // one per prime core; a third would be sped up in place instead
private const val TOP_PROCESSES = 3
private const val PACE_WINDOW_S = 3
private const val PACE_LEAVE_S = 10
internal const val PACE_WINDOW_S = 3
internal const val PACE_LEAVE_S = 10
private const val PACE_TOLERANCE = 0.08
/** Busy, but not the game: the app itself and the Steam client, whose UI would otherwise hold the prime cores. */
private val NOT_GAMES = setOf("steam", "steamwebhelper")
/** The Odin 3 (CQ8725S), where the boost was measured. */
val measuredHere: Boolean get() = Build.VERSION.SDK_INT >= 31 && Build.SOC_MODEL == "CQ8725S"
private var worker: Thread? = null
/** At a session's start. The line is for the log. */
@Synchronized
fun start(context: Context): String {
if (!SessionPrefs.gameThreadHints(context)) return "game thread hints: off"
// Measured on the Odin 3 (CQ8725S) only; another HAL may boost differently, or not at all.
if (Build.VERSION.SDK_INT < 31 || Build.SOC_MODEL != "CQ8725S") return "game thread hints: Odin 3 only"
if (!SessionPrefs.performanceBoost(context)) return "game thread hints: off"
if (Build.VERSION.SDK_INT < 31) return "game thread hints: needs Android 12"
val hm = context.getSystemService(PerformanceHintManager::class.java)
?: return "game thread hints: no PerformanceHintManager"
val pm = context.getSystemService(PowerManager::class.java)
@@ -0,0 +1,193 @@
package com.droiddeck.launcher.session
import android.content.Context
import android.os.Build
import android.os.PowerManager
import android.util.Log
import com.android.dx.command.dexer.DxContext
import com.android.dx.command.dexer.Main
import com.droiddeck.launcher.wayland.WaylandCompositor
import dalvik.system.DexClassLoader
import java.io.ByteArrayOutputStream
import java.io.File
import java.lang.reflect.Method
import java.util.zip.ZipFile
/**
* Samsung's Galaxy Performance SDK (com.samsung.sdk.sperf): CPU, GPU and memory-bus clock floors from
* Samsung's own service. Its licence does not allow passing the jar on, so a Samsung owner downloads
* it from [DOWNLOAD_URL] and imports it; it is turned into dex on the phone and loaded from there.
*
* The presets are renewed every [RENEW_S] seconds with a [HOLD_MS] timeout, so Samsung's service
* lets go on its own if the app dies.
*/
object SamsungPerf {
enum class ImportResult { IMPORTED, INVALID, FAILED }
private const val TAG = "SamsungPerf"
const val DOWNLOAD_URL = "https://developer.samsung.com/galaxy-performance/download.html"
private const val SPERF = "com.samsung.sdk.sperf"
private val REQUIRED = listOf("SPerf", "PerformanceManager").map { "${SPERF.replace('.', '/')}/$it.class" }
private const val PRESETS = 64 or 128 or 256 // PerformanceManager.PRESET_TYPE_CPU | _GPU | _BUS
private const val HOLD_MS = 5000
private const val RENEW_S = 2
val samsung: Boolean get() = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
fun offered(context: Context) = samsung || SessionPrefs.samsungPerfAnyPhone(context)
fun imported(context: Context) = dexFile(context).isFile
private fun dir(context: Context) = File(context.filesDir, "samsung-perf")
private fun dexFile(context: Context) = File(dir(context), "perfsdk.dex.jar")
@Synchronized
fun import(context: Context, source: File): ImportResult {
val dir = dir(context).apply { mkdirs() }
val jar = File(dir, "perfsdk.jar.staged")
val dex = File(dir, "perfsdk.dex.jar.staged")
try {
if (!extractJar(source, jar)) return ImportResult.INVALID
val log = ByteArrayOutputStream()
val dx = DxContext(log, log)
val args = Main.Arguments(dx).apply {
fileNames = arrayOf(jar.path)
outName = dex.path
jarOutput = true
minSdkVersion = 26
makeOptionsObjects()
}
val status = Main(dx).runDx(args)
if (status != 0 || !dex.isFile) {
Log.w(TAG, "dx exit $status: $log")
return ImportResult.FAILED
}
val target = dexFile(context)
target.setWritable(true)
target.delete()
if (!dex.renameTo(target)) return ImportResult.FAILED
target.setReadOnly()
sdk = null
unusable = false
Log.i(TAG, "SDK from $source ready (${target.length()} bytes of dex)")
return ImportResult.IMPORTED
} catch (t: Throwable) {
Log.w(TAG, "could not import $source", t)
return ImportResult.FAILED
} finally {
jar.delete()
dex.delete()
}
}
/** Samsung's zip also holds the sample app's different perfsdk.jar; Libs/ is the SDK's own. */
internal fun extractJar(source: File, target: File): Boolean = try {
ZipFile(source).use { zip -> extractJar(zip, source, target) }
} catch (_: java.util.zip.ZipException) {
false
}
private fun extractJar(zip: ZipFile, source: File, target: File): Boolean {
if (REQUIRED.all { zip.getEntry(it) != null }) {
source.copyTo(target, overwrite = true)
return true
}
val jars = zip.entries().asSequence()
.filter { !it.isDirectory && it.name.endsWith(".jar", ignoreCase = true) }
.sortedByDescending { it.name.contains("Libs/") }
.toList()
for (entry in jars) {
zip.getInputStream(entry).use { input -> target.outputStream().use { input.copyTo(it) } }
val found = runCatching { ZipFile(target).use { inner -> REQUIRED.all { inner.getEntry(it) != null } } }.getOrDefault(false)
if (found) return true
}
return false
}
private class Sdk(val manager: Any, val start: Method, val stop: Method)
private var sdk: Sdk? = null
private var unusable = false
/** SPerf.initialize is false where Samsung's service is missing. */
@Synchronized
private fun load(context: Context): Sdk? {
sdk?.let { return it }
if (unusable || !imported(context)) return null
try {
val loader = DexClassLoader(dexFile(context).path, context.codeCacheDir.path, null, context.classLoader)
val sperf = loader.loadClass("$SPERF.SPerf")
runCatching { sperf.getMethod("setDebugModeEnabled", Boolean::class.javaPrimitiveType).invoke(null, false) }
if (sperf.getMethod("initialize", Context::class.java).invoke(null, context.applicationContext) != true) {
Log.i(TAG, "SDK loaded, but this phone does not support it")
unusable = true
return null
}
val pm = loader.loadClass("$SPERF.PerformanceManager")
val manager = pm.getMethod("getInstance").invoke(null) ?: return null
val int = Int::class.javaPrimitiveType
return Sdk(manager, pm.getMethod("start", int, int), pm.getMethod("stop")).also { sdk = it }
} catch (t: Throwable) {
Log.w(TAG, "SDK not usable", t)
unusable = true
return null
}
}
private var worker: Thread? = null
/** At a session's start. The line is for the log. */
@Synchronized
fun start(context: Context): String {
if (!offered(context)) return "samsung boost: not a Samsung phone"
if (!SessionPrefs.performanceBoost(context)) return "samsung boost: off"
if (!imported(context)) return "samsung boost: SDK not imported"
val app = context.applicationContext
worker?.interrupt()
worker = Thread({ run(app) }, "samsung-boost").apply { isDaemon = true; start() }
return "samsung boost: watching"
}
/** At a session's end. */
@Synchronized
fun stop() {
worker?.interrupt()
worker = null
}
private fun run(context: Context) {
val sdk = load(context) ?: return
val pm = context.getSystemService(PowerManager::class.java)
val frames = ArrayDeque<Long>()
var pacedFor = 0
var boosting = false
var tick = 0
try {
while (!Thread.currentThread().isInterrupted) {
frames.addLast(WaylandCompositor.gameFrameCount())
if (frames.size > GameThreadHints.PACE_WINDOW_S + 1) frames.removeFirst()
val fps = (frames.last() - frames.first()).toDouble() / (frames.size - 1).coerceAtLeast(1)
val paced = frames.size > GameThreadHints.PACE_WINDOW_S && GameThreadHints.holdsPace(fps, SessionState.refreshHz.toDouble())
pacedFor = if (paced) pacedFor + 1 else 0
val hot = Build.VERSION.SDK_INT >= 29 && pm.currentThermalStatus >= PowerManager.THERMAL_STATUS_SEVERE
val want = fps >= 1 && !pm.isPowerSaveMode && !hot && pacedFor < GameThreadHints.PACE_LEAVE_S
if (want && (!boosting || tick % RENEW_S == 0)) {
val result = sdk.start.invoke(sdk.manager, PRESETS, HOLD_MS)
if (!boosting) Log.i(TAG, "boosting (game ${fps.toInt()} fps, start $result)")
} else if (!want && boosting) {
sdk.stop.invoke(sdk.manager)
Log.i(TAG, "let go (game ${fps.toInt()} fps${if (hot) ", hot" else ""}${if (paced) ", holding pace" else ""})")
}
boosting = want
tick++
Thread.sleep(1000)
}
} catch (_: InterruptedException) {
} catch (t: Throwable) {
Log.w(TAG, "stopped", t)
} finally {
if (boosting) runCatching { sdk.stop.invoke(sdk.manager) }
}
}
}
@@ -615,8 +615,18 @@ object SessionPrefs {
prefs(context).edit().putBoolean("gpuClockPin", on).apply()
}
/** Whether the game's busiest threads are named to the power HAL for a boost (GameThreadHints). */
fun gameThreadHints(context: Context): Boolean = prefs(context).getBoolean("gameThreadHints", true)
/**
* The performance boost: the game's busiest threads named to the power HAL (GameThreadHints) and,
* on a Samsung phone with Samsung's SDK imported, its clock floors (SamsungPerf).
*/
fun performanceBoost(context: Context): Boolean = prefs(context).getBoolean("performanceBoost", GameThreadHints.measuredHere)
fun setPerformanceBoost(context: Context, on: Boolean) {
prefs(context).edit().putBoolean("performanceBoost", on).apply()
}
/** Hidden: offers the Samsung SDK import on any phone, to test the import where there is no Samsung. */
fun samsungPerfAnyPhone(context: Context): Boolean = prefs(context).getBoolean("samsungPerfAnyPhone", false)
fun prootNoSeccomp(context: Context): Boolean = prefs(context).getBoolean("prootNoSeccomp", false)
@@ -499,6 +499,7 @@ class SessionService : Service() {
Log.i(TAG, GpuClockPin.start(this))
Log.i(TAG, GameThreadHints.start(this))
Log.i(TAG, SamsungPerf.start(this))
val size = SessionState.outputSize
val guest = ArrayList<String>()
@@ -1811,6 +1812,7 @@ class SessionService : Service() {
SessionState.stopRequested = false
GpuClockPin.stop(this)
GameThreadHints.stop()
SamsungPerf.stop()
suspendOperationPending = false
freezePending = false
launchWatcher?.stopWatching()
@@ -1891,6 +1893,7 @@ class SessionService : Service() {
override fun onDestroy() {
stopSession(0)
GameThreadHints.stop() // also when the session never got as far as running
SamsungPerf.stop()
if (screenReceiverRegistered) {
unregisterReceiver(screenReceiver)
screenReceiverRegistered = false
@@ -19,7 +19,7 @@ object SettingsReset {
"gamepadLeft", "gamepadRight", "pipAutoEnter", "unfoldedControls", "unfoldedControlsWithController",
"stretch16x9", "picturePosition", "railLabels",
"gpuDriverMode", "androidDriver", "linuxDriver", "linuxDriver.steam", "linuxDriver.desktop",
"tuSysmem", "glThread", "noGlError", "zinkLazy", "mangoapp", "gpuClockPin", "gamescopeRealtime", "ramCapMb", "vramCapMb", "zramClient",
"tuSysmem", "glThread", "noGlError", "zinkLazy", "mangoapp", "gpuClockPin", "performanceBoost", "gamescopeRealtime", "ramCapMb", "vramCapMb", "zramClient",
"directAudio", "mic", "wifiDiscovery", "clientCpusOverride", "clientCpus", "gameCpus",
"noXalia", "fastSync", "fsyncFirst", "syncFallback", "prootNoSeccomp", "prootFastPath", "cursorComposite",
"guestHostname", "fexPreset", "forceSsbs", "logs", "gameDebugLogsEnabled", "storageDiagnostics",
@@ -46,6 +46,8 @@ fun PerformancePage(
fsyncFirst: Boolean,
gamescopeRealtime: Boolean,
gpuClockPin: Boolean,
performanceBoost: Boolean,
samsungSdk: Boolean?,
ramCapMb: Int,
zramClient: Boolean,
vramCapMb: Int,
@@ -67,6 +69,8 @@ fun PerformancePage(
onFsyncFirst: (Boolean) -> Unit,
onGamescopeRealtime: (Boolean) -> Unit,
onGpuClockPin: (Boolean) -> Unit,
onPerformanceBoost: (Boolean) -> Unit,
onImportSamsungSdk: () -> Unit,
onRamCapMb: (Int) -> Unit,
onZramClient: (Boolean) -> Unit,
onVramCapMb: (Int) -> Unit,
@@ -126,6 +130,20 @@ fun PerformancePage(
gamescopeRealtime, onChange = onGamescopeRealtime,
)
}
SettingsGroup(stringResource(R.string.perf_boost_group)) {
ToggleRow(
host, "boost", stringResource(R.string.perf_boost),
stringResource(R.string.perf_boost_hint),
performanceBoost, onChange = onPerformanceBoost,
)
if (samsungSdk != null) {
ActionRow(
stringResource(R.string.perf_samsung_sdk),
stringResource(if (samsungSdk) R.string.perf_samsung_sdk_ready else R.string.perf_samsung_sdk_missing),
stringResource(R.string.perf_samsung_sdk_import), onClick = onImportSamsungSdk,
)
}
}
SettingsGroup(stringResource(R.string.perf_gpu)) {
ToggleRow(
host, "gpuclock", stringResource(R.string.perf_gpuclock),
+11
View File
@@ -597,6 +597,17 @@
<string name="perf_noglerror_hint">Turns off per-call GL validation.</string>
<string name="perf_gsrealtime">gamescope: realtime GPU queue</string>
<string name="perf_gsrealtime_hint">Prioritizes the compositor\'s GPU work. Can smooth frame pacing at some cost to games.</string>
<string name="perf_boost_group">Boost</string>
<string name="perf_boost">Performance boost</string>
<string name="perf_boost_hint">Asks the phone to run the game\'s busiest threads on its fastest cores while the game falls behind. Lets go when the game keeps up, the phone is hot or battery saver is on. Android 12 or newer. More battery use and heat.</string>
<string name="perf_samsung_sdk">Samsung performance SDK</string>
<string name="perf_samsung_sdk_missing">With the boost on, Samsung phones can also raise their CPU, GPU and memory clocks. Download the SDK from developer.samsung.com/galaxy-performance (free Samsung account) and import the zip.</string>
<string name="perf_samsung_sdk_ready">Imported. With the boost on, Samsung\'s CPU, GPU and memory clocks are raised too.</string>
<string name="perf_samsung_sdk_import">Import</string>
<string name="perf_samsung_sdk_pick">Choose PerformanceSDK_v1.0.0.zip from Samsung</string>
<string name="perf_samsung_sdk_imported">Samsung performance SDK imported</string>
<string name="perf_samsung_sdk_invalid">That file is not Samsung\'s Galaxy Performance SDK</string>
<string name="perf_samsung_sdk_failed">Could not import the Samsung performance SDK</string>
<string name="perf_gpu">GPU</string>
<string name="perf_memory">Memory</string>
<string name="perf_cap_off">Off</string>
@@ -0,0 +1,53 @@
package com.droiddeck.launcher.session
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class SamsungPerfTest {
@get:Rule val temp = TemporaryFolder()
private val sdkClasses = listOf("com/samsung/sdk/sperf/SPerf.class", "com/samsung/sdk/sperf/PerformanceManager.class")
private fun zip(entries: Map<String, ByteArray>): ByteArray {
val bytes = ByteArrayOutputStream()
ZipOutputStream(bytes).use { out ->
entries.forEach { (name, data) -> out.putNextEntry(ZipEntry(name)); out.write(data); out.closeEntry() }
}
return bytes.toByteArray()
}
private fun file(name: String, data: ByteArray) = File(temp.root, name).apply { writeBytes(data) }
@Test fun `the SDK jar is taken from Samsung's zip, not the sample's copy`() {
val sdkJar = zip(sdkClasses.associateWith { byteArrayOf(1) })
val sampleJar = zip(mapOf("com/samsung/sdk/sperf/SPerf.class" to byteArrayOf(2)))
val download = file("PerformanceSDK_v1.0.0.zip", zip(mapOf(
"Samples/Performance/lib/perfsdk.jar" to sampleJar,
"Libs/perfsdk-v1.0.0.jar" to sdkJar,
)))
val target = File(temp.root, "out.jar")
assertTrue(SamsungPerf.extractJar(download, target))
assertArrayEquals(sdkJar, target.readBytes())
}
@Test fun `the jar itself is taken as it is`() {
val jar = file("perfsdk-v1.0.0.jar", zip(sdkClasses.associateWith { byteArrayOf(1) }))
val target = File(temp.root, "out.jar")
assertTrue(SamsungPerf.extractJar(jar, target))
assertArrayEquals(jar.readBytes(), target.readBytes())
}
@Test fun `other files are refused`() {
val target = File(temp.root, "out.jar")
assertFalse(SamsungPerf.extractJar(file("Samples.zip", zip(mapOf("a.txt" to byteArrayOf(1)))), target))
assertFalse(SamsungPerf.extractJar(file("notes.zip", byteArrayOf(1, 2, 3)), target))
}
}