Merge branch 'v3' into q-beta
This commit is contained in:
@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.UiModeManager
|
||||
import android.content.ClipboardManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.wifi.WifiManager
|
||||
@@ -70,6 +71,7 @@ class App : Application() {
|
||||
}
|
||||
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
|
||||
val connectivity by lazy { getSystemService<ConnectivityManager>()!! }
|
||||
val clipboard by lazy { getSystemService<ClipboardManager>()!! }
|
||||
val uiMode by lazy { getSystemService<UiModeManager>()!! }
|
||||
val wifi by lazy { getSystemService<WifiManager>()!! }
|
||||
|
||||
|
||||
@@ -49,10 +49,12 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
val supported get() = p2pManager != null
|
||||
var persistentSupported = false
|
||||
|
||||
val operatingChannel: Int get() {
|
||||
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
||||
return if (result in 1..165) result else 0
|
||||
}
|
||||
var operatingChannel: Int
|
||||
get() {
|
||||
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
||||
return if (result in 1..165) result else 0
|
||||
}
|
||||
set(value) = app.pref.edit().putString(RepeaterService.KEY_OPERATING_CHANNEL, value.toString()).apply()
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
@@ -92,18 +94,6 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
fun shutdown() {
|
||||
if (active) removeGroup()
|
||||
}
|
||||
|
||||
fun resetCredentials() {
|
||||
val channel = channel
|
||||
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||
else p2pManager.deletePersistentGroup(channel, (group ?: return).netId,
|
||||
object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = SmartSnackbar.make(R.string.repeater_reset_credentials_success)
|
||||
.shortToast().show()
|
||||
override fun onFailure(reason: Int) = SmartSnackbar.make(
|
||||
formatReason(R.string.repeater_reset_credentials_failure, reason)).show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private val p2pManager get() = RepeaterService.p2pManager!!
|
||||
|
||||
@@ -34,7 +34,7 @@ object ServiceNotification {
|
||||
context.resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev)
|
||||
}
|
||||
if (inactive.isNotEmpty()) {
|
||||
lines += context.getString(R.string.notification_interfaces_inactive) + inactive.joinToString()
|
||||
lines += context.getString(R.string.notification_interfaces_inactive, inactive.joinToString())
|
||||
}
|
||||
return if (lines.size <= 1) builder.setContentText(lines.singleOrNull()).build() else {
|
||||
val deviceCount = deviceCounts.sumBy { it.value }
|
||||
|
||||
@@ -7,14 +7,10 @@ import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Typeface
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.provider.Settings
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.getSystemService
|
||||
@@ -25,7 +21,6 @@ import be.mygod.vpnhotspot.DebugHelper
|
||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import be.mygod.vpnhotspot.util.formatAddresses
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
@@ -80,15 +75,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
||||
private val lookup: Map<String, NetworkInterface> get() = parent.ifaceLookup
|
||||
|
||||
override val icon get() = R.drawable.ic_action_perm_scan_wifi
|
||||
override val title: CharSequence get() {
|
||||
val configuration = binder?.configuration ?: return parent.getString(R.string.tethering_temp_hotspot)
|
||||
return SpannableStringBuilder("${configuration.SSID} - ").apply {
|
||||
val start = length
|
||||
append(configuration.preSharedKey)
|
||||
setSpan(if (Build.VERSION.SDK_INT >= 28) TypefaceSpan(Typeface.MONOSPACE) else
|
||||
TypefaceSpan("monospace"), start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
override val title: CharSequence get() = parent.getString(R.string.tethering_temp_hotspot)
|
||||
override val text: CharSequence get() {
|
||||
return lookup[binder?.iface ?: return ""]?.formatAddresses() ?: ""
|
||||
}
|
||||
@@ -103,7 +90,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
||||
|
||||
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
|
||||
private val data = Data()
|
||||
private var binder: LocalOnlyHotspotService.Binder? = null
|
||||
internal var binder: LocalOnlyHotspotService.Binder? = null
|
||||
|
||||
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
||||
viewHolder as ViewHolder
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
@@ -24,10 +25,8 @@ import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.get
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import be.mygod.vpnhotspot.*
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
|
||||
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pDialogFragment
|
||||
import be.mygod.vpnhotspot.net.wifi.configuration.*
|
||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import be.mygod.vpnhotspot.util.formatAddresses
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
@@ -54,7 +53,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
else -> false
|
||||
}
|
||||
|
||||
val ssid @Bindable get() = binder?.group?.networkName ?: ""
|
||||
val addresses: CharSequence @Bindable get() {
|
||||
return try {
|
||||
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
||||
@@ -62,12 +60,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
""
|
||||
}
|
||||
}
|
||||
var oc: CharSequence
|
||||
@Bindable get() {
|
||||
val oc = RepeaterService.operatingChannel
|
||||
return if (oc in 1..165) oc.toString() else ""
|
||||
}
|
||||
set(value) = app.pref.edit().putString(RepeaterService.KEY_OPERATING_CHANNEL, value.toString()).apply()
|
||||
|
||||
fun onStatusChanged() {
|
||||
notifyPropertyChanged(BR.switchEnabled)
|
||||
@@ -75,7 +67,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
notifyPropertyChanged(BR.addresses)
|
||||
}
|
||||
fun onGroupChanged(group: WifiP2pGroup? = null) {
|
||||
notifyPropertyChanged(BR.ssid)
|
||||
p2pInterface = group?.`interface`
|
||||
notifyPropertyChanged(BR.addresses)
|
||||
}
|
||||
@@ -101,22 +92,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
fun wps() {
|
||||
if (binder?.active == true) WpsDialogFragment().show(parent, TetheringFragment.REPEATER_WPS)
|
||||
}
|
||||
|
||||
fun editConfigurations() {
|
||||
val group = binder?.group
|
||||
if (group != null) try {
|
||||
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
|
||||
holder.config = config
|
||||
WifiP2pDialogFragment().withArg(WifiP2pDialogFragment.Arg(WifiConfiguration().apply {
|
||||
SSID = group.networkName
|
||||
preSharedKey = config.psk
|
||||
})).show(parent, TetheringFragment.REPEATER_EDIT_CONFIGURATION)
|
||||
return
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
}
|
||||
SmartSnackbar.make(R.string.repeater_configure_failure).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@@ -177,19 +152,35 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
}
|
||||
}
|
||||
|
||||
fun onEditResult(which: Int, data: Intent?) {
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> try {
|
||||
val master = holder.config ?: return
|
||||
val config = AlertDialogFragment.getRet<WifiP2pDialogFragment.Arg>(data!!).configuration
|
||||
val configuration: WifiConfiguration? get() {
|
||||
val group = binder?.group
|
||||
if (group != null) try {
|
||||
val config = 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
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
apBand = AP_BAND_ANY
|
||||
apChannel = RepeaterService.operatingChannel
|
||||
}
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
}
|
||||
SmartSnackbar.make(R.string.repeater_configure_failure).show()
|
||||
return null
|
||||
}
|
||||
fun updateConfiguration(config: WifiConfiguration) {
|
||||
holder.config?.let { master ->
|
||||
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try {
|
||||
master.update(config.SSID, config.preSharedKey)
|
||||
binder!!.group = null
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
SmartSnackbar.make(e).show()
|
||||
}
|
||||
DialogInterface.BUTTON_NEUTRAL -> binder!!.resetCredentials()
|
||||
holder.config = null
|
||||
}
|
||||
holder.config = null
|
||||
RepeaterService.operatingChannel = config.apChannel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -13,6 +10,7 @@ import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -20,34 +18,37 @@ import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.TetheringService
|
||||
import be.mygod.vpnhotspot.*
|
||||
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
|
||||
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.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
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import timber.log.Timber
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
|
||||
class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClickListener {
|
||||
class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
const val START_REPEATER = 4
|
||||
const val START_LOCAL_ONLY_HOTSPOT = 1
|
||||
const val REPEATER_EDIT_CONFIGURATION = 2
|
||||
const val REPEATER_WPS = 3
|
||||
const val CONFIGURE_REPEATER = 2
|
||||
const val CONFIGURE_AP = 4
|
||||
}
|
||||
|
||||
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager) {
|
||||
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
|
||||
private val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
|
||||
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
|
||||
private val tetherManagers by lazy @TargetApi(24) {
|
||||
listOf(TetherManager.Wifi(this@TetheringFragment),
|
||||
TetherManager.Usb(this@TetheringFragment),
|
||||
@@ -108,13 +109,47 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
|
||||
item.isNotGone = canMonitor.isNotEmpty()
|
||||
item.subMenu.apply {
|
||||
clear()
|
||||
canMonitor.sorted().forEach { add(it).setOnMenuItemClickListener(this@TetheringFragment) }
|
||||
for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener {
|
||||
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, item?.title ?: return false))
|
||||
return true
|
||||
return when (item?.itemId) {
|
||||
R.id.configuration -> item.subMenu.run {
|
||||
findItem(R.id.configuration_repeater).isNotGone = RepeaterService.supported
|
||||
findItem(R.id.configuration_temp_hotspot).isNotGone =
|
||||
adapter.localOnlyHotspotManager.binder?.configuration != null
|
||||
true
|
||||
}
|
||||
R.id.configuration_repeater -> {
|
||||
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
|
||||
adapter.repeaterManager.configuration ?: return false,
|
||||
p2pMode = true
|
||||
)).show(this, CONFIGURE_REPEATER)
|
||||
true
|
||||
}
|
||||
R.id.configuration_temp_hotspot -> {
|
||||
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
|
||||
adapter.localOnlyHotspotManager.binder?.configuration ?: return false,
|
||||
readOnly = true
|
||||
)).show(this, 0) // read-only, no callback needed
|
||||
true
|
||||
}
|
||||
R.id.configuration_ap -> try {
|
||||
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
|
||||
WifiApManager.configuration
|
||||
)).show(this, CONFIGURE_AP)
|
||||
true
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException !is SecurityException) Timber.w(e)
|
||||
SmartSnackbar.make(e.targetException).show()
|
||||
false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@@ -125,13 +160,19 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
|
||||
binding.interfaces.adapter = adapter
|
||||
adapter.update(emptyList(), emptyList(), emptyList())
|
||||
ServiceForegroundConnector(this, this, TetheringService::class)
|
||||
requireActivity().toolbar.inflateMenu(R.menu.toolbar_tethering)
|
||||
requireActivity().toolbar.apply {
|
||||
inflateMenu(R.menu.toolbar_tethering)
|
||||
setOnMenuItemClickListener(this@TetheringFragment)
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
requireActivity().toolbar.menu.clear()
|
||||
requireActivity().toolbar.apply {
|
||||
menu.clear()
|
||||
setOnMenuItemClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -139,10 +180,20 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
|
||||
if (Build.VERSION.SDK_INT >= 27) ManageBar.Data.notifyChange()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = when (requestCode) {
|
||||
REPEATER_WPS -> adapter.repeaterManager.onWpsResult(resultCode, data)
|
||||
REPEATER_EDIT_CONFIGURATION -> adapter.repeaterManager.onEditResult(resultCode, data)
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
val configuration by lazy { AlertDialogFragment.getRet<WifiApDialogFragment.Arg>(data!!).configuration }
|
||||
when (requestCode) {
|
||||
REPEATER_WPS -> adapter.repeaterManager.onWpsResult(resultCode, data)
|
||||
CONFIGURE_REPEATER -> if (resultCode == DialogInterface.BUTTON_POSITIVE) {
|
||||
adapter.repeaterManager.updateConfiguration(configuration)
|
||||
}
|
||||
CONFIGURE_AP -> if (resultCode == DialogInterface.BUTTON_POSITIVE) try {
|
||||
WifiApManager.configuration = configuration
|
||||
} catch (e: IllegalArgumentException) {
|
||||
SmartSnackbar.make(R.string.configuration_rejected).show()
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
|
||||
@@ -3,15 +3,23 @@ package be.mygod.vpnhotspot.net.wifi
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
|
||||
*
|
||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
||||
*/
|
||||
object WifiApManager {
|
||||
private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
WifiConfiguration::class.java, Boolean::class.java)
|
||||
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
||||
private val setWifiApConfiguration by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("setWifiApConfiguration", WifiConfiguration::class.java)
|
||||
}
|
||||
var configuration: WifiConfiguration
|
||||
get() = getWifiApConfiguration.invoke(app.wifi) as WifiConfiguration
|
||||
set(value) {
|
||||
if (setWifiApConfiguration.invoke(app.wifi, value) as? Boolean != true) throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
private val setWifiApEnabled by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
WifiConfiguration::class.java, Boolean::class.java)
|
||||
}
|
||||
/**
|
||||
* Start AccessPoint mode with the specified
|
||||
* configuration. If the radio is already running in
|
||||
@@ -25,6 +33,11 @@ object WifiApManager {
|
||||
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
|
||||
setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean
|
||||
|
||||
/**
|
||||
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
|
||||
*
|
||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||
fun start(wifiConfig: WifiConfiguration? = null) {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiConfiguration.AuthAlgorithm
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import be.mygod.vpnhotspot.AlertDialogFragment
|
||||
import be.mygod.vpnhotspot.R
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
|
||||
*
|
||||
* This dialog has been deprecated in API 28, but we are still using it since it works better for our purposes.
|
||||
* Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
|
||||
*/
|
||||
class WifiP2pDialogFragment : AlertDialogFragment<WifiP2pDialogFragment.Arg, WifiP2pDialogFragment.Arg>(), TextWatcher {
|
||||
@Parcelize
|
||||
data class Arg(val configuration: WifiConfiguration) : Parcelable
|
||||
|
||||
private lateinit var mView: View
|
||||
private lateinit var mSsid: TextView
|
||||
private lateinit var mPassword: EditText
|
||||
override val ret: Arg? get() {
|
||||
val config = WifiConfiguration()
|
||||
config.SSID = mSsid.text.toString()
|
||||
config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN)
|
||||
if (mPassword.length() != 0) {
|
||||
val password = mPassword.text.toString()
|
||||
config.preSharedKey = password
|
||||
}
|
||||
return Arg(config)
|
||||
}
|
||||
|
||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||
mView = requireActivity().layoutInflater.inflate(R.layout.dialog_wifi_ap, null)
|
||||
setView(mView)
|
||||
setTitle(R.string.repeater_configure)
|
||||
mSsid = mView.ssid
|
||||
mPassword = mView.password
|
||||
setPositiveButton(context.getString(R.string.wifi_save), listener)
|
||||
setNegativeButton(context.getString(R.string.wifi_cancel), null)
|
||||
setNeutralButton(context.getString(R.string.repeater_reset_credentials), listener)
|
||||
mSsid.text = arg.configuration.SSID
|
||||
mSsid.addTextChangedListener(this@WifiP2pDialogFragment)
|
||||
mPassword.setText(arg.configuration.preSharedKey)
|
||||
mPassword.addTextChangedListener(this@WifiP2pDialogFragment)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
validate()
|
||||
}
|
||||
|
||||
private fun validate() {
|
||||
val mSsidString = mSsid.text.toString()
|
||||
val ssidValid = mSsid.length() != 0 && Charset.forName("UTF-8").encode(mSsidString).limit() <= 32
|
||||
val passwordValid = mPassword.length() >= 8
|
||||
mView.password_wrapper.error =
|
||||
if (passwordValid) null else requireContext().getString(R.string.credentials_password_too_short)
|
||||
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).isEnabled = ssidValid && passwordValid
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
|
||||
override fun afterTextChanged(editable: Editable) = validate()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
package be.mygod.vpnhotspot.net.wifi.configuration
|
||||
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.Build
|
||||
@@ -0,0 +1,190 @@
|
||||
package be.mygod.vpnhotspot.net.wifi.configuration
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ClipData
|
||||
import android.content.DialogInterface
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiConfiguration.AuthAlgorithm
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Base64
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isGone
|
||||
import be.mygod.vpnhotspot.AlertDialogFragment
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.util.QRCodeDialog
|
||||
import be.mygod.vpnhotspot.util.toByteArray
|
||||
import be.mygod.vpnhotspot.util.toParcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
|
||||
import java.lang.IllegalStateException
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
|
||||
*
|
||||
* This dialog has been deprecated in API 28, but we are still using it since it works better for our purposes.
|
||||
* Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
|
||||
*/
|
||||
class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiApDialogFragment.Arg>(), TextWatcher,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
private val channels by lazy { (1..165).map { BandOption.Channel(it) } }
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Arg(val configuration: WifiConfiguration,
|
||||
val readOnly: Boolean = false,
|
||||
/**
|
||||
* KeyMgmt is enforced to WPA_PSK.
|
||||
* Various values for apBand are allowed according to different rules.
|
||||
*/
|
||||
val p2pMode: Boolean = false) : Parcelable
|
||||
|
||||
@TargetApi(23)
|
||||
private sealed class BandOption {
|
||||
open val apBand get() = AP_BAND_2GHZ
|
||||
open val apChannel get() = 0
|
||||
|
||||
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 fun toString() = app.getString(R.string.wifi_ap_choose_2G)
|
||||
}
|
||||
object Band5GHz : BandOption() {
|
||||
override val apBand get() = AP_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)"
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var dialogView: View
|
||||
private lateinit var bandOptions: MutableList<BandOption>
|
||||
private var started = false
|
||||
override val ret get() = Arg(WifiConfiguration().apply {
|
||||
SSID = dialogView.ssid.text.toString()
|
||||
allowedKeyManagement.set(
|
||||
if (arg.p2pMode) WifiConfiguration.KeyMgmt.WPA_PSK else dialogView.security.selectedItemPosition)
|
||||
allowedAuthAlgorithms.set(AuthAlgorithm.OPEN)
|
||||
if (dialogView.password.length() != 0) preSharedKey = dialogView.password.text.toString()
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
val bandOption = dialogView.band.selectedItem as BandOption
|
||||
apBand = bandOption.apBand
|
||||
apChannel = bandOption.apChannel
|
||||
}
|
||||
})
|
||||
|
||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||
val activity = requireActivity()
|
||||
dialogView = activity.layoutInflater.inflate(R.layout.dialog_wifi_ap, null)
|
||||
setView(dialogView)
|
||||
if (!arg.readOnly) setPositiveButton(R.string.wifi_save, listener)
|
||||
setNegativeButton(R.string.donations__button_close, null)
|
||||
dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration)
|
||||
dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment)
|
||||
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (arg.p2pMode) dialogView.security_wrapper.isGone = true else dialogView.security.apply {
|
||||
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
||||
WifiConfiguration.KeyMgmt.strings).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) =
|
||||
throw IllegalStateException("Must select something")
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
dialogView.password_wrapper.isGone = position == WifiConfiguration.KeyMgmt.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
bandOptions = mutableListOf<BandOption>().apply {
|
||||
if (arg.p2pMode) add(BandOption.BandAny) else {
|
||||
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
|
||||
add(BandOption.Band2GHz)
|
||||
add(BandOption.Band5GHz)
|
||||
}
|
||||
addAll(channels)
|
||||
}
|
||||
dialogView.band.adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
||||
bandOptions).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
} else dialogView.band_wrapper.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)
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
started = true
|
||||
if (!arg.readOnly) validate()
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is reached only if not arg.readOnly.
|
||||
*/
|
||||
private fun validate() {
|
||||
if (!started) return
|
||||
val ssidValid = dialogView.ssid.length() != 0 &&
|
||||
Charset.forName("UTF-8").encode(dialogView.ssid.text.toString()).limit() <= 32
|
||||
val passwordValid = when (dialogView.security.selectedItemPosition) {
|
||||
WifiConfiguration.KeyMgmt.WPA_PSK, WPA2_PSK -> dialogView.password.length() >= 8
|
||||
else -> true // do not try to validate
|
||||
}
|
||||
dialogView.password_wrapper.error = if (passwordValid) null else {
|
||||
requireContext().getString(R.string.credentials_password_too_short)
|
||||
}
|
||||
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).isEnabled = ssidValid && passwordValid
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
|
||||
override fun afterTextChanged(editable: Editable) = validate()
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
return when (item?.itemId) {
|
||||
android.R.id.copy -> {
|
||||
app.clipboard.primaryClip = ClipData.newPlainText(null,
|
||||
Base64.encodeToString(ret.configuration.toByteArray(), BASE64_FLAGS))
|
||||
true
|
||||
}
|
||||
android.R.id.paste -> {
|
||||
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
|
||||
Base64.decode(toString(), BASE64_FLAGS).toParcelable<WifiConfiguration>()
|
||||
?.let { populateFromConfiguration(it) }
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.share_qr -> {
|
||||
QRCodeDialog().withArg(ret.configuration.toQRString())
|
||||
.show(fragmentManager ?: return false, "QRCodeDialog")
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
val WifiConfiguration.apKeyManagement get() = allowedKeyManagement.nextSetBit(0).also { selected ->
|
||||
check(selected >= 0) { "No key management selected" }
|
||||
check(allowedKeyManagement.nextSetBit(selected + 1) < 0) { "More than 1 key managements supplied" }
|
||||
}
|
||||
|
||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||
/**
|
||||
* Documentation: https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
|
||||
*/
|
||||
fun WifiConfiguration.toQRString() = StringBuilder("WIFI:").apply {
|
||||
fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
|
||||
var password = true
|
||||
when (apKeyManagement) {
|
||||
WifiConfiguration.KeyMgmt.NONE -> password = false
|
||||
WifiConfiguration.KeyMgmt.WPA_PSK, WifiConfiguration.KeyMgmt.WPA_EAP, WPA2_PSK -> append("T:WPA;")
|
||||
else -> throw IllegalArgumentException("Unsupported authentication type")
|
||||
}
|
||||
append("S:")
|
||||
append(SSID.sanitize())
|
||||
append(';')
|
||||
if (password) {
|
||||
append("P:")
|
||||
append(preSharedKey.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)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package be.mygod.vpnhotspot.room
|
||||
|
||||
import android.os.Parcel
|
||||
import android.text.TextUtils
|
||||
import androidx.room.TypeConverter
|
||||
import be.mygod.vpnhotspot.util.useParcel
|
||||
import java.net.InetAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -10,27 +10,17 @@ import java.nio.ByteOrder
|
||||
object Converters {
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun persistCharSequence(cs: CharSequence): ByteArray {
|
||||
val p = Parcel.obtain()
|
||||
try {
|
||||
TextUtils.writeToParcel(cs, p, 0)
|
||||
return p.marshall()
|
||||
} finally {
|
||||
p.recycle()
|
||||
}
|
||||
fun persistCharSequence(cs: CharSequence) = useParcel { p ->
|
||||
TextUtils.writeToParcel(cs, p, 0)
|
||||
p.marshall()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun unpersistCharSequence(data: ByteArray): CharSequence {
|
||||
val p = Parcel.obtain()
|
||||
try {
|
||||
p.unmarshall(data, 0, data.size)
|
||||
p.setDataPosition(0)
|
||||
return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)
|
||||
} finally {
|
||||
p.recycle()
|
||||
}
|
||||
fun unpersistCharSequence(data: ByteArray) = useParcel { p ->
|
||||
p.unmarshall(data, 0, data.size)
|
||||
p.setDataPosition(0)
|
||||
TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package be.mygod.vpnhotspot.util
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import be.mygod.vpnhotspot.R
|
||||
import net.glxn.qrgen.android.QRCode
|
||||
|
||||
class QRCodeDialog : DialogFragment() {
|
||||
companion object {
|
||||
private const val KEY_ARG = "arg"
|
||||
}
|
||||
|
||||
fun withArg(arg: String) = apply { arguments = bundleOf(KEY_ARG to arg) }
|
||||
private val arg get() = arguments?.getString(KEY_ARG)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
|
||||
ImageView(context).apply {
|
||||
val size = resources.getDimensionPixelSize(R.dimen.qr_code_size)
|
||||
layoutParams = ViewGroup.LayoutParams(size, size)
|
||||
setImageBitmap((QRCode.from(arg).withSize(size, size) as QRCode).bitmap())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package be.mygod.vpnhotspot.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.net.InetAddresses
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
@@ -17,7 +20,6 @@ import androidx.databinding.BindingAdapter
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.room.macToString
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import java.lang.RuntimeException
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
@@ -34,6 +36,25 @@ fun Long.toPluralInt(): Int {
|
||||
return (this % 1000000000).toInt() + 1000000000
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
|
||||
try {
|
||||
block(this)
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun Parcelable.toByteArray(parcelableFlags: Int = 0) = useParcel { p ->
|
||||
p.writeParcelable(this, parcelableFlags)
|
||||
p.marshall()
|
||||
}
|
||||
fun <T : Parcelable> ByteArray.toParcelable() = useParcel { p ->
|
||||
p.unmarshall(this, 0, size)
|
||||
p.setDataPosition(0)
|
||||
p.readParcelable<T>(javaClass.classLoader)
|
||||
}
|
||||
|
||||
fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = receiver(context, intent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user