diff --git a/README.md b/README.md
index 36e3710b..2409c0ec 100644
--- a/README.md
+++ b/README.md
@@ -43,10 +43,12 @@ Installing as system app also has the side benefit of launching root daemon less
* `android.permission.LOCAL_MAC_ADDRESS`
* `android.permission.MANAGE_USB`
+* `android.permission.OVERRIDE_WIFI_CONFIG`
+* `android.permission.READ_WIFI_CREDENTIAL`
* `android.permission.TETHER_PRIVILEGED`
* `android.permission.WRITE_SECURE_SETTINGS`
-Whenever you install an app update, if there was a new protected permission addition (last updated in v2.10.2), you should update the app installed in system as well to make the system grant the privileged permission.
+Whenever you install an app update, if there was a new protected permission addition (last updated in v2.10.4), you should update the app installed in system as well to make the system grant the privileged permission.
## Settings and How to Use Them
diff --git a/build.gradle.kts b/build.gradle.kts
index 86d3c69d..78880366 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -13,7 +13,7 @@ buildscript {
dependencies {
classpath(kotlin("gradle-plugin", kotlinVersion))
- classpath("com.android.tools.build:gradle:4.1.0-beta01")
+ classpath("com.android.tools.build:gradle:4.1.0-beta02")
classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.2.0")
classpath("com.google.android.gms:oss-licenses-plugin:0.10.2")
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index 2b3ca094..757c49d2 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -38,6 +38,8 @@
tools:ignore="ProtectedPermissions"/>
+
diff --git a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt b/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt
index f5f11b12..c0d27f04 100644
--- a/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt
+++ b/mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt
@@ -179,6 +179,27 @@ data class ParcelableSize(val value: Size) : Parcelable
@Parcelize
data class ParcelableSizeF(val value: SizeF) : Parcelable
+@Parcelize
+data class ParcelableArray(val value: Array) : Parcelable {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ParcelableArray
+
+ if (!value.contentEquals(other.value)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return value.contentHashCode()
+ }
+}
+
+@Parcelize
+data class ParcelableList(val value: List) : Parcelable
+
@SuppressLint("Recycle")
inline fun useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
try {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
index 70619056..6e674779 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
@@ -5,6 +5,7 @@ import android.annotation.TargetApi
import android.app.Service
import android.content.Intent
import android.content.SharedPreferences
+import android.content.pm.PackageManager
import android.net.wifi.WpsInfo
import android.net.wifi.p2p.*
import android.os.Build
@@ -32,6 +33,7 @@ import kotlinx.coroutines.*
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface
+import java.util.concurrent.atomic.AtomicBoolean
/**
* Service for handling Wi-Fi P2P. `supported` must be checked before this service is started otherwise it would crash.
@@ -168,6 +170,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = null
private var persistNextGroup = false
+ private val deinitPending = AtomicBoolean(true)
var status = Status.IDLE
private set(value) {
@@ -197,41 +200,42 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
override fun onBind(intent: Intent) = binder
- private fun setOperatingChannel(forceReinit: Boolean = false, oc: Int = operatingChannel) = try {
+ private fun setOperatingChannel(oc: Int = operatingChannel) {
val channel = channel
- if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
- // we don't care about listening channel
- else p2pManager.setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
- override fun onSuccess() { }
- override fun onFailure(reason: Int) {
- if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) launch(start = CoroutineStart.UNDISPATCHED) {
- val rootReason = try {
- RootManager.use {
- if (forceReinit) it.execute(RepeaterCommands.Deinit())
- it.execute(RepeaterCommands.SetChannel(oc))
- }
- } catch (e: Exception) {
- Timber.w(e)
- SmartSnackbar.make(e).show()
- null
- } ?: return@launch
- SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, rootReason.value)).show()
- } else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
+ if (channel != null) launch(start = CoroutineStart.UNDISPATCHED) {
+ val reason = try {
+ // we don't care about listening channel
+ p2pManager.setWifiP2pChannels(channel, 0, oc) ?: return@launch
+ } catch (e: InvocationTargetException) {
+ if (oc != 0) {
+ val message = getString(R.string.repeater_set_oc_failure, e.message)
+ SmartSnackbar.make(message).show()
+ Timber.w(RuntimeException("Failed to set operating channel $oc", e))
+ } else Timber.w(e)
+ return@launch
}
- })
- } catch (e: InvocationTargetException) {
- if (oc != 0) {
- val message = getString(R.string.repeater_set_oc_failure, e.message)
- SmartSnackbar.make(message).show()
- Timber.w(RuntimeException("Failed to set operating channel $oc", e))
- } else Timber.w(e)
+ if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) {
+ val rootReason = try {
+ RootManager.use {
+ if (deinitPending.getAndSet(false)) it.execute(RepeaterCommands.Deinit())
+ it.execute(RepeaterCommands.SetChannel(oc))
+ }
+ } catch (e: Exception) {
+ Timber.w(e)
+ SmartSnackbar.make(e).show()
+ null
+ } ?: return@launch
+ SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, rootReason.value)).show()
+ } else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
+ } else SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
}
override fun onChannelDisconnected() {
channel = null
+ deinitPending.set(true)
if (status != Status.DESTROYED) try {
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
- if (!safeMode) setOperatingChannel(true)
+ if (!safeMode) setOperatingChannel()
} catch (e: RuntimeException) {
Timber.w(e)
launch(Dispatchers.Main) {
@@ -244,7 +248,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (!safeMode) when (key) {
KEY_OPERATING_CHANNEL -> setOperatingChannel()
- KEY_SAFE_MODE -> setOperatingChannel(true)
+ KEY_SAFE_MODE -> {
+ deinitPending.set(true)
+ setOperatingChannel()
+ onPersistentGroupsChanged()
+ }
}
}
@@ -257,27 +265,43 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
null
} ?: return@launch
val channel = channel ?: return@launch
+ fun Collection.filterUselessGroups(): List {
+ if (isNotEmpty()) persistentSupported = true
+ val ownedGroups = filter {
+ if (!it.isGroupOwner) return@filter false
+ val address = MacAddressCompat.fromString(it.owner.deviceAddress)
+ // WifiP2pServiceImpl only removes self address
+ Build.VERSION.SDK_INT >= 29 && address == MacAddressCompat.ANY_ADDRESS || address == ownerAddress
+ }
+ val main = ownedGroups.minBy { it.networkId }
+ // do not replace current group if it's better
+ if (binder.group?.passphrase == null) binder.group = main
+ return if (main != null) ownedGroups.filter { it.networkId != main.networkId } else emptyList()
+ }
+ fun Int?.print(group: WifiP2pGroup) {
+ if (this == null) Timber.i("Removed redundant owned group: $group")
+ else SmartSnackbar.make(formatReason(R.string.repeater_clean_pog_failure, this)).show()
+ }
+ // we only get empty list on permission denial. Is there a better permission check?
+ if (Build.VERSION.SDK_INT < 30 || checkSelfPermission("android.permission.READ_WIFI_CREDENTIAL") ==
+ PackageManager.PERMISSION_GRANTED) try {
+ for (group in p2pManager.requestPersistentGroupInfo(channel).filterUselessGroups()) {
+ p2pManager.deletePersistentGroup(channel, group.networkId).print(group)
+ }
+ return@launch
+ } catch (e: ReflectiveOperationException) {
+ Timber.w(e)
+ }
try {
- p2pManager.requestPersistentGroupInfo(channel) { groups ->
- if (groups.isNotEmpty()) persistentSupported = true
- val ownedGroups = groups.filter {
- if (!it.isGroupOwner) return@filter false
- val address = MacAddressCompat.fromString(it.owner.deviceAddress)
- // WifiP2pServiceImpl only removes self address
- Build.VERSION.SDK_INT >= 29 && address == MacAddressCompat.ANY_ADDRESS || address == ownerAddress
- }
- val main = ownedGroups.minBy { it.networkId }
- // do not replace current group if it's better
- if (binder.group?.passphrase == null) binder.group = main
- if (main != null) ownedGroups.filter { it.networkId != main.networkId }.forEach {
- p2pManager.deletePersistentGroup(channel, it.networkId, object : WifiP2pManager.ActionListener {
- override fun onSuccess() = Timber.i("Removed redundant owned group: $it")
- override fun onFailure(reason: Int) = SmartSnackbar.make(
- formatReason(R.string.repeater_clean_pog_failure, reason)).show()
- })
+ RootManager.use { server ->
+ if (deinitPending.getAndSet(false)) server.execute(RepeaterCommands.Deinit())
+ @Suppress("UNCHECKED_CAST")
+ val groups = server.execute(RepeaterCommands.RequestPersistentGroupInfo()).value as List
+ for (group in groups.filterUselessGroups()) {
+ server.execute(RepeaterCommands.DeletePersistentGroup(group.networkId))?.value.print(group)
}
}
- } catch (e: ReflectiveOperationException) {
+ } catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
index 19b462ef..07ba35b5 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
@@ -22,6 +22,9 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
}
+ /**
+ * 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") {
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
?: SoftApConfigurationCompat.empty()
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt
index c62f7c6a..ce248d6a 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt
@@ -7,12 +7,25 @@ import android.net.wifi.p2p.WifiP2pManager
import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.callSuper
+import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
object WifiP2pManagerHelper {
+ private class ResultListener : WifiP2pManager.ActionListener {
+ val future = CompletableDeferred()
+
+ override fun onSuccess() {
+ future.complete(null)
+ }
+
+ override fun onFailure(reason: Int) {
+ future.complete(reason)
+ }
+ }
+
const val UNSUPPORTED = -2
val ACTION_WIFI_P2P_PERSISTENT_GROUPS_CHANGED = if (Build.VERSION.SDK_INT >= 30) {
"android.net.wifi.p2p.action.WIFI_P2P_PERSISTENT_GROUPS_CHANGED"
@@ -28,14 +41,18 @@ object WifiP2pManagerHelper {
WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::class.java,
Int::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
}
- fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int,
- listener: WifiP2pManager.ActionListener) {
+ /**
+ * Requires one of NETWORK_SETTING, NETWORK_STACK, or OVERRIDE_WIFI_CONFIG permission since API 30.
+ */
+ suspend fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int): Int? {
+ val result = ResultListener()
try {
- setWifiP2pChannels(this, c, lc, oc, listener)
+ setWifiP2pChannels(this, c, lc, oc, result)
} catch (_: NoSuchMethodException) {
app.logEvent("NoSuchMethod_setWifiP2pChannels")
- listener.onFailure(UNSUPPORTED)
+ return UNSUPPORTED
}
+ return result.future.await()
}
/**
@@ -66,14 +83,18 @@ object WifiP2pManagerHelper {
WifiP2pManager::class.java.getDeclaredMethod("deletePersistentGroup",
WifiP2pManager.Channel::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
}
- fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int,
- listener: WifiP2pManager.ActionListener) {
+ /**
+ * Requires one of NETWORK_SETTING, NETWORK_STACK, or READ_WIFI_CREDENTIAL permission since API 30.
+ */
+ suspend fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int): Int? {
+ val result = ResultListener()
try {
- deletePersistentGroup(this, c, netId, listener)
+ deletePersistentGroup(this, c, netId, result)
} catch (_: NoSuchMethodException) {
app.logEvent("NoSuchMethod_deletePersistentGroup")
- listener.onFailure(UNSUPPORTED)
+ return UNSUPPORTED
}
+ return result.future.await()
}
private val interfacePersistentGroupInfoListener by lazy @SuppressLint("PrivateApi") {
@@ -89,21 +110,24 @@ object WifiP2pManagerHelper {
/**
* Request a list of all the persistent p2p groups stored in system.
*
+ * Requires one of NETWORK_SETTING, NETWORK_STACK, or READ_WIFI_CREDENTIAL permission since API 30.
+ *
* @param c is the channel created at {@link #initialize}
* @param listener for callback when persistent group info list is available. Can be null.
*/
- fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel,
- listener: (Collection) -> Unit) {
- val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
+ suspend fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel): Collection {
+ val result = CompletableDeferred>()
+ requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array?): Any? = when (method.name) {
"onPersistentGroupInfoAvailable" -> {
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
- @Suppress("UNCHECKED_CAST") listener(getGroupList(args!![0]) as Collection)
+ @Suppress("UNCHECKED_CAST")
+ result.complete(getGroupList(args!![0]) as Collection)
}
else -> callSuper(interfacePersistentGroupInfoListener, proxy, method, args)
}
- })
- requestPersistentGroupInfo(this, c, proxy)
+ }))
+ return result.await()
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt b/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt
index e968baab..9bfdaa23 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/root/RepeaterCommands.kt
@@ -7,13 +7,15 @@ import android.system.Os
import android.system.OsConstants
import android.text.TextUtils
import be.mygod.librootkotlinx.ParcelableInt
+import be.mygod.librootkotlinx.ParcelableList
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandNoResult
+import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
+import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.util.Services
import eu.chainfire.librootjava.RootJava
import kotlinx.android.parcel.Parcelize
-import kotlinx.coroutines.CompletableDeferred
import java.io.File
object RepeaterCommands {
@@ -25,30 +27,24 @@ object RepeaterCommands {
}
}
+ @Parcelize
+ data class DeletePersistentGroup(val netId: Int) : RootCommand {
+ override suspend fun execute() = Services.p2p!!.run {
+ deletePersistentGroup(obtainChannel(), netId)?.let { ParcelableInt(it) }
+ }
+ }
+
+ @Parcelize
+ class RequestPersistentGroupInfo : RootCommand {
+ override suspend fun execute() = Services.p2p!!.run {
+ ParcelableList(requestPersistentGroupInfo(obtainChannel()).toList())
+ }
+ }
+
@Parcelize
class SetChannel(private val oc: Int) : RootCommand {
override suspend fun execute() = Services.p2p!!.run {
- val uninitializer = object : WifiP2pManager.ChannelListener {
- var target: WifiP2pManager.Channel? = null
- override fun onChannelDisconnected() {
- if (target == channel) channel = null
- }
- }
- val channel = channel ?: initialize(RootJava.getSystemContext(),
- Looper.getMainLooper(), uninitializer)
- uninitializer.target = channel
- RepeaterCommands.channel = channel // cache the instance until invalidated
- val future = CompletableDeferred()
- setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
- override fun onSuccess() {
- future.complete(null)
- }
-
- override fun onFailure(reason: Int) {
- future.complete(reason)
- }
- })
- future.await()?.let { ParcelableInt(it) }
+ setWifiP2pChannels(obtainChannel(), 0, oc)?.let { ParcelableInt(it) }
}
}
@@ -82,4 +78,18 @@ object RepeaterCommands {
private const val CONF_PATH_TREBLE = "/data/vendor/wifi/wpa/p2p_supplicant.conf"
private const val CONF_PATH_LEGACY = "/data/misc/wifi/p2p_supplicant.conf"
private var channel: WifiP2pManager.Channel? = null
+
+ private fun WifiP2pManager.obtainChannel(): WifiP2pManager.Channel {
+ channel?.let { return it }
+ val uninitializer = object : WifiP2pManager.ChannelListener {
+ var target: WifiP2pManager.Channel? = null
+ override fun onChannelDisconnected() {
+ if (target == channel) channel = null
+ }
+ }
+ return initialize(RootJava.getSystemContext(), Looper.getMainLooper(), uninitializer).also {
+ uninitializer.target = it
+ channel = it // cache the instance until invalidated
+ }
+ }
}