Allow user to turn off safe mode on Android 10 (Mar 2020 or newer)

Fixes #153.

Basically, this "forward"-ports a workaround for Android 9- thanks to Jimmy Chen.
As a consequence, #31 might reoccur if you turn off safe mode.
This commit is contained in:
Mygod
2020-04-22 12:21:14 +08:00
parent 0dbf4b3b64
commit 3ba9a322c2
12 changed files with 121 additions and 88 deletions

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Service
import android.content.Intent
import android.content.SharedPreferences
@@ -24,11 +25,13 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupI
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface
/**
* Service for handling Wi-Fi P2P. `supported` must be checked before this service is started otherwise it would crash.
@@ -36,6 +39,9 @@ import java.lang.reflect.InvocationTargetException
class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListener,
SharedPreferences.OnSharedPreferenceChangeListener {
companion object {
const val KEY_SAFE_MODE = "service.repeater.safeMode"
private const val KEY_LAST_MAC = "service.repeater.lastMac"
private const val KEY_NETWORK_NAME = "service.repeater.networkName"
private const val KEY_PASSPHRASE = "service.repeater.passphrase"
private const val KEY_OPERATING_BAND = "service.repeater.band"
@@ -57,9 +63,19 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}
}
val supported get() = p2pManager != null
@Deprecated("Not initialized and no use at all since API 29")
var persistentSupported = false
private val hasP2pValidateName by lazy {
val (y, m, _) = Build.VERSION.SECURITY_PATCH.split('-', limit = 3).map { it.toInt() }
y > 2020 || y == 2020 && m >= 3
}
val safeModeConfigurable get() = Build.VERSION.SDK_INT >= 29 && hasP2pValidateName
val safeMode get() = Build.VERSION.SDK_INT >= 29 &&
(!hasP2pValidateName || app.pref.getBoolean(KEY_SAFE_MODE, true))
var lastMac: String?
get() = app.pref.getString(KEY_LAST_MAC, null)
private set(value) = app.pref.edit { putString(KEY_LAST_MAC, value) }
var networkName: String?
get() = app.pref.getString(KEY_NETWORK_NAME, null)
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) }
@@ -94,7 +110,6 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
}
}
val groupChanged = StickyEvent1 { group }
@Deprecated("Not initialized and no use at all since API 29")
var thisDevice: WifiP2pDevice? = null
fun startWps(pin: String? = null) {
@@ -136,13 +151,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
}
}
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
private val deviceListener = broadcastReceiver { _, intent ->
when (intent.action) {
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> binder.thisDevice =
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE)
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION -> onPersistentGroupsChanged()
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION -> if (!safeMode) onPersistentGroupsChanged()
}
}
/**
@@ -174,17 +187,13 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
override fun onCreate() {
super.onCreate()
onChannelDisconnected()
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
app.pref.registerOnSharedPreferenceChangeListener(this)
}
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
app.pref.registerOnSharedPreferenceChangeListener(this)
}
override fun onBind(intent: Intent) = binder
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
private fun setOperatingChannel(oc: Int = operatingChannel) = try {
val channel = channel
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
@@ -207,21 +216,17 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
channel = null
if (status != Status.DESTROYED) try {
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") setOperatingChannel()
if (!safeMode) setOperatingChannel()
} catch (e: RuntimeException) {
Timber.w(e)
handler.postDelayed(this::onChannelDisconnected, 1000)
}
}
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == KEY_OPERATING_CHANNEL) setOperatingChannel()
if (!safeMode && key == KEY_OPERATING_CHANNEL) setOperatingChannel()
}
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
private fun onPersistentGroupsChanged() {
val channel = channel ?: return
val device = binder.thisDevice ?: return
@@ -300,43 +305,45 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
val networkName = networkName
val passphrase = passphrase
try {
if (Build.VERSION.SDK_INT < 29 || networkName == null || passphrase == null) {
if (!safeMode || networkName == null || passphrase == null) {
persistNextGroup = true
p2pManager.createGroup(channel, listener)
} else p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
setNetworkName(PLACEHOLDER_NETWORK_NAME)
setPassphrase(passphrase)
operatingChannel.let { oc ->
if (oc == 0) setGroupOperatingBand(operatingBand)
else setGroupOperatingFrequency(channelToFrequency(oc))
}
}.build().run {
useParcel { p ->
p.writeParcelable(this, 0)
val end = p.dataPosition()
p.setDataPosition(0)
val creator = p.readString()
val deviceAddress = p.readString()
val wps = p.readParcelable<WpsInfo>(javaClass.classLoader)
val long = p.readLong()
check(p.readString() == PLACEHOLDER_NETWORK_NAME)
check(p.readString() == passphrase)
val extrasLength = end - p.dataPosition()
check(extrasLength and 3 == 0) // parcel should be padded
if (extrasLength != 4) Timber.w(Exception("Unexpected extrasLength $extrasLength"))
val extras = (0 until extrasLength / 4).map { p.readInt() }
p.setDataPosition(0)
p.writeString(creator)
p.writeString(deviceAddress)
p.writeParcelable(wps, 0)
p.writeLong(long)
p.writeString(networkName)
p.writeString(passphrase)
extras.forEach(p::writeInt)
p.setDataPosition(0)
p.readParcelable<WifiP2pConfig>(javaClass.classLoader)
}
}, listener)
} else @TargetApi(29) {
p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
setNetworkName(PLACEHOLDER_NETWORK_NAME)
setPassphrase(passphrase)
operatingChannel.let { oc ->
if (oc == 0) setGroupOperatingBand(operatingBand)
else setGroupOperatingFrequency(channelToFrequency(oc))
}
}.build().run {
useParcel { p ->
p.writeParcelable(this, 0)
val end = p.dataPosition()
p.setDataPosition(0)
val creator = p.readString()
val deviceAddress = p.readString()
val wps = p.readParcelable<WpsInfo>(javaClass.classLoader)
val long = p.readLong()
check(p.readString() == PLACEHOLDER_NETWORK_NAME)
check(p.readString() == passphrase)
val extrasLength = end - p.dataPosition()
check(extrasLength and 3 == 0) // parcel should be padded
if (extrasLength != 4) Timber.w(Exception("Unexpected extrasLength $extrasLength"))
val extras = (0 until extrasLength / 4).map { p.readInt() }
p.setDataPosition(0)
p.writeString(creator)
p.writeString(deviceAddress)
p.writeParcelable(wps, 0)
p.writeLong(long)
p.writeString(networkName)
p.writeString(passphrase)
extras.forEach(p::writeInt)
p.setDataPosition(0)
p.readParcelable<WifiP2pConfig>(javaClass.classLoader)
}
}, listener)
}
} catch (e: SecurityException) {
Timber.w(e)
startFailure(e.readableMessage)
@@ -371,7 +378,11 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
persistNextGroup = false
}
check(routingManager == null)
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { start() }
routingManager = object : RoutingManager.LocalOnly(this, group.`interface`!!) {
override fun ifaceHandler(iface: NetworkInterface) {
iface.hardwareAddress?.asIterable()?.macToString()?.let { lastMac = it }
}
}.apply { start() }
status = Status.ACTIVE
showNotification(group)
}
@@ -426,10 +437,8 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
cancel()
dispatcher.close()
}
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
app.pref.unregisterOnSharedPreferenceChangeListener(this)
unregisterReceiver(deviceListener)
}
app.pref.unregisterOnSharedPreferenceChangeListener(this)
unregisterReceiver(deviceListener)
status = Status.DESTROYED
if (Build.VERSION.SDK_INT >= 27) channel?.close()
super.onDestroy()

View File

@@ -7,6 +7,7 @@ import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber
import java.net.NetworkInterface
abstract class RoutingManager(private val caller: Any, val downstream: String, private val isWifi: Boolean) {
companion object {
@@ -42,7 +43,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
/**
* Both repeater and local-only hotspot are Wi-Fi based.
*/
class LocalOnly(caller: Any, downstream: String) : RoutingManager(caller, downstream, true) {
open class LocalOnly(caller: Any, downstream: String) : RoutingManager(caller, downstream, true) {
override fun Routing.configure() {
ipForward() // local only interfaces need to enable ip_forward
forward()
@@ -63,8 +64,10 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
else -> error("Double routing detected for $downstream from $caller != ${other.caller}")
}
open fun ifaceHandler(iface: NetworkInterface) { }
private fun initRouting() = try {
routing = Routing(caller, downstream).apply {
routing = Routing(caller, downstream, this::ifaceHandler).apply {
try {
configure()
} catch (e: Exception) {

View File

@@ -74,6 +74,10 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
}
boot.isChecked = BootReceiver.enabled
} else boot.parent!!.removePreference(boot)
if (!RepeaterService.supported || !RepeaterService.safeModeConfigurable) {
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
safeMode.parent!!.removePreference(safeMode)
}
findPreference<Preference>("service.clean")!!.setOnPreferenceClickListener {
GlobalScope.launch { RoutingManager.clean() }
true

View File

@@ -56,7 +56,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
val title: CharSequence @Bindable get() {
if (Build.VERSION.SDK_INT >= 29) binder?.group?.frequency?.let {
return parent.getString(R.string.repeater_channel, it, frequencyToChannel(it))
if (it != 0) return parent.getString(R.string.repeater_channel, it, frequencyToChannel(it))
}
return parent.getString(R.string.title_repeater)
}
@@ -120,8 +120,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
}
@Deprecated("No longer used since API 29")
@Suppress("DEPRECATION")
class ConfigHolder : ViewModel() {
var config: P2pSupplicantConfiguration? = null
}
@@ -134,7 +132,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
private val data = Data()
internal var binder: RepeaterService.Binder? = null
private var p2pInterface: String? = null
@Suppress("DEPRECATION")
private val holder by parent.viewModels<ConfigHolder>()
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
@@ -164,7 +161,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
val configuration: WifiConfiguration? get() {
if (Build.VERSION.SDK_INT >= 29) {
if (RepeaterService.safeMode) {
val networkName = RepeaterService.networkName
val passphrase = RepeaterService.passphrase
if (networkName != null && passphrase != null) {
@@ -179,7 +176,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
apChannel = RepeaterService.operatingChannel
}
}
} else @Suppress("DEPRECATION") {
} else {
val group = binder?.group
if (group != null) try {
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
@@ -199,7 +196,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
return null
}
suspend fun updateConfiguration(config: WifiConfiguration) {
if (Build.VERSION.SDK_INT >= 29) {
if (RepeaterService.safeMode) {
RepeaterService.networkName = config.SSID
RepeaterService.passphrase = config.preSharedKey
RepeaterService.operatingBand = when (config.apBand) {
@@ -208,7 +205,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
AP_BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
else -> throw IllegalArgumentException("Unknown apBand")
}
} else @Suppress("DEPRECATION") holder.config?.let { master ->
} else holder.config?.let { master ->
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try {
withContext(Dispatchers.Default) { master.update(config.SSID, config.preSharedKey) }
binder!!.group = null

View File

@@ -25,7 +25,8 @@ import java.net.SocketException
*
* Once revert is called, this object no longer serves any purpose.
*/
class Routing(private val caller: Any, private val downstream: String) : IpNeighbourMonitor.Callback {
class Routing(private val caller: Any, private val downstream: String,
ifaceHandler: (NetworkInterface) -> Unit) : IpNeighbourMonitor.Callback {
companion object {
/**
* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
@@ -117,8 +118,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
}
private val hostAddress = try {
val addresses = NetworkInterface.getByName(downstream)!!.interfaceAddresses!!
.filter { it.address is Inet4Address }
val iface = NetworkInterface.getByName(downstream)!!
ifaceHandler(iface)
val addresses = iface.interfaceAddresses!!.filter { it.address is Inet4Address }
if (addresses.size > 1) error("More than one addresses was found: $addresses")
addresses.first()
} catch (e: Exception) {

View File

@@ -11,7 +11,6 @@ import java.lang.reflect.Proxy
object WifiP2pManagerHelper {
const val UNSUPPORTED = -2
@Deprecated("No longer used since API 29")
const val WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION = "android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED"
/**
@@ -24,7 +23,6 @@ object WifiP2pManagerHelper {
WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::class.java,
Int::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
}
@Deprecated("No longer used since API 29")
fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int,
listener: WifiP2pManager.ActionListener) {
try {
@@ -63,7 +61,6 @@ object WifiP2pManagerHelper {
WifiP2pManager::class.java.getDeclaredMethod("deletePersistentGroup",
WifiP2pManager.Channel::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
}
@Deprecated("No longer used since API 29")
fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int,
listener: WifiP2pManager.ActionListener) {
try {
@@ -90,7 +87,6 @@ object WifiP2pManagerHelper {
* @param c is the channel created at {@link #initialize}
* @param listener for callback when persistent group info list is available. Can be null.
*/
@Deprecated("No longer used since API 29")
fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel,
listener: (Collection<WifiP2pGroup>) -> Unit) {
val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
@@ -115,6 +111,5 @@ object WifiP2pManagerHelper {
private val getNetworkId by lazy @SuppressLint("DiscouragedPrivateApi") {
WifiP2pGroup::class.java.getDeclaredMethod("getNetworkId")
}
@Deprecated("No longer used since API 29")
val WifiP2pGroup.netId get() = getNetworkId.invoke(this) as Int
}

View File

@@ -13,7 +13,6 @@ import java.io.File
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
*/
@Deprecated("No longer used since API 29")
class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress: String?) {
companion object {
private const val TAG = "P2pSupplicantConfiguration"
@@ -29,7 +28,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress:
var pskLine: Int? = null
var psk: String? = null
var groupOwner = false
var bssidMatches = false
var bssid: String? = null
override fun toString() = joinToString("\n")
}
@@ -53,9 +52,9 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress:
RootSession.checkOutput(command, shell, false, false)
val parser = Parser(shell.out)
try {
val bssids = listOfNotNull(group.owner.deviceAddress, ownerAddress)
val bssids = listOfNotNull(group.owner.deviceAddress, ownerAddress, RepeaterService.lastMac)
.distinct()
.filter { it != "00:00:00:00:00:00" }
.filter { it != "00:00:00:00:00:00" && it != "02:00:00:00:00:00" }
while (parser.next()) {
if (parser.trimmed.startsWith("network={")) {
val block = NetworkBlock()
@@ -73,21 +72,20 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress:
block.psk = match.groupValues[5].apply { check(length in 8..63) }
}
block.pskLine = block.size
} else if (bssids.any { matchedBssid.equals(it, true) }) block.bssidMatches = true
} else if (bssids.any { matchedBssid.equals(it, true) }) block.bssid = matchedBssid
}
}
block.add(parser.line)
}
block.add(parser.line)
result.add(block)
if (block.bssidMatches && block.groupOwner && target == null) { // keep first only
if (block.bssid != null && block.groupOwner && target == null) { // keep first only
check(block.ssidLine != null && block.pskLine != null)
target = block
}
} else result.add(parser.line)
}
if (target == null && !RepeaterService.persistentSupported) {
val bssid = bssids.single()
result.add("")
result.add(NetworkBlock().apply {
// generate a basic network block, it is likely that vendor is going to add more stuff here

View File

@@ -28,7 +28,6 @@ import be.mygod.vpnhotspot.util.toByteArray
import be.mygod.vpnhotspot.util.toParcelable
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.android.parcel.Parcelize
import java.nio.charset.Charset
/**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
@@ -117,7 +116,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
bandOptions = mutableListOf<BandOption>().apply {
if (arg.p2pMode) {
add(BandOption.BandAny)
if (Build.VERSION.SDK_INT >= 29) {
if (RepeaterService.safeMode) {
add(BandOption.Band2GHz)
add(BandOption.Band5GHz)
}
@@ -160,8 +159,10 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
*/
private fun validate() {
if (!started) return
val ssidValid = dialogView.ssid.length() != 0 &&
Charset.forName("UTF-8").encode(dialogView.ssid.text.toString()).limit() <= 32
val ssidLength = dialogView.ssid.text.toString().toByteArray().size
dialogView.ssidWrapper.error = if (RepeaterService.safeModeConfigurable && ssidLength < 9) {
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
} else null
val passwordValid = when (selectedSecurity) {
WifiConfiguration.KeyMgmt.WPA_PSK, WPA2_PSK -> dialogView.password.length() >= 8
else -> true // do not try to validate
@@ -169,7 +170,8 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.passwordWrapper.error = if (passwordValid) null else {
requireContext().getString(R.string.credentials_password_too_short)
}
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = ssidValid && passwordValid
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
ssidLength in 1..32 && passwordValid
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }