From 8aa7d6d8c762bb9eb639bb126b9bd633d3721d46 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 1 Jun 2018 20:21:05 +0800 Subject: [PATCH] Huge refactor for better maintainability --- .../vpnhotspot/LocalOnlyHotspotService.kt | 5 +- .../java/be/mygod/vpnhotspot/MainActivity.kt | 14 +- .../be/mygod/vpnhotspot/RepeaterFragment.kt | 208 ---------- .../be/mygod/vpnhotspot/SettingsFragment.kt | 12 - .../be/mygod/vpnhotspot/TetheringFragment.kt | 379 ------------------ .../be/mygod/vpnhotspot/TetheringService.kt | 1 + .../java/be/mygod/vpnhotspot/client/Client.kt | 2 +- .../vpnhotspot/client/ClientsFragment.kt | 68 ++++ .../java/be/mygod/vpnhotspot/manage/Data.kt | 11 + .../vpnhotspot/manage/InterfaceManager.kt | 63 +++ .../manage/LocalOnlyHotspotManager.kt | 88 ++++ .../be/mygod/vpnhotspot/manage/ManageBar.kt | 24 ++ .../be/mygod/vpnhotspot/manage/Manager.kt | 46 +++ .../vpnhotspot/manage/RepeaterManager.kt | 159 ++++++++ .../mygod/vpnhotspot/manage/TetherManager.kt | 174 ++++++++ .../vpnhotspot/manage/TetheringFragment.kt | 123 ++++++ .../util/ServiceForegroundConnector.kt | 19 +- .../java/be/mygod/vpnhotspot/util/Utils.kt | 6 + .../vpnhotspot/widget/AutoCollapseTextView.kt | 15 + .../src/main/res/drawable/ic_content_wave.xml | 11 + .../main/res/drawable/ic_device_devices.xml | 9 + .../main/res/drawable/ic_device_wifi_lock.xml | 9 + mobile/src/main/res/layout/activity_main.xml | 12 +- .../src/main/res/layout/fragment_repeater.xml | 115 +----- .../src/main/res/layout/fragment_settings.xml | 22 +- .../main/res/layout/fragment_tethering.xml | 26 +- .../src/main/res/layout/listitem_client.xml | 5 +- .../main/res/layout/listitem_interface.xml | 7 +- .../res/layout/listitem_manage_tether.xml | 20 +- .../src/main/res/layout/listitem_repeater.xml | 190 +++++++++ mobile/src/main/res/menu/navigation.xml | 10 +- mobile/src/main/res/menu/repeater.xml | 15 - mobile/src/main/res/values-zh-rCN/strings.xml | 8 +- mobile/src/main/res/values/strings.xml | 8 +- mobile/src/main/res/xml/pref_settings.xml | 12 +- 35 files changed, 1072 insertions(+), 824 deletions(-) delete mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt delete mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt delete mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/Data.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/widget/AutoCollapseTextView.kt create mode 100644 mobile/src/main/res/drawable/ic_content_wave.xml create mode 100644 mobile/src/main/res/drawable/ic_device_devices.xml create mode 100644 mobile/src/main/res/drawable/ic_device_wifi_lock.xml create mode 100644 mobile/src/main/res/layout/listitem_repeater.xml delete mode 100644 mobile/src/main/res/menu/repeater.xml diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt index c5779b0a..44841d7b 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt @@ -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) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt index 29a4d211..b327d0e3 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt @@ -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 } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt deleted file mode 100644 index e5e234f6..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt +++ /dev/null @@ -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) { - override fun submitList(list: MutableList?) { - 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(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() - } -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt deleted file mode 100644 index f468315e..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt +++ /dev/null @@ -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) -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt deleted file mode 100644 index e11ef8ab..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt +++ /dev/null @@ -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) : 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() - @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 = 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) : Comparable { - 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() { - 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.DiffCallback) { - private var lookup: Map = emptyMap() - - fun update(activeIfaces: List, localOnlyIfaces: List) { - 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, 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 - } -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index 69bc0079..feb039cc 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -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 diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt index 04287cab..58c4c7e4 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt @@ -8,7 +8,7 @@ import be.mygod.vpnhotspot.net.TetherType import java.util.* abstract class Client { - companion object : DiffUtil.ItemCallback() { + companion object DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Client, newItem: Client) = oldItem.iface == newItem.iface && oldItem.mac == newItem.mac override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt new file mode 100644 index 00000000..6e13b7cf --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/client/ClientsFragment.kt @@ -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) { + override fun submitList(list: MutableList?) { + 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 + } + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/Data.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/Data.kt new file mode 100644 index 00000000..ed6c4e62 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/Data.kt @@ -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 +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt new file mode 100644 index 00000000..4d9204c5 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt @@ -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) +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt new file mode 100644 index 00000000..07148997 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt @@ -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 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 + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt new file mode 100644 index 00000000..46e4bf6e --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt @@ -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 +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt new file mode 100644 index 00000000..7bbe5c9e --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt @@ -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() { + 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 +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt new file mode 100644 index 00000000..7b44634c --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterManager.kt @@ -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(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() + } +} \ No newline at end of file diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt new file mode 100644 index 00000000..4fbf9dfa --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetherManager.kt @@ -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() + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt new file mode 100644 index 00000000..f1e9dc30 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/manage/TetheringFragment.kt @@ -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) { + 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, localOnlyIfaces: List) { + 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(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 = emptyMap() + var enabledTypes = emptySet() + 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, 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) + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/ServiceForegroundConnector.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/ServiceForegroundConnector.kt index cd9194d7..710107bb 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/ServiceForegroundConnector.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/ServiceForegroundConnector.kt @@ -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>) : - LifecycleObserver { +class ServiceForegroundConnector(private val owner: LifecycleOwner, private val connection: ServiceConnection, + private val clazz: KClass) : LifecycleObserver { init { - (host as LifecycleOwner).lifecycle.addObserver(this) + owner.lifecycle.addObserver(this) } - constructor(host: ServiceConnection, vararg classes: KClass) : 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) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt index 0b40129b..7c1fe3a6 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt @@ -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}" } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/widget/AutoCollapseTextView.kt b/mobile/src/main/java/be/mygod/vpnhotspot/widget/AutoCollapseTextView.kt new file mode 100644 index 00000000..afeaa0b4 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/widget/AutoCollapseTextView.kt @@ -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 + } +} diff --git a/mobile/src/main/res/drawable/ic_content_wave.xml b/mobile/src/main/res/drawable/ic_content_wave.xml new file mode 100644 index 00000000..68e040eb --- /dev/null +++ b/mobile/src/main/res/drawable/ic_content_wave.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/mobile/src/main/res/drawable/ic_device_devices.xml b/mobile/src/main/res/drawable/ic_device_devices.xml new file mode 100644 index 00000000..150ced43 --- /dev/null +++ b/mobile/src/main/res/drawable/ic_device_devices.xml @@ -0,0 +1,9 @@ + + + diff --git a/mobile/src/main/res/drawable/ic_device_wifi_lock.xml b/mobile/src/main/res/drawable/ic_device_wifi_lock.xml new file mode 100644 index 00000000..8eba49d0 --- /dev/null +++ b/mobile/src/main/res/drawable/ic_device_wifi_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index d625ee21..f34b3a4e 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -8,16 +8,22 @@ android:layout_height="match_parent" tools:context="be.mygod.vpnhotspot.MainActivity"> + + + app:layout_constraintTop_toBottomOf="@+id/toolbar"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:scrollbars="vertical" + tools:listitem="@layout/listitem_client"/> + diff --git a/mobile/src/main/res/layout/fragment_settings.xml b/mobile/src/main/res/layout/fragment_settings.xml index a6b1b81f..bfb8bbbc 100644 --- a/mobile/src/main/res/layout/fragment_settings.xml +++ b/mobile/src/main/res/layout/fragment_settings.xml @@ -1,21 +1,7 @@ - - - - + android:layout_height="match_parent" + android:id="@+id/preference"/> diff --git a/mobile/src/main/res/layout/fragment_tethering.xml b/mobile/src/main/res/layout/fragment_tethering.xml index 2f0b4e44..00023420 100644 --- a/mobile/src/main/res/layout/fragment_tethering.xml +++ b/mobile/src/main/res/layout/fragment_tethering.xml @@ -1,26 +1,12 @@ - - - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:scrollbars="vertical" + tools:listitem="@layout/listitem_interface"/> diff --git a/mobile/src/main/res/layout/listitem_client.xml b/mobile/src/main/res/layout/listitem_client.xml index d3c4b7bd..b1c9fce0 100644 --- a/mobile/src/main/res/layout/listitem_client.xml +++ b/mobile/src/main/res/layout/listitem_client.xml @@ -11,10 +11,7 @@ + android:padding="16dp"> + type="be.mygod.vpnhotspot.manage.Data"/> + android:orientation="vertical" + android:layout_gravity="center_vertical"> - - - - - + name="data" + type="be.mygod.vpnhotspot.manage.TetherManager.Data"/> @@ -43,7 +33,7 @@ android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="center_vertical" - android:text="@{title}" + android:text="@{data.title}" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" tools:text="@string/tethering_manage_wifi"/> @@ -51,7 +41,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:checked="@{tetherListener.isStarted(type, tetherListener.enabledTypes)}" + android:checked="@{data.isStarted}" android:clickable="false" android:ellipsize="end" android:focusable="false" diff --git a/mobile/src/main/res/layout/listitem_repeater.xml b/mobile/src/main/res/layout/listitem_repeater.xml new file mode 100644 index 00000000..7f8ab562 --- /dev/null +++ b/mobile/src/main/res/layout/listitem_repeater.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/src/main/res/menu/navigation.xml b/mobile/src/main/res/menu/navigation.xml index b5ffddd6..34f64818 100644 --- a/mobile/src/main/res/menu/navigation.xml +++ b/mobile/src/main/res/menu/navigation.xml @@ -1,16 +1,16 @@ - - + + - - - - - \ No newline at end of file diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index da9f64c7..301cad90 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -2,11 +2,10 @@ VPN 热点 无线中继 - 系统共享 + 共享管理 + 已连设备 设置选项 - 未打开 - 中继地址 输入 PIN 一键加密 请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。 @@ -51,7 +50,6 @@ 蓝牙网络共享 Android 系统无法打开网络共享。 - 已连接设备 %s (正在连接) %s (已连上) %s (已断开) @@ -60,7 +58,7 @@ Wi\u2011Fi 运行频段 (不稳定) "自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)" 严格模式 - 只允许通过 VPN 隧道的包通过,也适用于临时热点 + 只允许通过 VPN 隧道的包通过,不适用于系统共享 备用 DNS 服务器[:端口] 清理/重新应用路由规则 杂项 diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index da509be3..403e3f0b 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -12,10 +12,9 @@ VPN Hotspot Repeater Tethering + Clients Settings - Service inactive - Addresses WPS Enter PIN Push Button @@ -54,7 +53,6 @@ Bluetooth tethering Android system has failed to start tethering. - Connected devices %s (connecting) %s (reachable) %s (lost) @@ -63,8 +61,8 @@ Operating Wi\u2011Fi channel (unstable) Auto (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz) Strict mode - Only allow packets that goes through VPN tunnel, also - applies to temporary Wi\u2011Fi hotspot. + Only allow packets that goes through VPN tunnel. Does not + apply to system tethering. Fallback DNS server[:port] Clean/reapply routing rules Misc diff --git a/mobile/src/main/res/xml/pref_settings.xml b/mobile/src/main/res/xml/pref_settings.xml index 831b4596..28a0d525 100644 --- a/mobile/src/main/res/xml/pref_settings.xml +++ b/mobile/src/main/res/xml/pref_settings.xml @@ -1,21 +1,11 @@ - + android:title="@string/settings_service"> - -