feat: adb pair; mDNS discovering service

This commit is contained in:
Miuzarte
2026-03-19 22:24:48 +08:00
parent 32e1f48423
commit 43d3b3f818
24 changed files with 1460 additions and 29 deletions

View File

@@ -12,9 +12,13 @@
<img src="https://github.com/user-attachments/assets/6301f2fb-624b-4209-b548-6f37b9bcedc8" height="320" alt="Screenshot 3" /> <img src="https://github.com/user-attachments/assets/6301f2fb-624b-4209-b548-6f37b9bcedc8" height="320" alt="Screenshot 3" />
</p> </p>
## Features
- 可替换 scrcpy-server
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
## 已知问题 ## 已知问题
- ADB 配对流程未实现
- 多指触控抬起后滞留 - 多指触控抬起后滞留
- 快速离开再进入全屏会导致视频流关键帧丢失 - 快速离开再进入全屏会导致视频流关键帧丢失
@@ -37,8 +41,9 @@ specific abi:
## Credits ## Credits
- [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy) - [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy)
- [YuKongA/miuix](https://github.com/compose-miuix-ui/miuix) - JNI ADB 实现: [rikkaapps/shizuku](https://github.com/rikkaapps/shizuku), [vvb2060/ndk.boringssl](https://github.com/vvb2060), [lsposed/libcxx](https://github.com/lsposed/libcxx)
- [tiann/KernelSU/manager](https://github.com/tiann/KernelSU/tree/main/manager) - 界面组件: [YuKongA/miuix](https://github.com/compose-miuix-ui/miuix)
- 界面设计参考: [tiann/KernelSU/manager](https://github.com/tiann/KernelSU/tree/main/manager)
## License ## License

View File

@@ -1,19 +1,28 @@
{ {
"folders": [ "folders": [
{ {
"name": "ScrcpyForAndroid",
"path": "." "path": "."
}, },
{ {
"name": "scrcpy",
"path": "../scrcpy" "path": "../scrcpy"
}, },
{ {
"name": "miuix",
"path": "../miuix" "path": "../miuix"
}, },
{ {
"path": "../adblib" "name": "shizuku",
"path": "../shizuku"
}, },
{ {
"name": "KernelSU",
"path": "../KernelSU" "path": "../KernelSU"
},
{
"name": "adblib",
"path": "../adblib"
} }
], ],
} }

View File

@@ -23,8 +23,14 @@ android {
applicationId = "io.github.miuzarte.scrcpyforandroid" applicationId = "io.github.miuzarte.scrcpyforandroid"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 2 versionCode = 3
versionName = "0.0.2" versionName = "0.0.3"
externalNativeBuild {
cmake {
arguments += listOf("-DANDROID_STL=none")
}
}
ndk { ndk {
abiFilters.clear() abiFilters.clear()
@@ -59,7 +65,15 @@ android {
} }
buildFeatures { buildFeatures {
compose = true compose = true
prefab = true
} }
externalNativeBuild {
cmake {
path = file("src/main/jni/CMakeLists.txt")
}
}
buildToolsVersion = "36.0.0" buildToolsVersion = "36.0.0"
ndkVersion = "28.2.13676358" ndkVersion = "28.2.13676358"
} }
@@ -78,6 +92,10 @@ dependencies {
implementation(libs.miuix) implementation(libs.miuix)
implementation(libs.miuix.icons) implementation(libs.miuix.icons)
implementation(libs.miuix.navigation3.ui) implementation(libs.miuix.navigation3.ui)
implementation("io.github.vvb2060.ndk:boringssl:20250114")
implementation("org.lsposed.libcxx:libcxx:27.0.12077973")
implementation("org.bouncycastle:bcpkix-jdk18on:1.80")
implementation("org.conscrypt:conscrypt-android:2.5.2")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(platform(libs.androidx.compose.bom))

View File

@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -34,6 +34,7 @@ class NativeCoreFacade(private val appContext: Context) {
private val bootstrapLock = Any() private val bootstrapLock = Any()
private val bootstrapPackets = ArrayDeque<CachedPacket>() private val bootstrapPackets = ArrayDeque<CachedPacket>()
private var packetCount: Long = 0 private var packetCount: Long = 0
@Volatile @Volatile
private var audioPlayer: ScrcpyAudioPlayer? = null private var audioPlayer: ScrcpyAudioPlayer? = null
@@ -91,6 +92,20 @@ class NativeCoreFacade(private val appContext: Context) {
return ioCall { adbService.pair(host, port, pairingCode) } return ioCall { adbService.pair(host, port, pairingCode) }
} }
fun adbDiscoverPairingService(
timeoutMs: Long = 12_000,
includeLanDevices: Boolean = true,
): Pair<String, Int>? {
return ioCall { adbService.discoverPairingService(timeoutMs, includeLanDevices) }
}
fun adbDiscoverConnectService(
timeoutMs: Long = 12_000,
includeLanDevices: Boolean = true,
): Pair<String, Int>? {
return ioCall { adbService.discoverConnectService(timeoutMs, includeLanDevices) }
}
fun adbConnect(host: String, port: Int): Boolean = ioCall { adbService.connect(host, port) } fun adbConnect(host: String, port: Int): Boolean = ioCall { adbService.connect(host, port) }
fun adbDisconnect(): Boolean { fun adbDisconnect(): Boolean {

View File

@@ -76,4 +76,7 @@ object AppDefaults {
const val ADB_KEY_NAME = "scrcpy" const val ADB_KEY_NAME = "scrcpy"
const val ADB_KEY_NAME_INPUT = "" const val ADB_KEY_NAME_INPUT = ""
const val ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN = true
const val ADB_AUTO_RECONNECT_PAIRED_DEVICE = true
const val ADB_MDNS_LAN_DISCOVERY = true
} }

View File

@@ -75,4 +75,7 @@ object AppPreferenceKeys {
const val SERVER_REMOTE_PATH = "server_remote_path" const val SERVER_REMOTE_PATH = "server_remote_path"
const val ADB_KEY_NAME = "adb_key_name" const val ADB_KEY_NAME = "adb_key_name"
const val ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN = "adb_pairing_auto_discover_on_dialog_open"
const val ADB_AUTO_RECONNECT_PAIRED_DEVICE = "adb_auto_reconnect_paired_device"
const val ADB_MDNS_LAN_DISCOVERY = "adb_mdns_lan_discovery"
} }

View File

@@ -0,0 +1,127 @@
package io.github.miuzarte.scrcpyforandroid.nativecore
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import java.io.IOException
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.ServerSocket
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
@RequiresApi(Build.VERSION_CODES.R)
internal class AdbMdnsDiscoverer(context: Context) {
private val nsdManager = context.getSystemService(NsdManager::class.java)
fun discoverPairingService(timeoutMs: Long, includeLanDevices: Boolean): Pair<String, Int>? {
return discoverService(TLS_PAIRING, timeoutMs, includeLanDevices)
}
fun discoverConnectService(timeoutMs: Long, includeLanDevices: Boolean): Pair<String, Int>? {
return discoverService(TLS_CONNECT, timeoutMs, includeLanDevices)
}
private fun discoverService(
serviceType: String,
timeoutMs: Long,
includeLanDevices: Boolean,
): Pair<String, Int>? {
val resultPort = AtomicInteger(-1)
val resultHost = AtomicReference<String?>(null)
val discoveryFinished = AtomicBoolean(false)
val latch = CountDownLatch(1)
val discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(serviceType: String) {
Log.v(TAG, "discovery started: $serviceType")
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.w(TAG, "start discovery failed: $serviceType, error=$errorCode")
latch.countDown()
}
override fun onDiscoveryStopped(serviceType: String) {
Log.v(TAG, "discovery stopped: $serviceType")
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.w(TAG, "stop discovery failed: $serviceType, error=$errorCode")
}
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
if (discoveryFinished.get()) return
Log.v(TAG, "service found: ${serviceInfo.serviceName}")
val resolveListener = object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
Log.v(TAG, "resolve failed: ${serviceInfo.serviceName}, error=$errorCode")
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
if (discoveryFinished.get()) return
val hostAddress = serviceInfo.host?.hostAddress ?: return
if (hostAddress.isBlank()) return
if (!includeLanDevices) {
val isLocalHost = runCatching {
NetworkInterface.getNetworkInterfaces().asSequence().any { intf ->
intf.inetAddresses.asSequence().any { addr ->
addr.hostAddress == hostAddress
}
}
}.getOrDefault(false)
if (!isLocalHost) return
if (!isPortOpened(serviceInfo.port)) return
}
if (resultPort.compareAndSet(-1, serviceInfo.port)) {
resultHost.set(hostAddress)
discoveryFinished.set(true)
latch.countDown()
}
}
}
runCatching {
nsdManager.resolveService(serviceInfo, resolveListener)
}.onFailure { e ->
Log.w(TAG, "resolveService failed for ${serviceInfo.serviceName}", e)
}
}
override fun onServiceLost(serviceInfo: NsdServiceInfo) {
Log.v(TAG, "service lost: ${serviceInfo.serviceName}")
}
}
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
latch.await(timeoutMs, TimeUnit.MILLISECONDS)
runCatching { nsdManager.stopServiceDiscovery(discoveryListener) }
val port = resultPort.get()
val host = resultHost.get()
return if (port > 0 && !host.isNullOrBlank()) host to port else null
}
private fun isPortOpened(port: Int): Boolean = try {
ServerSocket().use {
it.bind(InetSocketAddress("127.0.0.1", port), 1)
false
}
} catch (_: IOException) {
true
}
companion object {
private const val TAG = "AdbMdnsDiscoverer"
private const val TLS_CONNECT = "_adb-tls-connect._tcp"
private const val TLS_PAIRING = "_adb-tls-pairing._tcp"
}
}

