Add settings

This commit is contained in:
Mygod
2018-01-03 22:58:45 +08:00
parent 3e7fae95cf
commit 826f601301
17 changed files with 313 additions and 75 deletions

View File

@@ -4,12 +4,20 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import android.preference.PreferenceManager
class App : Application() {
companion object {
lateinit var app: App
}
override fun onCreate() {
super.onCreate()
app = this
if (Build.VERSION.SDK_INT >= 26) getSystemService(NotificationManager::class.java)
.createNotificationChannel(NotificationChannel(HotspotService.CHANNEL,
"Hotspot Service", NotificationManager.IMPORTANCE_LOW))
}
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
}

View File

@@ -2,22 +2,26 @@ package be.mygod.vpnhotspot
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.NetworkInfo
import android.net.wifi.p2p.*
import android.net.wifi.p2p.WifiP2pGroup
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager
import android.os.Binder
import android.os.Looper
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
import android.support.v4.content.LocalBroadcastManager
import android.util.Log
import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app
import java.net.InetAddress
class HotspotService : Service(), WifiP2pManager.ChannelListener {
companion object {
const val CHANNEL = "hotspot"
const val STATUS_CHANGED = "be.mygod.vpnhotspot.HotspotService.STATUS_CHANGED"
private const val TAG = "HotspotService"
}
@@ -27,7 +31,6 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
inner class HotspotBinder : Binder() {
val service get() = this@HotspotService
val status get() = this@HotspotService.status
var data: MainActivity.Data? = null
fun shutdown() {
@@ -35,9 +38,9 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
override fun onSuccess() = clean()
override fun onFailure(reason: Int) {
if (reason == WifiP2pManager.BUSY) clean() else { // assuming it's already gone
Toast.makeText(this@HotspotService, "Failed to remove P2P group (reason: $reason)",
Toast.makeText(this@HotspotService, "Failed to remove P2P group (${formatReason(reason)})",
Toast.LENGTH_SHORT).show()
binder.data?.onStatusChanged()
LocalBroadcastManager.getInstance(this@HotspotService).sendBroadcast(Intent(STATUS_CHANGED))
}
}
})
@@ -52,43 +55,41 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
private set
private val binder = HotspotBinder()
private var receiverRegistered = false
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION ->
if (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0) ==
WifiP2pManager.WIFI_P2P_STATE_DISABLED) clean() // group may be enabled by other apps
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
val info = intent.getParcelableExtra<WifiP2pInfo>(WifiP2pManager.EXTRA_WIFI_P2P_INFO)
val net = intent.getParcelableExtra<NetworkInfo>(WifiP2pManager.EXTRA_NETWORK_INFO)
val group = intent.getParcelableExtra<WifiP2pGroup>(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
hostAddress = info.groupOwnerAddress
val downstream = group.`interface`
if (net.isConnected && downstream != null && this@HotspotService.downstream == null) {
this@HotspotService.downstream = downstream
if (noisySu("echo 1 >/proc/sys/net/ipv4/ip_forward",
"ip route add default dev $upstream scope link table 62",
"ip route add $route dev $downstream scope link table 62",
"ip route add broadcast 255.255.255.255 dev $downstream scope link table 62",
"ip rule add from $route lookup 62",
"iptables -N vpnhotspot_fwd",
"iptables -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT",
"iptables -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT",
"iptables -I FORWARD -j vpnhotspot_fwd",
"iptables -t nat -A PREROUTING -i $downstream -p tcp --dport 53 -j DNAT --to-destination $dns",
"iptables -t nat -A PREROUTING -i $downstream -p udp --dport 53 -j DNAT --to-destination $dns")) {
doStart(group)
} else startFailure("Something went wrong, please check logcat.")
}
this@HotspotService.group = group
binder.data?.onGroupChanged()
showNotification(group)
Log.d(TAG, "${intent.action}: $info, $net, $group")
}
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
val info = intent.getParcelableExtra<WifiP2pInfo>(WifiP2pManager.EXTRA_WIFI_P2P_INFO)
Log.d(TAG, "${intent.action}: $info")
private val receiver = broadcastReceiver { _, intent ->
when (intent.action) {
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION ->
if (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0) ==
WifiP2pManager.WIFI_P2P_STATE_DISABLED) clean() // group may be enabled by other apps
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
val info = intent.getParcelableExtra<WifiP2pInfo>(WifiP2pManager.EXTRA_WIFI_P2P_INFO)
val net = intent.getParcelableExtra<NetworkInfo>(WifiP2pManager.EXTRA_NETWORK_INFO)
val group = intent.getParcelableExtra<WifiP2pGroup>(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
hostAddress = info.groupOwnerAddress
val downstream = group.`interface`
if (net.isConnected && downstream != null && this@HotspotService.downstream == null) {
this@HotspotService.downstream = downstream
if (noisySu("echo 1 >/proc/sys/net/ipv4/ip_forward",
"ip route add default dev $upstream scope link table 62",
"ip route add $route dev $downstream scope link table 62",
"ip route add broadcast 255.255.255.255 dev $downstream scope link table 62",
"ip rule add from $route lookup 62",
"iptables -N vpnhotspot_fwd",
"iptables -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT",
"iptables -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT",
"iptables -I FORWARD -j vpnhotspot_fwd",
"iptables -t nat -A PREROUTING -i $downstream -p tcp --dport 53 -j DNAT --to-destination $dns",
"iptables -t nat -A PREROUTING -i $downstream -p udp --dport 53 -j DNAT --to-destination $dns")) {
doStart(group)
} else startFailure("Something went wrong, please check logcat.")
}
this@HotspotService.group = group
binder.data?.onGroupChanged()
showNotification(group)
Log.d(TAG, "${intent.action}: $info, $net, $group")
}
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
val info = intent.getParcelableExtra<WifiP2pInfo>(WifiP2pManager.EXTRA_WIFI_P2P_INFO)
Log.d(TAG, "${intent.action}: $info")
}
}
}
@@ -96,16 +97,25 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
// TODO: do something to these hardcoded strings
var downstream: String? = null
private set
private val upstream = "tun0"
private val route = "192.168.49.0/24"
private val dns = "8.8.8.8:53"
private val upstream get() = app.pref.getString("service.upstream", "tun0")
private val route get() = app.pref.getString("service.route", "192.168.49.0/24")
private val dns get() = app.pref.getString("service.dns", "8.8.8.8:53")
var status = Status.IDLE
private set(value) {
if (field == value) return
field = value
binder.data?.onStatusChanged()
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(STATUS_CHANGED))
}
private fun formatReason(reason: Int) = when (reason) {
WifiP2pManager.ERROR -> "ERROR"
WifiP2pManager.P2P_UNSUPPORTED -> "P2P_UNSUPPORTED"
WifiP2pManager.BUSY -> "BUSY"
WifiP2pManager.NO_SERVICE_REQUESTS -> "NO_SERVICE_REQUESTS"
else -> "unknown reason: $reason"
}
override fun onBind(intent: Intent) = binder
override fun onCreate() {
@@ -122,7 +132,7 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
if (status != Status.IDLE) return START_NOT_STICKY
status = Status.STARTING
if (!receiverRegistered) {
registerReceiver(receiver, createIntentFilter(
registerReceiver(receiver, intentFilter(
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION))
@@ -137,8 +147,9 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() = doStart()
override fun onFailure(reason: Int) {
Toast.makeText(this@HotspotService, "Failed to remove old P2P group (reason: $reason)",
Toast.LENGTH_SHORT).show()
Toast.makeText(this@HotspotService,
"Failed to remove old P2P group (${formatReason(reason)})", Toast.LENGTH_SHORT)
.show()
}
})
}
@@ -153,7 +164,7 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
clean()
}
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
override fun onFailure(reason: Int) = startFailure("Failed to create P2P group (reason: $reason)")
override fun onFailure(reason: Int) = startFailure("Failed to create P2P group (${formatReason(reason)})")
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire
})
private fun doStart(group: WifiP2pGroup) {

View File

@@ -1,9 +1,6 @@
package be.mygod.vpnhotspot
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.*
import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.DataBindingUtil
@@ -11,32 +8,35 @@ import android.net.wifi.p2p.WifiP2pDevice
import android.os.Bundle
import android.os.IBinder
import android.support.v4.content.ContextCompat
import android.support.v4.content.LocalBroadcastManager
import android.support.v7.app.AppCompatActivity
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.LayoutInflater
import android.view.MenuItem
import android.view.ViewGroup
import be.mygod.vpnhotspot.databinding.ClientBinding
import be.mygod.vpnhotspot.databinding.MainActivityBinding
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
class MainActivity : AppCompatActivity(), ServiceConnection {
class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemClickListener {
inner class Data : BaseObservable() {
val switchEnabled: Boolean
@Bindable get() = when (binder?.status) {
@Bindable get() = when (binder?.service?.status) {
HotspotService.Status.IDLE -> true
HotspotService.Status.ACTIVE -> true
else -> false
}
var serviceStarted: Boolean
@Bindable get() = when (binder?.status) {
@Bindable get() = when (binder?.service?.status) {
HotspotService.Status.STARTING -> true
HotspotService.Status.ACTIVE -> true
else -> false
}
set(value) {
val binder = binder
when (binder?.status) {
when (binder?.service?.status) {
HotspotService.Status.IDLE ->
if (value) ContextCompat.startForegroundService(this@MainActivity,
Intent(this@MainActivity, HotspotService::class.java))
@@ -44,7 +44,7 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
}
}
val running get() = binder?.status == HotspotService.Status.ACTIVE
val running get() = binder?.service?.status == HotspotService.Status.ACTIVE
val ssid: String @Bindable get() = if (running) binder!!.service.group.networkName else ""
val password: String @Bindable get() = if (running) binder!!.service.group.passphrase else ""
@@ -58,17 +58,19 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
notifyPropertyChanged(BR.password)
adapter.fetchClients()
}
val statusListener = broadcastReceiver { _, _ -> onStatusChanged() }
}
class ClientViewHolder(val binding: ClientBinding) : RecyclerView.ViewHolder(binding.root)
class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
inner class ClientAdapter : RecyclerView.Adapter<ClientViewHolder>() {
private var owner: WifiP2pDevice? = null
private lateinit var clients: MutableCollection<WifiP2pDevice>
private lateinit var arpCache: ArpCache
fun fetchClients() {
val binder = binder!!
if (data.running) {
val binder = binder!!
owner = binder.service.group.owner
clients = binder.service.group.clientList
arpCache = ArpCache(binder.service.downstream)
@@ -77,7 +79,7 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ClientViewHolder(ClientBinding.inflate(LayoutInflater.from(parent.context)))
ClientViewHolder(ListitemClientBinding.inflate(LayoutInflater.from(parent.context)))
override fun onBindViewHolder(holder: ClientViewHolder, position: Int) {
val device = when (position) {
@@ -95,20 +97,30 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
override fun getItemCount() = if (owner == null) 0 else 1 + clients.size
}
private lateinit var binding: MainActivityBinding
private lateinit var binding: ActivityMainBinding
private val data = Data()
private val adapter = ClientAdapter()
private var binder: HotspotService.HotspotBinder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.data = data
binding.clients.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
val animator = DefaultItemAnimator()
animator.supportsChangeAnimations = false // prevent fading-in/out when rebinding
binding.clients.itemAnimator = animator
binding.clients.adapter = adapter
binding.toolbar.inflateMenu(R.menu.main)
binding.toolbar.setOnMenuItemClickListener(this)
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.settings -> {
startActivity(Intent(this, SettingsActivity::class.java))
true
}
else -> false
}
override fun onStart() {
@@ -117,6 +129,7 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
}
override fun onStop() {
onServiceDisconnected(null)
unbindService(this)
super.onStop()
}
@@ -126,11 +139,14 @@ class MainActivity : AppCompatActivity(), ServiceConnection {
binder.data = data
this.binder = binder
data.onStatusChanged()
LocalBroadcastManager.getInstance(this)
.registerReceiver(data.statusListener, intentFilter(HotspotService.STATUS_CHANGED))
}
override fun onServiceDisconnected(name: ComponentName?) {
binder?.data = null
binder = null
LocalBroadcastManager.getInstance(this).unregisterReceiver(data.statusListener)
data.onStatusChanged()
}
}

View File

@@ -0,0 +1,14 @@
package be.mygod.vpnhotspot
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import be.mygod.vpnhotspot.databinding.ActivitySettingsBinding
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivitySettingsBinding>(this, R.layout.activity_settings)
.toolbar.setNavigationOnClickListener({ finish() })
}
}

View File

@@ -0,0 +1,82 @@
package be.mygod.vpnhotspot
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.support.customtabs.CustomTabsIntent
import android.support.v4.content.ContextCompat
import android.support.v4.content.LocalBroadcastManager
import android.support.v7.preference.Preference
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
class SettingsFragment : PreferenceFragmentCompatDividers(), ServiceConnection {
private lateinit var service: Preference
private var binder: HotspotService.HotspotBinder? = null
private val statusListener = broadcastReceiver { _, _ -> onStatusChanged() }
private val customTabsIntent by lazy {
CustomTabsIntent.Builder().setToolbarColor(ContextCompat.getColor(activity!!, R.color.colorPrimary)).build()
}
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_settings)
service = findPreference("service")
findPreference("service.clean").setOnPreferenceClickListener {
noisySu("iptables -t nat -F PREROUTING",
"while iptables -D FORWARD -j vpnhotspot_fwd; do done",
"iptables -F vpnhotspot_fwd",
"iptables -X vpnhotspot_fwd",
"ip rule del lookup 62",
"ip route flush table 62")
true
}
findPreference("misc.logcat").setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
.inputStream.bufferedReader().use { it.readText() })
startActivity(Intent.createChooser(intent, getString(R.string.abc_shareactionprovider_share_with)))
true
}
findPreference("misc.source").setOnPreferenceClickListener {
customTabsIntent.launchUrl(activity, Uri.parse("https://github.com/Mygod/VPNHotspot"))
true
}
findPreference("misc.donate").setOnPreferenceClickListener {
customTabsIntent.launchUrl(activity, Uri.parse("https://mygod.be/donate/"))
true
}
}
override fun onStart() {
super.onStart()
val activity = activity!!
activity.bindService(Intent(activity, HotspotService::class.java), this, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
onServiceDisconnected(null)
activity!!.unbindService(this)
super.onStop()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as HotspotService.HotspotBinder
onStatusChanged()
LocalBroadcastManager.getInstance(activity!!)
.registerReceiver(statusListener, intentFilter(HotspotService.STATUS_CHANGED))
}
override fun onServiceDisconnected(name: ComponentName?) {
LocalBroadcastManager.getInstance(activity!!).unregisterReceiver(statusListener)
binder = null
service.isEnabled = false
}
private fun onStatusChanged() {
service.isEnabled = binder!!.service.status == HotspotService.Status.IDLE
}
}

View File

@@ -1,9 +1,16 @@
package be.mygod.vpnhotspot
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
fun createIntentFilter(vararg actions: String): IntentFilter {
fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = receiver(context, intent)
}
fun intentFilter(vararg actions: String): IntentFilter {
val result = IntentFilter()
actions.forEach { result.addAction(it) }
return result