Merge branch 'v3' into q-beta

This commit is contained in:
Mygod
2019-04-04 19:37:27 +08:00
28 changed files with 676 additions and 358 deletions

View File

@@ -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:

View File

@@ -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) {

View File

@@ -32,6 +32,8 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_USB"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.TETHER_PRIVILEGED"
tools:ignore="ProtectedPermissions"/>

View File

@@ -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>()!! }

View File

@@ -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!!

View File

@@ -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 }

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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())
}
}

View File

@@ -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)
}

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17 16.99c-1.35 0-2.2 0.42 -2.95 0.8 -0.65 0.33 -1.18 0.6 -2.05 0.6 -0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.57-0.8-2.95-0.8s-2.2 0.42 -2.95 0.8 c-0.65 0.33 -1.17 0.6 -2.05 0.6 v1.95c1.35 0 2.2-0.42 2.95-0.8 0.65 -0.33 1.17-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.42 2.95-0.8c0.65-0.33 1.18-0.6 2.05-0.6 0.9 0 1.4 0.25 2.05 0.6 0.75 0.38 1.58 0.8 2.95 0.8 v-1.95c-0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.6-0.8-2.95-0.8zm0-4.45c-1.35 0-2.2 0.43 -2.95 0.8 -0.65 0.32 -1.18 0.6 -2.05 0.6 -0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.57-0.8-2.95-0.8s-2.2 0.43 -2.95 0.8 c-0.65 0.32 -1.17 0.6 -2.05 0.6 v1.95c1.35 0 2.2-0.43 2.95-0.8 0.65 -0.35 1.15-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.43 2.95-0.8c0.65-0.35 1.15-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.58 0.8 2.95 0.8 v-1.95c-0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.6-0.8-2.95-0.8zm2.95-8.08c-0.75-0.38-1.58-0.8-2.95-0.8s-2.2 0.42 -2.95 0.8 c-0.65 0.32 -1.18 0.6 -2.05 0.6 -0.9 0-1.4-0.25-2.05-0.6-0.75-0.37-1.57-0.8-2.95-0.8s-2.2 0.42 -2.95 0.8 c-0.65 0.33 -1.17 0.6 -2.05 0.6 v1.93c1.35 0 2.2-0.43 2.95-0.8 0.65 -0.33 1.17-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.43 2.95-0.8c0.65-0.32 1.18-0.6 2.05-0.6 0.9 0 1.4 0.25 2.05 0.6 0.75 0.38 1.58 0.8 2.95 0.8 V5.04c-0.9 0-1.4-0.25-2.05-0.58zM17 8.09c-1.35 0-2.2 0.43 -2.95 0.8 -0.65 0.35 -1.15 0.6 -2.05 0.6 s-1.4-0.25-2.05-0.6c-0.75-0.38-1.57-0.8-2.95-0.8s-2.2 0.43 -2.95 0.8 c-0.65 0.35 -1.15 0.6 -2.05 0.6 v1.95c1.35 0 2.2-0.43 2.95-0.8 0.65 -0.32 1.18-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.43 2.95-0.8c0.65-0.32 1.18-0.6 2.05-0.6 0.9 0 1.4 0.25 2.05 0.6 0.75 0.38 1.58 0.8 2.95 0.8 V9.49c-0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.6-0.8-2.95-0.8z" />
</vector>

View File