View File

@@ -0,0 +1,201 @@
package io.github.miuzarte.scrcpyforandroid.nativecore
import android.annotation.SuppressLint
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.conscrypt.Conscrypt
import java.io.ByteArrayInputStream
import java.math.BigInteger
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.Provider
import java.security.SecureRandom
import java.security.Security
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.RSAPublicKeySpec
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLEngine
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509ExtendedTrustManager
internal class AdbPairingKey(
private val privateKey: PrivateKey,
private val alias: String,
) {
private val rsaPrivateKey: RSAPrivateKey = privateKey as? RSAPrivateKey
?: throw IllegalStateException("Expected RSA private key")
private val rsaPublicKey: RSAPublicKey by lazy {
val keyFactory = KeyFactory.getInstance("RSA")
keyFactory.generatePublic(
RSAPublicKeySpec(rsaPrivateKey.modulus, BigInteger.valueOf(65537L)),
) as RSAPublicKey
}
private val certificate: X509Certificate by lazy {
val signer = JcaContentSignerBuilder("SHA256withRSA").build(rsaPrivateKey)
val builder = X509v3CertificateBuilder(
X500Name("CN=00"),
BigInteger.ONE,
java.util.Date(0),
java.util.Date(2_461_449_600_000L),
X500Name("CN=00"),
SubjectPublicKeyInfo.getInstance(rsaPublicKey.encoded),
)
val encoded = builder.build(signer).encoded
CertificateFactory.getInstance("X.509")
.generateCertificate(ByteArrayInputStream(encoded)) as X509Certificate
}
val adbPublicKey: ByteArray by lazy { rsaPublicKey.adbEncoded(alias) }
val sslContext: SSLContext by lazy {
val conscryptProvider: Provider = Conscrypt.newProviderBuilder().build()
if (Security.getProvider(conscryptProvider.name) == null) {
Security.insertProviderAt(conscryptProvider, 1)
}
val context = SSLContext.getInstance("TLSv1.3", conscryptProvider)
context.init(arrayOf(keyManager), arrayOf(trustManager), SecureRandom())
context
}
private val keyManager: X509ExtendedKeyManager
get() = object : X509ExtendedKeyManager() {
private val keyAlias = "adbkey"
override fun chooseClientAlias(
keyType: Array<out String>?,
issuers: Array<out java.security.Principal>?,
socket: Socket?,
): String = keyAlias
override fun getCertificateChain(alias: String?): Array<X509Certificate>? {
return if (alias == keyAlias) arrayOf(certificate) else null
}
override fun getPrivateKey(alias: String?): PrivateKey? {
return if (alias == keyAlias) rsaPrivateKey else null
}
override fun getClientAliases(
keyType: String?,
issuers: Array<out java.security.Principal>?,
): Array<String>? = null
override fun getServerAliases(
keyType: String?,
issuers: Array<out java.security.Principal>?,
): Array<String>? = null
override fun chooseServerAlias(
keyType: String?,
issuers: Array<out java.security.Principal>?,
socket: Socket?,
): String? = null
}
@get:SuppressLint("CustomX509TrustManager")
@get:Suppress("TrustAllX509TrustManager")
private val trustManager: X509ExtendedTrustManager
get() = object : X509ExtendedTrustManager() {
// ADB pairing uses SPAKE2 + exported keying material to authenticate the peer.
// The peer cert is ephemeral/self-signed, so PKIX validation is intentionally bypassed here.
private fun acceptForPairing(
chain: Array<out X509Certificate>?,
authType: String?,
) {
if (chain.isNullOrEmpty()) return
if (authType.isNullOrBlank()) return
}
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
socket: Socket?,
) {
acceptForPairing(chain, authType)
}
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
engine: SSLEngine?,
) {
acceptForPairing(chain, authType)
}
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
acceptForPairing(chain, authType)
}
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
socket: Socket?,
) {
acceptForPairing(chain, authType)
}
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
engine: SSLEngine?,
) {
acceptForPairing(chain, authType)
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
acceptForPairing(chain, authType)
}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
}
private const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8
private const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4
private const val RSA_PUBLIC_KEY_SIZE = 524
private fun BigInteger.toAdbEncoded(): IntArray {
val encoded = IntArray(ANDROID_PUBKEY_MODULUS_SIZE_WORDS)
val r32 = BigInteger.ZERO.setBit(32)
var tmp = this
for (i in 0 until ANDROID_PUBKEY_MODULUS_SIZE_WORDS) {
val out = tmp.divideAndRemainder(r32)
tmp = out[0]
encoded[i] = out[1].toInt()
}
return encoded
}
private fun RSAPublicKey.adbEncoded(name: String): ByteArray {
val r32 = BigInteger.ZERO.setBit(32)
val n0inv = modulus.remainder(r32).modInverse(r32).negate()
val r = BigInteger.ZERO.setBit(ANDROID_PUBKEY_MODULUS_SIZE * 8)
val rr = r.modPow(BigInteger.valueOf(2), modulus)
val buffer = ByteBuffer.allocate(RSA_PUBLIC_KEY_SIZE).order(ByteOrder.LITTLE_ENDIAN)
buffer.putInt(ANDROID_PUBKEY_MODULUS_SIZE_WORDS)
buffer.putInt(n0inv.toInt())
modulus.toAdbEncoded().forEach { buffer.putInt(it) }
rr.toAdbEncoded().forEach { buffer.putInt(it) }
buffer.putInt(publicExponent.toInt())
val base64 = android.util.Base64.encode(buffer.array(), android.util.Base64.NO_WRAP)
val suffix = " $name\u0000".toByteArray(Charsets.UTF_8)
return ByteArray(base64.size + suffix.size).also {
base64.copyInto(it)
suffix.copyInto(it, base64.size)
}
}
internal class AdbInvalidPairingCodeException : Exception()

View File

