Huge refactor for better maintainability

This commit is contained in:
Mygod
2018-06-01 20:21:05 +08:00
parent a5bec59bbe
commit 8aa7d6d8c7
35 changed files with 1072 additions and 824 deletions

View File

@@ -6,6 +6,7 @@ import android.net.wifi.WifiManager
import android.support.annotation.RequiresApi
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.manage.LocalOnlyHotspotManager
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.broadcastReceiver
@@ -18,7 +19,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
}
inner class Binder : android.os.Binder() {
var fragment: TetheringFragment? = null
var manager: LocalOnlyHotspotManager? = null
var iface: String? = null
val configuration get() = reservation?.wifiConfiguration
@@ -48,7 +49,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
IpNeighbourMonitor.registerCallback(this)
} else check(iface == routingManager.downstream)
}
app.handler.post { binder.fragment?.adapter?.updateLocalOnlyViewHolder() }
app.handler.post { binder.manager?.update() }
}
override val activeIfaces get() = listOfNotNull(binder.iface)

View File

@@ -13,7 +13,9 @@ import android.support.v7.app.AppCompatActivity
import android.view.Gravity
import android.view.MenuItem
import be.mygod.vpnhotspot.client.ClientMonitorService
import be.mygod.vpnhotspot.client.ClientsFragment
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import q.rorbin.badgeview.QBadgeView
@@ -26,21 +28,21 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.navigation.setOnNavigationItemSelectedListener(this)
if (savedInstanceState == null) displayFragment(RepeaterFragment())
if (savedInstanceState == null) displayFragment(TetheringFragment())
badge = QBadgeView(this)
badge.bindTarget((binding.navigation.getChildAt(0) as BottomNavigationMenuView).getChildAt(0))
badge.bindTarget((binding.navigation.getChildAt(0) as BottomNavigationMenuView).getChildAt(1))
badge.badgeBackgroundColor = ContextCompat.getColor(this, R.color.colorAccent)
badge.badgeTextColor = ContextCompat.getColor(this, R.color.primary_text_default_material_light)
badge.badgeGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
badge.setGravityOffset(16f, 0f, true)
ServiceForegroundConnector(this, ClientMonitorService::class)
ServiceForegroundConnector(this, this, ClientMonitorService::class)
}
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
R.id.navigation_repeater -> {
R.id.navigation_clients -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(RepeaterFragment())
displayFragment(ClientsFragment())
}
true
}
@@ -54,7 +56,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
R.id.navigation_settings -> {
if (!item.isChecked) {
item.isChecked = true
displayFragment(SettingsFragment())
displayFragment(SettingsPreferenceFragment())
}
true
}

View File

@@ -1,208 +0,0 @@
package be.mygod.vpnhotspot
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.DataBindingUtil
import android.net.wifi.WifiConfiguration
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Bundle
import android.os.IBinder
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatDialog
import android.support.v7.recyclerview.extensions.ListAdapter
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.*
import android.widget.EditText
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.client.Client
import be.mygod.vpnhotspot.client.ClientMonitorService
import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.WifiP2pDialog
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
import java.net.NetworkInterface
import java.net.SocketException
class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
inner class Data : BaseObservable() {
val switchEnabled: Boolean
@Bindable get() = when (binder?.service?.status) {
RepeaterService.Status.IDLE, RepeaterService.Status.ACTIVE -> true
else -> false
}
var serviceStarted: Boolean
@Bindable get() = when (binder?.service?.status) {
RepeaterService.Status.STARTING, RepeaterService.Status.ACTIVE -> true
else -> false
}
set(value) {
val binder = binder
when (binder?.service?.status) {
RepeaterService.Status.IDLE ->
if (value) {
val context = requireContext()
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
}
RepeaterService.Status.ACTIVE -> if (!value) binder.shutdown()
else -> { }
}
}
val ssid @Bindable get() = binder?.service?.group?.networkName ?: getText(R.string.service_inactive)
val addresses @Bindable get(): String {
return try {
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
} catch (e: SocketException) {
e.printStackTrace()
""
}
}
fun onStatusChanged() {
notifyPropertyChanged(BR.switchEnabled)
notifyPropertyChanged(BR.serviceStarted)
notifyPropertyChanged(BR.addresses)
}
fun onGroupChanged(group: WifiP2pGroup? = null) {
notifyPropertyChanged(BR.ssid)
p2pInterface = group?.`interface`
notifyPropertyChanged(BR.addresses)
}
}
private class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
private inner class ClientAdapter : ListAdapter<Client, ClientViewHolder>(Client) {
override fun submitList(list: MutableList<Client>?) {
super.submitList(list)
binding.swipeRefresher.isRefreshing = false
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ClientViewHolder(ListitemClientBinding.inflate(LayoutInflater.from(parent.context)))
override fun onBindViewHolder(holder: ClientViewHolder, position: Int) {
holder.binding.client = getItem(position)
holder.binding.executePendingBindings()
}
}
private lateinit var binding: FragmentRepeaterBinding
private val data = Data()
private val adapter = ClientAdapter()
private var binder: RepeaterService.Binder? = null
private var p2pInterface: String? = null
private var clients: ClientMonitorService.Binder? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_repeater, container, false)
binding.data = data
binding.clients.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.clients.itemAnimator = DefaultItemAnimator()
binding.clients.adapter = adapter
binding.swipeRefresher.setColorSchemeResources(R.color.colorAccent)
binding.swipeRefresher.setOnRefreshListener {
IpNeighbourMonitor.instance?.flush()
val binder = binder
if (binder?.active == false) {
try {
binder.requestGroupUpdate()
} catch (exc: UninitializedPropertyAccessException) {
exc.printStackTrace()
}
}
}
binding.toolbar.inflateMenu(R.menu.repeater)
binding.toolbar.setOnMenuItemClickListener(this)
ServiceForegroundConnector(this, RepeaterService::class, ClientMonitorService::class)
return binding.root
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
if (service is ClientMonitorService.Binder) {
clients = service
service.clientsChanged[this] = { adapter.submitList(it.toMutableList()) }
return
}
val binder = service as RepeaterService.Binder
this.binder = binder
binder.statusChanged[this] = data::onStatusChanged
binder.groupChanged[this] = data::onGroupChanged
}
override fun onServiceDisconnected(name: ComponentName?) {
val clients = clients
if (clients != null) {
this.clients = null
clients.clientsChanged -= this
}
val binder = binder ?: return
this.binder = null
binder.statusChanged -= this
binder.groupChanged -= this
data.onStatusChanged()
}
override fun onMenuItemClick(item: MenuItem) = when (item.itemId) {
R.id.wps -> if (binder?.active == true) {
val dialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.repeater_wps_dialog_title)
.setView(R.layout.dialog_wps)
.setPositiveButton(android.R.string.ok, { dialog, _ -> binder?.startWps((dialog as AppCompatDialog)
.findViewById<EditText>(android.R.id.edit)!!.text.toString()) })
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.repeater_wps_dialog_pbc, { _, _ -> binder?.startWps(null) })
.create()
dialog.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
dialog.show()
true
} else false
R.id.edit -> {
editConfigurations()
true
}
else -> false
}
private fun editConfigurations() {
val binder = binder
val group = binder?.service?.group
val ssid = group?.networkName
val context = requireContext()
if (ssid != null) {
val wifi = WifiConfiguration()
val conf = P2pSupplicantConfiguration()
wifi.SSID = ssid
wifi.preSharedKey = group.passphrase
if (wifi.preSharedKey == null) wifi.preSharedKey = conf.readPsk()
if (wifi.preSharedKey != null) {
var dialog: WifiP2pDialog? = null
dialog = WifiP2pDialog(context, DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> when (conf.update(dialog!!.config!!)) {
true -> app.handler.postDelayed(binder::requestGroupUpdate, 1000)
false -> Toast.makeText(context, R.string.noisy_su_failure, Toast.LENGTH_SHORT).show()
null -> Toast.makeText(context, R.string.root_unavailable, Toast.LENGTH_SHORT).show()
}
DialogInterface.BUTTON_NEUTRAL -> binder.resetCredentials()
}
}, wifi)
dialog.show()
return
}
}
Toast.makeText(context, R.string.repeater_configure_failure, Toast.LENGTH_LONG).show()
}
}

