diff --git a/README.md b/README.md
index 2fde4596..6b54d09a 100644
--- a/README.md
+++ b/README.md
@@ -128,6 +128,9 @@ Undocumented API list:
* (since API 24) [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112882)
* (since API 24) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112972)
* (since API 24) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112974)
+* [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#121357)
+* [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#121416)
+* (deprecated since API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
* [`Landroid/net/wifi/p2p/WifiP2pGroup;->getNetworkId()I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123194)
* [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/Collection;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123239)
* [`Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123431)
@@ -135,7 +138,6 @@ Undocumented API list:
* [`Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123458)
* [`Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123459)
* [`Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#299587)
-* (deprecated since API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
Undocumented system configurations:
diff --git a/mobile/build.gradle b/mobile/build.gradle
index 4629ec9c..efc94153 100644
--- a/mobile/build.gradle
+++ b/mobile/build.gradle
@@ -19,8 +19,8 @@ android {
minSdkVersion 21
targetSdkVersion 28
resConfigs "ru", "zh-rCN"
- versionCode 122
- versionName "2.3.3"
+ versionCode 200
+ versionName "3.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
@@ -86,6 +86,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.linkedin.dexmaker:dexmaker:2.25.0'
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
+ implementation 'net.glxn.qrgen:android:2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
for (dep in aux) {
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index 3d9e95fe..3cd0dc8b 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -32,6 +32,8 @@
+
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
index 0e0eaa55..32878a84 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt
@@ -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()!! }
+ val clipboard by lazy { getSystemService()!! }
val uiMode by lazy { getSystemService()!! }
val wifi by lazy { getSystemService()!! }
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
index bf3787d0..db464636 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt
@@ -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!!
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt
index 0830a3f1..8ae2650b 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt
@@ -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 }
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt
index 19454a5e..2b0de909 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt
@@ -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 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
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt
index 3501a041..f49bdb05 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt
@@ -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(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
}
}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
index 1b616cea..65161c04 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt
@@ -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) {
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(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, grantResults: IntArray) {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
index c4fbd5ec..7d9740ac 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt
@@ -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) {
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pDialogFragment.kt
deleted file mode 100644
index f430baf3..00000000
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pDialogFragment.kt
+++ /dev/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(), 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()
-}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/P2pSupplicantConfiguration.kt
similarity index 99%
rename from mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt
rename to mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/P2pSupplicantConfiguration.kt
index 06d9b626..396086b4 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/P2pSupplicantConfiguration.kt
@@ -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
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiApDialogFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiApDialogFragment.kt
new file mode 100644
index 00000000..ed61142b
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiApDialogFragment.kt
@@ -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(), 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
+ 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().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()
+ ?.let { populateFromConfiguration(it) }
+ }
+ true
+ }
+ R.id.share_qr -> {
+ QRCodeDialog().withArg(ret.configuration.toQRString())
+ .show(fragmentManager ?: return false, "QRCodeDialog")
+ true
+ }
+ else -> false
+ }
+ }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiConfiguration.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiConfiguration.kt
new file mode 100644
index 00000000..661be180
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/configuration/WifiConfiguration.kt
@@ -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)
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt b/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
index 9a4c95de..4199a33b 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
@@ -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
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/QRCodeDialog.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/QRCodeDialog.kt
new file mode 100644
index 00000000..34344c83
--- /dev/null
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/QRCodeDialog.kt
@@ -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())
+ }
+}
diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt
index e0824b0b..45037d7e 100644
--- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt
+++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt
@@ -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 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 ByteArray.toParcelable() = useParcel { p ->
+ p.unmarshall(this, 0, size)
+ p.setDataPosition(0)
+ p.readParcelable(javaClass.classLoader)
+}
+
fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = receiver(context, intent)
}
diff --git a/mobile/src/main/res/drawable/ic_content_wave.xml b/mobile/src/main/res/drawable/ic_content_wave.xml
deleted file mode 100644
index 68e040eb..00000000
--- a/mobile/src/main/res/drawable/ic_content_wave.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
diff --git a/mobile/src/main/res/drawable/ic_social_share.xml b/mobile/src/main/res/drawable/ic_social_share.xml
new file mode 100644
index 00000000..479da079
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_social_share.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/mobile/src/main/res/layout/dialog_wifi_ap.xml b/mobile/src/main/res/layout/dialog_wifi_ap.xml
index 2d3a445a..f89c79d4 100644
--- a/mobile/src/main/res/layout/dialog_wifi_ap.xml
+++ b/mobile/src/main/res/layout/dialog_wifi_ap.xml
@@ -1,6 +1,11 @@
-
-
+
-
-
+
+
-
+
+
-
+
+
+
+
-
-
-
+
+
+
+
-
-
-
+ android:layout_marginTop="8dip"
+ app:passwordToggleEnabled="true"
+ app:errorEnabled="true">
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/main/res/layout/listitem_repeater.xml b/mobile/src/main/res/layout/listitem_repeater.xml
index 12fe3340..d8723abb 100644
--- a/mobile/src/main/res/layout/listitem_repeater.xml
+++ b/mobile/src/main/res/layout/listitem_repeater.xml
@@ -69,100 +69,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/mobile/src/main/res/menu/toolbar_tethering.xml b/mobile/src/main/res/menu/toolbar_tethering.xml
index e1799d11..d67e6f58 100644
--- a/mobile/src/main/res/menu/toolbar_tethering.xml
+++ b/mobile/src/main/res/menu/toolbar_tethering.xml
@@ -9,4 +9,18 @@
app:showAsAction="always">
+
+ -
+
+
diff --git a/mobile/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml
index 1578c690..ad690c45 100644
--- a/mobile/src/main/res/values-ru/strings.xml
+++ b/mobile/src/main/res/values-ru/strings.xml
@@ -1,7 +1,10 @@
+
- Настройка Wi-Fi ретранслятора
Действительный конфиг не найден. Пожалуйста, сначала запустите ретранслятор.
Не удалось удалить избыточную группу P2P (причина: %s)
@@ -15,7 +18,9 @@
неподдерживаемая операция
Сервис недоступен. Попробуйте позже
-
+ "USB-модем"
+ "Точка доступа Wi‑Fi"
+ "Bluetooth-модем"
" (подключение)"
" (доступный)"
@@ -37,7 +42,16 @@
Ошибка: Нисходящий интерфейс не найден
Что-то пошло не так, пожалуйста, проверьте отладочную информацию.
+ Настройка Wi-Fi ретранслятора
+ "Имя сети"
+ "Защита"
+ "Пароль"
Пароль должен содержать не менее 8 символов.
+ "Диапазон частот Wi-Fi"
+ "Авто"
+ "2,4 ГГц"
+ "5,0 ГГц"
+ "Сохранить"
Закрыть
diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml
index 615d8312..1da58c64 100644
--- a/mobile/src/main/res/values-zh-rCN/strings.xml
+++ b/mobile/src/main/res/values-zh-rCN/strings.xml
@@ -12,11 +12,7 @@
请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。
成功注册 PIN。
打开 WPS 失败(原因:%s)
- 设置 WLAN 中继
未能找到有效的档案。请尝试先打开中继。
- 重置
- 凭据已重置。
- 重置凭据失败(原因:%s)
删除多余 P2P 群组失败(原因:%s)
Wi\u2011Fi 直连不可用,请打开 Wi\u2011Fi
@@ -47,7 +43,7 @@
关闭
diff --git a/mobile/src/main/res/values/dimen.xml b/mobile/src/main/res/values/dimen.xml
index 7cc70bcc..3a4f06a6 100644
--- a/mobile/src/main/res/values/dimen.xml
+++ b/mobile/src/main/res/values/dimen.xml
@@ -1,4 +1,5 @@
56dp
+ 250dp
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index 6b8a73b8..97f3aa67 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -2,8 +2,7 @@
Close
diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml
index 87353f85..3a9273fc 100644
--- a/mobile/src/main/res/values/styles.xml
+++ b/mobile/src/main/res/values/styles.xml
@@ -13,7 +13,6 @@
+
+