@@ -1,6 +1,7 @@
package io.github.miuzarte.scrcpyforandroid.nativecore package io.github.miuzarte.scrcpyforandroid.nativecore
import android.content.Context import android.content.Context
import android.os.Build
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
@@ -32,6 +33,7 @@ import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.net.ssl.SSLSocket
import kotlin.concurrent.thread import kotlin.concurrent.thread
internal class DirectAdbTransport(private val context: Context) { internal class DirectAdbTransport(private val context: Context) {
@@ -57,6 +59,41 @@ internal class DirectAdbTransport(private val context: Context) {
return conn return conn
} }
fun pair(host: String, port: Int, pairingCode: String): Boolean {
val targetHost = host.trim()
val targetCode = pairingCode.trim()
require(targetHost.isNotBlank()) { "host is blank" }
require(targetCode.isNotBlank()) { "pairing code is blank" }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
throw UnsupportedOperationException("ADB pairing requires Android 11+")
}
val pairingKey = AdbPairingKey(
privateKey = privateKey,
alias = keyName.ifBlank { AppDefaults.ADB_KEY_NAME },
)
return DirectAdbPairingClient(targetHost, port, targetCode, pairingKey).use {
it.start()
}
}
fun discoverPairingService(
timeoutMs: Long = 12_000,
includeLanDevices: Boolean = true
): Pair<String, Int>? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
return AdbMdnsDiscoverer(context).discoverPairingService(timeoutMs, includeLanDevices)
}
fun discoverConnectService(
timeoutMs: Long = 12_000,
includeLanDevices: Boolean = true
): Pair<String, Int>? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
return AdbMdnsDiscoverer(context).discoverConnectService(timeoutMs, includeLanDevices)
}
private fun loadOrCreate(): Pair<PrivateKey, ByteArray> { private fun loadOrCreate(): Pair<PrivateKey, ByteArray> {
val prefs = context.getSharedPreferences( val prefs = context.getSharedPreferences(
AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME, AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME,
@@ -104,6 +141,41 @@ internal class DirectAdbTransport(private val context: Context) {
return digest.joinToString(":") { b -> "%02x".format(b) } return digest.joinToString(":") { b -> "%02x".format(b) }
} }
private fun encodeAdbPublicKey(modulus: BigInteger, exponent: Int): ByteArray {
val words = 64
val bytes = 256
val two32 = BigInteger.ONE.shiftLeft(32)
val mask32 = two32.subtract(BigInteger.ONE)
fun toBigEndianPadded(n: BigInteger): ByteArray {
val raw = n.toByteArray()
val arr = ByteArray(bytes)
val src = if (raw[0] == 0.toByte()) raw.copyOfRange(1, raw.size) else raw
src.copyInto(arr, destinationOffset = bytes - src.size)
return arr
}
val modBE = toBigEndianPadded(modulus)
val n0 = modulus.and(mask32)
val n0inv = n0.modInverse(two32).negate().mod(two32).toInt()
val r = BigInteger.ONE.shiftLeft(bytes * 8)
val rrBE = toBigEndianPadded(r.multiply(r).mod(modulus))
val buf = ByteBuffer.allocate(4 + 4 + bytes + bytes + 4).order(ByteOrder.LITTLE_ENDIAN)
buf.putInt(words)
buf.putInt(n0inv)
for (i in words - 1 downTo 0) {
val o = i * 4
buf.put(modBE[o + 3]); buf.put(modBE[o + 2]); buf.put(modBE[o + 1]); buf.put(modBE[o])
}
for (i in words - 1 downTo 0) {
val o = i * 4
buf.put(rrBE[o + 3]); buf.put(rrBE[o + 2]); buf.put(rrBE[o + 1]); buf.put(rrBE[o])
}
buf.putInt(exponent)
return buf.array()
}
companion object { companion object {
private const val TAG = "DirectAdbTransport" private const val TAG = "DirectAdbTransport"
} }
@@ -138,6 +210,7 @@ internal class DirectAdbConnection(
private val socket = Socket() private val socket = Socket()
private lateinit var rawIn: BufferedInputStream private lateinit var rawIn: BufferedInputStream
private lateinit var rawOut: OutputStream private lateinit var rawOut: OutputStream
private var tlsSocket: SSLSocket? = null
private val nextLocalId = AtomicInteger(1) private val nextLocalId = AtomicInteger(1)
private val streams = ConcurrentHashMap<Int, AdbSocketStream>() private val streams = ConcurrentHashMap<Int, AdbSocketStream>()
@@ -149,10 +222,12 @@ internal class DirectAdbConnection(
private const val TAG = "DirectAdbConnection" private const val TAG = "DirectAdbConnection"
private const val A_CNXN = 0x4e584e43 private const val A_CNXN = 0x4e584e43
private const val A_AUTH = 0x48545541 private const val A_AUTH = 0x48545541
private const val A_STLS = 0x534c5453
private const val A_OPEN = 0x4e45504f private const val A_OPEN = 0x4e45504f
private const val A_OKAY = 0x59414b4f private const val A_OKAY = 0x59414b4f
private const val A_CLSE = 0x45534c43 private const val A_CLSE = 0x45534c43
private const val A_WRTE = 0x45545257 private const val A_WRTE = 0x45545257
private const val STLS_VERSION = 0x01000000
private const val AUTH_TOKEN = 1 private const val AUTH_TOKEN = 1
private const val AUTH_SIGNATURE = 2 private const val AUTH_SIGNATURE = 2
private const val AUTH_RSAPUBLICKEY = 3 private const val AUTH_RSAPUBLICKEY = 3
@@ -170,7 +245,13 @@ internal class DirectAdbConnection(
sendMsg(A_CNXN, VERSION, MAX_PAYLOAD, "host::\u0000".toByteArray(Charsets.UTF_8)) sendMsg(A_CNXN, VERSION, MAX_PAYLOAD, "host::\u0000".toByteArray(Charsets.UTF_8))
val first = recvMsg() var first = recvMsg()
if (first.command == A_STLS) {
sendMsg(A_STLS, STLS_VERSION, 0)
upgradeToTls()
first = recvMsg()
}
when (first.command) { when (first.command) {
A_CNXN -> Unit A_CNXN -> Unit
A_AUTH -> { A_AUTH -> {
@@ -209,6 +290,19 @@ internal class DirectAdbConnection(
readerThread = thread(isDaemon = true, name = "adb-reader-$host:$port") { readLoop() } readerThread = thread(isDaemon = true, name = "adb-reader-$host:$port") { readLoop() }
} }
private fun upgradeToTls() {
val pairingKey = AdbPairingKey(
privateKey = privateKey,
alias = keyName,
)
val sslSocket = pairingKey.sslContext.socketFactory
.createSocket(socket, host, port, true) as SSLSocket
sslSocket.startHandshake()
tlsSocket = sslSocket
rawIn = BufferedInputStream(sslSocket.inputStream, 65_536)
rawOut = sslSocket.outputStream
}
fun openStream(service: String): AdbSocketStream { fun openStream(service: String): AdbSocketStream {
val localId = nextLocalId.getAndIncrement() val localId = nextLocalId.getAndIncrement()
val stream = AdbSocketStream(localId) { cmd, a0, a1, d -> sendMsg(cmd, a0, a1, d) } val stream = AdbSocketStream(localId) { cmd, a0, a1, d -> sendMsg(cmd, a0, a1, d) }
@@ -270,6 +364,7 @@ internal class DirectAdbConnection(
closed = true closed = true
streams.values.forEach { runCatching { it.forceClose() } } streams.values.forEach { runCatching { it.forceClose() } }
streams.clear() streams.clear()
runCatching { tlsSocket?.close() }
runCatching { socket.close() } runCatching { socket.close() }
runCatching { readerThread?.interrupt() } runCatching { readerThread?.interrupt() }
} }

View File

@@ -0,0 +1,267 @@
package io.github.miuzarte.scrcpyforandroid.nativecore
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import org.conscrypt.Conscrypt
import java.io.Closeable
import java.io.DataInputStream
import java.io.DataOutputStream
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import javax.net.ssl.SSLSocket
private const val TAG = "DirectAdbPairing"
private const val CURRENT_KEY_HEADER_VERSION = 1.toByte()
private const val MIN_SUPPORTED_KEY_HEADER_VERSION = 1.toByte()
private const val MAX_SUPPORTED_KEY_HEADER_VERSION = 1.toByte()
private const val MAX_PEER_INFO_SIZE = 8192
private const val MAX_PAYLOAD_SIZE = MAX_PEER_INFO_SIZE * 2
private const val EXPORTED_KEY_LABEL = "adb-label\u0000"
private const val EXPORTED_KEY_SIZE = 64
private const val PAIRING_PACKET_HEADER_SIZE = 6
private class PeerInfo(val type: Byte, rawData: ByteArray) {
val data = ByteArray(MAX_PEER_INFO_SIZE - 1)
init {
rawData.copyInto(data, endIndex = rawData.size.coerceAtMost(MAX_PEER_INFO_SIZE - 1))
}
fun writeTo(buffer: ByteBuffer) {
buffer.put(type)
buffer.put(data)
}
companion object {
const val ADB_RSA_PUB_KEY: Byte = 0
fun readFrom(buffer: ByteBuffer): PeerInfo {
val type = buffer.get()
val data = ByteArray(MAX_PEER_INFO_SIZE - 1)
buffer.get(data)
return PeerInfo(type, data)
}
}
}
private class PairingPacketHeader(val version: Byte, val type: Byte, val payload: Int) {
object Type {
const val SPAKE2_MSG: Byte = 0
const val PEER_INFO: Byte = 1
}
fun writeTo(buffer: ByteBuffer) {
buffer.put(version)
buffer.put(type)
buffer.putInt(payload)
}
companion object {
fun readFrom(buffer: ByteBuffer): PairingPacketHeader? {
val version = buffer.get()
val type = buffer.get()
val payload = buffer.int
if (version !in MIN_SUPPORTED_KEY_HEADER_VERSION..MAX_SUPPORTED_KEY_HEADER_VERSION) {
Log.e(TAG, "header version mismatch: $version")
return null
}
if (type != Type.SPAKE2_MSG && type != Type.PEER_INFO) {
Log.e(TAG, "unknown packet type: $type")
return null
}
if (payload !in 1..MAX_PAYLOAD_SIZE) {
Log.e(TAG, "unsafe payload size: $payload")
return null
}
return PairingPacketHeader(version, type, payload)
}
}
}
private class PairingContext private constructor(private val nativePtr: Long) {
val msg: ByteArray = nativeMsg(nativePtr)
fun initCipher(theirMsg: ByteArray): Boolean = nativeInitCipher(nativePtr, theirMsg)
fun encrypt(input: ByteArray): ByteArray? = nativeEncrypt(nativePtr, input)
fun decrypt(input: ByteArray): ByteArray? = nativeDecrypt(nativePtr, input)
fun destroy() {
nativeDestroy(nativePtr)
}
private external fun nativeMsg(nativePtr: Long): ByteArray
private external fun nativeInitCipher(nativePtr: Long, theirMsg: ByteArray): Boolean
private external fun nativeEncrypt(nativePtr: Long, inbuf: ByteArray): ByteArray?
private external fun nativeDecrypt(nativePtr: Long, inbuf: ByteArray): ByteArray?
private external fun nativeDestroy(nativePtr: Long)
companion object {
fun create(password: ByteArray): PairingContext? {
val ptr = nativeConstructor(true, password)
return if (ptr != 0L) PairingContext(ptr) else null
}
@JvmStatic
private external fun nativeConstructor(isClient: Boolean, password: ByteArray): Long
}
}
@RequiresApi(Build.VERSION_CODES.R)
internal class DirectAdbPairingClient(
private val host: String,
private val port: Int,
private val pairingCode: String,
private val key: AdbPairingKey,
) : Closeable {
private enum class State {
READY,
EXCHANGING_MSGS,
EXCHANGING_PEER_INFO,
STOPPED,
}
private lateinit var socket: Socket
private lateinit var inputStream: DataInputStream
private lateinit var outputStream: DataOutputStream
private val peerInfo = PeerInfo(PeerInfo.ADB_RSA_PUB_KEY, key.adbPublicKey)
private lateinit var pairingContext: PairingContext
private var state: State = State.READY
fun start(): Boolean {
check(isLibraryLoaded) { "Pairing native bridge is unavailable" }
setupTlsConnection()
state = State.EXCHANGING_MSGS
if (!doExchangeMsgs()) {
state = State.STOPPED
return false
}
state = State.EXCHANGING_PEER_INFO
if (!doExchangePeerInfo()) {
state = State.STOPPED
return false
}
state = State.STOPPED
return true
}
private fun setupTlsConnection() {
socket = Socket(host, port)
socket.tcpNoDelay = true
val sslSocket = key.sslContext.socketFactory
.createSocket(socket, host, port, true) as SSLSocket
sslSocket.startHandshake()
inputStream = DataInputStream(sslSocket.inputStream)
outputStream = DataOutputStream(sslSocket.outputStream)
val codeBytes = pairingCode.toByteArray(Charsets.UTF_8)
val keyMaterial = exportTlsKeyingMaterial(sslSocket)
val password = ByteArray(codeBytes.size + keyMaterial.size)
codeBytes.copyInto(password)
keyMaterial.copyInto(password, destinationOffset = codeBytes.size)
pairingContext = checkNotNull(PairingContext.create(password)) {
"Unable to create pairing context"
}
}
private fun exportTlsKeyingMaterial(sslSocket: SSLSocket): ByteArray {
if (Conscrypt.isConscrypt(sslSocket)) {
return Conscrypt.exportKeyingMaterial(
sslSocket,
EXPORTED_KEY_LABEL,
null,
EXPORTED_KEY_SIZE,
)
}
throw IllegalStateException(
"TLS socket is not backed by bundled Conscrypt: ${sslSocket.javaClass.name}",
)
}
private fun createHeader(type: Byte, payloadSize: Int): PairingPacketHeader {
return PairingPacketHeader(CURRENT_KEY_HEADER_VERSION, type, payloadSize)
}
private fun readHeader(): PairingPacketHeader? {
val bytes = ByteArray(PAIRING_PACKET_HEADER_SIZE)
inputStream.readFully(bytes)
val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
return PairingPacketHeader.readFrom(buffer)
}
private fun writeHeader(header: PairingPacketHeader, payload: ByteArray) {
val buffer = ByteBuffer.allocate(PAIRING_PACKET_HEADER_SIZE).order(ByteOrder.BIG_ENDIAN)
header.writeTo(buffer)
outputStream.write(buffer.array())
outputStream.write(payload)
}
private fun doExchangeMsgs(): Boolean {
val msg = pairingContext.msg
writeHeader(createHeader(PairingPacketHeader.Type.SPAKE2_MSG, msg.size), msg)
val theirHeader = readHeader() ?: return false
if (theirHeader.type != PairingPacketHeader.Type.SPAKE2_MSG) return false
val theirMessage = ByteArray(theirHeader.payload)
inputStream.readFully(theirMessage)
return pairingContext.initCipher(theirMessage)
}
private fun doExchangePeerInfo(): Boolean {
val plain = ByteBuffer.allocate(MAX_PEER_INFO_SIZE).order(ByteOrder.BIG_ENDIAN)
peerInfo.writeTo(plain)
val encrypted = pairingContext.encrypt(plain.array()) ?: return false
writeHeader(createHeader(PairingPacketHeader.Type.PEER_INFO, encrypted.size), encrypted)
val theirHeader = readHeader() ?: return false
if (theirHeader.type != PairingPacketHeader.Type.PEER_INFO) return false
val peerMessage = ByteArray(theirHeader.payload)
inputStream.readFully(peerMessage)
val decrypted =
pairingContext.decrypt(peerMessage) ?: throw AdbInvalidPairingCodeException()
if (decrypted.size != MAX_PEER_INFO_SIZE) {
Log.e(TAG, "invalid peer info size: ${decrypted.size}")
return false
}
PeerInfo.readFrom(ByteBuffer.wrap(decrypted).order(ByteOrder.BIG_ENDIAN))
return true
}
override fun close() {
runCatching { inputStream.close() }
runCatching { outputStream.close() }
runCatching { socket.close() }
if (state != State.READY) {
runCatching { pairingContext.destroy() }
}
}
companion object {
private val isLibraryLoaded: Boolean = runCatching {
System.loadLibrary("adbpairing")
true
}.onFailure {
Log.e(TAG, "loadLibrary(adbpairing) failed", it)
}.getOrDefault(false)
}
}

View File

@@ -7,10 +7,13 @@ import java.nio.file.Path
class NativeAdbService(appContext: Context) { class NativeAdbService(appContext: Context) {
private val transport = DirectAdbTransport(appContext) private val transport = DirectAdbTransport(appContext)
@Volatile @Volatile
private var connection: DirectAdbConnection? = null private var connection: DirectAdbConnection? = null
@Volatile @Volatile
private var connectedHost: String? = null private var connectedHost: String? = null
@Volatile @Volatile
private var connectedPort: Int? = null private var connectedPort: Int? = null
@@ -22,9 +25,44 @@ class NativeAdbService(appContext: Context) {
@Synchronized @Synchronized
fun pair(host: String, port: Int, pairingCode: String): Boolean { fun pair(host: String, port: Int, pairingCode: String): Boolean {
throw UnsupportedOperationException( val h = host.trim()
"Wireless pairing is not yet implemented. Please enable TCP ADB via USB first.", val code = pairingCode.trim()
) require(h.isNotBlank()) { "host is blank" }
require(code.isNotBlank()) { "pairing code is blank" }
Log.i(TAG, "pair(): host=$h port=$port")
return try {
transport.pair(h, port, code)
} catch (e: Exception) {
Log.e(TAG, "pair(): failed host=$h port=$port", e)
val detail = e.message ?: "${e.javaClass.simpleName} (no message)"
throw IllegalStateException("ADB pair failed for $h:$port -> $detail", e)
}
}
@Synchronized
fun discoverPairingService(
timeoutMs: Long = 12_000,
includeLanDevices: Boolean = true
): Pair<String, Int>? {
return try {
transport.discoverPairingService(timeoutMs, includeLanDevices)
} catch (e: Exception) {
Log.w(TAG, "discoverPairingService(): failed", e)
null
}
}
@Synchronized
fun discoverConnectService(
timeoutMs: Long = 12_000,
includeLanDevices: Boolean = true
): Pair<String, Int>? {
return try {
transport.discoverConnectService(timeoutMs, includeLanDevices)
} catch (e: Exception) {
Log.w(TAG, "discoverConnectService(): failed", e)
null
}
} }
@Synchronized @Synchronized

View File

@@ -24,6 +24,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
@Volatile @Volatile
private var prepared = false private var prepared = false
@Volatile @Volatile
private var released = false private var released = false
private var packetCount = 0L private var packetCount = 0L

View File

@@ -38,6 +38,7 @@ import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings
import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices
import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget
import io.github.miuzarte.scrcpyforandroid.services.replaceQuickDevicePort
import io.github.miuzarte.scrcpyforandroid.services.saveDevicePageSettings import io.github.miuzarte.scrcpyforandroid.services.saveDevicePageSettings
import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices
import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty
@@ -67,14 +68,19 @@ import kotlinx.coroutines.withTimeout
import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.SnackbarHostState import top.yukonga.miuix.kmp.basic.SnackbarHostState
import top.yukonga.miuix.kmp.extra.SuperBottomSheet import top.yukonga.miuix.kmp.extra.SuperBottomSheet
import java.net.InetSocketAddress
import java.net.Socket
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val ADB_CONNECT_TIMEOUT_MS = 3_000L private const val ADB_CONNECT_TIMEOUT_MS = 3_000L
private const val ADB_KEEPALIVE_INTERVAL_MS = 15_000L private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L
private const val ADB_KEEPALIVE_TIMEOUT_MS = 2_000L private const val ADB_KEEPALIVE_TIMEOUT_MS = 1_500L
private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 1_200L
private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 1_500L
private const val ADB_TCP_PROBE_TIMEOUT_MS = 600
private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F" private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F"
private const val LOG_TAG = "DevicePage" private const val LOG_TAG = "DevicePage"
@@ -207,6 +213,9 @@ fun DeviceTabScreen(
onOpenReorderDevicesActionChange: ((() -> Unit)?) -> Unit, onOpenReorderDevicesActionChange: ((() -> Unit)?) -> Unit,
onOpenAdvancedPage: () -> Unit, onOpenAdvancedPage: () -> Unit,
onOpenFullscreenPage: (ScrcpySessionInfo) -> Unit, onOpenFullscreenPage: (ScrcpySessionInfo) -> Unit,
adbPairingAutoDiscoverOnDialogOpen: Boolean,
adbAutoReconnectPairedDevice: Boolean,
adbMdnsLanDiscoveryEnabled: Boolean,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val haptics = rememberAppHaptics() val haptics = rememberAppHaptics()
@@ -273,6 +282,7 @@ fun DeviceTabScreen(
val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() } val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() }
val quickDevices = val quickDevices =
rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() } rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() }
val sessionReconnectBlacklistHosts = remember { mutableSetOf<String>() }
LaunchedEffect(eventLog.size) { LaunchedEffect(eventLog.size) {
onCanClearLogsChange(eventLog.isNotEmpty()) onCanClearLogsChange(eventLog.isNotEmpty())
@@ -349,6 +359,17 @@ fun DeviceTabScreen(
} }
} }
suspend fun probeTcpReachable(host: String, port: Int): Boolean {
return withContext(Dispatchers.IO) {
runCatching {
Socket().use { socket ->
socket.connect(InetSocketAddress(host, port), ADB_TCP_PROBE_TIMEOUT_MS)
true
}
}.getOrDefault(false)
}
}
fun runBusy(label: String, onFinished: (() -> Unit)? = null, block: suspend () -> Unit) { fun runBusy(label: String, onFinished: (() -> Unit)? = null, block: suspend () -> Unit) {
if (busy) return if (busy) return
scope.launch { scope.launch {
@@ -606,6 +627,7 @@ fun DeviceTabScreen(
snack.showSnackbar("ADB 自动重连成功") snack.showSnackbar("ADB 自动重连成功")
} }
} else { } else {
runCatching { nativeCore.adbDisconnect() }
statusLine = "ADB 连接断开" statusLine = "ADB 连接断开"
connectedDeviceLabel = "未连接" connectedDeviceLabel = "未连接"
sessionInfo = null sessionInfo = null
@@ -618,6 +640,115 @@ fun DeviceTabScreen(
} }
} }
LaunchedEffect(adbConnected, adbAutoReconnectPairedDevice, adbMdnsLanDiscoveryEnabled) {
if (adbConnected || !adbAutoReconnectPairedDevice) return@LaunchedEffect
val quickConnectTriedOnce = mutableSetOf<String>()
while (!adbConnected && adbAutoReconnectPairedDevice) {
if (busy || adbConnecting || sessionInfo != null) {
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue
}
val quickCandidates = quickDevices.toList()
if (quickCandidates.isNotEmpty()) {
for (target in quickCandidates) {
if (adbConnected || adbConnecting) break
if (sessionReconnectBlacklistHosts.contains(target.host)) continue
val targetKey = "${target.host}:${target.port}"
if (quickConnectTriedOnce.contains(targetKey)) continue
val portReachable = probeTcpReachable(target.host, target.port)
if (!portReachable) continue
quickConnectTriedOnce += targetKey
runAdbConnect("快速设备端口可达,尝试连接一次") {
val ok = connectWithTimeout(target.host, target.port)
adbConnected = ok
upsertQuickDevice(
context,
quickDevices,
target.host,
target.port,
ok
)
if (ok) {
handleAdbConnected(target.host, target.port)
logEvent("ADB 快速探测连接成功: ${target.host}:${target.port}")
}
}
}
if (adbConnected) break
}
val discovered = withContext(Dispatchers.IO) {
nativeCore.adbDiscoverConnectService(
timeoutMs = ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS,
includeLanDevices = adbMdnsLanDiscoveryEnabled,
)
}
if (discovered == null) {
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue
}
val (discoveredHost, discoveredPort) = discovered
if (sessionReconnectBlacklistHosts.contains(discoveredHost)) {
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue
}
val knownDevice = quickDevices.firstOrNull { it.host == discoveredHost }
if (knownDevice == null) {
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue
}
val portToReplace = quickDevices.firstOrNull {
it.host == discoveredHost &&
it.port != AppDefaults.ADB_PORT &&
it.port != discoveredPort
}?.port
if (portToReplace != null) {
replaceQuickDevicePort(
context = context,
quickDevices = quickDevices,
host = discoveredHost,
oldPort = portToReplace,
newPort = discoveredPort,
online = false,
)
logEvent(
"mDNS 发现新端口,已更新快速设备: $discoveredHost:$portToReplace -> $discoveredHost:$discoveredPort"
)
}
if (adbConnected || adbConnecting) {
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue
}
runAdbConnect("自动重连 ADB") {
val ok = connectWithTimeout(discoveredHost, discoveredPort)
adbConnected = ok
upsertQuickDevice(
context,
quickDevices,
discoveredHost,
discoveredPort,
ok
)
if (ok) {
handleAdbConnected(discoveredHost, discoveredPort)
logEvent("ADB 自动重连成功: $discoveredHost:$discoveredPort")
} else {
logEvent("ADB 自动重连失败: $discoveredHost:$discoveredPort", Log.WARN)
}
}
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
}
}
DisposableEffect(nativeCore) { DisposableEffect(nativeCore) {
val listener: (Int, Int) -> Unit = { width, height -> val listener: (Int, Int) -> Unit = { width, height ->
sessionInfo = sessionInfo?.copy(width = width, height = height) sessionInfo = sessionInfo?.copy(width = width, height = height)
@@ -786,6 +917,7 @@ fun DeviceTabScreen(
activeDeviceActionId = device.id activeDeviceActionId = device.id
runBusy("断开 ADB", onFinished = { activeDeviceActionId = null }) { runBusy("断开 ADB", onFinished = { activeDeviceActionId = null }) {
nativeCore.adbDisconnect() nativeCore.adbDisconnect()
sessionReconnectBlacklistHosts += host
adbConnected = false adbConnected = false
currentTargetHost = "" currentTargetHost = ""
currentTargetPort = AppDefaults.ADB_PORT currentTargetPort = AppDefaults.ADB_PORT
@@ -862,12 +994,21 @@ fun DeviceTabScreen(
// "使用配对码配对设备" // "使用配对码配对设备"
PairingCard( PairingCard(
busy = busy, busy = busy,
autoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
onDiscoverTarget = {
nativeCore.adbDiscoverPairingService(
includeLanDevices = adbMdnsLanDiscoveryEnabled,
)
},
onPair = { host, port, code -> onPair = { host, port, code ->
runBusy("执行配对") { runBusy("执行配对") {
val resolvedHost = host.trim()
val resolvedPort = port.toIntOrNull() ?: return@runBusy
val resolvedCode = code.trim()
val ok = nativeCore.adbPair( val ok = nativeCore.adbPair(
host.trim(), resolvedHost,
port.toIntOrNull() ?: AppDefaults.ADB_PORT, resolvedPort,
code.trim(), resolvedCode,
) )
logEvent( logEvent(
if (ok) "配对成功" else "配对失败", if (ok) "配对成功" else "配对失败",
@@ -1075,11 +1216,11 @@ fun DeviceTabScreen(
) )
} }
} }
}
item { if (eventLog.isNotEmpty()) item {
Spacer(Modifier.height(UiSpacing.PageItem)) Spacer(Modifier.height(UiSpacing.PageItem))
LogsPanel(lines = eventLog) LogsPanel(lines = eventLog)
}
} }
// TODO: 放进 [AppPageLazyColumn] 里 // TODO: 放进 [AppPageLazyColumn] 里

View File

@@ -131,6 +131,15 @@ fun MainPage() {
var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) } var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) }
var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) } var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) }
var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) } var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) }
var adbPairingAutoDiscoverOnDialogOpen by rememberSaveable {
mutableStateOf(initialSettings.adbPairingAutoDiscoverOnDialogOpen)
}
var adbAutoReconnectPairedDevice by rememberSaveable {
mutableStateOf(initialSettings.adbAutoReconnectPairedDevice)
}
var adbMdnsLanDiscoveryEnabled by rememberSaveable {
mutableStateOf(initialSettings.adbMdnsLanDiscoveryEnabled)
}
var noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) } var noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) }
var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) } var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) }
var videoCodecOptions by rememberSaveable { mutableStateOf(initialDeviceSettings.videoCodecOptions) } var videoCodecOptions by rememberSaveable { mutableStateOf(initialDeviceSettings.videoCodecOptions) }
@@ -191,6 +200,9 @@ fun MainPage() {
customServerUri, customServerUri,
serverRemotePath, serverRemotePath,
adbKeyName, adbKeyName,
adbPairingAutoDiscoverOnDialogOpen,
adbAutoReconnectPairedDevice,
adbMdnsLanDiscoveryEnabled,
) { ) {
saveMainSettings( saveMainSettings(
context, context,
@@ -209,6 +221,9 @@ fun MainPage() {
customServerUri = customServerUri, customServerUri = customServerUri,
serverRemotePath = serverRemotePath, serverRemotePath = serverRemotePath,
adbKeyName = adbKeyName, adbKeyName = adbKeyName,
adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice,
adbMdnsLanDiscoveryEnabled = adbMdnsLanDiscoveryEnabled,
), ),
) )
} }
@@ -464,6 +479,9 @@ fun MainPage() {
) )
}, },
previewCardHeightDp = devicePreviewCardHeightDp, previewCardHeightDp = devicePreviewCardHeightDp,
adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice,
adbMdnsLanDiscoveryEnabled = adbMdnsLanDiscoveryEnabled,
) )
} }
@@ -508,6 +526,14 @@ fun MainPage() {
onServerRemotePathChange = { serverRemotePath = it }, onServerRemotePathChange = { serverRemotePath = it },
adbKeyName = adbKeyName, adbKeyName = adbKeyName,
onAdbKeyNameChange = { adbKeyName = it }, onAdbKeyNameChange = { adbKeyName = it },
adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
onAdbPairingAutoDiscoverOnDialogOpenChange = {
adbPairingAutoDiscoverOnDialogOpen = it
},
adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice,
onAdbAutoReconnectPairedDeviceChange = {
adbAutoReconnectPairedDevice = it
},
scrollBehavior = settingsScrollBehavior, scrollBehavior = settingsScrollBehavior,
) )
} }

