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.widget.Toast
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.manage.LocalOnlyHotspotManager
|
||||
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
@@ -18,7 +19,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
||||
}
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
var fragment: TetheringFragment? = null
|
||||
var manager: LocalOnlyHotspotManager? = null
|
||||
var iface: String? = null
|
||||
val configuration get() = reservation?.wifiConfiguration
|
||||
|
||||
@@ -48,7 +49,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
||||
IpNeighbourMonitor.registerCallback(this)
|
||||
} else check(iface == routingManager.downstream)
|
||||
}
|
||||
app.handler.post { binder.fragment?.adapter?.updateLocalOnlyViewHolder() }
|
||||
app.handler.post { binder.manager?.update() }
|
||||
}
|
||||
override val activeIfaces get() = listOfNotNull(binder.iface)
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ import android.support.v7.app.AppCompatActivity
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import be.mygod.vpnhotspot.client.ClientMonitorService
|
||||
import be.mygod.vpnhotspot.client.ClientsFragment
|
||||
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import q.rorbin.badgeview.QBadgeView
|
||||
|
||||
@@ -26,21 +28,21 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
|
||||
binding.navigation.setOnNavigationItemSelectedListener(this)
|
||||
if (savedInstanceState == null) displayFragment(RepeaterFragment())
|
||||
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
||||
badge = QBadgeView(this)
|
||||
badge.bindTarget((binding.navigation.getChildAt(0) as BottomNavigationMenuView).getChildAt(0))
|
||||
badge.bindTarget((binding.navigation.getChildAt(0) as BottomNavigationMenuView).getChildAt(1))
|
||||
badge.badgeBackgroundColor = ContextCompat.getColor(this, R.color.colorAccent)
|
||||
badge.badgeTextColor = ContextCompat.getColor(this, R.color.primary_text_default_material_light)
|
||||
badge.badgeGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
badge.setGravityOffset(16f, 0f, true)
|
||||
ServiceForegroundConnector(this, ClientMonitorService::class)
|
||||
ServiceForegroundConnector(this, this, ClientMonitorService::class)
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.navigation_repeater -> {
|
||||
R.id.navigation_clients -> {
|
||||
if (!item.isChecked) {
|
||||
item.isChecked = true
|
||||
displayFragment(RepeaterFragment())
|
||||
displayFragment(ClientsFragment())
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -54,7 +56,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
R.id.navigation_settings -> {
|
||||
if (!item.isChecked) {
|
||||
item.isChecked = true
|
||||
displayFragment(SettingsFragment())
|
||||
displayFragment(SettingsPreferenceFragment())
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -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.widget.Toast
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
|
||||
@@ -8,7 +8,7 @@ import be.mygod.vpnhotspot.net.TetherType
|
||||
import java.util.*
|
||||
|
||||
abstract class Client {
|
||||
companion object : DiffUtil.ItemCallback<Client>() {
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
|
||||
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
|
||||
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
|
||||
override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
* host also needs to be Context/Fragment and LifecycleOwner.
|
||||
* owner also needs to be Context/Fragment.
|
||||
*/
|
||||
class ServiceForegroundConnector(private val host: ServiceConnection, private val classes: List<KClass<out Service>>) :
|
||||
LifecycleObserver {
|
||||
class ServiceForegroundConnector(private val owner: LifecycleOwner, private val connection: ServiceConnection,
|
||||
private val clazz: KClass<out Service>) : LifecycleObserver {
|
||||
init {
|
||||
(host as LifecycleOwner).lifecycle.addObserver(this)
|
||||
owner.lifecycle.addObserver(this)
|
||||
}
|
||||
constructor(host: ServiceConnection, vararg classes: KClass<out Service>) : this(host, classes.toList())
|
||||
|
||||
private val context get() = if (host is Context) host else (host as Fragment).requireContext()
|
||||
private val context get() = when (owner) {
|
||||
is Context -> owner
|
||||
is Fragment -> owner.requireContext()
|
||||
else -> throw UnsupportedOperationException("Unsupported owner")
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun onStart() {
|
||||
val context = context
|
||||
for (clazz in classes) context.bindService(Intent(context, clazz.java), host, Context.BIND_AUTO_CREATE)
|
||||
context.bindService(Intent(context, clazz.java), connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
fun onStop() = context.stopAndUnbind(host)
|
||||
fun onStop() = context.stopAndUnbind(connection)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.*
|
||||
import android.databinding.BindingAdapter
|
||||
import android.support.annotation.DrawableRes
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.BuildConfig
|
||||
@@ -28,6 +29,11 @@ fun intentFilter(vararg actions: String): IntentFilter {
|
||||
@BindingAdapter("android:src")
|
||||
fun setImageResource(imageView: ImageView, @DrawableRes resource: Int) = imageView.setImageResource(resource)
|
||||
|
||||
@BindingAdapter("android:visibility")
|
||||
fun setVisibility(view: View, value: Boolean) {
|
||||
view.visibility = if (value) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun NetworkInterface.formatAddresses() =
|
||||
(interfaceAddresses.asSequence()
|
||||
.map { "${it.address.hostAddress}/${it.networkPrefixLength}" }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user