Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<uses-permission android:name="com.zacharee1.systemuituner.permission.WRITE_SETTINGS" />
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

<application
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.view.View
import android.widget.CompoundButton
import com.google.android.material.materialswitch.MaterialSwitch
import com.zacharee1.systemuituner.R
import com.zacharee1.systemuituner.prefs.secure.specific.AdbWifiPreference
import kotlinx.coroutines.launch

class SwitchOptionDialog : BaseOptionDialog() {
Expand Down Expand Up @@ -38,6 +39,25 @@ class SwitchOptionDialog : BaseOptionDialog() {
text = preference.title
isChecked = shouldBeChecked

val adbWifiPref = preference as? AdbWifiPreference
if (adbWifiPref != null) {
fun updateText(port: Int?) {
text = if (port != null) {
context.getString(
R.string.feature_enable_wireless_adb_title_with_port,
preference.title,
port
)
} else {
preference.title
}
}
updateText(adbWifiPref.discoveredPort)
adbWifiPref.onPortDiscoveredListener = { port ->
post { updateText(port) }
}
}

setOnCheckedChangeListener(object : CompoundButton.OnCheckedChangeListener {
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
val thisRef = this
Expand All @@ -54,4 +74,9 @@ class SwitchOptionDialog : BaseOptionDialog() {
})
}
}
}

override fun onDestroyView() {
(preference as? AdbWifiPreference)?.onPortDiscoveredListener = null
super.onDestroyView()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
package com.zacharee1.systemuituner.prefs.secure.specific

import android.content.Context
import android.database.ContentObserver
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.AttributeSet
import com.zacharee1.systemuituner.R
import com.zacharee1.systemuituner.prefs.secure.SecureSwitchPreference
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import java.net.Inet4Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.charset.StandardCharsets

class AdbWifiPreference(context: Context, attrs: AttributeSet) :
SecureSwitchPreference(context, attrs) {
companion object {
private const val ADB_SERVICE_TYPE = "_adb-tls-connect._tcp."
private const val DISCOVERY_TIMEOUT_MILLIS = 15_000L
private const val PORT_SCAN_ATTEMPTS = 3
private const val PORT_SCAN_CONCURRENCY = 128
private const val SOCKET_CONNECT_TIMEOUT_MILLIS = 50
private const val SOCKET_READ_TIMEOUT_MILLIS = 500
private val PORTS = listOf(5555) + (33000..47000)
}

private var scope: CoroutineScope? = null
private var discoveryJob: Job? = null
private val nsdManager by lazy { context.getSystemService(Context.NSD_SERVICE) as NsdManager }

var discoveredPort: Int? = null
private set(value) {
field = value
onPortDiscoveredListener?.invoke(value)
}

var onPortDiscoveredListener: ((Int?) -> Unit)? = null

private val settingsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
updateState()
}
}

override fun onAttached() {
super.onAttached()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
context.contentResolver.registerContentObserver(
Settings.Global.getUriFor("adb_wifi_enabled"),
true,
settingsObserver
)
updateState()
}

override fun onDetached() {
try {
context.contentResolver.unregisterContentObserver(settingsObserver)
} catch (_: Exception) {}
stopDiscovery()
scope?.cancel()
scope = null
super.onDetached()
}

private fun updateState() {
val isEnabled = Settings.Global.getInt(context.contentResolver, "adb_wifi_enabled", 0) != 0
if (isEnabled) {
startDiscovery()
} else {
stopDiscovery()
summary = context.getString(R.string.feature_enable_wireless_adb_desc)
}
}

@Suppress("DEPRECATION")
private fun startDiscovery() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return

stopDiscovery()
summary = context.getString(R.string.feature_enable_wireless_adb_searching_port)