View File

@@ -1,12 +0,0 @@
package be.mygod.vpnhotspot
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
class SettingsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_settings, container, false)
}

View File

@@ -1,379 +0,0 @@
package be.mygod.vpnhotspot
import android.Manifest
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.content.*
import android.content.pm.PackageManager
import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.DataBindingUtil
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.provider.Settings
import android.support.annotation.RequiresApi
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.recyclerview.extensions.ListAdapter
import android.support.v7.util.DiffUtil
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.databinding.ListitemManageTetherBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.formatAddresses
import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface
import java.net.SocketException
import java.util.*
class TetheringFragment : Fragment(), ServiceConnection {
companion object {
private const val VIEW_TYPE_INTERFACE = 0
private const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
private const val VIEW_TYPE_MANAGE = 1
private const val VIEW_TYPE_WIFI = 2
private const val VIEW_TYPE_USB = 3
private const val VIEW_TYPE_BLUETOOTH = 4
private const val VIEW_TYPE_WIFI_LEGACY = 5
private const val START_LOCAL_ONLY_HOTSPOT = 1
/**
* PAN Profile
* From BluetoothProfile.java.
*/
private const val PAN = 5
private val isTetheringOn by lazy @SuppressLint("PrivateApi") {
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn")
}
}
interface Data {
val icon: Int
val title: CharSequence
val text: CharSequence
val active: Boolean
val selectable: Boolean
}
inner class TetheredData(val iface: TetheredInterface) : Data {
override val icon get() = TetherType.ofInterface(iface.name).icon
override val title get() = iface.name
override val text get() = iface.addresses
override val active = tetheringBinder?.isActive(iface.name) == true
override val selectable get() = true
}
inner class LocalHotspotData(private val lookup: Map<String, NetworkInterface>) : Data {
override val icon: Int get() {
val iface = hotspotBinder?.iface ?: return TetherType.WIFI.icon
return TetherType.ofInterface(iface).icon
}
override val title get() = getString(R.string.tethering_temp_hotspot)
override val text by lazy {
val binder = hotspotBinder
val configuration = binder?.configuration ?: return@lazy getText(R.string.service_inactive)
val iface = binder.iface ?: return@lazy getText(R.string.service_inactive)
"${configuration.SSID} - ${configuration.preSharedKey}\n${TetheredInterface(iface, lookup).addresses}"
}
override val active = hotspotBinder?.iface != null
override val selectable get() = active
}
private open class InterfaceViewHolder(val binding: ListitemInterfaceBinding) :
RecyclerView.ViewHolder(binding.root), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
override fun onClick(view: View) {
val context = itemView.context
val data = binding.data as TetheredData
if (data.active) context.startService(Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, data.iface.name))
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACE, data.iface.name))
}
}
@RequiresApi(26)
private inner class LocalOnlyHotspotViewHolder(binding: ListitemInterfaceBinding) : InterfaceViewHolder(binding) {
override fun onClick(view: View) {
val binder = hotspotBinder
if (binder?.iface != null) binder.stop() else {
val context = requireContext()
if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED) {
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
} else {
requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), START_LOCAL_ONLY_HOTSPOT)
}
}
}
}
private class ManageViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
init {
view.setOnClickListener(this)
}
override fun onClick(v: View?) = try {
itemView.context.startActivity(Intent()
.setClassName("com.android.settings", "com.android.settings.Settings\$TetherSettingsActivity"))
} catch (e: ActivityNotFoundException) {
itemView.context.startActivity(Intent()
.setClassName("com.android.settings", "com.android.settings.TetherSettings"))
}
}
private inner class ManageItemHolder(binding: ListitemManageTetherBinding, private val type: Int)
: RecyclerView.ViewHolder(binding.root), View.OnClickListener, TetheringManager.OnStartTetheringCallback {
val tetherType = when (type) {
VIEW_TYPE_WIFI, VIEW_TYPE_WIFI_LEGACY -> TetherType.WIFI
VIEW_TYPE_USB -> TetherType.USB
VIEW_TYPE_BLUETOOTH -> TetherType.BLUETOOTH
else -> TetherType.NONE
}
init {
itemView.setOnClickListener(this)
binding.icon = tetherType.icon
binding.title = getString(when (type) {
VIEW_TYPE_USB -> R.string.tethering_manage_usb
VIEW_TYPE_WIFI -> R.string.tethering_manage_wifi
VIEW_TYPE_WIFI_LEGACY -> R.string.tethering_manage_wifi_legacy
VIEW_TYPE_BLUETOOTH -> R.string.tethering_manage_bluetooth
else -> throw IllegalStateException("Unexpected view type")
})
binding.tetherListener = tetherListener
binding.type = tetherType
}
override fun onClick(v: View?) {
val context = requireContext()
if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(context)) {
startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
Uri.parse("package:${context.packageName}")))
return
}
val started = tetherListener.isStarted(tetherType)
try {
when (type) {
VIEW_TYPE_WIFI -> @RequiresApi(24) {
if (started) TetheringManager.stop(TetheringManager.TETHERING_WIFI)
else TetheringManager.start(TetheringManager.TETHERING_WIFI, true, this)
}
VIEW_TYPE_USB -> @RequiresApi(24) {
if (started) TetheringManager.stop(TetheringManager.TETHERING_USB)
else TetheringManager.start(TetheringManager.TETHERING_USB, true, this)
}
VIEW_TYPE_BLUETOOTH -> @RequiresApi(24) {
if (started) {
TetheringManager.stop(TetheringManager.TETHERING_BLUETOOTH)
Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state
} else TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this)
}
VIEW_TYPE_WIFI_LEGACY -> @Suppress("DEPRECATION") {
if (started) WifiApManager.stop() else WifiApManager.start()
}
}
} catch (e: InvocationTargetException) {
e.printStackTrace()
var cause: Throwable? = e
while (cause != null) {
cause = cause.cause
if (cause != null && cause !is InvocationTargetException) {
Toast.makeText(context, cause.message, Toast.LENGTH_LONG).show()
break
}
}
}
}
override fun onTetheringStarted() = tetherListener.notifyPropertyChanged(BR.enabledTypes)
override fun onTetheringFailed() {
app.handler.post {
Toast.makeText(requireContext(), R.string.tethering_manage_failed, Toast.LENGTH_SHORT).show()
}
}
}
inner class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
var enabledTypes = emptySet<TetherType>()
@Bindable get
set(value) {
field = value
notifyPropertyChanged(BR.enabledTypes)
}
var pan: BluetoothProfile? = null
override fun onServiceDisconnected(profile: Int) {
pan = null
}
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
pan = proxy
}
/**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
*/
fun isStarted(type: TetherType, enabledTypes: Set<TetherType> = this.enabledTypes) =
if (type == TetherType.BLUETOOTH) {
val pan = pan
BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && pan != null &&
isTetheringOn.invoke(pan) as Boolean
} else enabledTypes.contains(type)
}
class TetheredInterface(val name: String, lookup: Map<String, NetworkInterface>) : Comparable<TetheredInterface> {
val addresses = lookup[name]?.formatAddresses() ?: ""
override fun compareTo(other: TetheredInterface) = name.compareTo(other.name)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TetheredInterface
if (name != other.name) return false
if (addresses != other.addresses) return false
return true
}
override fun hashCode(): Int = Objects.hash(name, addresses)
object DiffCallback : DiffUtil.ItemCallback<TetheredInterface>() {
override fun areItemsTheSame(oldItem: TetheredInterface, newItem: TetheredInterface) =
oldItem.name == newItem.name
override fun areContentsTheSame(oldItem: TetheredInterface, newItem: TetheredInterface) = oldItem == newItem
}
}
inner class TetheringAdapter :
ListAdapter<TetheredInterface, RecyclerView.ViewHolder>(TetheredInterface.DiffCallback) {
private var lookup: Map<String, NetworkInterface> = emptyMap()
fun update(activeIfaces: List<String>, localOnlyIfaces: List<String>) {
lookup = try {
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
} catch (e: SocketException) {
e.printStackTrace()
emptyMap()
}
this@TetheringFragment.tetherListener.enabledTypes =
(activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
submitList(activeIfaces.map { TetheredInterface(it, lookup) }.sorted())
if (Build.VERSION.SDK_INT >= 26) updateLocalOnlyViewHolder()
}
override fun getItemCount() = super.getItemCount() + if (Build.VERSION.SDK_INT < 24) 2 else 5
override fun getItemViewType(position: Int) = if (Build.VERSION.SDK_INT < 26) {
when (position - super.getItemCount()) {
0 -> VIEW_TYPE_MANAGE
1 -> if (Build.VERSION.SDK_INT >= 24) VIEW_TYPE_USB else VIEW_TYPE_WIFI_LEGACY
2 -> VIEW_TYPE_WIFI
3 -> VIEW_TYPE_BLUETOOTH
4 -> VIEW_TYPE_WIFI_LEGACY
else -> VIEW_TYPE_INTERFACE
}
} else {
when (position - super.getItemCount()) {
0 -> VIEW_TYPE_LOCAL_ONLY_HOTSPOT
1 -> VIEW_TYPE_MANAGE
2 -> VIEW_TYPE_USB
3 -> VIEW_TYPE_WIFI
4 -> VIEW_TYPE_BLUETOOTH
else -> VIEW_TYPE_INTERFACE
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_INTERFACE -> InterfaceViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
VIEW_TYPE_MANAGE -> ManageViewHolder(inflater.inflate(R.layout.listitem_manage, parent, false))
VIEW_TYPE_WIFI, VIEW_TYPE_USB, VIEW_TYPE_BLUETOOTH, VIEW_TYPE_WIFI_LEGACY ->
ManageItemHolder(ListitemManageTetherBinding.inflate(inflater, parent, false), viewType)
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> @TargetApi(26) {
LocalOnlyHotspotViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
}
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is LocalOnlyHotspotViewHolder -> holder.binding.data = LocalHotspotData(lookup)
is InterfaceViewHolder -> holder.binding.data = TetheredData(getItem(position))
}
}
@RequiresApi(26)
fun updateLocalOnlyViewHolder() {
notifyItemChanged(super.getItemCount())
notifyItemChanged(super.getItemCount() + 3)
}
}
private val tetherListener = TetherListener()
private lateinit var binding: FragmentTetheringBinding
private var hotspotBinder: LocalOnlyHotspotService.Binder? = null
private var tetheringBinder: TetheringService.Binder? = null
val adapter = TetheringAdapter()
private val receiver = broadcastReceiver { _, intent ->
adapter.update(TetheringManager.getTetheredIfaces(intent.extras),
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false)
binding.interfaces.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(requireContext(), tetherListener, PAN)
ServiceForegroundConnector(this, if (Build.VERSION.SDK_INT >= 26)
listOf(TetheringService::class, LocalOnlyHotspotService::class) else listOf(TetheringService::class))
return binding.root
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == START_LOCAL_ONLY_HOTSPOT) @TargetApi(26) {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
val context = requireContext()
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
}
} else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onDestroy() {
tetherListener.pan = null
super.onDestroy()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) = when (service) {
is TetheringService.Binder -> {
tetheringBinder = service
service.fragment = this
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
while (false) { }
}
is LocalOnlyHotspotService.Binder -> @TargetApi(26) {
hotspotBinder = service
service.fragment = this
adapter.updateLocalOnlyViewHolder()
}
else -> throw IllegalArgumentException("service")
}
override fun onServiceDisconnected(name: ComponentName?) {
val context = requireContext()
tetheringBinder?.fragment = null
tetheringBinder = null
context.unregisterReceiver(receiver)
hotspotBinder?.fragment = null
hotspotBinder = null
}
}