View File

@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
@@ -71,6 +72,10 @@ fun SettingsScreen(
onServerRemotePathChange: (String) -> Unit, onServerRemotePathChange: (String) -> Unit,
adbKeyName: String, adbKeyName: String,
onAdbKeyNameChange: (String) -> Unit, onAdbKeyNameChange: (String) -> Unit,
adbPairingAutoDiscoverOnDialogOpen: Boolean,
onAdbPairingAutoDiscoverOnDialogOpenChange: (Boolean) -> Unit,
adbAutoReconnectPairedDevice: Boolean,
onAdbAutoReconnectPairedDeviceChange: (Boolean) -> Unit,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
) { ) {
val baseModeItems = THEME_BASE_OPTIONS.map { it.label } val baseModeItems = THEME_BASE_OPTIONS.map { it.label }
@@ -206,7 +211,22 @@ fun SettingsScreen(
.padding(horizontal = UiSpacing.CardContent) .padding(horizontal = UiSpacing.CardContent)
.padding(bottom = UiSpacing.CardContent), .padding(bottom = UiSpacing.CardContent),
) )
SuperSwitch(
title = "配对时自动启用发现服务",
summary = "打开配对弹窗后自动搜索可用配对端口",
checked = adbPairingAutoDiscoverOnDialogOpen,
onCheckedChange = onAdbPairingAutoDiscoverOnDialogOpenChange,
)
SuperSwitch(
title = "自动重连已配对设备",
summary = "自动发现开启无线调试的设备,更新快速设备的随机端口并尝试连接(效果比较随缘)",
checked = adbAutoReconnectPairedDevice,
onCheckedChange = onAdbAutoReconnectPairedDeviceChange,
)
} }
} }
// TODO: 放进 [AppPageLazyColumn] 里
item { Spacer(Modifier.height(UiSpacing.BottomContent)) }
} }
} }