discoveryJob = scope?.launch {
val portFound = CompletableDeferred<Int>()

val multicastLock = try {
val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
wifi.createMulticastLock("AdbWifiPreference").apply {
setReferenceCounted(false)
acquire()
}
} catch (_: Exception) {
null
}

val discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) {}
override fun onDiscoveryStopped(serviceType: String) {}
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
nsdManager.resolveService(serviceInfo, object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {}
override fun onServiceResolved(resolvedServiceInfo: NsdServiceInfo) {
val resolvedHost = resolvedServiceInfo.host
val isLocal = resolvedHost?.isLoopbackAddress == true ||
getLocalIpAddresses().any { it == resolvedHost }
if (isLocal) {
portFound.complete(resolvedServiceInfo.port)
}
}
})
}
override fun onServiceLost(serviceInfo: NsdServiceInfo) {}
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {}
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {}
}

try {
nsdManager.discoverServices(
ADB_SERVICE_TYPE,
NsdManager.PROTOCOL_DNS_SD,
discoveryListener
)
} catch (_: Exception) {}

val portScanJob = launch(Dispatchers.IO) scan@{
repeat(PORT_SCAN_ATTEMPTS) {
if (!isActive) return@scan
scanLocalAdbPort()?.let {
portFound.complete(it)
return@scan
}
delay(1000)
}
}

try {
val port = withTimeoutOrNull(DISCOVERY_TIMEOUT_MILLIS) {
portFound.await()
}
if (port != null) {
discoveredPort = port
summary = context.getString(
R.string.feature_enable_wireless_adb_desc_with_port,
port
)
} else {
summary = context.getString(R.string.feature_enable_wireless_adb_port_not_found)
}
} catch (e: CancellationException) {
throw e
} finally {
portScanJob.cancel()
try { nsdManager.stopServiceDiscovery(discoveryListener) } catch (_: Exception) {}
try { multicastLock?.release() } catch (_: Exception) {}
}
}
}

private fun stopDiscovery() {
discoveryJob?.cancel()
discoveryJob = null
discoveredPort = null
}

private fun getLocalIpAddresses(): List<InetAddress> {
val addresses = mutableListOf<InetAddress>()
try {
val interfaces = NetworkInterface.getNetworkInterfaces()
while (interfaces.hasMoreElements()) {
val networkInterface = interfaces.nextElement()
val inetAddresses = networkInterface.inetAddresses
while (inetAddresses.hasMoreElements()) {
addresses.add(inetAddresses.nextElement())
}
}
} catch (_: Exception) {}
return addresses
}

private suspend fun scanLocalAdbPort(): Int? = coroutineScope {
val targets = buildList {
add("127.0.0.1")
addAll(
getLocalIpAddresses()
.filterIsInstance<Inet4Address>()
.mapNotNull { it.hostAddress }
)
}.distinct()

PORTS.chunked(PORT_SCAN_CONCURRENCY).forEach { ports ->
val discoveredPort = ports.map { port ->
async(Dispatchers.IO) {
targets.firstNotNullOfOrNull { ip ->
if (isAdbPort(ip, port)) port else null
}
}
}.awaitAll().firstOrNull()

if (discoveredPort != null) {
return@coroutineScope discoveredPort
}
}
null
}

private fun isAdbPort(ip: String, port: Int): Boolean {
return try {
Socket().use { socket ->
socket.connect(
InetSocketAddress(ip, port),
SOCKET_CONNECT_TIMEOUT_MILLIS
)
socket.soTimeout = SOCKET_READ_TIMEOUT_MILLIS
val outputStream = socket.getOutputStream()
outputStream.write(createAdbCnxnPacket())
outputStream.flush()

val inputStream = socket.getInputStream()
val response = ByteArray(24)
var offset = 0
while (offset < response.size) {
val read = inputStream.read(response, offset, response.size - offset)
if (read < 0) {
return false
}
offset += read
}

when (ByteBuffer.wrap(response).order(ByteOrder.LITTLE_ENDIAN).getInt()) {
0x4E584E43, // CNXN
0x48545541, // AUTH
0x534C5453, // STLS
-> true
else -> false
}
}
} catch (_: Exception) {
false
}
}

