diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 10ccdf499..15b085fad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ + + post { updateText(port) } + } + } + setOnCheckedChangeListener(object : CompoundButton.OnCheckedChangeListener { override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { val thisRef = this @@ -54,4 +74,9 @@ class SwitchOptionDialog : BaseOptionDialog() { }) } } -} \ No newline at end of file + + override fun onDestroyView() { + (preference as? AdbWifiPreference)?.onPortDiscoveredListener = null + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/zacharee1/systemuituner/prefs/secure/specific/AdbWifiPreference.kt b/app/src/main/java/com/zacharee1/systemuituner/prefs/secure/specific/AdbWifiPreference.kt new file mode 100644 index 000000000..1bf7a8ffd --- /dev/null +++ b/app/src/main/java/com/zacharee1/systemuituner/prefs/secure/specific/AdbWifiPreference.kt @@ -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() + + 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 { + val addresses = mutableListOf() + 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() + .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() + } +} diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 7e3a6d6f3..0f583badd 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -90,6 +90,10 @@ Некоторые устройства автоматически отключают ADB после периода бездействия. Включите эту опцию и установите её в качестве постоянной настройки для принудительного включения ADB. Включить беспроводной ADB Беспроводной ADB, представленный пользователям в Android 11, позволяет соединить компьютеры и другие устройства с вашим устройством и использовать ADB без кабеля или специальных драйверов. + Поиск активного порта беспроводного ADB… + Беспроводной ADB активен. Текущий порт: %1$d\n\nБеспроводной ADB, представленный пользователям в Android 11, позволяет соединить компьютеры и другие устройства с вашим устройством и использовать ADB без кабеля или специальных драйверов. + Не удалось найти активный порт беспроводного ADB. Убедитесь, что беспроводной ADB включён. + %1$s (порт: %2$d) Включить Глобальный тёмный режим В Android версий Pie и 10 присутствует функция глобального тёмного режима, но она уже была частью Android до этого. Эта опция может включить тёмный режим в приложениях, поддерживающих эту функцию (например, Instagram). Может потребоваться перезагрузка для вступления изменений. Показывать секунды на часах diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91c6b9614..f97721047 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + SystemUI Tuner Manager (Turn Off) @@ -117,6 +117,10 @@ Enable Wireless ADB 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. + Searching for active Wireless ADB port… + 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. + Couldn\'t find the active Wireless ADB port. Make sure Wireless ADB is enabled. + %1$s (port: %2$d) Enable Global Dark Mode 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. diff --git a/app/src/main/res/xml/prefs_developer.xml b/app/src/main/res/xml/prefs_developer.xml index 38cf9e66b..ca3071bc3 100644 --- a/app/src/main/res/xml/prefs_developer.xml +++ b/app/src/main/res/xml/prefs_developer.xml @@ -11,7 +11,7 @@ app:settings_type="global" /> -