Draft for supporting using system configuration for temporary hotspot
Attempt at addressing #166.
This commit is contained in:
@@ -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 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`
|
* (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`
|
* (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`
|
* (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/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`
|
* `Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;->onPersistentGroupInfoAvailable(Landroid/net/wifi/p2p/WifiP2pGroupList;)V,sdk,system-api,test-api`
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ import android.content.IntentFilter
|
|||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
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.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
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.SoftApConfigurationCompat.Companion.toCompat
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
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.Services
|
||||||
import be.mygod.vpnhotspot.util.StickyEvent1
|
import be.mygod.vpnhotspot.util.StickyEvent1
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
@@ -23,6 +29,10 @@ import java.net.Inet4Address
|
|||||||
|
|
||||||
@RequiresApi(26)
|
@RequiresApi(26)
|
||||||
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||||
|
companion object {
|
||||||
|
const val KEY_USE_SYSTEM = "service.tempHotspot.useSystem"
|
||||||
|
}
|
||||||
|
|
||||||
inner class Binder : android.os.Binder() {
|
inner class Binder : android.os.Binder() {
|
||||||
/**
|
/**
|
||||||
* null represents IDLE, "" represents CONNECTING, "something" represents CONNECTED.
|
* null represents IDLE, "" represents CONNECTING, "something" represents CONNECTED.
|
||||||
@@ -33,10 +43,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
ifaceChanged(value)
|
ifaceChanged(value)
|
||||||
}
|
}
|
||||||
val ifaceChanged = StickyEvent1 { iface }
|
val ifaceChanged = StickyEvent1 { iface }
|
||||||
|
val configuration get() = reservation?.configuration
|
||||||
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
|
||||||
reservation?.wifiConfiguration?.toCompat()
|
|
||||||
} else reservation?.softApConfiguration?.toCompat()
|
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
when (iface) {
|
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 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.
|
* 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
|
if (binder.iface != null) return START_STICKY
|
||||||
binder.iface = ""
|
binder.iface = ""
|
||||||
updateNotification() // show invisible foreground notification to avoid being killed
|
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 {
|
try {
|
||||||
Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
|
Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
|
||||||
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
|
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
|
||||||
if (reservation == null) onFailed(-2) else {
|
if (reservation == null) onFailed(-2) else {
|
||||||
this@LocalOnlyHotspotService.reservation = reservation
|
this@LocalOnlyHotspotService.reservation = Framework(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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopped() {
|
override fun onStopped() {
|
||||||
Timber.d("LOHCallback.onStopped")
|
|
||||||
reservation = null
|
reservation = null
|
||||||
}
|
}
|
||||||
|
override fun onFailed(reason: Int) = onFrameworkFailed(reason)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}, null)
|
}, null)
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
// throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
|
// throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
|
||||||
@@ -128,7 +187,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
stopService()
|
stopService()
|
||||||
}
|
}
|
||||||
return START_STICKY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -38,6 +39,8 @@ import java.io.PrintWriter
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
private fun Preference.remove() = parent!!.removePreference(this)
|
||||||
|
@TargetApi(26)
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
// handle complicated default value and possible system upgrades
|
// handle complicated default value and possible system upgrades
|
||||||
WifiDoubleLock.mode = WifiDoubleLock.mode
|
WifiDoubleLock.mode = WifiDoubleLock.mode
|
||||||
@@ -65,7 +68,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else parent!!.removePreference(this)
|
} else remove()
|
||||||
}
|
}
|
||||||
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
|
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
|
||||||
if (Services.p2p != null) {
|
if (Services.p2p != null) {
|
||||||
@@ -74,11 +77,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
boot.isChecked = BootReceiver.enabled
|
boot.isChecked = BootReceiver.enabled
|
||||||
} else boot.parent!!.removePreference(boot)
|
} else boot.remove()
|
||||||
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
|
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
|
||||||
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
|
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 {
|
findPreference<Preference>("service.clean")!!.setOnPreferenceClickListener {
|
||||||
GlobalScope.launch { RoutingManager.clean() }
|
GlobalScope.launch { RoutingManager.clean() }
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
apConfigurationRunning = true
|
apConfigurationRunning = true
|
||||||
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
||||||
try {
|
try {
|
||||||
WifiApManager.configuration
|
WifiApManager.configurationCompat
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
if (e.targetException !is SecurityException) Timber.w(e)
|
if (e.targetException !is SecurityException) Timber.w(e)
|
||||||
try {
|
try {
|
||||||
@@ -237,7 +237,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
}
|
}
|
||||||
val success = try {
|
val success = try {
|
||||||
WifiApManager.setConfiguration(configuration)
|
WifiApManager.setConfigurationCompat(configuration)
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
try {
|
try {
|
||||||
RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) }
|
RootManager.use { it.execute(WifiApCommands.SetConfiguration(configuration)) }
|
||||||
|
|||||||
@@ -72,16 +72,20 @@ object WifiApManager {
|
|||||||
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
|
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-.
|
* 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()
|
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
|
||||||
?: SoftApConfigurationCompat()
|
?: SoftApConfigurationCompat()
|
||||||
} else (getSoftApConfiguration(Services.wifi) as SoftApConfiguration).toCompat()
|
} else configuration.toCompat()
|
||||||
fun setConfiguration(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
fun setConfigurationCompat(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
setSoftApConfiguration(Services.wifi, value.toPlatform())
|
||||||
|
} else @Suppress("DEPRECATION") {
|
||||||
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
||||||
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
|
}) as Boolean
|
||||||
|
|
||||||
@RequiresApi(28)
|
@RequiresApi(28)
|
||||||
interface SoftApCallbackCompat {
|
interface SoftApCallbackCompat {
|
||||||
@@ -236,6 +240,16 @@ object WifiApManager {
|
|||||||
@RequiresApi(28)
|
@RequiresApi(28)
|
||||||
fun unregisterSoftApCallback(key: Any) = unregisterSoftApCallback(Services.wifi, key)
|
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 {
|
private val cancelLocalOnlyHotspotRequest by lazy {
|
||||||
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.root
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.librootkotlinx.ParcelableBoolean
|
import be.mygod.librootkotlinx.ParcelableBoolean
|
||||||
@@ -62,6 +63,7 @@ object WifiApCommands {
|
|||||||
private fun push(parcel: SoftApCallbackParcel) {
|
private fun push(parcel: SoftApCallbackParcel) {
|
||||||
trySend(parcel).onClosed {
|
trySend(parcel).onClosed {
|
||||||
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
|
finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally"))
|
||||||
|
return
|
||||||
}.onFailure { throw it!! }
|
}.onFailure { throw it!! }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,11 +151,60 @@ object WifiApCommands {
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
|
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
|
||||||
override suspend fun execute() = WifiApManager.configuration
|
override suspend fun execute() = WifiApManager.configurationCompat
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
mobile/src/main/res/drawable/ic_content_file_copy.xml
Normal file
5
mobile/src/main/res/drawable/ic_content_file_copy.xml
Normal 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>
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
<string name="settings_service_repeater_safe_mode">中继安全模式</string>
|
<string name="settings_service_repeater_safe_mode">中继安全模式</string>
|
||||||
<string name="settings_service_repeater_safe_mode_summary">不对系统配置进行修改,但是可能须要较长的网络名称。</string>
|
<string name="settings_service_repeater_safe_mode_summary">不对系统配置进行修改,但是可能须要较长的网络名称。</string>
|
||||||
<string name="settings_service_repeater_safe_mode_warning">使用短名称可能需要关闭安全模式。</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">保持 Wi\u2011Fi 开启</string>
|
||||||
<string name="settings_service_wifi_lock_none">系统默认</string>
|
<string name="settings_service_wifi_lock_none">系统默认</string>
|
||||||
<string name="settings_service_wifi_lock_full">开</string>
|
<string name="settings_service_wifi_lock_full">开</string>
|
||||||
|
|||||||
@@ -131,6 +131,9 @@
|
|||||||
not work with short network names.</string>
|
not work with short network names.</string>
|
||||||
<string name="settings_service_repeater_safe_mode_warning">Short network names might require turning off safe
|
<string name="settings_service_repeater_safe_mode_warning">Short network names might require turning off safe
|
||||||
mode.</string>
|
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">Keep Wi\u2011Fi alive</string>
|
||||||
<string name="settings_service_wifi_lock_none">System default</string>
|
<string name="settings_service_wifi_lock_none">System default</string>
|
||||||
<string name="settings_service_wifi_lock_full">On</string>
|
<string name="settings_service_wifi_lock_full">On</string>
|
||||||
|
|||||||
@@ -68,6 +68,11 @@
|
|||||||
app:title="@string/settings_service_repeater_safe_mode"
|
app:title="@string/settings_service_repeater_safe_mode"
|
||||||
app:summary="@string/settings_service_repeater_safe_mode_summary"
|
app:summary="@string/settings_service_repeater_safe_mode_summary"
|
||||||
app:defaultValue="true"/>
|
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
|
<com.takisoft.preferencex.SimpleMenuPreference
|
||||||
app:key="service.ipMonitor"
|
app:key="service.ipMonitor"
|
||||||
app:icon="@drawable/ic_hardware_device_hub"
|
app:icon="@drawable/ic_hardware_device_hub"
|
||||||
|
|||||||
Reference in New Issue
Block a user