View File

@@ -20,6 +20,10 @@ internal data class MainSettings(
val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI, val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI,
val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT, val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT,
val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT, val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT,
val adbPairingAutoDiscoverOnDialogOpen: Boolean =
AppDefaults.ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN,
val adbAutoReconnectPairedDevice: Boolean = AppDefaults.ADB_AUTO_RECONNECT_PAIRED_DEVICE,
val adbMdnsLanDiscoveryEnabled: Boolean = AppDefaults.ADB_MDNS_LAN_DISCOVERY,
) )
internal data class DevicePageSettings( internal data class DevicePageSettings(
@@ -125,6 +129,18 @@ internal fun loadMainSettings(context: Context): MainSettings {
AppPreferenceKeys.ADB_KEY_NAME, AppPreferenceKeys.ADB_KEY_NAME,
AppDefaults.ADB_KEY_NAME_INPUT, AppDefaults.ADB_KEY_NAME_INPUT,
).orEmpty(), ).orEmpty(),
adbPairingAutoDiscoverOnDialogOpen = prefs.getBoolean(
AppPreferenceKeys.ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN,
AppDefaults.ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN,
),
adbAutoReconnectPairedDevice = prefs.getBoolean(
AppPreferenceKeys.ADB_AUTO_RECONNECT_PAIRED_DEVICE,
AppDefaults.ADB_AUTO_RECONNECT_PAIRED_DEVICE,
),
adbMdnsLanDiscoveryEnabled = prefs.getBoolean(
AppPreferenceKeys.ADB_MDNS_LAN_DISCOVERY,
AppDefaults.ADB_MDNS_LAN_DISCOVERY,
),
) )
} }
@@ -189,6 +205,18 @@ internal fun saveMainSettings(context: Context, settings: MainSettings) {
AppPreferenceKeys.ADB_KEY_NAME, AppPreferenceKeys.ADB_KEY_NAME,
settings.adbKeyName, settings.adbKeyName,
) )
.putBoolean(
AppPreferenceKeys.ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN,
settings.adbPairingAutoDiscoverOnDialogOpen,
)
.putBoolean(
AppPreferenceKeys.ADB_AUTO_RECONNECT_PAIRED_DEVICE,
settings.adbAutoReconnectPairedDevice,
)
.putBoolean(
AppPreferenceKeys.ADB_MDNS_LAN_DISCOVERY,
settings.adbMdnsLanDiscoveryEnabled,
)
} }
} }

