Draft for supporting using system configuration for temporary hotspot

Attempt at addressing #166.
This commit is contained in:
Mygod
2021-05-31 01:21:40 -04:00
parent 229b190c22
commit bc25cdb0bb
11 changed files with 204 additions and 42 deletions

View File

@@ -302,6 +302,7 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
* (since API 28) `Landroid/net/wifi/WifiManager;->registerSoftApCallback(Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->setSoftApConfiguration(Landroid/net/wifi/SoftApConfiguration;)Z,sdk,system-api,test-api`
* (prior to API 30) `Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,sdk,system-api,test-api`
* (since API 30) `Landroid/net/wifi/WifiManager;->startLocalOnlyHotspot(Landroid/net/wifi/SoftApConfiguration;Ljava/util/concurrent/Executor;Landroid/net/wifi/WifiManager$LocalOnlyHotspotCallback;)V,sdk,system-api,test-api`
* (since API 28) `Landroid/net/wifi/WifiManager;->unregisterSoftApCallback(Landroid/net/wifi/WifiManager$SoftApCallback;)V,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,sdk,system-api,test-api`
* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api`

View File

@@ -5,14 +5,20 @@ import android.content.IntentFilter
import android.net.wifi.WifiManager
import android.os.Build
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.RootServer
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.root.LocalOnlyHotspotCallbacks
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -23,6 +29,10 @@ import java.net.Inet4Address
@RequiresApi(26)
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
companion object {
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
}
inner class Binder : android.os.Binder() {
/**
* null represents IDLE, "" represents CONNECTING, "something" represents CONNECTED.
@@ -33,10 +43,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
ifaceChanged(value)
}
val ifaceChanged = StickyEvent1 { iface }
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
reservation?.wifiConfiguration?.toCompat()
} else reservation?.softApConfiguration?.toCompat()
val configuration get() = reservation?.configuration
fun stop() {
when (iface) {
@@ -47,8 +54,65 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
}
}
interface Reservation : AutoCloseable {
val configuration: SoftApConfigurationCompat?
}
class Framework(private val reservation: WifiManager.LocalOnlyHotspotReservation) : Reservation {
override val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
reservation.wifiConfiguration?.toCompat()
} else reservation.softApConfiguration.toCompat()
override fun close() = reservation.close()
}
@RequiresApi(30)
inner class Root(rootServer: RootServer) : Reservation {
private val channel = rootServer.create(WifiApCommands.StartLocalOnlyHotspot(), this@LocalOnlyHotspotService)
override var configuration: SoftApConfigurationCompat? = null
private set
override fun close() = channel.cancel()
suspend fun work() {
for (callback in channel) when (callback) {
is LocalOnlyHotspotCallbacks.OnStarted -> configuration = callback.config.toCompat()
is LocalOnlyHotspotCallbacks.OnStopped -> reservation = null
is LocalOnlyHotspotCallbacks.OnFailed -> onFrameworkFailed(callback.reason)
}
}
}
private val binder = Binder()
private var reservation: WifiManager.LocalOnlyHotspotReservation? = null
private var reservation: Reservation? = null
set(value) {
field = value
if (value != null && !receiverRegistered) {
val configuration = binder.configuration
if (Build.VERSION.SDK_INT < 30 && configuration!!.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis, coroutineContext) {
value.close()
}
}
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true
}
}
private fun onFrameworkFailed(reason: Int) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure, when (reason) {
WifiManager.LocalOnlyHotspotCallback.ERROR_NO_CHANNEL -> {
getString(R.string.tethering_temp_hotspot_failure_no_channel)
}
WifiManager.LocalOnlyHotspotCallback.ERROR_GENERIC -> {
getString(R.string.tethering_temp_hotspot_failure_generic)
}
WifiManager.LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE -> {
getString(R.string.tethering_temp_hotspot_failure_incompatible_mode)
}
WifiManager.LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED -> {
getString(R.string.tethering_temp_hotspot_failure_tethering_disallowed)
}
else -> getString(R.string.failure_reason_unknown, reason)
})).show()
stopService()
}
/**
* Writes and critical reads to routingManager should be protected with this context.
*/
@@ -82,40 +146,35 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
if (binder.iface != null) return START_STICKY
binder.iface = ""
updateNotification() // show invisible foreground notification to avoid being killed
launch(start = CoroutineStart.UNDISPATCHED) { doStart() }
return START_STICKY
}
private suspend fun doStart() {
if (Build.VERSION.SDK_INT >= 30 && app.pref.getBoolean(KEY_USE_SYSTEM, false)) try {
RootManager.use {
Root(it).apply {
reservation = this
work()
}
}
return
} catch (_: CancellationException) {
return
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
try {
Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = reservation
if (!receiverRegistered) {
val configuration = binder.configuration!!
if (Build.VERSION.SDK_INT < 30 && configuration.isAutoShutdownEnabled) {
timeoutMonitor = TetherTimeoutMonitor(configuration.shutdownTimeoutMillis,
coroutineContext) { reservation.close() }
}
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
receiverRegistered = true
}
this@LocalOnlyHotspotService.reservation = Framework(reservation)
}
}
override fun onStopped() {
Timber.d("LOHCallback.onStopped")
reservation = null
}
override fun onFailed(reason: Int) {
SmartSnackbar.make(getString(R.string.tethering_temp_hotspot_failure, when (reason) {
ERROR_NO_CHANNEL -> getString(R.string.tethering_temp_hotspot_failure_no_channel)
ERROR_GENERIC -> getString(R.string.tethering_temp_hotspot_failure_generic)
ERROR_INCOMPATIBLE_MODE -> getString(R.string.tethering_temp_hotspot_failure_incompatible_mode)
ERROR_TETHERING_DISALLOWED -> {
getString(R.string.tethering_temp_hotspot_failure_tethering_disallowed)
}
else -> getString(R.string.failure_reason_unknown, reason)
})).show()
stopService()
}
override fun onFailed(reason: Int) = onFrameworkFailed(reason)
}, null)
} catch (e: IllegalStateException) {
// throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
@@ -128,7 +187,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
SmartSnackbar.make(e).show()
stopService()
}
return START_STICKY
}
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot
import android.annotation.TargetApi
import android.content.Intent
import android.os.Build
import android.os.Bundle
@@ -38,6 +39,8 @@ import java.io.PrintWriter
import kotlin.system.exitProcess
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
private fun Preference.remove() = parent!!.removePreference(this)
@TargetApi(26)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// handle complicated default value and possible system upgrades
WifiDoubleLock.mode = WifiDoubleLock.mode
@@ -65,7 +68,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
}
false
}
} else parent!!.removePreference(this)
} else remove()
}
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
if (Services.p2p != null) {
@@ -74,11 +77,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
true
}
boot.isChecked = BootReceiver.enabled
} else boot.parent!!.removePreference(boot)
} else boot.remove()
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
safeMode.parent!!.removePreference(safeMode)
safeMode.remove()
}
if (Build.VERSION.SDK_INT < 30) findPreference<Preference>(LocalOnlyHotspotService.KEY_USE_SYSTEM)!!.remove()
findPreference<Preference>("service.clean")!!.setOnPreferenceClickListener {
GlobalScope.launch { RoutingManager.clean() }
true

View File

@@ -191,7 +191,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
apConfigurationRunning = true
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
try {
WifiApManager.configuration
WifiApManager.configurationCompat
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
try {
@@ -237,7 +237,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
SmartSnackbar.make(e).show()
}
val success = try {
WifiApManager.setConfiguration(configuration)
WifiApManager.setConfigurationCompat(configuration)
} catch (e: InvocationTargetException) {
try {
RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) }

View File

@@ -72,16 +72,20 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
}
@get:RequiresApi(30)
val configuration get() = getSoftApConfiguration(Services.wifi) as SoftApConfiguration
/**
* Requires NETWORK_SETTINGS permission (or root) on API 30+, and OVERRIDE_WIFI_CONFIG on API 29-.
*/
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
val configurationCompat get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
?: SoftApConfigurationCompat()
} else (getSoftApConfiguration(Services.wifi) as SoftApConfiguration).toCompat()
fun setConfiguration(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
} else configuration.toCompat()
fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) {
setSoftApConfiguration(Services.wifi, value.toPlatform())
} else @Suppress("DEPRECATION") {
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
}) as Boolean
@RequiresApi(28)
interface SoftApCallbackCompat {
@@ -236,6 +240,16 @@ object WifiApManager {
@RequiresApi(28)
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
@get:RequiresApi(30)
private val startLocalOnlyHotspot by lazy @TargetApi(30) {
WifiManager::class.java.getDeclaredMethod("startLocalOnlyHotspot", SoftApConfiguration::class.java,
Executor::class.java, WifiManager.LocalOnlyHotspotCallback::class.java)
}
@RequiresApi(30)
fun startLocalOnlyHotspot(config: SoftApConfiguration, callback: WifiManager.LocalOnlyHotspotCallback?,
executor: Executor? = null) =
startLocalOnlyHotspot(Services.wifi, config, executor, callback)
private val cancelLocalOnlyHotspotRequest by lazy {
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
}

View File

@@ -0,0 +1,19 @@
package be.mygod.vpnhotspot.root
import android.net.wifi.SoftApConfiguration
import android.os.Parcelable
import androidx.annotation.RequiresApi
import kotlinx.parcelize.Parcelize
@RequiresApi(30)
sealed class LocalOnlyHotspotCallbacks : Parcelable {
@Parcelize
data class OnStarted(val config: SoftApConfiguration) : LocalOnlyHotspotCallbacks()
@Parcelize
class OnStopped : LocalOnlyHotspotCallbacks() {
override fun equals(other: Any?) = other is OnStopped
override fun hashCode() = 0x80acd3ca.toInt()
}
@Parcelize
data class OnFailed(val reason: Int) : LocalOnlyHotspotCallbacks()
}

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot.root
import android.net.wifi.WifiManager
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.ParcelableBoolean
@@ -62,6 +63,7 @@ object WifiApCommands {
private fun push(parcel: SoftApCallbackParcel) {
trySend(parcel).onClosed {
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
return
}.onFailure { throw it!! }
}
@@ -149,11 +151,60 @@ object WifiApCommands {
@Parcelize
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
override suspend fun execute() = WifiApManager.configuration
override suspend fun execute() = WifiApManager.configurationCompat
}
@Parcelize
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfigurationCompat(configuration))
}
@Parcelize
@RequiresApi(30)
class StartLocalOnlyHotspot : RootCommandChannel<LocalOnlyHotspotCallbacks> {
override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) {
val finish = CompletableDeferred<Unit>()
var lohr: WifiManager.LocalOnlyHotspotReservation? = null
WifiApManager.startLocalOnlyHotspot(WifiApManager.configuration, object :
WifiManager.LocalOnlyHotspotCallback() {
private fun push(parcel: LocalOnlyHotspotCallbacks) {
trySend(parcel).onClosed {
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
return
}.onFailure { throw it!! }
}
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) onFailed(-3) else {
require(lohr == null)
lohr = reservation
push(LocalOnlyHotspotCallbacks.OnStarted(reservation.softApConfiguration))
}
}
override fun onStopped() {
push(LocalOnlyHotspotCallbacks.OnStopped())
finish.complete(Unit)
}
override fun onFailed(reason: Int) {
push(LocalOnlyHotspotCallbacks.OnFailed(reason))
finish.complete(Unit)
}
}) {
scope.launch {
try {
it.run()
} catch (e: Throwable) {
finish.completeExceptionally(e)
}
}
}
try {
finish.await()
} catch (e: Exception) {
WifiApManager.cancelLocalOnlyHotspotRequest()
throw e
} finally {
lohr?.close()
}
}
}
}

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"/>
</vector>

View File

@@ -113,6 +113,8 @@
<string name="settings_service_repeater_safe_mode">中继安全模式</string>
<string name="settings_service_repeater_safe_mode_summary">不对系统配置进行修改,但是可能须要较长的网络名称。</string>
<string name="settings_service_repeater_safe_mode_warning">使用短名称可能需要关闭安全模式。</string>
<string name="settings_service_temp_hotspot_use_system">临时 WLAN 热点使用系统配置</string>
<string name="settings_service_temp_hotspot_use_system_summary">这将与其他使用本地热点的应用冲突</string>
<string name="settings_service_wifi_lock">保持 Wi\u2011Fi 开启</string>
<string name="settings_service_wifi_lock_none">系统默认</string>
<string name="settings_service_wifi_lock_full"></string>

View File

@@ -131,6 +131,9 @@
not work with short network names.</string>
<string name="settings_service_repeater_safe_mode_warning">Short network names might require turning off safe
mode.</string>
<string name="settings_service_temp_hotspot_use_system">Use system configuration for temporary hotspot</string>
<string name="settings_service_temp_hotspot_use_system_summary">Will conflict with other apps using local only
hotspot</string>
<string name="settings_service_wifi_lock">Keep Wi\u2011Fi alive</string>
<string name="settings_service_wifi_lock_none">System default</string>
<string name="settings_service_wifi_lock_full">On</string>

View File

@@ -68,6 +68,11 @@
app:title="@string/settings_service_repeater_safe_mode"
app:summary="@string/settings_service_repeater_safe_mode_summary"
app:defaultValue="true"/>
<SwitchPreference
app:key="service.tempHotspot.useSystem"
app:icon="@drawable/ic_content_file_copy"
app:title="@string/settings_service_temp_hotspot_use_system"
app:summary="@string/settings_service_temp_hotspot_use_system_summary"/>
<com.takisoft.preferencex.SimpleMenuPreference
app:key="service.ipMonitor"
app:icon="@drawable/ic_hardware_device_hub"