View File

@@ -4,6 +4,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.net.TetheringManager

View File

@@ -8,7 +8,7 @@ import be.mygod.vpnhotspot.net.TetherType
import java.util.*
abstract class Client {
companion object : DiffUtil.ItemCallback<Client>() {
companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem

View File

@@ -0,0 +1,68 @@
package be.mygod.vpnhotspot.client
import android.content.ComponentName
import android.content.ServiceConnection
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.os.IBinder
import android.support.v4.app.Fragment
import android.support.v7.recyclerview.extensions.ListAdapter
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
class ClientsFragment : Fragment(), ServiceConnection {
private class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
private inner class ClientAdapter : ListAdapter<Client, ClientViewHolder>(Client) {
override fun submitList(list: MutableList<Client>?) {
super.submitList(list)
binding.swipeRefresher.isRefreshing = false
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ClientViewHolder(ListitemClientBinding.inflate(LayoutInflater.from(parent.context)))
override fun onBindViewHolder(holder: ClientViewHolder, position: Int) {
holder.binding.client = getItem(position)
holder.binding.executePendingBindings()
}
}
private lateinit var binding: FragmentRepeaterBinding
private val adapter = ClientAdapter()
private var clients: ClientMonitorService.Binder? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_repeater, container, false)
binding.clients.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.clients.itemAnimator = DefaultItemAnimator()
binding.clients.adapter = adapter
binding.swipeRefresher.setColorSchemeResources(R.color.colorAccent)
binding.swipeRefresher.setOnRefreshListener {
IpNeighbourMonitor.instance?.flush()
}
ServiceForegroundConnector(this, this, ClientMonitorService::class)
return binding.root
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
clients = service as ClientMonitorService.Binder
service.clientsChanged[this] = { adapter.submitList(it.toMutableList()) }
}
override fun onServiceDisconnected(name: ComponentName?) {
val clients = clients
if (clients != null) {
clients.clientsChanged -= this
this.clients = null
}
}
}

View File

@@ -0,0 +1,11 @@
package be.mygod.vpnhotspot.manage
import android.databinding.BaseObservable
abstract class Data : BaseObservable() {
abstract val icon: Int
abstract val title: CharSequence
abstract val text: CharSequence
abstract val active: Boolean
abstract val selectable: Boolean
}

View File

@@ -0,0 +1,63 @@
package be.mygod.vpnhotspot.manage
import android.content.Intent
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
import android.view.View
import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.util.formatAddresses
import java.util.*
class InterfaceManager(private val parent: TetheringFragment, val iface: String) : Manager() {
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
lateinit var iface: String
override fun onClick(view: View) {
val context = itemView.context
val data = binding.data as Data
if (data.active) context.startService(Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACE, iface))
}
}
private inner class Data : be.mygod.vpnhotspot.manage.Data() {
override val icon get() = TetherType.ofInterface(iface).icon
override val title get() = iface
override val text get() = addresses
override val active get() = parent.tetheringBinder?.isActive(iface) == true
override val selectable get() = true
}
val addresses = parent.ifaceLookup[iface]?.formatAddresses() ?: ""
override val type get() = VIEW_TYPE_INTERFACE
private val data = Data()
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
viewHolder as ViewHolder
viewHolder.binding.data = data
viewHolder.iface = iface
}
override fun isSameItemAs(other: Manager) = when (other) {
is InterfaceManager -> iface == other.iface
else -> false
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as InterfaceManager
if (iface != other.iface) return false
if (addresses != other.addresses) return false
return true
}
override fun hashCode(): Int = Objects.hash(iface, addresses)
}

