First draft

This commit is contained in:
Mygod
2020-06-02 20:04:05 -04:00
parent 0366fc6bc6
commit 6ab763013b
14 changed files with 474 additions and 213 deletions

View File

@@ -193,6 +193,8 @@ Hidden whitelisted APIs: (same catch as above, however, things in this list are
* (since API 30) `Landroid/net/TetheringManager;->startTethering(Landroid/net/TetheringManager$TetheringRequest;Ljava/util/concurrent/Executor;Landroid/net/TetheringManager$StartTetheringCallback;)V,system-api,test-api,whitelist`
* (since API 30) `Landroid/net/TetheringManager;->stopTethering(I)V,system-api,test-api,whitelist`
* (since API 30) `Landroid/net/TetheringManager;->unregisterTetheringEventCallback(Landroid/net/TetheringManager$TetheringEventCallback;)V,system-api,test-api,whitelist`
* [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA2_PSK:I,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131365)
* [`Landroid/net/wifi/WifiConfiguration$KeyMgmt;->WPA_PSK_SHA256:I,blacklist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131367)
* [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132289)
* [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132358)
* `Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/List;,system-api,whitelist`

View File

@@ -5,12 +5,14 @@ import android.content.IntentFilter
import android.net.wifi.WifiManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -31,7 +33,11 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
}
val ifaceChanged = StickyEvent1 { iface }
val configuration get() = reservation?.wifiConfiguration
val configuration get() = if (BuildCompat.isAtLeastR()) {
reservation?.softApConfiguration?.toCompat()
} else @Suppress("DEPRECATION") {
reservation?.wifiConfiguration?.toCompat()
}
fun stop() {
when (iface) {

View File

@@ -18,12 +18,12 @@ import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
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.startWps
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
@@ -315,7 +315,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
setPassphrase(passphrase)
operatingChannel.let { oc ->
if (oc == 0) setGroupOperatingBand(operatingBand)
else setGroupOperatingFrequency(channelToFrequency(oc))
else setGroupOperatingFrequency(SoftApConfigurationCompat.channelToFrequency(oc))
}
}.build().run {
useParcel { p ->

View File

@@ -47,7 +47,7 @@ class LocalOnlyHotspotTileService : KillableTileService() {
label = getText(R.string.tethering_temp_hotspot)
} else {
state = Tile.STATE_ACTIVE
label = service.configuration?.SSID ?: getText(R.string.tethering_temp_hotspot)
label = service.configuration?.ssid ?: getText(R.string.tethering_temp_hotspot)
}
updateTile()
}

View File

@@ -5,7 +5,7 @@ import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.net.wifi.WifiConfiguration
import android.net.wifi.SoftApConfiguration
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build
@@ -26,7 +26,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.*
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
import be.mygod.vpnhotspot.net.wifi.configuration.*
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.util.showAllowingStateLoss
@@ -60,7 +62,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
val title: CharSequence @Bindable get() {
if (Build.VERSION.SDK_INT >= 29) binder?.group?.frequency?.let {
if (it != 0) return parent.getString(R.string.repeater_channel, it, frequencyToChannel(it))
if (it != 0) return parent.getString(R.string.repeater_channel,
it, SoftApConfigurationCompat.frequencyToChannel(it))
}
return parent.getString(R.string.title_repeater)
}
@@ -173,20 +176,22 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
@MainThread
private suspend fun getConfiguration(): WifiConfiguration? {
private suspend fun getConfiguration(): SoftApConfigurationCompat? {
if (RepeaterService.safeMode) {
val networkName = RepeaterService.networkName
val passphrase = RepeaterService.passphrase
if (networkName != null && passphrase != null) {
return newWifiApConfiguration(networkName, passphrase).apply {
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
apBand = when (RepeaterService.operatingBand) {
WifiP2pConfig.GROUP_OWNER_BAND_AUTO -> AP_BAND_ANY
WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> AP_BAND_2GHZ
WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> AP_BAND_5GHZ
return SoftApConfigurationCompat.empty().apply {
ssid = networkName
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used
this.passphrase = passphrase
band = when (RepeaterService.operatingBand) {
WifiP2pConfig.GROUP_OWNER_BAND_AUTO -> SoftApConfigurationCompat.BAND_ANY
WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> SoftApConfigurationCompat.BAND_2GHZ
WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> SoftApConfigurationCompat.BAND_5GHZ
else -> throw IllegalArgumentException("Unknown operatingBand")
}
apChannel = RepeaterService.operatingChannel
channel = RepeaterService.operatingChannel
}
}
} else {
@@ -196,11 +201,13 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
}
holder.config = config
return newWifiApConfiguration(group.networkName, config.psk).apply {
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
return SoftApConfigurationCompat.empty().apply {
ssid = group.networkName
securityType = SoftApConfiguration.SECURITY_TYPE_WPA2_PSK // is not actually used
passphrase = config.psk
if (Build.VERSION.SDK_INT >= 23) {
apBand = AP_BAND_ANY
apChannel = RepeaterService.operatingChannel
band = SoftApConfigurationCompat.BAND_ANY
channel = RepeaterService.operatingChannel
}
}
} catch (e: RuntimeException) {
@@ -210,20 +217,20 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
SmartSnackbar.make(R.string.repeater_configure_failure).show()
return null
}
private suspend fun updateConfiguration(config: WifiConfiguration) {
private suspend fun updateConfiguration(config: SoftApConfigurationCompat) {
if (RepeaterService.safeMode) {
RepeaterService.networkName = config.SSID
RepeaterService.passphrase = config.preSharedKey
RepeaterService.operatingBand = when (config.apBand) {
AP_BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO
AP_BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
AP_BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
else -> throw IllegalArgumentException("Unknown apBand")
RepeaterService.networkName = config.ssid
RepeaterService.passphrase = config.passphrase
RepeaterService.operatingBand = when (config.band) {
SoftApConfigurationCompat.BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO
SoftApConfigurationCompat.BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
SoftApConfigurationCompat.BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
else -> throw IllegalArgumentException("Unknown band")
}
} else holder.config?.let { master ->
val binder = binder
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try {
withContext(Dispatchers.Default) { master.update(config.SSID, config.preSharedKey) }
if (binder?.group?.networkName != config.ssid || master.psk != config.passphrase) try {
withContext(Dispatchers.Default) { master.update(config.ssid!!, config.passphrase!!) }
(this.binder ?: binder)?.group = null
} catch (e: Exception) {
Timber.w(e)
@@ -231,6 +238,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
}
holder.config = null
}
if (Build.VERSION.SDK_INT >= 23) RepeaterService.operatingChannel = config.apChannel
if (Build.VERSION.SDK_INT >= 23) RepeaterService.operatingChannel = config.channel
}
}

View File

@@ -26,8 +26,8 @@ import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.net.wifi.configuration.WifiApDialogFragment
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.isNotGone

View File

@@ -1,14 +1,17 @@
package be.mygod.vpnhotspot.net
import android.net.MacAddress
import android.os.Parcelable
import androidx.annotation.RequiresApi
import kotlinx.android.parcel.Parcelize
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* Compat support class for [MacAddress].
*/
inline class MacAddressCompat(val addr: Long) {
@Parcelize
inline class MacAddressCompat(val addr: Long) : Parcelable {
companion object {
private const val ETHER_ADDR_LEN = 6
/**
@@ -28,17 +31,44 @@ inline class MacAddressCompat(val addr: Long) {
return addr.joinToString(":") { "%02x".format(it) }
}
fun fromString(addr: String) = MacAddressCompat(ByteBuffer.allocate(8).run {
/**
* Creates a MacAddress from the given byte array representation.
* A valid byte array representation for a MacAddress is a non-null array of length 6.
*
* @param addr a byte array representation of a MAC address.
* @return the MacAddress corresponding to the given byte array representation.
* @throws IllegalArgumentException if the given byte array is not a valid representation.
*/
fun fromBytes(addr: ByteArray): MacAddressCompat {
require(addr.size == ETHER_ADDR_LEN) { addr.joinToString() + " was not a valid MAC address" }
return ByteBuffer.allocate(Long.SIZE_BYTES).run {
put(addr)
rewind()
MacAddressCompat(long)
}
}
/**
* Creates a MacAddress from the given String representation. A valid String representation
* for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal
* and joined by ':' characters.
*
* @param addr a String representation of a MAC address.
* @return the MacAddress corresponding to the given String representation.
* @throws IllegalArgumentException if the given String is not a valid representation.
*/
fun fromString(addr: String) = ByteBuffer.allocate(Long.SIZE_BYTES).run {
order(ByteOrder.LITTLE_ENDIAN)
mark()
try {
put(addr.split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray())
} catch (e: NumberFormatException) {
throw IllegalArgumentException(e)
}
reset()
long
})
rewind()
MacAddressCompat(long)
}
@RequiresApi(28)
fun MacAddress.toCompat() = fromBytes(toByteArray())
}
fun toList() = ByteBuffer.allocate(8).run {

View File

@@ -35,10 +35,17 @@ class TetherTimeoutMonitor(private val context: Context, private val onTimeout:
/**
* Minimum limit to use for timeout delay if the value from overlay setting is too small.
*/
private const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
@RequiresApi(21)
const val MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000 // 10 minutes
private val enabled get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
private val timeout by lazy {
@Deprecated("Use SoftApConfigurationCompat instead")
var enabled
get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
set(value) {
check(Settings.Global.putInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0))
}
@Deprecated("Use SoftApConfigurationCompat instead")
val timeout by lazy {
val delay = try {
app.resources.getInteger(Resources.getSystem().getIdentifier(
"config_wifi_framework_soft_ap_timeout_delay", "integer", "android"))

View File

@@ -1,4 +1,4 @@
package be.mygod.vpnhotspot.net.wifi.configuration
package be.mygod.vpnhotspot.net.wifi
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build

View File

@@ -0,0 +1,306 @@
package be.mygod.vpnhotspot.net.wifi
import android.annotation.TargetApi
import android.net.MacAddress
import android.net.wifi.SoftApConfiguration
import android.os.Build
import android.os.Parcelable
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import kotlinx.android.parcel.Parcelize
@Parcelize
data class SoftApConfigurationCompat(
var ssid: String?,
var securityType: Int,
var passphrase: String?,
@RequiresApi(23)
var band: Int,
@RequiresApi(23)
var channel: Int,
var bssid: MacAddressCompat?,
var maxNumberOfClients: Int,
@RequiresApi(28)
var shutdownTimeoutMillis: Long,
@RequiresApi(28)
var isAutoShutdownEnabled: Boolean,
var isClientControlByUserEnabled: Boolean,
var isHiddenSsid: Boolean,
// TODO: WifiClient? nullable?
var allowedClientList: List<Parcelable>?,
var blockedClientList: List<Parcelable>?,
val underlying: Parcelable? = null) : Parcelable {
companion object {
/**
* TODO
*/
const val BAND_ANY = -1
const val BAND_2GHZ = 0
const val BAND_5GHZ = 1
const val BAND_6GHZ = 2
const val CH_INVALID = 0
// TODO: localize?
val securityTypes = arrayOf("OPEN", "WPA2-PSK", "WPA3-SAE", "WPA3-SAE Transition mode")
private val qrSanitizer = Regex("([\\\\\":;,])")
/**
* The frequency which AP resides on (MHz). Resides in range [2412, 5815].
*/
fun channelToFrequency(channel: Int) = when (channel) {
in 1..14 -> 2407 + 5 * channel
in 15..165 -> 5000 + 5 * channel
else -> throw IllegalArgumentException("Invalid channel $channel")
}
fun frequencyToChannel(frequency: Int) = when (frequency % 5) {
2 -> ((frequency - 2407) / 5).also { check(it in 1..14) { "Invalid 2.4 GHz frequency $frequency" } }
0 -> ((frequency - 5000) / 5).also { check(it in 15..165) { "Invalid 5 GHz frequency $frequency" } }
else -> throw IllegalArgumentException("Invalid frequency $frequency")
}
/**
* apBand and apChannel is available since API 23.
*
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
*/
@get:RequiresApi(23)
@Suppress("DEPRECATION")
/**
* The band which AP resides on
* -1:Any 0:2G 1:5G
* By default, 2G is chosen
*/
private val apBand by lazy { android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apBand") }
@get:RequiresApi(23)
@Suppress("DEPRECATION")
/**
* The channel which AP resides on
* 2G 1-11
* 5G 36,40,44,48,149,153,157,161,165
* 0 - find a random available channel according to the apBand
*/
private val apChannel by lazy {
android.net.wifi.WifiConfiguration::class.java.getDeclaredField("apChannel")
}
@get:RequiresApi(30)
private val getAllowedClientList by lazy {
SoftApConfiguration::class.java.getDeclaredMethod("getAllowedClientList")
}
@get:RequiresApi(30)
private val getBand by lazy { SoftApConfiguration::class.java.getDeclaredMethod("getBand") }
@get:RequiresApi(30)
private val getBlockedClientList by lazy {
SoftApConfiguration::class.java.getDeclaredMethod("getBlockedClientList")
}
@get:RequiresApi(30)
private val getChannel by lazy { SoftApConfiguration::class.java.getDeclaredMethod("getChannel") }
@get:RequiresApi(30)
private val getMaxNumberOfClients by lazy {
SoftApConfiguration::class.java.getDeclaredMethod("getMaxNumberOfClients")
}
@get:RequiresApi(30)
private val getShutdownTimeoutMillis by lazy {
SoftApConfiguration::class.java.getDeclaredMethod("getShutdownTimeoutMillis")
}
@get:RequiresApi(30)
private val isAutoShutdownEnabled by lazy {
SoftApConfiguration::class.java.getDeclaredMethod("isAutoShutdownEnabled")
}
@get:RequiresApi(30)
private val isClientControlByUserEnabled by lazy {
SoftApConfiguration::class.java.getDeclaredMethod("isClientControlByUserEnabled")
}
@get:RequiresApi(30)
private val classBuilder by lazy { Class.forName("android.net.wifi.SoftApConfiguration\$Builder") }
@get:RequiresApi(30)
private val newBuilder by lazy { classBuilder.getConstructor(SoftApConfiguration::class.java) }
@get:RequiresApi(30)
private val build by lazy { classBuilder.getDeclaredMethod("build") }
@get:RequiresApi(30)
private val setAllowedClientList by lazy {
classBuilder.getDeclaredMethod("setAllowedClientList", java.util.List::class.java)
}
@get:RequiresApi(30)
private val setAutoShutdownEnabled by lazy {
classBuilder.getDeclaredMethod("setAutoShutdownEnabled", Boolean::class.java)
}
@get:RequiresApi(30)
private val setBand by lazy { classBuilder.getDeclaredMethod("setBand", Int::class.java) }
@get:RequiresApi(30)
private val setBlockedClientList by lazy {
classBuilder.getDeclaredMethod("setBlockedClientList", java.util.List::class.java)
}
@get:RequiresApi(30)
private val setBssid by lazy @TargetApi(30) {
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
}
@get:RequiresApi(30)
private val setChannel by lazy { classBuilder.getDeclaredMethod("setChannel", Int::class.java) }
@get:RequiresApi(30)
private val setClientControlByUserEnabled by lazy {
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
}
@get:RequiresApi(30)
private val setHiddenSsid by lazy { classBuilder.getDeclaredMethod("setHiddenSsid", Boolean::class.java) }
@get:RequiresApi(30)
private val setMaxNumberOfClients by lazy {
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
}
@get:RequiresApi(30)
private val setPassphrase by lazy { classBuilder.getDeclaredMethod("setPassphrase", String::class.java) }
@get:RequiresApi(30)
private val setShutdownTimeoutMillis by lazy {
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
}
@get:RequiresApi(30)
private val setSsid by lazy { classBuilder.getDeclaredMethod("setSsid", String::class.java) }
@Deprecated("Class deprecated in framework")
@Suppress("DEPRECATION")
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
SSID,
allowedKeyManagement.nextSetBit(0).let { selected ->
require(allowedKeyManagement.nextSetBit(selected + 1) < 0) {
"More than 1 key managements supplied: $allowedKeyManagement"
}
when (if (selected < 0) -1 else selected) {
-1, // getAuthType returns NONE if nothing is selected
android.net.wifi.WifiConfiguration.KeyMgmt.NONE -> SoftApConfiguration.SECURITY_TYPE_OPEN
android.net.wifi.WifiConfiguration.KeyMgmt.WPA_PSK,
4, // WPA2_PSK
11 -> { // WPA_PSK_SHA256
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
}
android.net.wifi.WifiConfiguration.KeyMgmt.SAE -> SoftApConfiguration.SECURITY_TYPE_WPA3_SAE
// TODO: check source code
else -> throw IllegalArgumentException("Unrecognized key management: $allowedKeyManagement")
}
},
preSharedKey,
if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) else BAND_ANY, // TODO
if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else CH_INVALID, // TODO
BSSID?.let { MacAddressCompat.fromString(it) },
0, // TODO: unsupported field should have @RequiresApi?
if (Build.VERSION.SDK_INT >= 28) {
TetherTimeoutMonitor.timeout.toLong()
} else TetherTimeoutMonitor.MIN_SOFT_AP_TIMEOUT_DELAY_MS.toLong(),
if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false,
false, // TODO
hiddenSSID,
null,
null,
this)
@RequiresApi(30)
@Suppress("UNCHECKED_CAST")
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
ssid,
securityType,
passphrase,
getBand(this) as Int,
getChannel(this) as Int,
bssid?.toCompat(),
getMaxNumberOfClients(this) as Int,
getShutdownTimeoutMillis(this) as Long,
isAutoShutdownEnabled(this) as Boolean,
isClientControlByUserEnabled(this) as Boolean,
isHiddenSsid,
getAllowedClientList(this) as List<Parcelable>?,
getBlockedClientList(this) as List<Parcelable>?,
this)
fun empty() = SoftApConfigurationCompat(
null, SoftApConfiguration.SECURITY_TYPE_OPEN, null, BAND_ANY, CH_INVALID, null, 0,
if (Build.VERSION.SDK_INT >= 28) {
TetherTimeoutMonitor.timeout.toLong()
} else TetherTimeoutMonitor.MIN_SOFT_AP_TIMEOUT_DELAY_MS.toLong(),
if (Build.VERSION.SDK_INT >= 28) TetherTimeoutMonitor.enabled else false, false, false, null, null)
}
/**
* Based on:
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/wifi/tether/WifiTetherSettings.java#162
*/
@Deprecated("Class deprecated in framework")
@Suppress("DEPRECATION")
fun toWifiConfiguration(): android.net.wifi.WifiConfiguration {
val wc = underlying as? android.net.wifi.WifiConfiguration
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
val original = wc?.toCompat()
result.SSID = ssid
if (original?.securityType != securityType) {
result.allowedKeyManagement.clear()
result.allowedKeyManagement.set(when (securityType) {
SoftApConfiguration.SECURITY_TYPE_OPEN -> android.net.wifi.WifiConfiguration.KeyMgmt.NONE
// not actually used on API 30-
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> android.net.wifi.WifiConfiguration.KeyMgmt.WPA_PSK
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE,
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> android.net.wifi.WifiConfiguration.KeyMgmt.SAE
else -> throw IllegalArgumentException("Unsupported securityType $securityType")
})
result.allowedAuthAlgorithms.clear()
result.allowedAuthAlgorithms.set(android.net.wifi.WifiConfiguration.AuthAlgorithm.OPEN)
}
result.preSharedKey = passphrase
if (Build.VERSION.SDK_INT >= 23) {
apBand.setInt(result, band)
apChannel.setInt(result, channel)
}
if (bssid != original?.bssid) result.BSSID = bssid?.toString()
result.hiddenSSID = isHiddenSsid
return result
}
@RequiresApi(30)
fun toPlatform(): SoftApConfiguration {
val sac = underlying as? SoftApConfiguration
// TODO: can we always call copy constructor?
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
setSsid(builder, ssid)
// TODO: setSecurityType
setPassphrase(builder, passphrase)
setBand(builder, band)
setChannel(builder, channel)
setBssid(builder, bssid?.toPlatform())
setMaxNumberOfClients(builder, maxNumberOfClients)
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
setAutoShutdownEnabled(builder, isAutoShutdownEnabled)
setClientControlByUserEnabled(builder, isClientControlByUserEnabled)
setHiddenSsid(builder, isHiddenSsid)
setAllowedClientList(builder, allowedClientList)
setBlockedClientList(builder, blockedClientList)
return build(builder) as SoftApConfiguration
}
/**
* Documentation: https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161
*/
fun toQrCode() = StringBuilder("WIFI:").apply {
fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
when (securityType) {
SoftApConfiguration.SECURITY_TYPE_OPEN -> { }
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> append("T:WPA;")
SoftApConfiguration.SECURITY_TYPE_WPA3_SAE, SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION -> {
append("T:SAE;")
}
else -> throw IllegalArgumentException("Unsupported authentication type")
}
append("S:")
append(ssid!!.sanitize())
append(';')
passphrase?.let { passphrase ->
append("P:")
append(passphrase.sanitize())
append(';')
}
if (isHiddenSsid) append("H:true;")
append(';')
}.toString()
}

View File

@@ -1,10 +1,9 @@
package be.mygod.vpnhotspot.net.wifi.configuration
package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.ClipData
import android.content.DialogInterface
import android.net.wifi.WifiConfiguration
import android.net.wifi.SoftApConfiguration
import android.os.Build
import android.os.Parcelable
import android.text.Editable
@@ -14,8 +13,10 @@ import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.os.BuildCompat
import androidx.core.view.isGone
import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.App.Companion.app
@@ -43,7 +44,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
}
@Parcelize
data class Arg(val configuration: WifiConfiguration,
data class Arg(val configuration: SoftApConfigurationCompat,
val readOnly: Boolean = false,
/**
* KeyMgmt is enforced to WPA_PSK.
@@ -51,41 +52,43 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
*/
val p2pMode: Boolean = false) : Parcelable
@TargetApi(23)
@RequiresApi(23)
private sealed class BandOption {
open val apBand get() = AP_BAND_2GHZ
open val apChannel get() = 0
open val band get() = SoftApConfigurationCompat.BAND_ANY
open val channel get() = SoftApConfigurationCompat.CH_INVALID
object BandAny : BandOption() {
override val apBand get() = AP_BAND_ANY
override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
}
object Band2GHz : BandOption() {
override val band get() = SoftApConfigurationCompat.BAND_2GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_2G)
}
object Band5GHz : BandOption() {
override val apBand get() = AP_BAND_5GHZ
override val band get() = SoftApConfigurationCompat.BAND_5GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_5G)
}
class Channel(override val apChannel: Int) : BandOption() {
override fun toString() = "${channelToFrequency(apChannel)} MHz ($apChannel)"
@RequiresApi(30)
object Band6GHz : BandOption() {
override val band get() = SoftApConfigurationCompat.BAND_6GHZ
override fun toString() = app.getString(R.string.wifi_ap_choose_6G)
}
class Channel(override val channel: Int) : BandOption() {
override fun toString() = "${SoftApConfigurationCompat.channelToFrequency(channel)} MHz ($channel)"
}
}
private lateinit var dialogView: DialogWifiApBinding
private lateinit var bandOptions: MutableList<BandOption>
private var started = false
private val selectedSecurity get() =
if (arg.p2pMode) WifiConfiguration.KeyMgmt.WPA_PSK else dialogView.security.selectedItemPosition
override val ret get() = Arg(WifiConfiguration().apply {
SSID = dialogView.ssid.text.toString()
allowedKeyManagement.set(selectedSecurity)
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
if (dialogView.password.length() != 0) preSharedKey = dialogView.password.text.toString()
override val ret get() = Arg(arg.configuration.copy(
ssid = dialogView.ssid.text.toString(),
passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
if (!arg.p2pMode) securityType = dialogView.security.selectedItemPosition
if (Build.VERSION.SDK_INT >= 23) {
val bandOption = dialogView.band.selectedItem as BandOption
apBand = bandOption.apBand
apChannel = bandOption.apChannel
band = bandOption.band
channel = bandOption.channel
}
})
@@ -101,13 +104,13 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
WifiConfiguration.KeyMgmt.strings).apply {
SoftApConfigurationCompat.securityTypes).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) = error("Must select something")
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
dialogView.passwordWrapper.isGone = position == WifiConfiguration.KeyMgmt.NONE
dialogView.passwordWrapper.isGone = position == SoftApConfiguration.SECURITY_TYPE_OPEN
}
}
}
@@ -124,6 +127,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
add(BandOption.Band2GHz)
add(BandOption.Band5GHz)
if (BuildCompat.isAtLeastR()) add (BandOption.Band6GHz)
}
addAll(channels)
}
@@ -131,21 +135,22 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
if (Build.VERSION.SDK_INT < 23) {
setSelection(bandOptions.indexOfFirst { it.apChannel == RepeaterService.operatingChannel })
setSelection(bandOptions.indexOfFirst { it.channel == RepeaterService.operatingChannel })
}
} else dialogView.bandWrapper.isGone = true
populateFromConfiguration(arg.configuration)
}
private fun populateFromConfiguration(configuration: WifiConfiguration) {
dialogView.ssid.setText(configuration.SSID)
if (!arg.p2pMode) dialogView.security.setSelection(configuration.apKeyManagement)
dialogView.password.setText(configuration.preSharedKey)
private fun populateFromConfiguration(configuration: SoftApConfigurationCompat) {
dialogView.ssid.setText(configuration.ssid)
if (!arg.p2pMode) dialogView.security.setSelection(configuration.securityType)
dialogView.password.setText(configuration.passphrase)
if (Build.VERSION.SDK_INT >= 23) {
dialogView.band.setSelection(if (configuration.apChannel in 1..165) {
bandOptions.indexOfFirst { it.apChannel == configuration.apChannel }
} else bandOptions.indexOfFirst { it.apBand == configuration.apBand })
dialogView.band.setSelection(if (configuration.channel in 1..165) {
bandOptions.indexOfFirst { it.channel == configuration.channel }
} else bandOptions.indexOfFirst { it.band == configuration.band })
}
// TODO support more fields from SACC
}
override fun onStart() {
@@ -163,8 +168,12 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.ssidWrapper.error = if (RepeaterService.safeModeConfigurable && ssidLength < 9) {
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
} else null
val selectedSecurity = if (arg.p2pMode) {
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
} else dialogView.security.selectedItemPosition
val passwordValid = when (selectedSecurity) {
WifiConfiguration.KeyMgmt.WPA_PSK, WPA2_PSK -> dialogView.password.length() >= 8
// TODO
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK -> dialogView.password.length() >= 8
else -> true // do not try to validate
}
dialogView.passwordWrapper.error = if (passwordValid) null else {
@@ -187,7 +196,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
}
android.R.id.paste -> try {
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
Base64.decode(toString(), BASE64_FLAGS).toParcelable<WifiConfiguration>()
Base64.decode(toString(), BASE64_FLAGS).toParcelable<SoftApConfigurationCompat>()
?.let { populateFromConfiguration(it) }
}
true
@@ -197,7 +206,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
}
R.id.share_qr -> {
val qrString = try {
ret.configuration.toQRString()
ret.configuration.toQrCode()
} catch (e: IllegalArgumentException) {
SmartSnackbar.make(e).show()
return false
@@ -212,7 +221,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
override fun onClick(dialog: DialogInterface?, which: Int) {
super.onClick(dialog, which)
if (Build.VERSION.SDK_INT < 23 && arg.p2pMode && which == DialogInterface.BUTTON_POSITIVE) {
RepeaterService.operatingChannel = (dialogView.band.selectedItem as BandOption).apChannel
RepeaterService.operatingChannel = (dialogView.band.selectedItem as BandOption).channel
}
}
}

View File

@@ -1,18 +1,38 @@
package be.mygod.vpnhotspot.net.wifi
import android.net.wifi.WifiConfiguration
import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager
import androidx.annotation.RequiresApi
import androidx.core.os.BuildCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
object WifiApManager {
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
@Suppress("DEPRECATION")
private val setWifiApConfiguration by lazy {
WifiManager::class.java.getDeclaredMethod("setWifiApConfiguration", WifiConfiguration::class.java)
WifiManager::class.java.getDeclaredMethod("setWifiApConfiguration",
android.net.wifi.WifiConfiguration::class.java)
}
var configuration: WifiConfiguration
get() = getWifiApConfiguration(app.wifi) as? WifiConfiguration ?: WifiConfiguration()
set(value) = require(setWifiApConfiguration(app.wifi, value) as? Boolean == true) {
"setWifiApConfiguration failed"
@get:RequiresApi(30)
private val getSoftApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getSoftApConfiguration") }
@get:RequiresApi(30)
private val setSoftApConfiguration by lazy {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
}
var configuration: SoftApConfigurationCompat
get() = if (BuildCompat.isAtLeastR()) {
(getSoftApConfiguration(app.wifi) as SoftApConfiguration).toCompat()
} else @Suppress("DEPRECATION") {
(getWifiApConfiguration(app.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
?: SoftApConfigurationCompat.empty()
}
set(value) = if (BuildCompat.isAtLeastR()) {
require(setSoftApConfiguration(app.wifi, value.toPlatform()) as Boolean) { "setSoftApConfiguration failed" }
} else @Suppress("DEPRECATION") {
require(setWifiApConfiguration(app.wifi,
value.toWifiConfiguration()) as Boolean) { "setWifiApConfiguration failed" }
}
private val cancelLocalOnlyHotspotRequest by lazy {
@@ -20,9 +40,10 @@ object WifiApManager {
}
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(app.wifi)
@Suppress("DEPRECATION")
private val setWifiApEnabled by lazy {
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
WifiConfiguration::class.java, Boolean::class.java)
android.net.wifi.WifiConfiguration::class.java, Boolean::class.java)
}
/**
* Start AccessPoint mode with the specified
@@ -34,7 +55,8 @@ object WifiApManager {
* part of WifiConfiguration
* @return {@code true} if the operation succeeds, {@code false} otherwise
*/
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
@Suppress("DEPRECATION")
private fun WifiManager.setWifiApEnabled(wifiConfig: android.net.wifi.WifiConfiguration?, enabled: Boolean) =
setWifiApEnabled(this, wifiConfig, enabled) as Boolean
/**
@@ -44,7 +66,7 @@ object WifiApManager {
*/
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26, malfunctioning on API 25")
fun start(wifiConfig: WifiConfiguration? = null) {
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
app.wifi.isWifiEnabled = false
app.wifi.setWifiApEnabled(wifiConfig, true)
}

View File

@@ -1,129 +0,0 @@
package be.mygod.vpnhotspot.net.wifi.configuration
import android.net.wifi.WifiConfiguration
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import timber.log.Timber
import java.lang.reflect.InvocationTargetException
/**
* WPA2 pre-shared key for use with soft access point
* (requires {@code preSharedKey} to be specified).
*/
val WPA2_PSK = WifiConfiguration.KeyMgmt.strings.indexOf("WPA2_PSK")
/**
* apBand and apChannel is available since API 23.
*
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
*/
private val apBandField by lazy { WifiConfiguration::class.java.getDeclaredField("apBand") }
private val apChannelField by lazy { WifiConfiguration::class.java.getDeclaredField("apChannel") }
/**
* 2GHz band.
*
* https://android.googlesource.com/platform/frameworks/base/+/android-7.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#241
*/
@RequiresApi(23)
const val AP_BAND_2GHZ = 0
/**
* 5GHz band.
*/
@RequiresApi(23)
const val AP_BAND_5GHZ = 1
/**
* Device is allowed to choose the optimal band (2Ghz or 5Ghz) based on device capability,
* operating country code and current radio conditions.
*
* Introduced in 9.0, but we will abuse this constant anyway.
* https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#295
*/
@RequiresApi(23)
const val AP_BAND_ANY = -1
/**
* The band which AP resides on
* -1:Any 0:2G 1:5G
* By default, 2G is chosen
*/
var WifiConfiguration.apBand: Int
@RequiresApi(23) get() = apBandField.get(this) as Int
@RequiresApi(23) set(value) = apBandField.set(this, value)
/**
* The channel which AP resides on
* 2G 1-11
* 5G 36,40,44,48,149,153,157,161,165
* 0 - find a random available channel according to the apBand
*/
var WifiConfiguration.apChannel: Int
@RequiresApi(23) get() = apChannelField.get(this) as Int
@RequiresApi(23) set(value) = apChannelField.set(this, value)
/**
* The frequency which AP resides on (MHz). Resides in range [2412, 5815].
*/
fun channelToFrequency(channel: Int) = when (channel) {
in 1..14 -> 2407 + 5 * channel
in 15..165 -> 5000 + 5 * channel
else -> throw IllegalArgumentException("Invalid channel $channel")
}
fun frequencyToChannel(frequency: Int) = when (frequency % 5) {
2 -> ((frequency - 2407) / 5).also { check(it in 1..14) { "Invalid 2.4 GHz frequency $frequency" } }
0 -> ((frequency - 5000) / 5).also { check(it in 15..165) { "Invalid 5 GHz frequency $frequency" } }
else -> throw IllegalArgumentException("Invalid frequency $frequency")
}
val WifiConfiguration.apKeyManagement get() = allowedKeyManagement.nextSetBit(0).let { selected ->
check(allowedKeyManagement.nextSetBit(selected + 1) < 0) {
"More than 1 key managements supplied: $allowedKeyManagement"
}
if (selected < 0) WifiConfiguration.KeyMgmt.NONE else selected // getAuthType returns NONE if nothing is selected
}
private val qrSanitizer = Regex("([\\\\\":;,])")
/**
* Documentation: https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161
*/
fun WifiConfiguration.toQRString() = StringBuilder("WIFI:").apply {
fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
var password: String? = preSharedKey
when (apKeyManagement) {
WifiConfiguration.KeyMgmt.NONE -> if (wepKeys != null) {
password = wepKeys[0]
append("T:WEP;")
}
WifiConfiguration.KeyMgmt.WPA_PSK, WifiConfiguration.KeyMgmt.WPA_EAP, WPA2_PSK -> append("T:WPA;")
WifiConfiguration.KeyMgmt.SAE -> append("T:SAE;")
else -> throw IllegalArgumentException("Unsupported authentication type")
}
append("S:")
append(SSID.sanitize())
append(';')
if (password != null) {
append("P:")
append(password.sanitize())
append(';')
}
if (hiddenSSID) append("H:true;")
append(';')
}.toString()
/**
* Based on:
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/wifi/tether/WifiTetherSettings.java#162
*/
fun newWifiApConfiguration(ssid: String?, passphrase: String?) = try {
WifiApManager.configuration
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
WifiConfiguration()
}.apply {
SSID = ssid
preSharedKey = passphrase
allowedKeyManagement.clear()
allowedAuthAlgorithms.clear()
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
}

View File

@@ -162,6 +162,7 @@
<string name="wifi_ap_choose_auto">Auto</string>
<string name="wifi_ap_choose_2G">2.4 GHz Band</string>
<string name="wifi_ap_choose_5G">5.0 GHz Band</string>
<string name="wifi_ap_choose_6G">6.0 GHz Band</string>
<string name="wifi_save">Save</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml -->