Temporary Wi-Fi hotspot for bypassing tethering limits (#18)
* First draft of temporary hotspot * Refactor with LocalOnlyInterfaceManager * Refactor LocalOnlyHotspotService * Localize * Update strict summary
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
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
|
||||
@@ -39,12 +42,15 @@ 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.
|
||||
@@ -55,26 +61,63 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
}
|
||||
|
||||
inner class Data(val iface: TetheredInterface) : BaseObservable() {
|
||||
val icon: Int get() = TetherType.ofInterface(iface.name).icon
|
||||
val active = binder?.isActive(iface.name) == true
|
||||
interface Data {
|
||||
val icon: Int
|
||||
val title: CharSequence
|
||||
val text: CharSequence
|
||||
val active: Boolean
|
||||
}
|
||||
inner class TetheredData(val iface: TetheredInterface) : Data {
|
||||
override val icon: Int 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
private class InterfaceViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
|
||||
View.OnClickListener {
|
||||
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!!
|
||||
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)
|
||||
@@ -160,7 +203,7 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
}
|
||||
|
||||
class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
|
||||
inner class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
|
||||
var enabledTypes = emptySet<TetherType>()
|
||||
@Bindable get
|
||||
set(value) {
|
||||
@@ -208,29 +251,40 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
inner class TetheringAdapter :
|
||||
ListAdapter<TetheredInterface, RecyclerView.ViewHolder>(TetheredInterface.DiffCallback) {
|
||||
fun update(data: Set<String>) {
|
||||
val lookup = try {
|
||||
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<String, NetworkInterface>()
|
||||
emptyMap()
|
||||
}
|
||||
this@TetheringFragment.tetherListener.enabledTypes = data.map { TetherType.ofInterface(it) }.toSet()
|
||||
submitList(data.map { TetheredInterface(it, lookup) }.sorted())
|
||||
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() + when (Build.VERSION.SDK_INT) {
|
||||
in 0 until 24 -> 2
|
||||
in 24..25 -> 5
|
||||
else -> 4
|
||||
}
|
||||
override fun getItemViewType(position: Int) = 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
|
||||
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)
|
||||
@@ -239,22 +293,33 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
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 InterfaceViewHolder -> holder.binding.data = Data(getItem(position))
|
||||
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 binder: TetheringService.TetheringBinder? = null
|
||||
private var hotspotBinder: LocalOnlyHotspotService.HotspotBinder? = null
|
||||
private var tetheringBinder: TetheringService.TetheringBinder? = null
|
||||
val adapter = TetheringAdapter()
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
adapter.update(TetheringManager.getTetheredIfaces(intent.extras))
|
||||
adapter.update(TetheringManager.getTetheredIfaces(intent.extras),
|
||||
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras))
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@@ -269,14 +334,23 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val context = requireContext()
|
||||
context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
context.bindService(Intent(context, TetheringService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.bindService(Intent(context, LocalOnlyHotspotService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
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 onStop() {
|
||||
val context = requireContext()
|
||||
context.unbindService(this)
|
||||
context.unregisterReceiver(receiver)
|
||||
requireContext().unbindService(this)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -285,14 +359,34 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val binder = service as TetheringService.TetheringBinder
|
||||
this.binder = binder
|
||||
binder.fragment = this
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) = when (service) {
|
||||
is TetheringService.TetheringBinder -> {
|
||||
tetheringBinder = service
|
||||
service.fragment = this
|
||||
requireContext().registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
while (false) { }
|
||||
}
|
||||
is LocalOnlyHotspotService.HotspotBinder -> @TargetApi(26) {
|
||||
hotspotBinder = service
|
||||
service.fragment = this
|
||||
adapter.updateLocalOnlyViewHolder()
|
||||
}
|
||||
else -> throw IllegalArgumentException("service")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
binder?.fragment = null
|
||||
binder = null
|
||||
val context = requireContext()
|
||||
when (name) {
|
||||
ComponentName(context, TetheringService::class.java) -> {
|
||||
tetheringBinder?.fragment = null
|
||||
tetheringBinder = null
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
ComponentName(context, LocalOnlyHotspotService::class.java) -> {
|
||||
hotspotBinder?.fragment = null
|
||||
hotspotBinder = null
|
||||
}
|
||||
else -> throw IllegalArgumentException("name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user