View File

@@ -0,0 +1,88 @@
package be.mygod.vpnhotspot.manage
import android.Manifest
import android.annotation.TargetApi
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.IBinder
import android.support.v7.widget.RecyclerView
import android.view.View
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 java.net.NetworkInterface
@TargetApi(26)
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
lateinit var manager: LocalOnlyHotspotManager
override fun onClick(view: View) {
val binder = manager.binder
if (binder?.iface != null) binder.stop() else {
val context = manager.parent.requireContext()
if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED) {
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
} else {
manager.parent.requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
TetheringFragment.START_LOCAL_ONLY_HOTSPOT)
}
}
}
}
private inner class Data : be.mygod.vpnhotspot.manage.Data() {
private val lookup: Map<String, NetworkInterface> get() = parent.ifaceLookup
override val icon: Int get() {
val iface = binder?.iface ?: return TetherType.WIFI.icon
return TetherType.ofInterface(iface).icon
}
override val title: CharSequence get() {
val configuration = binder?.configuration ?: return parent.getString(R.string.tethering_temp_hotspot)
return "${configuration.SSID} - ${configuration.preSharedKey}"
}
override val text: CharSequence get() {
return lookup[binder?.iface ?: return ""]?.formatAddresses() ?: ""
}
override val active get() = binder?.iface != null
override val selectable get() = active
}
init {
ServiceForegroundConnector(parent, this, LocalOnlyHotspotService::class)
}
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
private val data = Data()
private var binder: LocalOnlyHotspotService.Binder? = null
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
viewHolder as ViewHolder
viewHolder.binding.data = data
viewHolder.manager = this
}
fun update() = data.notifyChange()
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as LocalOnlyHotspotService.Binder
service.manager = this
update()
}
override fun onServiceDisconnected(name: ComponentName?) {
binder?.manager = null
binder = null
}
}

View File

@@ -0,0 +1,24 @@
package be.mygod.vpnhotspot.manage
import android.content.ActivityNotFoundException
import android.content.Intent
import android.support.v7.widget.RecyclerView
import android.view.View
object ManageBar : Manager() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
init {
view.setOnClickListener(this)
}
override fun onClick(v: View?) = try {
itemView.context.startActivity(Intent()
.setClassName("com.android.settings", "com.android.settings.Settings\$TetherSettingsActivity"))
} catch (e: ActivityNotFoundException) {
itemView.context.startActivity(Intent()
.setClassName("com.android.settings", "com.android.settings.TetherSettings"))
}
}
override val type: Int get() = VIEW_TYPE_MANAGE
}

