feat: show subscription

This commit is contained in:
metacubex 2022-12-10 02:05:45 +08:00
parent d2f5503330
commit e06c901bf5
12 changed files with 280 additions and 38 deletions

View File

@ -20,15 +20,6 @@
<package name="io.ktor" alias="false" withSubpackages="true" /> <package name="io.ktor" alias="false" withSubpackages="true" />
</value> </value>
</option> </option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">

View File

@ -65,4 +65,12 @@ fun Long.toBytesString(): String {
else -> else ->
"$this Bytes" "$this Bytes"
} }
}
fun Double.toProgress(): Int {
return this.toInt()
}
fun Long.toDateStr(): String {
val simpleDateFormat =SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
return simpleDateFormat.format(Date(this*1000))
} }

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="?attr/colorSurface" />
<corners
android:radius="8dp"
/>
</shape>
</item>
</layer-list>

View File

@ -13,16 +13,18 @@
<variable <variable
name="menu" name="menu"
type="android.view.View.OnClickListener" /> type="android.view.View.OnClickListener" />
<import type="android.view.View" />
<import type="com.github.kr328.clash.design.util.I18nKt" /> <import type="com.github.kr328.clash.design.util.I18nKt" />
<import type="com.github.kr328.clash.design.util.IntervalKt" /> <import type="com.github.kr328.clash.design.util.IntervalKt" />
</data> </data>
<RelativeLayout <RelativeLayout
android:elevation="5dp"
android:id="@+id/root_view" android:id="@+id/root_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" android:layout_margin="5dp"
android:background="@drawable/bg_b"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:minHeight="@dimen/item_min_height" android:minHeight="@dimen/item_min_height"
@ -61,6 +63,43 @@
android:layout_marginTop="@dimen/item_text_margin" android:layout_marginTop="@dimen/item_text_margin"
android:text="@{profile.pending ? @string/format_type_unsaved(I18nKt.toString(profile.type, context)) : I18nKt.toString(profile.type, context)}" android:text="@{profile.pending ? @string/format_type_unsaved(I18nKt.toString(profile.type, context)) : I18nKt.toString(profile.type, context)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" /> android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
>
<TextView
android:textSize="12sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{profile.download ==0 ? View.GONE : View.VISIBLE}"
android:text='@{profile.download >0 ?I18nKt.toBytesString(profile.download+profile.upload)+"/" :""}'
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
<TextView
android:textSize="12sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{profile.download ==0 ? View.GONE : View.VISIBLE}"
android:text='@{profile.download >0 ?I18nKt.toBytesString(profile.total) : ""}'
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
</LinearLayout>
<TextView
android:textSize="12sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{profile.expire==0 ? View.GONE : View.VISIBLE}"
android:text='@{profile.expire>0 ? I18nKt.toDateStr(profile.expire):""}'
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
<ProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="1000"
android:visibility="@{profile.download==0 ? View.GONE : View.VISIBLE}"
android:progress="@{profile.download>0 ?I18nKt.toProgress ((profile.download+profile.upload)/(profile.total/1000)) :0}"
/>
</LinearLayout> </LinearLayout>
<TextView <TextView
@ -74,12 +113,10 @@
android:textAppearance="@style/TextAppearance.MaterialComponents.Tooltip" /> android:textAppearance="@style/TextAppearance.MaterialComponents.Tooltip" />
<View <View
android:layout_width="@dimen/divider_size" android:layout_width="2dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/menu_view" android:layout_toStartOf="@id/menu_view"
android:background="?attr/colorControlHighlight" android:background="@color/color_clash_dark" />
android:minHeight="@{@dimen/item_tailing_component_size * 1.5f}" />
<FrameLayout <FrameLayout
android:id="@+id/menu_view" android:id="@+id/menu_view"

View File

