Huge refactor for better maintainability
This commit is contained in:
@@ -6,6 +6,7 @@ import android.net.wifi.WifiManager
|
|||||||
import android.support.annotation.RequiresApi
|
import android.support.annotation.RequiresApi
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.manage.LocalOnlyHotspotManager
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
@@ -18,7 +19,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner class Binder : android.os.Binder() {
|
inner class Binder : android.os.Binder() {
|
||||||
var fragment: TetheringFragment? = null
|
var manager: LocalOnlyHotspotManager? = null
|
||||||
var iface: String? = null
|
var iface: String? = null
|
||||||
val configuration get() = reservation?.wifiConfiguration
|
val configuration get() = reservation?.wifiConfiguration
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
IpNeighbourMonitor.registerCallback(this)
|
IpNeighbourMonitor.registerCallback(this)
|
||||||
} else check(iface == routingManager.downstream)
|
} 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)
|
override val activeIfaces get() = listOfNotNull(binder.iface)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import android.support.v7.app.AppCompatActivity
|
|||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import be.mygod.vpnhotspot.client.ClientMonitorService
|
import be.mygod.vpnhotspot.client.ClientMonitorService
|
||||||
|
import be.mygod.vpnhotspot.client.ClientsFragment
|
||||||
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
||||||
|
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||||
import q.rorbin.badgeview.QBadgeView
|
import q.rorbin.badgeview.QBadgeView
|
||||||
|
|
||||||
@@ -26,21 +28,21 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
|
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
|
||||||
binding.navigation.setOnNavigationItemSelectedListener(this)
|
binding.navigation.setOnNavigationItemSelectedListener(this)
|
||||||
if (savedInstanceState == null) displayFragment(RepeaterFragment())
|
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
||||||
badge = QBadgeView(this)
|
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.badgeBackgroundColor = ContextCompat.getColor(this, R.color.colorAccent)
|
||||||
badge.badgeTextColor = ContextCompat.getColor(this, R.color.primary_text_default_material_light)
|
badge.badgeTextColor = ContextCompat.getColor(this, R.color.primary_text_default_material_light)
|
||||||
badge.badgeGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
badge.badgeGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||||
badge.setGravityOffset(16f, 0f, true)
|
badge.setGravityOffset(16f, 0f, true)
|
||||||
ServiceForegroundConnector(this, ClientMonitorService::class)
|
ServiceForegroundConnector(this, this, ClientMonitorService::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.navigation_repeater -> {
|
R.id.navigation_clients -> {
|
||||||
if (!item.isChecked) {
|
if (!item.isChecked) {
|
||||||
item.isChecked = true
|
item.isChecked = true
|
||||||
displayFragment(RepeaterFragment())
|
displayFragment(ClientsFragment())
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
R.id.navigation_settings -> {
|
R.id.navigation_settings -> {
|
||||||
if (!item.isChecked) {
|
if (!item.isChecked) {
|
||||||
item.isChecked = true
|
item.isChecked = true
|
||||||
displayFragment(SettingsFragment())
|
displayFragment(SettingsPreferenceFragment())
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import be.mygod.vpnhotspot.net.TetherType
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class Client {
|
abstract class Client {
|
||||||
companion object : DiffUtil.ItemCallback<Client>() {
|
companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
|
||||||
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
|
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
|
||||||
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
|
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
|
||||||
override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem
|
override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
mobile/src/main/java/be/mygod/vpnhotspot/manage/Data.kt
Normal file
11
mobile/src/main/java/be/mygod/vpnhotspot/manage/Data.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
24
mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt
Normal file
24
mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt
Normal 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
|
||||||
|
}
|
||||||
46
mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt
Normal file
46
mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
174
mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt
Normal file
174
mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,23 +12,26 @@ import android.support.v4.app.Fragment
|
|||||||
import kotlin.reflect.KClass
|
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>>) :
|
class ServiceForegroundConnector(private val owner: LifecycleOwner, private val connection: ServiceConnection,
|
||||||
LifecycleObserver {
|
private val clazz: KClass<out Service>) : LifecycleObserver {
|
||||||
init {
|
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)
|
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||||
fun onStart() {
|
fun onStart() {
|
||||||
val context = context
|
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)
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
fun onStop() = context.stopAndUnbind(host)
|
fun onStop() = context.stopAndUnbind(connection)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.*
|
|||||||
import android.databinding.BindingAdapter
|
import android.databinding.BindingAdapter
|
||||||
import android.support.annotation.DrawableRes
|
import android.support.annotation.DrawableRes
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.BuildConfig
|
import be.mygod.vpnhotspot.BuildConfig
|
||||||
@@ -28,6 +29,11 @@ fun intentFilter(vararg actions: String): IntentFilter {
|
|||||||
@BindingAdapter("android:src")
|
@BindingAdapter("android:src")
|
||||||
fun setImageResource(imageView: ImageView, @DrawableRes resource: Int) = imageView.setImageResource(resource)
|
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() =
|
fun NetworkInterface.formatAddresses() =
|
||||||
(interfaceAddresses.asSequence()
|
(interfaceAddresses.asSequence()
|
||||||
.map { "${it.address.hostAddress}/${it.networkPrefixLength}" }
|
.map { "${it.address.hostAddress}/${it.networkPrefixLength}" }
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
mobile/src/main/res/drawable/ic_content_wave.xml
Normal file
11
mobile/src/main/res/drawable/ic_content_wave.xml
Normal 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>
|
||||||
9
mobile/src/main/res/drawable/ic_device_devices.xml
Normal file
9
mobile/src/main/res/drawable/ic_device_devices.xml
Normal 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>
|
||||||
9
mobile/src/main/res/drawable/ic_device_wifi_lock.xml
Normal file
9
mobile/src/main/res/drawable/ic_device_wifi_lock.xml
Normal 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>
|
||||||
@@ -8,16 +8,22 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context="be.mygod.vpnhotspot.MainActivity">
|
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
|
<FrameLayout
|
||||||
android:id="@+id/fragmentHolder"
|
android:id="@+id/fragmentHolder"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginEnd="0dp"
|
|
||||||
android:layout_marginStart="0dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/navigation"
|
app:layout_constraintBottom_toTopOf="@+id/navigation"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
app:layout_constraintTop_toBottomOf="@+id/toolbar"/>
|
||||||
|
|
||||||
<android.support.design.widget.BottomNavigationView
|
<android.support.design.widget.BottomNavigationView
|
||||||
android:id="@+id/navigation"
|
android:id="@+id/navigation"
|
||||||
|
|||||||
@@ -2,111 +2,18 @@
|
|||||||
<layout
|
<layout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<data>
|
|
||||||
<variable
|
<android.support.v4.widget.SwipeRefreshLayout
|
||||||
name="data"
|
android:id="@+id/swipeRefresher"
|
||||||
type="be.mygod.vpnhotspot.RepeaterFragment.Data"/>
|
|
||||||
</data>
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
<android.support.v7.widget.Toolbar
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/clients"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:background="?attr/colorPrimary"
|
android:layout_height="match_parent"
|
||||||
android:elevation="4dp"
|
android:clipToPadding="false"
|
||||||
android:touchscreenBlocksFocus="false"
|
android:scrollbars="vertical"
|
||||||
android:id="@+id/toolbar">
|
tools:listitem="@layout/listitem_client"/>
|
||||||
|
</android.support.v4.widget.SwipeRefreshLayout>
|
||||||
<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"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
|
||||||
android:id="@+id/clients"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
tools:listitem="@layout/listitem_client"/>
|
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
</layout>
|
</layout>
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<fragment
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
class="be.mygod.vpnhotspot.SettingsPreferenceFragment"
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
<android.support.v7.widget.Toolbar
|
android:id="@+id/preference"/>
|
||||||
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
|
|
||||||
class="be.mygod.vpnhotspot.SettingsPreferenceFragment"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:id="@+id/preference"/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|||||||
@@ -1,26 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout
|
<layout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
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">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<LinearLayout
|
<android.support.v7.widget.RecyclerView
|
||||||
android:orientation="vertical"
|
android:id="@+id/interfaces"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
<android.support.v7.widget.Toolbar
|
android:clipToPadding="false"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:scrollbars="vertical"
|
||||||
android:layout_width="match_parent"
|
tools:listitem="@layout/listitem_interface"/>
|
||||||
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:clipToPadding="false"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
tools:listitem="@layout/listitem_interface"/>
|
|
||||||
</LinearLayout>
|
|
||||||
</layout>
|
</layout>
|
||||||
|
|||||||
@@ -11,10 +11,7 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="8dp"
|
android:padding="16dp">
|
||||||
android:paddingEnd="16dp"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:paddingTop="8dp">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<data>
|
<data>
|
||||||
<variable
|
<variable
|
||||||
name="data"
|
name="data"
|
||||||
type="be.mygod.vpnhotspot.TetheringFragment.Data"/>
|
type="be.mygod.vpnhotspot.manage.Data"/>
|
||||||
</data>
|
</data>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -30,7 +30,8 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||||
tools:text="wlan0"/>
|
tools:text="wlan0"/>
|
||||||
|
|
||||||
<TextView
|
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{data.text}"
|
android:text="@{data.text}"
|
||||||
|
|||||||
@@ -3,19 +3,9 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<data>
|
<data>
|
||||||
<import type="be.mygod.vpnhotspot.TetheringFragment"/>
|
|
||||||
<variable
|
<variable
|
||||||
name="icon"
|
name="data"
|
||||||
type="Integer"/>
|
type="be.mygod.vpnhotspot.manage.TetherManager.Data"/>
|
||||||
<variable
|
|
||||||
name="title"
|
|
||||||
type="String"/>
|
|
||||||
<variable
|
|
||||||
name="tetherListener"
|
|
||||||
type="TetheringFragment.TetherListener"/>
|
|
||||||
<variable
|
|
||||||
name="type"
|
|
||||||
type="be.mygod.vpnhotspot.net.TetherType"/>
|
|
||||||
</data>
|
</data>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -30,7 +20,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:src="@{safeUnbox(icon)}"
|
android:src="@{data.tetherType.icon}"
|
||||||
android:tint="?android:attr/textColorPrimary"
|
android:tint="?android:attr/textColorPrimary"
|
||||||
tools:src="@drawable/ic_device_network_wifi"/>
|
tools:src="@drawable/ic_device_network_wifi"/>
|
||||||
|
|
||||||
@@ -43,7 +33,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:text="@{title}"
|
android:text="@{data.title}"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||||
tools:text="@string/tethering_manage_wifi"/>
|
tools:text="@string/tethering_manage_wifi"/>
|
||||||
|
|
||||||
@@ -51,7 +41,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:checked="@{tetherListener.isStarted(type, tetherListener.enabledTypes)}"
|
android:checked="@{data.isStarted}"
|
||||||
android:clickable="false"
|
android:clickable="false"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
|
|||||||
190
mobile/src/main/res/layout/listitem_repeater.xml
Normal file
190
mobile/src/main/res/layout/listitem_repeater.xml
Normal 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>
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<item
|
||||||
android:id="@+id/navigation_tethering"
|
android:id="@+id/navigation_tethering"
|
||||||
android:icon="@drawable/ic_device_wifi_tethering"
|
android:icon="@drawable/ic_device_wifi_tethering"
|
||||||
android:title="@string/title_tethering"/>
|
android:title="@string/title_tethering"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_clients"
|
||||||
|
android:icon="@drawable/ic_device_devices"
|
||||||
|
android:title="@string/title_clients"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/navigation_settings"
|
android:id="@+id/navigation_settings"
|
||||||
android:icon="@drawable/ic_action_settings"
|
android:icon="@drawable/ic_action_settings"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">VPN 热点</string>
|
<string name="app_name">VPN 热点</string>
|
||||||
<string name="title_repeater">无线中继</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="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_title">输入 PIN</string>
|
||||||
<string name="repeater_wps_dialog_pbc">一键加密</string>
|
<string name="repeater_wps_dialog_pbc">一键加密</string>
|
||||||
<string name="repeater_wps_success_pbc">请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。</string>
|
<string name="repeater_wps_success_pbc">请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。</string>
|
||||||
@@ -51,7 +50,6 @@
|
|||||||
<string name="tethering_manage_bluetooth">蓝牙网络共享</string>
|
<string name="tethering_manage_bluetooth">蓝牙网络共享</string>
|
||||||
<string name="tethering_manage_failed">Android 系统无法打开网络共享。</string>
|
<string name="tethering_manage_failed">Android 系统无法打开网络共享。</string>
|
||||||
|
|
||||||
<string name="connected_devices">已连接设备</string>
|
|
||||||
<string name="connected_state_incomplete">%s (正在连接)</string>
|
<string name="connected_state_incomplete">%s (正在连接)</string>
|
||||||
<string name="connected_state_valid">%s (已连上)</string>
|
<string name="connected_state_valid">%s (已连上)</string>
|
||||||
<string name="connected_state_failed">%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">Wi\u2011Fi 运行频段 (不稳定)</string>
|
||||||
<string name="settings_service_repeater_oc_summary">"自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)"</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">严格模式</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_dns">备用 DNS 服务器[:端口]</string>
|
||||||
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
||||||
<string name="settings_misc">杂项</string>
|
<string name="settings_misc">杂项</string>
|
||||||
|
|||||||
@@ -12,10 +12,9 @@
|
|||||||
<string name="app_name">VPN Hotspot</string>
|
<string name="app_name">VPN Hotspot</string>
|
||||||
<string name="title_repeater">Repeater</string>
|
<string name="title_repeater">Repeater</string>
|
||||||
<string name="title_tethering">Tethering</string>
|
<string name="title_tethering">Tethering</string>
|
||||||
|
<string name="title_clients">Clients</string>
|
||||||
<string name="title_settings">Settings</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">WPS</string>
|
||||||
<string name="repeater_wps_dialog_title">Enter PIN</string>
|
<string name="repeater_wps_dialog_title">Enter PIN</string>
|
||||||
<string name="repeater_wps_dialog_pbc">Push Button</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_bluetooth">Bluetooth tethering</string>
|
||||||
<string name="tethering_manage_failed">Android system has failed to start 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_incomplete">%s (connecting)</string>
|
||||||
<string name="connected_state_valid">%s (reachable)</string>
|
<string name="connected_state_valid">%s (reachable)</string>
|
||||||
<string name="connected_state_failed">%s (lost)</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">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_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">Strict mode</string>
|
||||||
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel, also
|
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel. Does not
|
||||||
applies to temporary Wi\u2011Fi hotspot.</string>
|
apply to system tethering.</string>
|
||||||
<string name="settings_service_dns">Fallback DNS server[:port]</string>
|
<string name="settings_service_dns">Fallback DNS server[:port]</string>
|
||||||
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
||||||
<string name="settings_misc">Misc</string>
|
<string name="settings_misc">Misc</string>
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/title_repeater">
|
android:title="@string/settings_service">
|
||||||
<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"/>
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:key="service.repeater.strict"
|
android:key="service.repeater.strict"
|
||||||
android:title="@string/settings_service_repeater_strict"
|
android:title="@string/settings_service_repeater_strict"
|
||||||
android:summary="@string/settings_service_repeater_strict_summary"/>
|
android:summary="@string/settings_service_repeater_strict_summary"/>
|
||||||
</PreferenceCategory>
|
|
||||||
<PreferenceCategory
|
|
||||||
android:title="@string/settings_service">
|
|
||||||
<AutoSummaryEditTextPreference
|
<AutoSummaryEditTextPreference
|
||||||
android:key="service.dns"
|
android:key="service.dns"
|
||||||
android:title="@string/settings_service_dns"
|
android:title="@string/settings_service_dns"
|
||||||
|
|||||||
Reference in New Issue
Block a user