View File

@@ -0,0 +1,46 @@
package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi
import android.support.v7.util.DiffUtil
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.databinding.ListitemManageTetherBinding
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
abstract class Manager {
companion object DiffCallback : DiffUtil.ItemCallback<Manager>() {
const val VIEW_TYPE_INTERFACE = 0
const val VIEW_TYPE_MANAGE = 1
const val VIEW_TYPE_WIFI = 2
const val VIEW_TYPE_USB = 3
const val VIEW_TYPE_BLUETOOTH = 4
const val VIEW_TYPE_WIFI_LEGACY = 5
const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
const val VIEW_TYPE_REPEATER = 7
override fun areItemsTheSame(oldItem: Manager, newItem: Manager) = oldItem.isSameItemAs(newItem)
override fun areContentsTheSame(oldItem: Manager, newItem: Manager) = oldItem == newItem
fun createViewHolder(inflater: LayoutInflater, parent: ViewGroup, type: Int): RecyclerView.ViewHolder = when (type) {
VIEW_TYPE_INTERFACE ->
InterfaceManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
VIEW_TYPE_MANAGE -> ManageBar.ViewHolder(inflater.inflate(R.layout.listitem_manage, parent, false))
VIEW_TYPE_WIFI, VIEW_TYPE_USB, VIEW_TYPE_BLUETOOTH, VIEW_TYPE_WIFI_LEGACY ->
TetherManager.ViewHolder(ListitemManageTetherBinding.inflate(inflater, parent, false))
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> @TargetApi(26) {
LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
}
VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false))
else -> throw IllegalArgumentException("Invalid view type")
}
}
abstract val type: Int
open fun bindTo(viewHolder: RecyclerView.ViewHolder) { }
open fun isSameItemAs(other: Manager) = javaClass == other.javaClass
}

View File

@@ -0,0 +1,159 @@
package be.mygod.vpnhotspot.manage
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.databinding.BaseObservable
import android.databinding.Bindable
import android.net.wifi.WifiConfiguration
import android.net.wifi.p2p.WifiP2pGroup
import android.os.IBinder
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatDialog
import android.support.v7.widget.RecyclerView
import android.view.WindowManager
import android.widget.EditText
import android.widget.Toast
import be.mygod.vpnhotspot.App
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BR
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
import be.mygod.vpnhotspot.net.wifi.WifiP2pDialog
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.formatAddresses
import java.net.NetworkInterface
import java.net.SocketException
class RepeaterManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
class ViewHolder(val binding: ListitemRepeaterBinding) : RecyclerView.ViewHolder(binding.root)
inner class Data : BaseObservable() {
val switchEnabled: Boolean
@Bindable get() = when (binder?.service?.status) {
RepeaterService.Status.IDLE, RepeaterService.Status.ACTIVE -> true
else -> false
}
val serviceStarted: Boolean
@Bindable get() = when (binder?.service?.status) {
RepeaterService.Status.STARTING, RepeaterService.Status.ACTIVE -> true
else -> false
}
val ssid @Bindable get() = binder?.service?.group?.networkName ?: ""
val addresses: CharSequence @Bindable get() {
return try {
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
} catch (e: SocketException) {
e.printStackTrace()
""
}
}
var oc: CharSequence
@Bindable get() {
val oc = app.operatingChannel
return if (oc in 1..165) oc.toString() else ""
}
set(value) = app.pref.edit().putString(App.KEY_OPERATING_CHANNEL, value.toString()).apply()
fun onStatusChanged() {
notifyPropertyChanged(BR.switchEnabled)
notifyPropertyChanged(BR.serviceStarted)
notifyPropertyChanged(BR.addresses)
}
fun onGroupChanged(group: WifiP2pGroup? = null) {
notifyPropertyChanged(BR.ssid)
p2pInterface = group?.`interface`
notifyPropertyChanged(BR.addresses)
}
fun toggle() {
val binder = binder
when (binder?.service?.status) {
RepeaterService.Status.IDLE -> {
val context = parent.requireContext()
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
}
RepeaterService.Status.ACTIVE -> binder.shutdown()
else -> { }
}
}
fun wps() {
if (binder?.active != true) return
val dialog = AlertDialog.Builder(parent.requireContext())
.setTitle(R.string.repeater_wps_dialog_title)
.setView(R.layout.dialog_wps)
.setPositiveButton(android.R.string.ok, { dialog, _ -> binder?.startWps((dialog as AppCompatDialog)
.findViewById<EditText>(android.R.id.edit)!!.text.toString()) })
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.repeater_wps_dialog_pbc, { _, _ -> binder?.startWps(null) })
.create()
dialog.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
dialog.show()
}
fun editConfigurations() {
val binder = binder
val group = binder?.service?.group
val ssid = group?.networkName
val context = parent.requireContext()
if (ssid != null) {
val wifi = WifiConfiguration()
val conf = P2pSupplicantConfiguration()
wifi.SSID = ssid
wifi.preSharedKey = group.passphrase
if (wifi.preSharedKey == null) wifi.preSharedKey = conf.readPsk()
if (wifi.preSharedKey != null) {
var dialog: WifiP2pDialog? = null
dialog = WifiP2pDialog(context, DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> when (conf.update(dialog!!.config!!)) {
true -> App.app.handler.postDelayed(binder::requestGroupUpdate, 1000)
false -> Toast.makeText(context, R.string.noisy_su_failure, Toast.LENGTH_SHORT).show()
null -> Toast.makeText(context, R.string.root_unavailable, Toast.LENGTH_SHORT).show()
}
DialogInterface.BUTTON_NEUTRAL -> binder.resetCredentials()
}
}, wifi)
dialog.show()
return
}
}
Toast.makeText(context, R.string.repeater_configure_failure, Toast.LENGTH_LONG).show()
}
}
init {
ServiceForegroundConnector(parent, this, RepeaterService::class)
}
override val type get() = VIEW_TYPE_REPEATER
private val data = Data()
private var binder: RepeaterService.Binder? = null
private var p2pInterface: String? = null
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
(viewHolder as ViewHolder).binding.data = data
}
fun update() = data.notifyChange()
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as RepeaterService.Binder
service.statusChanged[this] = data::onStatusChanged
service.groupChanged[this] = data::onGroupChanged
update()
}
override fun onServiceDisconnected(name: ComponentName?) {
val binder = binder ?: return
this.binder = null
binder.statusChanged -= this
binder.groupChanged -= this
data.onStatusChanged()
}
}

