Initial commit

Hotspot works. VPN not yet.
This commit is contained in:
Mygod
2018-01-03 10:45:14 +08:00
commit 41f3f79efe
29 changed files with 980 additions and 0 deletions

5
mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/build
# tests aren't ready yet
/src/androidTest
/src/test

43
mobile/build.gradle Normal file
View File

@@ -0,0 +1,43 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
buildToolsVersion "27.0.3"
compileSdkVersion 27
defaultConfig {
applicationId "be.mygod.vpnhotspot"
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "0.0.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary true
}
buildTypes {
debug {
pseudoLocalesEnabled true
}
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dataBinding.enabled = true
}
dependencies {
kapt "com.android.databinding:compiler:$androidPluginVersion"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "com.android.support:customtabs:$supportLibraryVersion"
implementation "com.android.support:preference-v14:$supportLibraryVersion"
implementation "com.takisoft.fix:preference-v7:$takisoftFixVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}
kapt.generateStubs = true

21
mobile/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="be.mygod.vpnhotspot">
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<application
android:name=".App"
android:allowBackup="false"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".HotspotService">
</service>
</application>
</manifest>

View File

@@ -0,0 +1,15 @@
package be.mygod.vpnhotspot
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
class App : Application() {
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= 26) getSystemService(NotificationManager::class.java)
.createNotificationChannel(NotificationChannel(HotspotService.CHANNEL,
"Hotspot Service", NotificationManager.IMPORTANCE_LOW))
}
}

View File

@@ -0,0 +1,156 @@
package be.mygod.vpnhotspot
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.wifi.p2p.WifiP2pGroup
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.util.Log
import android.widget.Toast
class HotspotService : Service(), WifiP2pManager.ChannelListener {
companion object {
const val CHANNEL = "hotspot"
private const val TAG = "HotspotService"
}
enum class Status {
IDLE, STARTING, ACTIVE
}
inner class HotspotBinder : Binder() {
val service get() = this@HotspotService
val status get() = this@HotspotService.status
var data: MainActivity.Data? = null
fun shutdown() {
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() = clean()
override fun onFailure(reason: Int) {
Toast.makeText(this@HotspotService, "Failed to remove P2P group (reason: $reason)",
Toast.LENGTH_SHORT).show()
}
})
}
}
private lateinit var p2pManager: WifiP2pManager
private lateinit var channel: WifiP2pManager.Channel
private lateinit var group: WifiP2pGroup
private var receiver: BroadcastReceiver? = null
private val binder = HotspotBinder()
private var status = Status.IDLE
set(value) {
field = value
binder.data?.onStatusChanged()
}
override fun onBind(intent: Intent) = binder
override fun onCreate() {
super.onCreate()
p2pManager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
onChannelDisconnected()
}
override fun onChannelDisconnected() {
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (status != Status.IDLE) return START_NOT_STICKY
status = Status.STARTING
initReceiver()
p2pManager.requestGroupInfo(channel, {
when {
it == null -> doStart()
it.isGroupOwner -> doStart(it)
else -> {
Log.i(TAG, "Removing old group ($it)")
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()
}
})
}
}
})
return START_NOT_STICKY
}
private fun doStart() {
p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener, WifiP2pManager.GroupInfoListener {
private fun shutdown() {
startForeground(0, NotificationCompat.Builder(this@HotspotService, CHANNEL)
.build())
clean()
}
private var tries = 0
override fun onSuccess() = p2pManager.requestGroupInfo(channel, this)
override fun onGroupInfoAvailable(group: WifiP2pGroup?) {
if (group != null && group.isGroupOwner) doStart(group) else if (tries < 10) {
Thread.sleep(30L shl tries++)
onSuccess()
} else {
Log.w(TAG, "Unexpected group: $group")
shutdown()
}
}
override fun onFailure(reason: Int) {
Toast.makeText(this@HotspotService, "Failed to create P2P group (reason: $reason)",
Toast.LENGTH_SHORT).show()
shutdown()
}
})
}
private fun doStart(group: WifiP2pGroup) {
status = Status.ACTIVE
this.group = group
startForeground(1, NotificationCompat.Builder(this@HotspotService, CHANNEL)
.setColor(ContextCompat.getColor(this@HotspotService, R.color.colorPrimary))
.setContentTitle(group.networkName)
.setSubText(group.passphrase)
.setSmallIcon(R.drawable.ic_device_wifi_tethering)
.build())
}
private fun initReceiver() {
return
receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// TODO
}
}
}
registerReceiver(receiver, createIntentFilter(
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION))
}
private fun clean() {
status = Status.IDLE
stopForeground(true)
}
override fun onDestroy() {
if (status != Status.IDLE) binder.shutdown()
if (receiver != null) {
unregisterReceiver(receiver)
receiver = null
}
super.onDestroy()
}
}