@ -19,6 +19,11 @@ dependencies {
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
implementation(libs.kaidl.runtime) implementation(libs.kaidl.runtime)
implementation(libs.rikkax.multiprocess) implementation(libs.rikkax.multiprocess)
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
// define any required OkHttp artifacts without version
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:logging-interceptor")
} }
afterEvaluate { afterEvaluate {

View File

@ -2,6 +2,7 @@ package com.github.kr328.clash.service
import android.content.Context import android.content.Context
import com.github.kr328.clash.service.data.Database import com.github.kr328.clash.service.data.Database
import com.github.kr328.clash.service.data.Imported
import com.github.kr328.clash.service.data.ImportedDao import com.github.kr328.clash.service.data.ImportedDao
import com.github.kr328.clash.service.data.Pending import com.github.kr328.clash.service.data.Pending
import com.github.kr328.clash.service.data.PendingDao import com.github.kr328.clash.service.data.PendingDao
@ -13,10 +14,13 @@ import com.github.kr328.clash.service.util.directoryLastModified
import com.github.kr328.clash.service.util.generateProfileUUID import com.github.kr328.clash.service.util.generateProfileUUID
import com.github.kr328.clash.service.util.importedDir import com.github.kr328.clash.service.util.importedDir
import com.github.kr328.clash.service.util.pendingDir import com.github.kr328.clash.service.util.pendingDir
import com.github.kr328.clash.service.util.sendProfileChanged
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.* import java.util.*
@ -40,6 +44,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
type = type, type = type,
source = source, source = source,
interval = 0, interval = 0,
upload = 0,
total = 0,
download = 0,
expire = 0,
) )
PendingDao().insert(pending) PendingDao().insert(pending)
@ -68,6 +76,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
type = Profile.Type.File, type = Profile.Type.File,
source = imported.source, source = imported.source,
interval = imported.interval, interval = imported.interval,
upload = imported.upload,
total = imported.total,
download = imported.download,
expire = imported.expire,
) )
cloneImportedFiles(uuid, newUUID) cloneImportedFiles(uuid, newUUID)
@ -93,13 +105,21 @@ class ProfileManager(private val context: Context) : IProfileManager,
type = imported.type, type = imported.type,
source = source, source = source,
interval = interval, interval = interval,
upload = 0,
total = 0,
download = 0,
expire = 0,
) )
) )
} else { } else {
val newPending = pending.copy( val newPending = pending.copy(
name = name, name = name,
source = source, source = source,
interval = interval interval = interval,
upload = 0,
total = 0,
download = 0,
expire = 0,
) )
PendingDao().update(newPending) PendingDao().update(newPending)
@ -108,6 +128,81 @@ class ProfileManager(private val context: Context) : IProfileManager,
override suspend fun update(uuid: UUID) { override suspend fun update(uuid: UUID) {
scheduleUpdate(uuid, true) scheduleUpdate(uuid, true)
ImportedDao().queryByUUID(uuid)?.let {
if (it.type == Profile.Type.Url) {
updateFlow(it)
}
}
}
suspend fun updateFlow(old: Imported) {
val client = OkHttpClient()
try {
val request = Request.Builder()
.url(old.source)
.header("User-Agent", "ClashforWindows/0.19.23")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful || response.headers["subscription-userinfo"] == null) return
var upload: Long = 0
var download: Long = 0
var total: Long = 0
var expire: Long = 0
val userinfo = response.headers["subscription-userinfo"]
if (response.isSuccessful && userinfo != null) {
val flags = userinfo.split(";")
for (flag in flags) {
val info = flag.split("=")
when {
info[0].contains("upload") -> upload =
info[1].toLong()
info[0].contains("download") -> download =
info[1].toLong()
info[0].contains("total") -> total =
info[1].toLong()
info[0].contains("expire") -> {
if (info[1].isNotEmpty()) {
expire = info[1].toLong()
}
}
}
}
}
val new = Imported(
old.uuid,
old.name,
old.type,
old.source,
old.interval,
upload,
download,
total,
expire,
old?.createdAt ?: System.currentTimeMillis()
)
if (old != null) {
ImportedDao().update(new)
} else {
ImportedDao().insert(new)
}
PendingDao().remove(new.uuid)
context.sendProfileChanged(new.uuid)
// println(response.body!!.string())
}
} catch (e: Exception) {
System.out.println(e)
}
} }
override suspend fun commit(uuid: UUID, callback: IFetchObserver?) { override suspend fun commit(uuid: UUID, callback: IFetchObserver?) {
@ -163,6 +258,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
val type = pending?.type ?: imported?.type ?: return null val type = pending?.type ?: imported?.type ?: return null
val source = pending?.source ?: imported?.source ?: return null val source = pending?.source ?: imported?.source ?: return null
val interval = pending?.interval ?: imported?.interval ?: return null val interval = pending?.interval ?: imported?.interval ?: return null
val upload = pending?.upload ?: imported?.upload ?: return null
val download = pending?.download ?: imported?.download ?: return null
val total = pending?.total ?: imported?.total ?: return null
val expire = pending?.expire ?: imported?.expire ?: return null
return Profile( return Profile(
uuid, uuid,
@ -171,6 +270,10 @@ class ProfileManager(private val context: Context) : IProfileManager,
source, source,
active != null && imported?.uuid == active, active != null && imported?.uuid == active,
interval, interval,
upload,
download,
total,
expire,
resolveUpdatedAt(uuid), resolveUpdatedAt(uuid),
imported != null, imported != null,
pending != null pending != null

View File

@ -19,6 +19,8 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -65,28 +67,93 @@ object ProfileProcessor {
.copyRecursively(context.importedDir.resolve(snapshot.uuid.toString())) .copyRecursively(context.importedDir.resolve(snapshot.uuid.toString()))
val old = ImportedDao().queryByUUID(snapshot.uuid) val old = ImportedDao().queryByUUID(snapshot.uuid)
var upload: Long = 0
var download: Long = 0
var total: Long = 0
var expire: Long = 0
if (snapshot?.type == Profile.Type.Url) {
val client = OkHttpClient()
val request = Request.Builder()
.url(snapshot.source)
.header("User-Agent", "ClashforWindows/0.19.23")
.build()
val new = Imported( client.newCall(request).execute().use { response ->
snapshot.uuid, val userinfo = response.headers["subscription-userinfo"]
snapshot.name, if (response.isSuccessful && userinfo != null) {
snapshot.type, val flags = userinfo.split(";")
snapshot.source, for (flag in flags) {
snapshot.interval, val info = flag.split("=")
old?.createdAt ?: System.currentTimeMillis() when {
) info[0].contains("upload") -> upload =
info[1].toLong()
if (old != null) { info[0].contains("download") -> download =
ImportedDao().update(new) info[1].toLong()
} else {
ImportedDao().insert(new) info[0].contains("total") -> total =
info[1].toLong()
info[0].contains("expire") -> {
if (info[1].isNotEmpty()) {
expire = info[1].toLong()
}
}
}
}
}
val new = Imported(
snapshot.uuid,
snapshot.name,
snapshot.type,
snapshot.source,
snapshot.interval,
upload,
download,
total,
expire,
old?.createdAt ?: System.currentTimeMillis()
)
if (old != null) {
ImportedDao().update(new)
} else {
ImportedDao().insert(new)
}
PendingDao().remove(snapshot.uuid)
context.pendingDir.resolve(snapshot.uuid.toString())
.deleteRecursively()
context.sendProfileChanged(snapshot.uuid)
}
} else if (snapshot?.type == Profile.Type.File) {
val new = Imported(
snapshot.uuid,
snapshot.name,
snapshot.type,
snapshot.source,
snapshot.interval,
upload,
download,
total,
expire,
old?.createdAt ?: System.currentTimeMillis()
)
if (old != null) {
ImportedDao().update(new)
} else {
ImportedDao().insert(new)
}
PendingDao().remove(snapshot.uuid)
context.pendingDir.resolve(snapshot.uuid.toString())
.deleteRecursively()
context.sendProfileChanged(snapshot.uuid)
} }
PendingDao().remove(snapshot.uuid)
context.pendingDir.resolve(snapshot.uuid.toString())
.deleteRecursively()
context.sendProfileChanged(snapshot.uuid)
} }
} }
} }
@ -181,10 +248,13 @@ object ProfileProcessor {
when { when {
name.isBlank() -> name.isBlank() ->
throw IllegalArgumentException("Empty name") throw IllegalArgumentException("Empty name")
source.isEmpty() && type != Profile.Type.File -> source.isEmpty() && type != Profile.Type.File ->
throw IllegalArgumentException("Invalid url") throw IllegalArgumentException("Invalid url")
source.isNotEmpty() && scheme != "https" && scheme != "http" && scheme != "content" -> source.isNotEmpty() && scheme != "https" && scheme != "http" && scheme != "content" ->
throw IllegalArgumentException("Unsupported url $source") throw IllegalArgumentException("Unsupported url $source")
interval != 0L && TimeUnit.MILLISECONDS.toMinutes(interval) < 15 -> interval != 0L && TimeUnit.MILLISECONDS.toMinutes(interval) < 15 ->
throw IllegalArgumentException("Invalid interval") throw IllegalArgumentException("Invalid interval")
} }

View File

@ -14,5 +14,9 @@ data class Imported(
@ColumnInfo(name = "type") val type: Profile.Type, @ColumnInfo(name = "type") val type: Profile.Type,
@ColumnInfo(name = "source") val source: String, @ColumnInfo(name = "source") val source: String,
@ColumnInfo(name = "interval") val interval: Long, @ColumnInfo(name = "interval") val interval: Long,
@ColumnInfo(name = "upload") val upload: Long,
@ColumnInfo(name = "download") val download: Long,
@ColumnInfo(name = "total") val total: Long,
@ColumnInfo(name = "expire") val expire: Long,
@ColumnInfo(name = "createdAt") val createdAt: Long, @ColumnInfo(name = "createdAt") val createdAt: Long,
) )