View File

@@ -0,0 +1,174 @@
package be.mygod.vpnhotspot.manage
import android.annotation.SuppressLint
import android.arch.lifecycle.Lifecycle
import android.arch.lifecycle.LifecycleObserver
import android.arch.lifecycle.OnLifecycleEvent
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.content.Intent
import android.databinding.BaseObservable
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.support.annotation.RequiresApi
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.ListitemManageTetherBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import java.lang.reflect.InvocationTargetException
abstract class TetherManager private constructor(protected val parent: TetheringFragment) : Manager(),
TetheringManager.OnStartTetheringCallback {
class ViewHolder(val binding: ListitemManageTetherBinding) : RecyclerView.ViewHolder(binding.root),
View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
var manager: TetherManager? = null
set(value) {
field = value!!
binding.data = value.data
}
override fun onClick(v: View?) {
val manager = manager!!
val context = manager.parent.requireContext()
if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(context)) {
manager.parent.startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
Uri.parse("package:${context.packageName}")))
return
}
val started = manager.isStarted
try {
if (started) manager.stop() else manager.start()
} catch (e: InvocationTargetException) {
e.printStackTrace()
var cause: Throwable? = e
while (cause != null) {
cause = cause.cause
if (cause != null && cause !is InvocationTargetException) {
Toast.makeText(context, cause.message, Toast.LENGTH_LONG).show()
break
}
}
}
}
}
/**
* A convenient class to delegate stuff to BaseObservable.
*/
inner class Data : BaseObservable() {
val tetherType get() = this@TetherManager.tetherType
val title get() = this@TetherManager.title
val isStarted get() = this@TetherManager.isStarted
}
val data = Data()
abstract val title: CharSequence
abstract val tetherType: TetherType
open val isStarted get() = parent.enabledTypes.contains(tetherType)
protected abstract fun start()
protected abstract fun stop()
override fun onTetheringStarted() = data.notifyChange()
override fun onTetheringFailed() {
app.handler.post {
Toast.makeText(parent.requireContext(), R.string.tethering_manage_failed, Toast.LENGTH_SHORT).show()
}
}
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
(viewHolder as ViewHolder).manager = this
}
@RequiresApi(24)
class Wifi(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_WIFI
override fun start() = TetheringManager.start(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_WIFI)
}
@RequiresApi(24)
class Usb(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_usb)
override val tetherType get() = TetherType.USB
override val type get() = VIEW_TYPE_USB
override fun start() = TetheringManager.start(TetheringManager.TETHERING_USB, true, this)
override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_USB)
}
@RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), LifecycleObserver,
BluetoothProfile.ServiceListener {
companion object {
/**
* PAN Profile
* From BluetoothProfile.java.
*/
private const val PAN = 5
private val isTetheringOn by lazy @SuppressLint("PrivateApi") {
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn")
}
}
private var pan: BluetoothProfile? = null
init {
parent.lifecycle.addObserver(this)
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(parent.requireContext(), this, PAN)
}
override fun onServiceDisconnected(profile: Int) {
pan = null
}
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
pan = proxy
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
pan = null
}
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
override val tetherType get() = TetherType.BLUETOOTH
override val type get() = VIEW_TYPE_BLUETOOTH
/**
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
*/
override val isStarted: Boolean
get() {
val pan = pan
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && pan != null &&
isTetheringOn.invoke(pan) as Boolean
}
override fun start() = TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this)
override fun stop() {
TetheringManager.stop(TetheringManager.TETHERING_BLUETOOTH)
Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state
}
}
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26")
class WifiLegacy(parent: TetheringFragment) : TetherManager(parent) {
override val title get() = parent.getString(R.string.tethering_manage_wifi_legacy)
override val tetherType get() = TetherType.WIFI
override val type get() = VIEW_TYPE_WIFI_LEGACY
override fun start() = WifiApManager.start()
override fun stop() = WifiApManager.stop()
}
}

View File

@@ -0,0 +1,123 @@
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.pm.PackageManager
import android.databinding.DataBindingUtil
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.support.v4.app.Fragment
import android.support.v7.recyclerview.extensions.ListAdapter
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import be.mygod.vpnhotspot.LocalOnlyHotspotService
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.broadcastReceiver
import java.net.NetworkInterface
import java.net.SocketException
class TetheringFragment : Fragment(), ServiceConnection {
companion object {
const val START_LOCAL_ONLY_HOTSPOT = 1
}
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager) {
private val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
private 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),
TetherManager.Bluetooth(this@TetheringFragment))
}
private val wifiManagerLegacy by lazy @Suppress("Deprecation") {
TetherManager.WifiLegacy(this@TetheringFragment)
}
fun update(activeIfaces: List<String>, localOnlyIfaces: List<String>) {
ifaceLookup = try {
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
} catch (e: SocketException) {
e.printStackTrace()
emptyMap()
}
this@TetheringFragment.enabledTypes =
(activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
val list = arrayListOf<Manager>(repeaterManager)
if (Build.VERSION.SDK_INT >= 26) {
list.add(localOnlyHotspotManager)
localOnlyHotspotManager.update()
}
list.addAll(activeIfaces.map { InterfaceManager(this@TetheringFragment, it) }.sortedBy { it.iface })
list.add(ManageBar)
if (Build.VERSION.SDK_INT >= 24) {
list.addAll(tetherManagers)
tetherManagers.forEach { it.onTetheringStarted() }
}
if (Build.VERSION.SDK_INT < 26) {
list.add(wifiManagerLegacy)
wifiManagerLegacy.onTetheringStarted()
}
submitList(list)
}
override fun getItemViewType(position: Int) = getItem(position).type
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
Manager.createViewHolder(LayoutInflater.from(parent.context), parent, viewType)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = getItem(position).bindTo(holder)
}
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
var enabledTypes = emptySet<TetherType>()
private lateinit var binding: FragmentTetheringBinding
var tetheringBinder: TetheringService.Binder? = null
val adapter = ManagerAdapter()
private val receiver = broadcastReceiver { _, intent ->
adapter.update(TetheringManager.getTetheredIfaces(intent.extras),
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_tethering, container, false)
binding.interfaces.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.interfaces.itemAnimator = DefaultItemAnimator()
binding.interfaces.adapter = adapter
ServiceForegroundConnector(this, this, TetheringService::class)
return binding.root
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == START_LOCAL_ONLY_HOTSPOT) @TargetApi(26) {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
val context = requireContext()
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
}
} else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
tetheringBinder = service as TetheringService.Binder
service.fragment = this
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
}
override fun onServiceDisconnected(name: ComponentName?) {
val context = requireContext()
tetheringBinder?.fragment = null
tetheringBinder = null
context.unregisterReceiver(receiver)
}
}