@@ -0,0 +1,6 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Source: https://android.googlesource.com/platform/packages/apps/Settings/+/6b4a31c/res/layout/wifi_ap_dialog.xml -->
<!-- Copyright (C) 2010 The Android Open Source Project
<!--
Based on:
* https://github.com/material-components/material-components-android/blob/da6096bb8df2ac5b0cabeaa7960501d4083e4ea9/lib/java/com/google/android/material/dialog/res/layout/mtrl_alert_dialog_title.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/6b4a31c/res/layout/wifi_ap_dialog.xml
-->
<!--
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -11,49 +16,97 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="300sp"
android:layout_height="wrap_content"
android:fadeScrollbars="false">
<LinearLayout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="300sp"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
style="@style/wifi_item">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ssid_wrapper"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:title="@string/configuration_view"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadeScrollbars="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:layout_marginBottom="8dip">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ssid"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
style="@style/wifi_item">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ssid_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ssid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:hint="@string/wifi_ssid"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLength="32" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/security_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:hint="@string/wifi_ssid"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLength="32" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_marginTop="8dip"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_label"
android:text="@string/wifi_security" />
<Spinner
android:id="@+id/security"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_content"
android:prompt="@string/wifi_security" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:singleLine="true"
android:hint="@string/wifi_password"
android:inputType="textPassword"
android:typeface="monospace"
android:maxLength="63"
android:imeOptions="flagForceAscii" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
android:layout_marginTop="8dip"
app:passwordToggleEnabled="true"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:singleLine="true"
android:hint="@string/wifi_password"
android:inputType="textPassword"
android:typeface="monospace"
android:maxLength="63"
android:imeOptions="flagForceAscii" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/band_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_label"
android:text="@string/wifi_hotspot_ap_band_title" />
<Spinner
android:id="@+id/band"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_content"
android:prompt="@string/wifi_hotspot_ap_band_title" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -69,100 +69,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:nextFocusDown="@+id/oc"
android:padding="16dp"
android:onClick="@{_ -> data.editConfigurations()}">
<Space
android:layout_width="40dp"
android:layout_height="0dp"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_device_wifi_lock"
android:tint="?android:attr/textColorPrimary"/>
<Space
android:layout_width="16dp"
android:layout_height="0dp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wifi_ssid"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{data.ssid}"
tools:text="…"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<Space
android:layout_width="40dp"
android:layout_height="0dp"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_content_wave"
android:tint="?android:attr/textColorPrimary"/>
<Space
android:layout_width="16dp"
android:layout_height="0dp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_service_repeater_oc"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<EditText
android:id="@+id/oc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={data.oc}"
android:inputType="number"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:maxLength="3"
android:hint="@string/settings_service_repeater_oc_summary"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:nextFocusUp="@+id/oc"
android:background="?android:attr/selectableItemBackground"
android:padding="16dp"
android:onClick="@{_ -> data.wps()}"

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@android:id/copy"
android:alphabeticShortcut="c"
android:icon="?attr/actionModeCopyDrawable"
android:title="@android:string/copy"
app:showAsAction="ifRoom"/>
<item android:id="@android:id/paste"
android:alphabeticShortcut="v"
android:icon="?attr/actionModePasteDrawable"
android:title="@android:string/paste"
app:showAsAction="ifRoom"/>
<item android:id="@+id/share_qr"
android:icon="@drawable/ic_social_share"
android:title="@string/configuration_share"
app:showAsAction="ifRoom"/>
</menu>

View File

@@ -9,4 +9,18 @@
app:showAsAction="always">
<menu/>
</item>
<item android:id="@+id/configuration"
android:icon="@drawable/ic_device_wifi_lock"
android:title="@string/configuration_view"
app:showAsAction="always">
<menu>
<item android:id="@+id/configuration_repeater"
android:title="@string/title_repeater"/>
<item android:id="@+id/configuration_temp_hotspot"
android:title="@string/tethering_temp_hotspot"/>
<item android:id="@+id/configuration_ap"
android:title="@string/tethering_manage_wifi"/>
</menu>
</item>
</menu>

View File

