Fix requestPersistentGroupInfo and deletePersistentGroup permissions
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.TETHER_PRIVILEGED"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
@@ -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?>) : 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?>) : Parcelable
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
|
||||
try {
|
||||
|
||||
@@ -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,17 +200,24 @@ 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()
|
||||
if (channel != null) launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
val reason = try {
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) {
|
||||
val rootReason = try {
|
||||
RootManager.use {
|
||||
if (forceReinit) it.execute(RepeaterCommands.Deinit())
|
||||
if (deinitPending.getAndSet(false)) it.execute(RepeaterCommands.Deinit())
|
||||
it.execute(RepeaterCommands.SetChannel(oc))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -217,21 +227,15 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
} ?: 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()
|
||||
}
|
||||
})
|
||||
} 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)
|
||||
} 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,10 +265,9 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
null
|
||||
} ?: return@launch
|
||||
val channel = channel ?: return@launch
|
||||
try {
|
||||
p2pManager.requestPersistentGroupInfo(channel) { groups ->
|
||||
if (groups.isNotEmpty()) persistentSupported = true
|
||||
val ownedGroups = groups.filter {
|
||||
fun Collection<WifiP2pGroup>.filterUselessGroups(): List<WifiP2pGroup> {
|
||||
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
|
||||
@@ -269,16 +276,33 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
||||
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()
|
||||
})
|
||||
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 {
|
||||
RootManager.use { server ->
|
||||
if (deinitPending.getAndSet(false)) server.execute(RepeaterCommands.Deinit())
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val groups = server.execute(RepeaterCommands.RequestPersistentGroupInfo()).value as List<WifiP2pGroup>
|
||||
for (group in groups.filterUselessGroups()) {
|
||||
server.execute(RepeaterCommands.DeletePersistentGroup(group.networkId))?.value.print(group)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
SmartSnackbar.make(e).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<Int?>()
|
||||
|
||||
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<WifiP2pGroup>) -> Unit) {
|
||||
val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
||||
suspend fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel): Collection<WifiP2pGroup> {
|
||||
val result = CompletableDeferred<Collection<WifiP2pGroup>>()
|
||||
requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
||||
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) {
|
||||
"onPersistentGroupInfoAvailable" -> {
|
||||
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args"))
|
||||
@Suppress("UNCHECKED_CAST") listener(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
result.complete(getGroupList(args!![0]) as Collection<WifiP2pGroup>)
|
||||
}
|
||||
else -> callSuper(interfacePersistentGroupInfoListener, proxy, method, args)
|
||||
}
|
||||
})
|
||||
requestPersistentGroupInfo(this, c, proxy)
|
||||
}))
|
||||
return result.await()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -26,29 +28,23 @@ object RepeaterCommands {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class SetChannel(private val oc: Int) : RootCommand<ParcelableInt?> {
|
||||
data class DeletePersistentGroup(val netId: Int) : RootCommand<ParcelableInt?> {
|
||||
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
|
||||
deletePersistentGroup(obtainChannel(), netId)?.let { ParcelableInt(it) }
|
||||
}
|
||||
}
|
||||
val channel = channel ?: initialize(RootJava.getSystemContext(),
|
||||
Looper.getMainLooper(), uninitializer)
|
||||
uninitializer.target = channel
|
||||
RepeaterCommands.channel = channel // cache the instance until invalidated
|
||||
val future = CompletableDeferred<Int?>()
|
||||
setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() {
|
||||
future.complete(null)
|
||||
}
|
||||
|
||||
override fun onFailure(reason: Int) {
|
||||
future.complete(reason)
|
||||
@Parcelize
|
||||
class RequestPersistentGroupInfo : RootCommand<ParcelableList> {
|
||||
override suspend fun execute() = Services.p2p!!.run {
|
||||
ParcelableList(requestPersistentGroupInfo(obtainChannel()).toList())
|
||||
}
|
||||
})
|
||||
future.await()?.let { ParcelableInt(it) }
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class SetChannel(private val oc: Int) : RootCommand<ParcelableInt?> {
|
||||
override suspend fun execute() = Services.p2p!!.run {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user