View File

@@ -12,23 +12,26 @@ import android.support.v4.app.Fragment
import kotlin.reflect.KClass
/**
* host also needs to be Context/Fragment and LifecycleOwner.
* owner also needs to be Context/Fragment.
*/
class ServiceForegroundConnector(private val host: ServiceConnection, private val classes: List<KClass<out Service>>) :
LifecycleObserver {
class ServiceForegroundConnector(private val owner: LifecycleOwner, private val connection: ServiceConnection,
private val clazz: KClass<out Service>) : LifecycleObserver {
init {
(host as LifecycleOwner).lifecycle.addObserver(this)
owner.lifecycle.addObserver(this)
}
constructor(host: ServiceConnection, vararg classes: KClass<out Service>) : this(host, classes.toList())
private val context get() = if (host is Context) host else (host as Fragment).requireContext()
private val context get() = when (owner) {
is Context -> owner
is Fragment -> owner.requireContext()
else -> throw UnsupportedOperationException("Unsupported owner")
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
val context = context
for (clazz in classes) context.bindService(Intent(context, clazz.java), host, Context.BIND_AUTO_CREATE)
context.bindService(Intent(context, clazz.java), connection, Context.BIND_AUTO_CREATE)
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() = context.stopAndUnbind(host)
fun onStop() = context.stopAndUnbind(connection)
}

View File

@@ -4,6 +4,7 @@ import android.content.*
import android.databinding.BindingAdapter
import android.support.annotation.DrawableRes
import android.util.Log
import android.view.View
import android.widget.ImageView
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
@@ -28,6 +29,11 @@ fun intentFilter(vararg actions: String): IntentFilter {
@BindingAdapter("android:src")
fun setImageResource(imageView: ImageView, @DrawableRes resource: Int) = imageView.setImageResource(resource)
@BindingAdapter("android:visibility")
fun setVisibility(view: View, value: Boolean) {
view.visibility = if (value) View.VISIBLE else View.GONE
}
fun NetworkInterface.formatAddresses() =
(interfaceAddresses.asSequence()
.map { "${it.address.hostAddress}/${it.networkPrefixLength}" }

View File

@@ -0,0 +1,15 @@
package be.mygod.vpnhotspot.widget
import android.content.Context
import android.support.v7.widget.AppCompatTextView
import android.util.AttributeSet
import android.view.View
class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle) :
AppCompatTextView(context, attrs, defStyleAttr) {
override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
visibility = if (text.isNullOrEmpty()) View.GONE else View.VISIBLE
}
}

View File

@@ -0,0 +1,11 @@
<?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,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,6h18L22,4L4,4c-1.1,0 -2,0.9 -2,2v11L0,17v3h14v-3L4,17L4,6zM23,8h-6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1L24,9c0,-0.55 -0.45,-1 -1,-1zM22,17h-4v-7h4v7z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20.5,9.5c0.28,0 0.55,0.04 0.81,0.08L24,6c-3.34,-2.51 -7.5,-4 -12,-4S3.34,3.49 0,6l12,16 3.5,-4.67L15.5,14.5c0,-2.76 2.24,-5 5,-5zM23,16v-1.5c0,-1.38 -1.12,-2.5 -2.5,-2.5S18,13.12 18,14.5L18,16c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1zM22,16h-3v-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5L22,16z"/>
</vector>

View File

@@ -8,16 +8,22 @@
android:layout_height="match_parent"
tools:context="be.mygod.vpnhotspot.MainActivity">
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:title="@string/app_name"
android:id="@+id/toolbar"/>
<FrameLayout
android:id="@+id/fragmentHolder"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
app:layout_constraintBottom_toTopOf="@+id/navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintTop_toBottomOf="@+id/toolbar"/>
<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"

View File

@@ -2,98 +2,6 @@
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="be.mygod.vpnhotspot.RepeaterFragment.Data"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:touchscreenBlocksFocus="false"
android:id="@+id/toolbar">
<Switch
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:enabled="@{data.switchEnabled}"
android:checked="@{data.serviceStarted}"
android:onCheckedChanged="@{(_, checked) -> data.setServiceStarted(checked)}"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"/>
</android.support.v7.widget.Toolbar>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:paddingTop="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wifi_ssid"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<Space
android:layout_width="8dp"
android:layout_height="0dp"
android:layout_column="1"
android:layout_row="0"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_column="2"
android:layout_row="0"
android:focusable="false"
android:layout_gravity="fill_horizontal"
android:layout_columnWeight="1"
android:text="@{data.ssid}"
tools:text="DIRECT-rA-nd0m"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="0"
android:layout_row="1"
android:text="@string/repeater_addresses"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_column="2"
android:layout_row="1"
android:layout_gravity="fill_horizontal"
android:layout_columnWeight="1"
android:text="@{data.addresses}"
android:textIsSelectable="true"
tools:text="192.168.49.1/24\nfe80::abcd:efff:1234:5678%p2p-p2p0-0/64\n01:23:45:ab:cd:ef"/>
</GridLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/connected_devices"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"
android:backgroundTint="?android:attr/textColorSecondary"/>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresher"
@@ -108,5 +16,4 @@
android:scrollbars="vertical"
tools:listitem="@layout/listitem_client"/>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>
</layout>

View File

@@ -1,21 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:title="@string/app_name"
android:id="@+id/toolbar"/>
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
class="be.mygod.vpnhotspot.SettingsPreferenceFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:id="@+id/preference"/>
</LinearLayout>

View File

@@ -1,26 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:title="@string/app_name"
android:id="@+id/toolbar"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/interfaces"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbars="vertical"
tools:listitem="@layout/listitem_interface"/>
</LinearLayout>
</layout>

View File

@@ -11,10 +11,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:paddingTop="8dp">
android:padding="16dp">
<ImageView
android:layout_width="wrap_content"

View File

@@ -5,7 +5,7 @@
<data>
<variable
name="data"
type="be.mygod.vpnhotspot.TetheringFragment.Data"/>
type="be.mygod.vpnhotspot.manage.Data"/>
</data>
<LinearLayout
android:layout_width="match_parent"
@@ -30,7 +30,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
android:orientation="vertical"
android:layout_gravity="center_vertical">
<TextView
android:layout_width="match_parent"
@@ -39,7 +40,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:text="wlan0"/>
<TextView
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{data.text}"

View File

@@ -3,19 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="be.mygod.vpnhotspot.TetheringFragment"/>
<variable
name="icon"
type="Integer"/>
<variable
name="title"
type="String"/>
<variable
name="tetherListener"
type="TetheringFragment.TetherListener"/>
<variable
name="type"
type="be.mygod.vpnhotspot.net.TetherType"/>
name="data"
type="be.mygod.vpnhotspot.manage.TetherManager.Data"/>
</data>
<LinearLayout
android:layout_width="match_parent"
@@ -30,7 +20,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@{safeUnbox(icon)}"
android:src="@{data.tetherType.icon}"
android:tint="?android:attr/textColorPrimary"
tools:src="@drawable/ic_device_network_wifi"/>
@@ -43,7 +33,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:text="@{title}"
android:text="@{data.title}"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:text="@string/tethering_manage_wifi"/>
@@ -51,7 +41,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@{tetherListener.isStarted(type, tetherListener.enabledTypes)}"
android:checked="@{data.isStarted}"
android:clickable="false"
android:ellipsize="end"
android:focusable="false"

View File

@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="be.mygod.vpnhotspot.manage.RepeaterManager.Data"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:padding="16dp"
android:onClick="@{_ -> data.toggle()}">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_device_network_wifi"
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/title_repeater"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{data.addresses}"
android:textIsSelectable="true"
tools:text="192.168.43.1/24\n01:23:45:ab:cd:ef"/>
</LinearLayout>
<Switch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@{data.serviceStarted}"
android:enabled="@{data.switchEnabled}"
android:clickable="false"
android:ellipsize="end"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={data.oc}"
android:inputType="number"
android:imeOptions="actionDone"
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:background="?android:attr/selectableItemBackground"
android:padding="16dp"
android:onClick="@{_ -> data.wps()}"
android:visibility="@{data.serviceStarted}">
<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_wps"
android:tint="?android:attr/textColorPrimary"/>
<Space
android:layout_width="16dp"
android:layout_height="0dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/repeater_wps"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
</LinearLayout>
</LinearLayout>
</layout>

View File

@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_repeater"
android:icon="@drawable/ic_device_network_wifi"
android:title="@string/title_repeater"/>
<item
android:id="@+id/navigation_tethering"
android:icon="@drawable/ic_device_wifi_tethering"
android:title="@string/title_tethering"/>
<item
android:id="@+id/navigation_clients"
android:icon="@drawable/ic_device_devices"
android:title="@string/title_clients"/>
<item
android:id="@+id/navigation_settings"
android:icon="@drawable/ic_action_settings"

View File

@@ -1,15 +0,0 @@
<?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="@+id/wps"
android:icon="@drawable/ic_wps"
android:title="@string/repeater_wps"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/edit"
android:icon="@drawable/ic_image_edit"
android:title="@string/repeater_configure"
app:showAsAction="ifRoom"/>
</menu>

View File

@@ -2,11 +2,10 @@
<resources>
<string name="app_name">VPN 热点</string>
<string name="title_repeater">无线中继</string>
<string name="title_tethering">系统共享</string>
<string name="title_tethering">共享管理</string>
<string name="title_clients">已连设备</string>
<string name="title_settings">设置选项</string>
<string name="service_inactive">未打开</string>
<string name="repeater_addresses">中继地址</string>
<string name="repeater_wps_dialog_title">输入 PIN</string>
<string name="repeater_wps_dialog_pbc">一键加密</string>
<string name="repeater_wps_success_pbc">请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。</string>
@@ -51,7 +50,6 @@
<string name="tethering_manage_bluetooth">蓝牙网络共享</string>
<string name="tethering_manage_failed">Android 系统无法打开网络共享。</string>
<string name="connected_devices">已连接设备</string>
<string name="connected_state_incomplete">%s (正在连接)</string>
<string name="connected_state_valid">%s (已连上)</string>
<string name="connected_state_failed">%s (已断开)</string>
@@ -60,7 +58,7 @@
<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_repeater_strict">严格模式</string>
<string name="settings_service_repeater_strict_summary">只允许通过 VPN 隧道的包通过,适用于临时热点</string>
<string name="settings_service_repeater_strict_summary">只允许通过 VPN 隧道的包通过,适用于系统共享</string>
<string name="settings_service_dns">备用 DNS 服务器[:端口]</string>
<string name="settings_service_clean">清理/重新应用路由规则</string>
<string name="settings_misc">杂项</string>

View File

@@ -12,10 +12,9 @@
<string name="app_name">VPN Hotspot</string>
<string name="title_repeater">Repeater</string>
<string name="title_tethering">Tethering</string>
<string name="title_clients">Clients</string>
<string name="title_settings">Settings</string>
<string name="service_inactive">Service inactive</string>
<string name="repeater_addresses">Addresses</string>
<string name="repeater_wps">WPS</string>
<string name="repeater_wps_dialog_title">Enter PIN</string>
<string name="repeater_wps_dialog_pbc">Push Button</string>
@@ -54,7 +53,6 @@
<string name="tethering_manage_bluetooth">Bluetooth tethering</string>
<string name="tethering_manage_failed">Android system has failed to start tethering.</string>
<string name="connected_devices">Connected devices</string>
<string name="connected_state_incomplete">%s (connecting)</string>
<string name="connected_state_valid">%s (reachable)</string>
<string name="connected_state_failed">%s (lost)</string>
@@ -63,8 +61,8 @@
<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_repeater_strict">Strict mode</string>
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel, also
applies to temporary Wi\u2011Fi hotspot.</string>
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel. Does not
apply to system tethering.</string>
<string name="settings_service_dns">Fallback DNS server[:port]</string>
<string name="settings_service_clean">Clean/reapply routing rules</string>
<string name="settings_misc">Misc</string>

View File

@@ -1,21 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/title_repeater">
<AutoSummaryEditTextPreference
android:key="service.repeater.oc"
android:title="@string/settings_service_repeater_oc"
android:summary="@string/settings_service_repeater_oc_summary"
android:hint="@string/settings_service_repeater_oc_summary"
android:inputType="number"
android:maxLength="3"/>
android:title="@string/settings_service">
<SwitchPreference
android:key="service.repeater.strict"
android:title="@string/settings_service_repeater_strict"
android:summary="@string/settings_service_repeater_strict_summary"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_service">
<AutoSummaryEditTextPreference
android:key="service.dns"
android:title="@string/settings_service_dns"