@@ -1,7 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Values copied from:
* https://android.googlesource.com/platform/packages/apps/Settings/+/419086d/res/values-ru/strings.xml
-->
<string name="repeater_configure">Настройка Wi-Fi ретранслятора</string>
<string name="repeater_configure_failure">Действительный конфиг не найден. Пожалуйста, сначала запустите ретранслятор.</string>
<string name="repeater_clean_pog_failure">Не удалось удалить избыточную группу P2P (причина: %s)</string>
@@ -15,7 +18,9 @@
<string name="repeater_failure_reason_unsupported_operation">неподдерживаемая операция</string>
<string name="repeater_failure_disconnected">Сервис недоступен. Попробуйте позже</string>
<string name="tethering_manage_usb" msgid="585829947108007917">"USB-модем"</string>
<string name="tethering_manage_wifi" msgid="7763495093333664887">"Точка доступа WiFi"</string>
<string name="tethering_manage_bluetooth" msgid="2379175828878753652">"Bluetooth-модем"</string>
<string name="connected_state_incomplete">" (подключение)"</string>
<string name="connected_state_valid">" (доступный)"</string>
@@ -37,7 +42,16 @@
<string name="exception_interface_not_found">Ошибка: Нисходящий интерфейс не найден</string>
<string name="noisy_su_failure">Что-то пошло не так, пожалуйста, проверьте отладочную информацию.</string>
<string name="configuration_view">Настройка Wi-Fi ретранслятора</string>
<string name="wifi_ssid" msgid="5519636102673067319">"Имя сети"</string>
<string name="wifi_security" msgid="6603611185592956936">"Защита"</string>
<string name="wifi_password" msgid="5948219759936151048">"Пароль"</string>
<string name="credentials_password_too_short">Пароль должен содержать не менее 8 символов.</string>
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"Диапазон частот Wi-Fi"</string>
<string name="wifi_ap_choose_auto" msgid="2677800651271769965">"Авто"</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2,4 ГГц"</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5,0 ГГц"</string>
<string name="wifi_save" msgid="3331121567988522826">"Сохранить"</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values-ru/donations__strings.xml -->
<string name="donations__button_close">Закрыть</string>

View File

