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:
24
mobile/src/main/java/be/mygod/vpnhotspot/room/AppDatabase.kt
Normal file
24
mobile/src/main/java/be/mygod/vpnhotspot/room/AppDatabase.kt
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
53
mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
Normal file
53
mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt
Normal 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) }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user