private fun createAdbCnxnPacket(): ByteArray {
val data = "host::\u0000".toByteArray(StandardCharsets.US_ASCII)
val checksum = data.sumOf { it.toInt() and 0xFF }
return ByteBuffer.allocate(24 + data.size)
.order(ByteOrder.LITTLE_ENDIAN)
.apply {
putInt(0x4E584E43) // CNXN
putInt(0x01000000) // Version 1.0
putInt(256 * 1024) // Max data
putInt(data.size)
putInt(checksum)
putInt(0x4E584E43.inv())
put(data)
}
.array()
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/values-ru-rRU/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
<string name="feature_enable_adb_desc">Некоторые устройства автоматически отключают ADB после периода бездействия. Включите эту опцию и установите её в качестве постоянной настройки для принудительного включения ADB.</string>
<string name="feature_enable_wireless_adb">Включить беспроводной ADB</string>
<string name="feature_enable_wireless_adb_desc">Беспроводной ADB, представленный пользователям в Android 11, позволяет соединить компьютеры и другие устройства с вашим устройством и использовать ADB без кабеля или специальных драйверов.</string>
<string name="feature_enable_wireless_adb_searching_port">Поиск активного порта беспроводного ADB…</string>
<string name="feature_enable_wireless_adb_desc_with_port">Беспроводной ADB активен. Текущий порт: %1$d\n\nБеспроводной ADB, представленный пользователям в Android 11, позволяет соединить компьютеры и другие устройства с вашим устройством и использовать ADB без кабеля или специальных драйверов.</string>
<string name="feature_enable_wireless_adb_port_not_found">Не удалось найти активный порт беспроводного ADB. Убедитесь, что беспроводной ADB включён.</string>
<string name="feature_enable_wireless_adb_title_with_port">%1$s (порт: %2$d)</string>
<string name="feature_dark_mode">Включить Глобальный тёмный режим</string>
<string name="feature_dark_mode_desc">В Android версий Pie и 10 присутствует функция глобального тёмного режима, но она уже была частью Android до этого. Эта опция может включить тёмный режим в приложениях, поддерживающих эту функцию (например, Instagram). Может потребоваться перезагрузка для вступления изменений.</string>
<string name="feature_clock_seconds">Показывать секунды на часах</string>
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">SystemUI Tuner</string>
<string name="manager_channel">Manager (Turn Off)</string>

Expand Down Expand Up @@ -117,6 +117,10 @@

<string name="feature_enable_wireless_adb">Enable Wireless ADB</string>
<string name="feature_enable_wireless_adb_desc">Wireless ADB, introduced to users in Android 11, allows you to pair computers and other devices with your device and use ADB without a cable or special drivers.</string>
<string name="feature_enable_wireless_adb_searching_port" tools:ignore="MissingTranslation">Searching for active Wireless ADB port…</string>
<string name="feature_enable_wireless_adb_desc_with_port" tools:ignore="MissingTranslation">Wireless ADB is active. Current port: %1$d\n\nWireless ADB, introduced to users in Android 11, allows you to pair computers and other devices with your device and use ADB without a cable or special drivers.</string>
<string name="feature_enable_wireless_adb_port_not_found" tools:ignore="MissingTranslation">Couldn\'t find the active Wireless ADB port. Make sure Wireless ADB is enabled.</string>
<string name="feature_enable_wireless_adb_title_with_port" tools:ignore="MissingTranslation">%1$s (port: %2$d)</string>

<string name="feature_dark_mode">Enable Global Dark Mode</string>
<string name="feature_dark_mode_desc">Android Pie and 10 introduced a global dark mode option, but this option has been part of Android for a while now. This option can enable dark mode in apps that support this feature (such as Instagram). A reboot may be required for changes to take effect.</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/xml/prefs_developer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
app:settings_type="global"
/>

<com.zacharee1.systemuituner.prefs.secure.SecureSwitchPreference
<com.zacharee1.systemuituner.prefs.secure.specific.AdbWifiPreference
android:title="@string/feature_enable_wireless_adb"
android:summary="@string/feature_enable_wireless_adb_desc"
android:key="adb_wifi_enabled"
Expand Down