Fix requestPersistentGroupInfo and deletePersistentGroup permissions

This commit is contained in:
Mygod
2020-07-01 15:37:21 -04:00
parent 600a99cd13
commit febf7f1c61
8 changed files with 170 additions and 84 deletions

View File

@@ -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.LOCAL_MAC_ADDRESS`
* `android.permission.MANAGE_USB` * `android.permission.MANAGE_USB`
* `android.permission.OVERRIDE_WIFI_CONFIG`
* `android.permission.READ_WIFI_CREDENTIAL`
* `android.permission.TETHER_PRIVILEGED` * `android.permission.TETHER_PRIVILEGED`
* `android.permission.WRITE_SECURE_SETTINGS` * `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 ## Settings and How to Use Them

View File

@@ -13,7 +13,7 @@ buildscript {
dependencies { dependencies {
classpath(kotlin("gradle-plugin", kotlinVersion)) 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.github.ben-manes:gradle-versions-plugin:0.28.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.2.0") classpath("com.google.firebase:firebase-crashlytics-gradle:2.2.0")
classpath("com.google.android.gms:oss-licenses-plugin:0.10.2") classpath("com.google.android.gms:oss-licenses-plugin:0.10.2")

View File

@@ -38,6 +38,8 @@
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG" <uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
tools:ignore="ProtectedPermissions"/> 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.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.TETHER_PRIVILEGED" <uses-permission android:name="android.permission.TETHER_PRIVILEGED"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>

View File

@@ -179,6 +179,27 @@ data class ParcelableSize(val value: Size) : Parcelable
@Parcelize @Parcelize
data class ParcelableSizeF(val value: SizeF) : Parcelable 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") @SuppressLint("Recycle")
inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run { inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
try { try {

View File

@@ -5,6 +5,7 @@ import android.annotation.TargetApi
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.wifi.WpsInfo import android.net.wifi.WpsInfo
import android.net.wifi.p2p.* import android.net.wifi.p2p.*
import android.os.Build import android.os.Build
@@ -32,6 +33,7 @@ import kotlinx.coroutines.*
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface 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. * 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() override val coroutineContext = dispatcher + Job()
private var routingManager: RoutingManager? = null private var routingManager: RoutingManager? = null
private var persistNextGroup = false private var persistNextGroup = false
private val deinitPending = AtomicBoolean(true)
var status = Status.IDLE var status = Status.IDLE
private set(value) { private set(value) {
@@ -197,41 +200,42 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
override fun onBind(intent: Intent) = binder 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 val channel = channel
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show() if (channel != null) launch(start = CoroutineStart.UNDISPATCHED) {
// we don't care about listening channel val reason = try {
else p2pManager.setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener { // we don't care about listening channel
override fun onSuccess() { } p2pManager.setWifiP2pChannels(channel, 0, oc) ?: return@launch
override fun onFailure(reason: Int) { } catch (e: InvocationTargetException) {
if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) launch(start = CoroutineStart.UNDISPATCHED) { if (oc != 0) {
val rootReason = try { val message = getString(R.string.repeater_set_oc_failure, e.message)
RootManager.use { SmartSnackbar.make(message).show()
if (forceReinit) it.execute(RepeaterCommands.Deinit()) Timber.w(RuntimeException("Failed to set operating channel $oc", e))
it.execute(RepeaterCommands.SetChannel(oc)) } else Timber.w(e)
} return@launch
} 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 (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) {
} catch (e: InvocationTargetException) { val rootReason = try {
if (oc != 0) { RootManager.use {
val message = getString(R.string.repeater_set_oc_failure, e.message) if (deinitPending.getAndSet(false)) it.execute(RepeaterCommands.Deinit())
SmartSnackbar.make(message).show() it.execute(RepeaterCommands.SetChannel(oc))
Timber.w(RuntimeException("Failed to set operating channel $oc", e)) }
} else Timber.w(e) } 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() { override fun onChannelDisconnected() {
channel = null channel = null
deinitPending.set(true)
if (status != Status.DESTROYED) try { if (status != Status.DESTROYED) try {
channel = p2pManager.initialize(this, Looper.getMainLooper(), this) channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
if (!safeMode) setOperatingChannel(true) if (!safeMode) setOperatingChannel()
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
Timber.w(e) Timber.w(e)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
@@ -244,7 +248,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (!safeMode) when (key) { if (!safeMode) when (key) {
KEY_OPERATING_CHANNEL -> setOperatingChannel() 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 null
} ?: return@launch } ?: return@launch
val channel = channel ?: return@launch val channel = channel ?: return@launch
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
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 { try {
p2pManager.requestPersistentGroupInfo(channel) { groups -> RootManager.use { server ->
if (groups.isNotEmpty()) persistentSupported = true if (deinitPending.getAndSet(false)) server.execute(RepeaterCommands.Deinit())
val ownedGroups = groups.filter { @Suppress("UNCHECKED_CAST")
if (!it.isGroupOwner) return@filter false val groups = server.execute(RepeaterCommands.RequestPersistentGroupInfo()).value as List<WifiP2pGroup>
val address = MacAddressCompat.fromString(it.owner.deviceAddress) for (group in groups.filterUselessGroups()) {
// WifiP2pServiceImpl only removes self address server.execute(RepeaterCommands.DeletePersistentGroup(group.networkId))?.value.print(group)
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()
})
} }
} }
} catch (e: ReflectiveOperationException) { } catch (e: Exception) {
Timber.w(e) Timber.w(e)
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
} }

View File

@@ -22,6 +22,9 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java) 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") { val configuration 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.empty() ?: SoftApConfigurationCompat.empty()

View File

@@ -7,12 +7,25 @@ import android.net.wifi.p2p.WifiP2pManager
import android.os.Build import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.callSuper import be.mygod.vpnhotspot.util.callSuper
import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Proxy import java.lang.reflect.Proxy
object WifiP2pManagerHelper { 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 const val UNSUPPORTED = -2
val ACTION_WIFI_P2P_PERSISTENT_GROUPS_CHANGED = if (Build.VERSION.SDK_INT >= 30) { val ACTION_WIFI_P2P_PERSISTENT_GROUPS_CHANGED = if (Build.VERSION.SDK_INT >= 30) {
"android.net.wifi.p2p.action.WIFI_P2P_PERSISTENT_GROUPS_CHANGED" "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, WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::class.java,
Int::class.java, Int::class.java, WifiP2pManager.ActionListener::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 { try {
setWifiP2pChannels(this, c, lc, oc, listener) setWifiP2pChannels(this, c, lc, oc, result)
} catch (_: NoSuchMethodException) { } catch (_: NoSuchMethodException) {
app.logEvent("NoSuchMethod_setWifiP2pChannels") 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::class.java.getDeclaredMethod("deletePersistentGroup",
WifiP2pManager.Channel::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java) 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 { try {
deletePersistentGroup(this, c, netId, listener) deletePersistentGroup(this, c, netId, result)
} catch (_: NoSuchMethodException) { } catch (_: NoSuchMethodException) {
app.logEvent("NoSuchMethod_deletePersistentGroup") app.logEvent("NoSuchMethod_deletePersistentGroup")
listener.onFailure(UNSUPPORTED) return UNSUPPORTED
} }
return result.future.await()
} }
private val interfacePersistentGroupInfoListener by lazy @SuppressLint("PrivateApi") { 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. * 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 c is the channel created at {@link #initialize}
* @param listener for callback when persistent group info list is available. Can be null. * @param listener for callback when persistent group info list is available. Can be null.
*/ */
fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel, suspend fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel): Collection<WifiP2pGroup> {
listener: (Collection<WifiP2pGroup>) -> Unit) { val result = CompletableDeferred<Collection<WifiP2pGroup>>()
val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader, requestPersistentGroupInfo(this, c, Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler { arrayOf(interfacePersistentGroupInfoListener), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) { override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? = when (method.name) {
"onPersistentGroupInfoAvailable" -> { "onPersistentGroupInfoAvailable" -> {
if (args?.size != 1) Timber.w(IllegalArgumentException("Unexpected args: $args")) 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) else -> callSuper(interfacePersistentGroupInfoListener, proxy, method, args)
} }
}) }))
requestPersistentGroupInfo(this, c, proxy) return result.await()
} }
} }

View File

@@ -7,13 +7,15 @@ import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import android.text.TextUtils import android.text.TextUtils
import be.mygod.librootkotlinx.ParcelableInt import be.mygod.librootkotlinx.ParcelableInt
import be.mygod.librootkotlinx.ParcelableList
import be.mygod.librootkotlinx.RootCommand import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandNoResult 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.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import eu.chainfire.librootjava.RootJava import eu.chainfire.librootjava.RootJava
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.CompletableDeferred
import java.io.File import java.io.File
object RepeaterCommands { object RepeaterCommands {
@@ -25,30 +27,24 @@ object RepeaterCommands {
} }
} }
@Parcelize
data class DeletePersistentGroup(val netId: Int) : RootCommand<ParcelableInt?> {
override suspend fun execute() = Services.p2p!!.run {
deletePersistentGroup(obtainChannel(), netId)?.let { ParcelableInt(it) }
}
}
@Parcelize
class RequestPersistentGroupInfo : RootCommand<ParcelableList> {
override suspend fun execute() = Services.p2p!!.run {
ParcelableList(requestPersistentGroupInfo(obtainChannel()).toList())
}
}
@Parcelize @Parcelize
class SetChannel(private val oc: Int) : RootCommand<ParcelableInt?> { class SetChannel(private val oc: Int) : RootCommand<ParcelableInt?> {
override suspend fun execute() = Services.p2p!!.run { override suspend fun execute() = Services.p2p!!.run {
val uninitializer = object : WifiP2pManager.ChannelListener { setWifiP2pChannels(obtainChannel(), 0, oc)?.let { ParcelableInt(it) }
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<Int?>()
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) }
} }
} }
@@ -82,4 +78,18 @@ object RepeaterCommands {
private const val CONF_PATH_TREBLE = "/data/vendor/wifi/wpa/p2p_supplicant.conf" 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 const val CONF_PATH_LEGACY = "/data/misc/wifi/p2p_supplicant.conf"
private var channel: WifiP2pManager.Channel? = null 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
}
}
} }