View File

@ -14,5 +14,9 @@ data class Pending(
@ColumnInfo(name = "type") val type: Profile.Type, @ColumnInfo(name = "type") val type: Profile.Type,
@ColumnInfo(name = "source") val source: String, @ColumnInfo(name = "source") val source: String,
@ColumnInfo(name = "interval") val interval: Long, @ColumnInfo(name = "interval") val interval: Long,
@ColumnInfo(name = "upload") val upload: Long,
@ColumnInfo(name = "download") val download: Long,
@ColumnInfo(name = "total") val total: Long,
@ColumnInfo(name = "expire") val expire: Long,
@ColumnInfo(name = "createdAt") val createdAt: Long = System.currentTimeMillis(), @ColumnInfo(name = "createdAt") val createdAt: Long = System.currentTimeMillis(),
) )

View File

@ -92,7 +92,8 @@ private suspend fun migrationFromLegacy234(
type = newType, type = newType,
source = if (newType != Profile.Type.File) cursor.getString(uri) else "", source = if (newType != Profile.Type.File) cursor.getString(uri) else "",
interval = if (version == 2) intervalValue * 1000 else intervalValue, interval = if (version == 2) intervalValue * 1000 else intervalValue,
) 0,0,0,0
)
val base = context.pendingDir.resolve(pending.uuid.toString()) val base = context.pendingDir.resolve(pending.uuid.toString())
@ -165,6 +166,7 @@ private suspend fun migrationFromLegacy1(context: Context, legacy: SQLiteDatabas
type = newType, type = newType,
source = source, source = source,
interval = 0, interval = 0,
0,0,0,0
) )
val base = context.pendingDir.resolve(pending.uuid.toString()) val base = context.pendingDir.resolve(pending.uuid.toString())

View File

@ -133,7 +133,8 @@ class Picker(private val context: Context) {
imported.name, imported.name,
imported.type, imported.type,
imported.source, imported.source,
imported.interval imported.interval,
0,0,0,0
) )
) )

View File

@ -18,6 +18,11 @@ data class Profile(
val source: String, val source: String,
val active: Boolean, val active: Boolean,
val interval: Long, val interval: Long,
val upload: Long,
var download: Long,
val total: Long,
val expire: Long,
val updatedAt: Long, val updatedAt: Long,
val imported: Boolean, val imported: Boolean,