View File

@@ -110,3 +110,28 @@ internal fun updateQuickDeviceNameIfEmpty(
saveQuickDevices(context, quickDevices) saveQuickDevices(context, quickDevices)
} }
} }
internal fun replaceQuickDevicePort(
context: Context,
quickDevices: MutableList<DeviceShortcut>,
host: String,
oldPort: Int,
newPort: Int,
online: Boolean,
) {
val idx = quickDevices.indexOfFirst { it.host == host && it.port == oldPort }
if (idx < 0) return
val old = quickDevices[idx]
val updated = old.copy(
id = "$host:$newPort",
port = newPort,
online = online,
)
quickDevices[idx] = updated
val dedup = quickDevices.distinctBy { it.id }
quickDevices.clear()
quickDevices.addAll(dedup)
saveQuickDevices(context, quickDevices)
}

View File

@@ -43,6 +43,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -70,6 +71,9 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.Button
import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
@@ -205,6 +209,8 @@ internal fun StatusCard(
@Composable @Composable
internal fun PairingCard( internal fun PairingCard(
busy: Boolean, busy: Boolean,
autoDiscoverOnDialogOpen: Boolean,
onDiscoverTarget: (() -> Pair<String, Int>?)? = null,
onPair: (host: String, port: String, code: String) -> Unit, onPair: (host: String, port: String, code: String) -> Unit,
) { ) {
val showPairDialog = remember { mutableStateOf(false) } val showPairDialog = remember { mutableStateOf(false) }
@@ -225,6 +231,8 @@ internal fun PairingCard(
PairingDialog( PairingDialog(
showDialog = showPairDialog.value, showDialog = showPairDialog.value,
enabled = !busy, enabled = !busy,
autoDiscoverOnDialogOpen = autoDiscoverOnDialogOpen,
onDiscoverTarget = onDiscoverTarget,
onDismissRequest = { showPairDialog.value = false }, onDismissRequest = { showPairDialog.value = false },
onDismissFinished = { holdDownState.value = false }, onDismissFinished = { holdDownState.value = false },
onConfirm = { host, port, code -> onConfirm = { host, port, code ->
@@ -480,6 +488,8 @@ internal fun ConfigPanel(
private fun PairingDialog( private fun PairingDialog(
showDialog: Boolean, showDialog: Boolean,
enabled: Boolean, enabled: Boolean,
autoDiscoverOnDialogOpen: Boolean,
onDiscoverTarget: (() -> Pair<String, Int>?)?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDismissFinished: () -> Unit, onDismissFinished: () -> Unit,
onConfirm: (host: String, port: String, code: String) -> Unit, onConfirm: (host: String, port: String, code: String) -> Unit,
@@ -487,11 +497,31 @@ private fun PairingDialog(
var host by rememberSaveable(showDialog) { mutableStateOf("") } var host by rememberSaveable(showDialog) { mutableStateOf("") }
var port by rememberSaveable(showDialog) { mutableStateOf("") } var port by rememberSaveable(showDialog) { mutableStateOf("") }
var code by rememberSaveable(showDialog) { mutableStateOf("") } var code by rememberSaveable(showDialog) { mutableStateOf("") }
var discoveringPort by rememberSaveable(showDialog) { mutableStateOf(false) }
val scope = rememberCoroutineScope()
suspend fun doDiscover() {
if (onDiscoverTarget == null || discoveringPort || !enabled) return
discoveringPort = true
val found = withContext(Dispatchers.IO) { onDiscoverTarget.invoke() }
if (found != null) {
host = found.first
port = found.second.toString()
}
discoveringPort = false
}
LaunchedEffect(showDialog, autoDiscoverOnDialogOpen, onDiscoverTarget, enabled) {
if (showDialog && autoDiscoverOnDialogOpen && onDiscoverTarget != null && !discoveringPort) {
doDiscover()
}
}
fun clearInputs() { fun clearInputs() {
host = "" host = ""
port = "" port = ""
code = "" code = ""
discoveringPort = false
} }
SuperDialog( SuperDialog(
@@ -530,9 +560,30 @@ private fun PairingDialog(
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = UiSpacing.Large), .padding(bottom = UiSpacing.CardContent),
) )
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal)) {
TextButton(
text = if (discoveringPort) "发现中..." else "自动发现",
onClick = {
if (onDiscoverTarget == null || discoveringPort || !enabled) return@TextButton
scope.launch {
doDiscover()
}
},
enabled = enabled && onDiscoverTarget != null && !discoveringPort,
modifier = Modifier
.fillMaxWidth()
.padding(
top = UiSpacing.Medium,
bottom = UiSpacing.CardContent,
),
)
Row(
modifier = Modifier
.padding(bottom = UiSpacing.PopupHorizontal),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal),
) {
TextButton( TextButton(
text = "取消", text = "取消",
onClick = { onClick = {
@@ -544,10 +595,13 @@ private fun PairingDialog(
TextButton( TextButton(
text = "配对", text = "配对",
onClick = { onClick = {
onConfirm(host.trim(), port.trim().ifBlank { "37099" }, code.trim()) onConfirm(host.trim(), port.trim(), code.trim())
clearInputs() clearInputs()
}, },
enabled = enabled && host.isNotBlank() && code.isNotBlank(), enabled = enabled &&
host.trim().isNotBlank() &&
port.trim().isNotBlank() &&
code.trim().isNotBlank(),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary(), colors = ButtonDefaults.textButtonColorsPrimary(),
) )
@@ -570,9 +624,7 @@ private fun formatBitRate(value: Float): String = String.format("%.1f", value)
@Composable @Composable
internal fun LogsPanel(lines: List<String>) { internal fun LogsPanel(lines: List<String>) {
Card( Card {
pressFeedbackType = PressFeedbackType.Sink,
) {
TextField( TextField(
value = lines.joinToString(separator = "\n"), value = lines.joinToString(separator = "\n"),
onValueChange = {}, onValueChange = {},

View File

@@ -129,9 +129,11 @@ internal fun StatusCardLayout(
} }
} }
Column(modifier = Modifier Column(
.weight(1f) modifier = Modifier
.fillMaxHeight()) { .weight(1f)
.fillMaxHeight()
) {
StatusMetricCard( StatusMetricCard(
spec = spec.firstSmall, spec = spec.firstSmall,
modifier = Modifier modifier = Modifier

View File

@@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.22.1)
project(scrcpyforandroid)
set(CMAKE_CXX_STANDARD 17)
add_compile_options(-fno-exceptions -fno-rtti -fno-threadsafe-statics)
find_package(boringssl REQUIRED CONFIG)
find_package(cxx REQUIRED CONFIG)
add_library(adbpairing SHARED adb_pairing.cpp)
target_link_libraries(
adbpairing
log
boringssl::crypto_static
cxx::cxx
)

View File

@@ -0,0 +1,302 @@
#include <jni.h>
#include <cstring>
#include <cstdlib>
#include <cinttypes>
#include <openssl/curve25519.h>
#include <openssl/hkdf.h>
#include <openssl/evp.h>
#include "adb_pairing.h"
#define LOG_TAG "AdbPairClient"
#include "logging.h"
static constexpr spake2_role_t kClientRole = spake2_role_alice;
static constexpr spake2_role_t kServerRole = spake2_role_bob;
static const uint8_t kClientName[] = "adb pair client";
static const uint8_t kServerName[] = "adb pair server";
static constexpr size_t kHkdfKeyLength = 16;
struct PairingContextNative {
SPAKE2_CTX* spake2_ctx;
uint8_t key[SPAKE2_MAX_MSG_SIZE];
size_t key_size;
EVP_AEAD_CTX* aes_ctx;
uint64_t dec_sequence;
uint64_t enc_sequence;
};
static jlong PairingContext_Constructor(JNIEnv* env, jclass clazz, jboolean isClient, jbyteArray jPassword) {
spake2_role_t spake_role;
const uint8_t* my_name;
const uint8_t* their_name;
size_t my_len;
size_t their_len;
if (isClient) {
spake_role = kClientRole;
my_name = kClientName;
my_len = sizeof(kClientName);
their_name = kServerName;
their_len = sizeof(kServerName);
} else {
spake_role = kServerRole;
my_name = kServerName;
my_len = sizeof(kServerName);
their_name = kClientName;
their_len = sizeof(kClientName);
}
auto spake2_ctx = SPAKE2_CTX_new(spake_role, my_name, my_len, their_name, their_len);
if (spake2_ctx == nullptr) {
LOGE("Unable to create a SPAKE2 context.");
return 0;
}
auto pswd_size = env->GetArrayLength(jPassword);
auto pswd = env->GetByteArrayElements(jPassword, nullptr);
size_t key_size = 0;
uint8_t key[SPAKE2_MAX_MSG_SIZE];
int status = SPAKE2_generate_msg(
spake2_ctx,
key,
&key_size,
SPAKE2_MAX_MSG_SIZE,
reinterpret_cast<uint8_t*>(pswd),
pswd_size
);
if (status != 1 || key_size == 0) {
LOGE("Unable to generate the SPAKE2 public key.");
env->ReleaseByteArrayElements(jPassword, pswd, 0);
SPAKE2_CTX_free(spake2_ctx);
return 0;
}
env->ReleaseByteArrayElements(jPassword, pswd, 0);
auto ctx = reinterpret_cast<PairingContextNative*>(malloc(sizeof(PairingContextNative)));
memset(ctx, 0, sizeof(PairingContextNative));
ctx->spake2_ctx = spake2_ctx;
memcpy(ctx->key, key, SPAKE2_MAX_MSG_SIZE);
ctx->key_size = key_size;
return reinterpret_cast<jlong>(ctx);
}
static jbyteArray PairingContext_Msg(JNIEnv* env, jobject obj, jlong ptr) {
auto ctx = reinterpret_cast<PairingContextNative*>(ptr);
jbyteArray our_msg = env->NewByteArray(static_cast<jsize>(ctx->key_size));
env->SetByteArrayRegion(our_msg, 0, static_cast<jsize>(ctx->key_size), reinterpret_cast<jbyte*>(ctx->key));
return our_msg;
}
static jboolean PairingContext_InitCipher(JNIEnv* env, jobject obj, jlong ptr, jbyteArray jTheirMsg) {
auto res = JNI_TRUE;
auto ctx = reinterpret_cast<PairingContextNative*>(ptr);
auto spake2_ctx = ctx->spake2_ctx;
auto their_msg_size = env->GetArrayLength(jTheirMsg);
if (their_msg_size > SPAKE2_MAX_MSG_SIZE) {
LOGE("their_msg size [%d] greater then max size [%d].", their_msg_size, SPAKE2_MAX_MSG_SIZE);
return JNI_FALSE;
}
auto their_msg = env->GetByteArrayElements(jTheirMsg, nullptr);
size_t key_material_len = 0;
uint8_t key_material[SPAKE2_MAX_KEY_SIZE];
int status = SPAKE2_process_msg(
spake2_ctx,
key_material,
&key_material_len,
sizeof(key_material),
reinterpret_cast<uint8_t*>(their_msg),
their_msg_size
);
env->ReleaseByteArrayElements(jTheirMsg, their_msg, 0);
if (status != 1) {
LOGE("Unable to process their public key");
return JNI_FALSE;
}
// --------
uint8_t key[kHkdfKeyLength];
uint8_t info[] = "adb pairing_auth aes-128-gcm key";
status = HKDF(
key,
sizeof(key),
EVP_sha256(),
key_material,
key_material_len,
nullptr,
0,
info,
sizeof(info) - 1
);
if (status != 1) {
LOGE("HKDF");
return JNI_FALSE;
}
ctx->aes_ctx = EVP_AEAD_CTX_new(EVP_aead_aes_128_gcm(), key, sizeof(key), EVP_AEAD_DEFAULT_TAG_LENGTH);
if (!ctx->aes_ctx) {
LOGE("EVP_AEAD_CTX_new");
return JNI_FALSE;
}
return res;
}
static jbyteArray PairingContext_Encrypt(JNIEnv* env, jobject obj, jlong ptr, jbyteArray jIn) {
auto ctx = reinterpret_cast<PairingContextNative*>(ptr);
auto aes_ctx = ctx->aes_ctx;
auto in = env->GetByteArrayElements(jIn, nullptr);
auto in_size = env->GetArrayLength(jIn);
auto out_size = static_cast<size_t>(in_size) + EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(aes_ctx));
auto out = reinterpret_cast<uint8_t*>(malloc(out_size));
if (!out) {
env->ReleaseByteArrayElements(jIn, in, 0);
return nullptr;
}
auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx));
auto nonce = reinterpret_cast<uint8_t*>(malloc(nonce_size));
if (!nonce) {
free(out);
env->ReleaseByteArrayElements(jIn, in, 0);
return nullptr;
}
memset(nonce, 0, nonce_size);
memcpy(nonce, &ctx->enc_sequence, sizeof(ctx->enc_sequence));
size_t written_sz;
int status = EVP_AEAD_CTX_seal(
aes_ctx,
out,
&written_sz,
out_size,
nonce,
nonce_size,
reinterpret_cast<uint8_t*>(in),
in_size,
nullptr,
0
);
env->ReleaseByteArrayElements(jIn, in, 0);
free(nonce);
if (!status) {
LOGE("Failed to encrypt (in_len=%d, out_cap=%" PRIuPTR ")", in_size, out_size);
free(out);
return nullptr;
}
++ctx->enc_sequence;
jbyteArray jOut = env->NewByteArray(static_cast<jsize>(written_sz));
env->SetByteArrayRegion(jOut, 0, static_cast<jsize>(written_sz), reinterpret_cast<jbyte*>(out));
free(out);
return jOut;
}
static jbyteArray PairingContext_Decrypt(JNIEnv* env, jobject obj, jlong ptr, jbyteArray jIn) {
auto ctx = reinterpret_cast<PairingContextNative*>(ptr);
auto aes_ctx = ctx->aes_ctx;
auto in = env->GetByteArrayElements(jIn, nullptr);
auto in_size = env->GetArrayLength(jIn);
auto out_size = static_cast<size_t>(in_size);
auto out = reinterpret_cast<uint8_t*>(malloc(out_size));
if (!out) {
env->ReleaseByteArrayElements(jIn, in, 0);
return nullptr;
}
auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx));
auto nonce = reinterpret_cast<uint8_t*>(malloc(nonce_size));
if (!nonce) {
free(out);
env->ReleaseByteArrayElements(jIn, in, 0);
return nullptr;
}
memset(nonce, 0, nonce_size);
memcpy(nonce, &ctx->dec_sequence, sizeof(ctx->dec_sequence));
size_t written_sz;
int status = EVP_AEAD_CTX_open(
aes_ctx,
out,
&written_sz,
out_size,
nonce,
nonce_size,
reinterpret_cast<uint8_t*>(in),
in_size,
nullptr,
0
);
env->ReleaseByteArrayElements(jIn, in, 0);
free(nonce);
if (!status) {
LOGE("Failed to decrypt (in_len=%d, out_cap=%" PRIuPTR ")", in_size, out_size);
free(out);
return nullptr;
}
++ctx->dec_sequence;
jbyteArray jOut = env->NewByteArray(static_cast<jsize>(written_sz));
env->SetByteArrayRegion(jOut, 0, static_cast<jsize>(written_sz), reinterpret_cast<jbyte*>(out));
free(out);
return jOut;
}
static void PairingContext_Destroy(JNIEnv* env, jobject obj, jlong ptr) {
auto ctx = reinterpret_cast<PairingContextNative*>(ptr);
if (!ctx) return;
SPAKE2_CTX_free(ctx->spake2_ctx);
if (ctx->aes_ctx) {
EVP_AEAD_CTX_free(ctx->aes_ctx);
}
free(ctx);
}
// ---------------------------------------------------------
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = nullptr;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
return -1;
JNINativeMethod methods[] = {
{"nativeConstructor", "(Z[B)J", reinterpret_cast<void*>(PairingContext_Constructor)},
{"nativeMsg", "(J)[B", reinterpret_cast<void*>(PairingContext_Msg)},
{"nativeInitCipher", "(J[B)Z", reinterpret_cast<void*>(PairingContext_InitCipher)},
{"nativeEncrypt", "(J[B)[B", reinterpret_cast<void*>(PairingContext_Encrypt)},
{"nativeDecrypt", "(J[B)[B", reinterpret_cast<void*>(PairingContext_Decrypt)},
{"nativeDestroy", "(J)V", reinterpret_cast<void*>(PairingContext_Destroy)},
};
jclass clazz = env->FindClass("io/github/miuzarte/scrcpyforandroid/nativecore/PairingContext");
if (clazz == nullptr) {
return -1;
}
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(JNINativeMethod)) != 0) {
return -1;
}
return JNI_VERSION_1_6;
}

View File

@@ -0,0 +1,4 @@
#ifndef SCRCPY_FOR_ANDROID_ADB_PAIRING_H
#define SCRCPY_FOR_ANDROID_ADB_PAIRING_H
#endif // SCRCPY_FOR_ANDROID_ADB_PAIRING_H

View File

@@ -0,0 +1,31 @@
#ifndef _LOGGING_H
#define _LOGGING_H
#include <errno.h>
#include <string.h>
#include "android/log.h"
#ifndef LOG_TAG
#define LOG_TAG "ScrcpyForAndroid"
#endif
#ifndef NO_LOG
#ifndef NO_DEBUG_LOG
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#else
#define LOGD(...)
#endif
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno))
#else
#define LOGD(...)
#define LOGV(...)
#define LOGI(...)
#define LOGW(...)
#define LOGE(...)
#define PLOGE(fmt, args...)
#endif
#endif // _LOGGING_H