VPN Hotspot 2.0: Client+ (#39)

Fix #13, #38. I don't have a lot of confidence that this would work very well for every device.

Also here's an SQL command that hopefully somebody could make into the app for me: `SELECT TrafficRecord.mac, SUM(TrafficRecord.sentPackets), SUM(TrafficRecord.sentBytes), SUM(TrafficRecord.receivedPackets), SUM(TrafficRecord.receivedBytes) FROM TrafficRecord LEFT JOIN TrafficRecord AS Next ON TrafficRecord.id = Next.previousId WHERE Next.id IS NULL GROUP BY TrafficRecord.mac;`
This commit is contained in:
Mygod
2018-10-02 21:12:19 +08:00
committed by GitHub
parent 16d1eda0d4
commit 38f95a382e
35 changed files with 946 additions and 98 deletions

View File

@@ -0,0 +1,24 @@
package be.mygod.vpnhotspot.room
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import be.mygod.vpnhotspot.App.Companion.app
@Database(entities = [ClientRecord::class, TrafficRecord::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
companion object {
const val DB_NAME = "app.db"
val instance by lazy {
Room.databaseBuilder(app.deviceStorage, AppDatabase::class.java, DB_NAME)
.allowMainThreadQueries()
.build()
}
}
abstract val clientRecordDao: ClientRecord.Dao
abstract val trafficRecordDao: TrafficRecord.Dao
}

View File

@@ -0,0 +1,21 @@
package be.mygod.vpnhotspot.room
import androidx.room.*
@Entity
data class ClientRecord(@PrimaryKey
val mac: Long,
var nickname: CharSequence = "",
var blocked: Boolean = false) {
@androidx.room.Dao
interface Dao {
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
fun lookupOrNull(mac: Long): ClientRecord?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun updateInternal(value: ClientRecord): Long
}
}
fun ClientRecord.Dao.lookup(mac: Long) = lookupOrNull(mac) ?: ClientRecord(mac)
fun ClientRecord.Dao.update(value: ClientRecord) = check(updateInternal(value) == value.mac)

View File

@@ -0,0 +1,53 @@
package be.mygod.vpnhotspot.room
import android.os.Parcel
import android.text.TextUtils
import androidx.room.TypeConverter
import java.net.InetAddress
import java.nio.ByteBuffer
import java.nio.ByteOrder
class Converters {
@TypeConverter
fun persistCharSequence(cs: CharSequence): ByteArray {
val p = Parcel.obtain()
try {
TextUtils.writeToParcel(cs, p, 0)
return p.marshall()
} finally {
p.recycle()
}
}
@TypeConverter
fun unpersistCharSequence(data: ByteArray): CharSequence {
val p = Parcel.obtain()
try {
p.unmarshall(data, 0, data.size)
p.setDataPosition(0)
return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)
} finally {
p.recycle()
}
}
@TypeConverter
fun persistInetAddress(address: InetAddress): ByteArray = address.address
@TypeConverter
fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data)
}
fun String.macToLong(): Long = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
mark()
put(split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray())
reset()
long
}
fun Long.macToString(): String = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
putLong(this@macToString)
array().take(6).joinToString(":") { "%02x".format(it) }
}

View File

@@ -0,0 +1,96 @@
package be.mygod.vpnhotspot.room
import android.os.Parcel
import android.os.Parcelable
import androidx.room.*
import java.net.InetAddress
@Entity(foreignKeys = [ForeignKey(entity = TrafficRecord::class, parentColumns = ["id"], childColumns = ["previousId"],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.RESTRICT)],
indices = [Index(value = ["previousId"], unique = true)])
data class TrafficRecord(
/**
* Setting id = null should only be used when a new row is created and not yet inserted into the database.
*
* https://www.sqlite.org/lang_createtable.html#primkeyconst:
* > Unless the column is an INTEGER PRIMARY KEY or the table is a WITHOUT ROWID table or the column is declared
* > NOT NULL, SQLite allows NULL values in a PRIMARY KEY column.
*/
@PrimaryKey(autoGenerate = true)
var id: Long? = null,
val timestamp: Long = System.currentTimeMillis(),
/**
* Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord.
*/
val mac: Long,
/**
* For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case.
*/
val ip: InetAddress,
val upstream: String? = null,
val downstream: String,
var sentPackets: Long = 0,
var sentBytes: Long = 0,
var receivedPackets: Long = 0,
var receivedBytes: Long = 0,
/**
* ID for the previous traffic record.
*/
val previousId: Long? = null) {
@androidx.room.Dao
interface Dao {
@Insert
fun insertInternal(value: TrafficRecord): Long
@Query("""
SELECT MIN(TrafficRecord.timestamp) AS timestamp,
COUNT(TrafficRecord.id) AS count,
SUM(TrafficRecord.sentPackets) AS sentPackets,
SUM(TrafficRecord.sentBytes) AS sentBytes,
SUM(TrafficRecord.receivedPackets) AS receivedPackets,
SUM(TrafficRecord.receivedBytes) AS receivedBytes
FROM TrafficRecord LEFT JOIN TrafficRecord AS Next ON TrafficRecord.id = Next.previousId
/* We only want to find the last record for each chain so that we don't double count */
WHERE TrafficRecord.mac = :mac AND Next.id IS NULL""")
fun queryStats(mac: Long): ClientStats
}
}
fun TrafficRecord.Dao.insert(value: TrafficRecord) {
check(value.id == null)
value.id = insertInternal(value)
}
data class ClientStats(
val timestamp: Long = 0,
val count: Long = 0,
val sentPackets: Long = 0,
val sentBytes: Long = 0,
val receivedPackets: Long = 0,
val receivedBytes: Long = 0
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readLong(),
parcel.readLong(),
parcel.readLong(),
parcel.readLong(),
parcel.readLong(),
parcel.readLong())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(timestamp)
parcel.writeLong(count)
parcel.writeLong(sentPackets)
parcel.writeLong(sentBytes)
parcel.writeLong(receivedPackets)
parcel.writeLong(receivedBytes)
}
override fun describeContents() = 0
companion object CREATOR : Parcelable.Creator<ClientStats> {
override fun createFromParcel(parcel: Parcel) = ClientStats(parcel)
override fun newArray(size: Int) = arrayOfNulls<ClientStats>(size)
}
}