View File

@@ -0,0 +1,80 @@
package be.mygod.vpnhotspot
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.databinding.BaseObservable
import android.databinding.Bindable
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.os.IBinder
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import be.mygod.vpnhotspot.databinding.MainActivityBinding
class MainActivity : AppCompatActivity(), ServiceConnection {
inner class Data : BaseObservable() {
val switchEnabled: Boolean
@Bindable get() = when (binder?.status) {
HotspotService.Status.IDLE -> true
HotspotService.Status.ACTIVE -> true
else -> false
}
var serviceStarted: Boolean
@Bindable get() = when (binder?.status) {
HotspotService.Status.STARTING -> true
HotspotService.Status.ACTIVE -> true
else -> false
}
set(value) {
val binder = binder
when (binder?.status) {
HotspotService.Status.IDLE ->
ContextCompat.startForegroundService(this@MainActivity,
Intent(this@MainActivity, HotspotService::class.java))
HotspotService.Status.ACTIVE -> binder.shutdown()
}
}
fun onStatusChanged() {
notifyPropertyChanged(BR.switchEnabled)
notifyPropertyChanged(BR.serviceStarted)
}
fun onBinderChanged() {
onStatusChanged()
}
}
private lateinit var binding: MainActivityBinding
private val data = Data()
private var binder: HotspotService.HotspotBinder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.data = data
}
override fun onStart() {
super.onStart()
bindService(Intent(this, HotspotService::class.java), this, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
unbindService(this)
super.onStop()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as HotspotService.HotspotBinder
binder.data = data
this.binder = binder
data.onBinderChanged()
}
override fun onServiceDisconnected(name: ComponentName?) {
binder?.data = null
binder = null
}
}

View File

@@ -0,0 +1,9 @@
package be.mygod.vpnhotspot
import android.content.IntentFilter
fun createIntentFilter(vararg actions: String): IntentFilter {
val result = IntentFilter()
actions.forEach { result.addAction(it) }
return result
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,11c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,13c0,-3.31 -2.69,-6 -6,-6s-6,2.69 -6,6c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,1.48 -0.81,2.75 -2,3.45l1,1.74c1.79,-1.04 3,-2.97 3,-5.19zM12,3C6.48,3 2,7.48 2,13c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,18.53 4,15.96 4,13c0,-4.42 3.58,-8 8,-8s8,3.58 8,8c0,2.96 -1.61,5.53 -4,6.92l1,1.73c2.99,-1.73 5,-4.95 5,-8.65 0,-5.52 -4.48,-10 -10,-10z"/>
</vector>

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="be.mygod.vpnhotspot.MainActivity.Data"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:id="@+id/toolbar">
<Switch
android:id="@+id/switchService"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
android:enabled="@{data.switchEnabled}"
android:checked="@{data.serviceStarted}"
android:onCheckedChanged="@{(_, checked) -> data.setServiceStarted(checked)}"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"/>
</android.support.v7.widget.Toolbar>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:paddingTop="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Network name"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<Space
android:layout_width="8dp"
android:layout_height="0dp"
android:layout_column="1"
android:layout_row="0"/>
<TextView
android:id="@+id/textSsid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="2"
android:layout_row="0"
tools:text="DIRECT-rAnd0m"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="0"
android:layout_row="1"
android:text="Password"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<TextView
android:id="@+id/textPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="2"
android:layout_row="1"
tools:text="p4ssW0rd"/>
</GridLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="Connected devices"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"
android:backgroundTint="?android:attr/textColorSecondary"/>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:paddingTop="8dp"/>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">VPN Hotspot</string>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>