@@ -12,11 +12,7 @@
<string name="repeater_wps_success_pbc">请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。</string>
<string name="repeater_wps_success_keypad">成功注册 PIN。</string>
<string name="repeater_wps_failure">打开 WPS 失败(原因:%s</string>
<string name="repeater_configure">设置 WLAN 中继</string>
<string name="repeater_configure_failure">未能找到有效的档案。请尝试先打开中继。</string>
<string name="repeater_reset_credentials">重置</string>
<string name="repeater_reset_credentials_success">凭据已重置。</string>
<string name="repeater_reset_credentials_failure">重置凭据失败(原因:%s</string>
<string name="repeater_clean_pog_failure">删除多余 P2P 群组失败(原因:%s</string>
<string name="repeater_p2p_unavailable">Wi\u2011Fi 直连不可用,请打开 Wi\u2011Fi</string>
@@ -47,7 +43,7 @@
<!--
Values copied from:
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/b63de87/res/values-zh-rCN/strings.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/419086d/res/values-zh-rCN/strings.xml
* @string/usb_tethering_button_text
* @string/wifi_hotspot_checkbox_text
* @string/bluetooth_tether_checkbox_text
@@ -86,8 +82,6 @@
<string name="settings_service_masquerade_none"></string>
<string name="settings_service_masquerade_simple">简易</string>
<string name="settings_service_masquerade_netd">Android Netd 服务</string>
<string name="settings_service_repeater_oc">Wi\u2011Fi 运行频段 (不稳定)</string>
<string name="settings_service_repeater_oc_summary">"自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)"</string>
<string name="settings_service_disable_ipv6">禁用 IPv6 共享</string>
<string name="settings_service_disable_ipv6_summary">防止 VPN 通过 IPv6 泄漏。</string>
<string name="settings_service_repeater_start_on_boot">开机自启动中继</string>
@@ -125,17 +119,24 @@
<plurals name="notification_interfaces">
<item quantity="other">%d 个接口</item>
</plurals>
<string name="notification_interfaces_inactive">不活跃:</string>
<string name="notification_interfaces_inactive">不活跃:%s</string>
<string name="failure_reason_unknown">未知 #%d</string>
<string name="exception_interface_not_found">错误:未找到下游接口</string>
<string name="noisy_su_failure">发生异常,详情请查看调试信息。</string>
<string name="configuration_view">设置 WLAN</string>
<string name="configuration_share">使用 QR 码分享</string>
<string name="configuration_rejected">Android 系统拒绝使用此配置。(详情参见日志)</string>
<string name="wifi_ssid" msgid="5519636102673067319">"网络名称"</string>
<string name="wifi_security" msgid="6603611185592956936">"安全性"</string>
<string name="wifi_password" msgid="5948219759936151048">"密码"</string>
<string name="credentials_password_too_short" msgid="7502749986405522663">"密码至少应包含 8 个字符。"</string>
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"AP 频段"</string>
<string name="wifi_ap_choose_auto" msgid="2677800651271769965">"自动"</string>
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2.4 GHz 频段"</string>
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5.0 GHz 频段"</string>
<string name="wifi_save" msgid="3331121567988522826">"保存"</string>
<string name="wifi_cancel" msgid="6763568902542968964">"取消"</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values-zh/donations__strings.xml -->
<string name="donations__button_close">关闭</string>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="listitem_manage_tether_padding_start">56dp</dimen>
<dimen name="qr_code_size">250dp</dimen>
</resources>

View File

@@ -2,8 +2,7 @@
<!--
Values copied from:
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/e5ed810/res/values/strings.xml
* @string/wifi_tether_configure_ap_text
* https://android.googlesource.com/platform/packages/apps/Settings/+/5697a7e/res/values/strings.xml
* @string/usb_tethering_button_text
* @string/wifi_hotspot_checkbox_text
* @string/bluetooth_tether_checkbox_text
@@ -22,11 +21,7 @@
device.</string>
<string name="repeater_wps_success_keypad">PIN registered.</string>
<string name="repeater_wps_failure">Failed to start WPS (reason: %s)</string>
<string name="repeater_configure">Configure Wi\u2011Fi repeater</string>
<string name="repeater_configure_failure">Valid config not found. Please start repeater first.</string>
<string name="repeater_reset_credentials">Reset</string>
<string name="repeater_reset_credentials_success">Credentials reset.</string>
<string name="repeater_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
<string name="repeater_clean_pog_failure">Failed to remove redundant P2P group (reason: %s)</string>
<string name="repeater_p2p_unavailable">Wi\u2011Fi direct unavailable, please enable Wi\u2011Fi</string>
@@ -92,8 +87,6 @@
<string name="settings_service_masquerade_none">None</string>
<string name="settings_service_masquerade_simple">Simple</string>
<string name="settings_service_masquerade_netd">Android Netd Service</string>
<string name="settings_service_repeater_oc">Operating Wi\u2011Fi channel (unstable)</string>
<string name="settings_service_repeater_oc_summary">Auto (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)</string>
<string name="settings_service_disable_ipv6">Disable IPv6 tethering</string>
<string name="settings_service_disable_ipv6_summary">Enabling this option will prevent VPN leaks via IPv6.</string>
<string name="settings_service_repeater_start_on_boot">Start repeater on boot</string>
@@ -136,17 +129,24 @@
<item quantity="one">%d interface</item>
<item quantity="other">%d interfaces</item>
</plurals>
<string name="notification_interfaces_inactive">"Inactive: "</string>
<string name="notification_interfaces_inactive">Inactive: %s</string>
<string name="failure_reason_unknown">unknown #%d</string>
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
<string name="noisy_su_failure">Something went wrong, please check the debug information.</string>
<string name="configuration_view">Wi\u2011Fi configuration</string>
<string name="configuration_share">Share via QR code</string>
<string name="configuration_rejected">Android system refuses such configuration. (see logcat)</string>
<string name="wifi_ssid">Network name</string>
<string name="wifi_security">Security</string>
<string name="wifi_password">Password</string>
<string name="credentials_password_too_short">The password must have at least 8 characters.</string>
<string name="wifi_hotspot_ap_band_title">AP Band</string>
<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_save">Save</string>
<string name="wifi_cancel">Cancel</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml -->
<string name="donations__button_close">Close</string>

View File

@@ -13,7 +13,6 @@
<!-- https://android.googlesource.com/platform/packages/apps/Settings/+/7efcc35/res/values/styles.xml -->
<style name="wifi_item">
<item name="android:layout_marginTop">8dip</item>
<item name="android:layout_marginStart">8dip</item>
<item name="android:layout_marginEnd">8dip</item>
<item name="android:paddingStart">8dip</item>
@@ -21,6 +20,18 @@
<item name="android:orientation">vertical</item>
<item name="android:gravity">start</item>
</style>
<style name="wifi_item_label">
<item name="android:paddingStart">8dip</item>
<item name="android:textSize">14sp</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:textAppearance">@android:style/TextAppearance.Material.Body1</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<style name="wifi_item_content">
<item name="android:textAlignment">viewStart</item>
<item name="android:textAppearance">@android:style/TextAppearance.Material.Subhead</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="wifi_item_edit_content">
<item name="android:paddingStart">4dip</item>
<item name="android:layout_marginStart">4dip</item>