new profile activity

This commit is contained in:
Kr328 committed 2019-11-22 19:22:51 +08:00
1 parent e3c5fb1a8c
commit 774d83ed9f
24 files changed
+533 -104

No files matched your search

+7 -2
View File
@@ -21,6 +21,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ProfilesActivity" android:exported="false" />
<activity android:name=".ProfilesActivity"
android:label="@string/clash_profiles"
android:exported="false" />
<activity android:name=".CreateProfileActivity"
android:label="@string/clash_new_profile"
android:exported="false" />
</application>
</manifest>
</manifest>
@@ -0,0 +1,62 @@
package com.github.kr328.clash
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import kotlinx.android.synthetic.main.activity_new_profile.*
class CreateProfileActivity : AppCompatActivity() {
companion object {
val NEW_PROFILE_SOURCE = listOf(
AdapterData(R.drawable.ic_new_profile_file,
R.string.clash_new_profile_file_title,
R.string.clash_new_profile_file_summary),
AdapterData(R.drawable.ic_new_profile_url,
R.string.clash_new_profile_url_title,
R.string.clash_new_profile_url_summary)
)
}
class Adapter(private val context: Context) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
return (convertView ?: LayoutInflater.from(context).inflate(R.layout.adapter_new_profile, parent, false)).apply {
val current = NEW_PROFILE_SOURCE[position]
findViewById<ImageView>(R.id.adapter_new_profile_icon).setImageResource(current.icon)
findViewById<TextView>(R.id.adapter_new_profile_title).text = context.getString(current.title)
findViewById<TextView>(R.id.adapter_new_profile_summary).text = context.getString(current.summary)
}
}
override fun getItem(position: Int): Any {
return NEW_PROFILE_SOURCE
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getCount(): Int {
return NEW_PROFILE_SOURCE.size
}
}
data class AdapterData(val icon: Int, val title: Int, val summary: Int)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_profile)
setSupportActionBar(activity_new_profile_toolbar)
activity_new_profile_list.adapter = Adapter(this)
}
}
@@ -59,6 +59,10 @@ class MainActivity : AppCompatActivity() {
setLogo(R.mipmap.ic_launcher_foreground)
})
activity_main_clash_profiles.setOnClickListener {
startActivity(Intent(this, ProfilesActivity::class.java))
}
activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_stopped)
activity_main_clash_status_title.text = getString(R.string.clash_status_stopped)
activity_main_clash_status_summary.text = getString(R.string.clash_status_click_to_start)
@@ -100,7 +104,7 @@ class MainActivity : AppCompatActivity() {
ClashDatabase.getInstance(this)
.openClashProfileDao()
.observeDefaultProfileUrl()
.observeDefaultProfileName()
.observe(this) {
activity_main_clash_profiles_summary.text =
if ( it == null )
@@ -1,14 +1,57 @@
package com.github.kr328.clash
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.observe
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.ProfileAdapter
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.data.ClashProfileEntity
import kotlinx.android.synthetic.main.activity_profiles.*
class ProfilesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_profiles)
setContentView(R.layout.activity_profiles)
setSupportActionBar(activity_profiles_toolbar)
activity_profiles_toolbar.elevation = 4.0f
activity_profiles_new_profile.setOnClickListener {
startActivity(Intent(this, CreateProfileActivity::class.java))
}
activity_profiles_main_list.layoutManager = object: LinearLayoutManager(this) {
override fun canScrollHorizontally(): Boolean = false
override fun canScrollVertically(): Boolean = false
}
activity_profiles_main_list.adapter = ProfileAdapter(this)
ClashDatabase.getInstance(this)
.openClashProfileDao()
.observeProfiles()
.observe(this, this::refreshList)
}
private fun refreshList(newData: List<ClashProfileEntity>) {
val adapter = activity_profiles_main_list.adapter as ProfileAdapter
val oldData = adapter.profiles
val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldData[oldItemPosition].id == newData[newItemPosition].id
override fun getOldListSize(): Int = oldData.size
override fun getNewListSize(): Int = newData.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldData[oldItemPosition] == newData[newItemPosition]
})
adapter.profiles = newData
result.dispatchUpdatesTo(adapter)
}
}
@@ -0,0 +1,45 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.fragment.FormFragment
import com.google.android.material.textfield.TextInputEditText
class FormAdapter(private val context: Context, private val elements: List<FormFragment.FormElement>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class TextHolder(view: View) : RecyclerView.ViewHolder(view) {
val icon: ImageView = view.findViewById(R.id.adapter_from_text_icon)
val edit: TextInputEditText = view.findViewById(R.id.adapter_from_text_text)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when ( viewType ) {
FormFragment.FormType.STRING.ordinal, FormFragment.FormType.FILE.ordinal, FormFragment.FormType.URL.ordinal ->
TextHolder(LayoutInflater.from(context).inflate(R.layout.adapter_from_text, parent, false))
else -> throw IllegalArgumentException()
}
}
override fun getItemCount(): Int {
return elements.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val current = elements[position]
when ( holder ) {
is TextHolder -> {
holder.icon.setImageResource(current.icon)
holder.edit.hint = context.getString(current.hint)
}
}
}
override fun getItemViewType(position: Int): Int {
return elements[position].type.ordinal
}
}
@@ -0,0 +1,55 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.service.data.ClashProfileEntity
class ProfileAdapter(private val context: Context) :
RecyclerView.Adapter<ProfileAdapter.ProfileViewHolder>() {
var profiles: List<ClashProfileEntity> = emptyList()
class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val checkable: View = view.findViewById(R.id.adapter_profiles_checked)
val title: TextView = view.findViewById(R.id.adapter_profiles_title)
val summary: TextView = view.findViewById(R.id.adapter_profiles_summary)
val operation: ImageView = view.findViewById(R.id.adapter_profiles_operation)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
return ProfileViewHolder(
LayoutInflater.from(context)
.inflate(R.layout.adapter_profile, parent, false)
)
}
override fun getItemCount(): Int {
return profiles.size
}
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
val current = profiles[position]
holder.title.text = current.name
holder.summary.text = context.getString(
R.string.clash_profile_item_summary,
current.proxies, current.proxyGroups, current.rules
)
with(current.token) {
when {
startsWith("http") || startsWith("content") -> {
holder.operation.setImageResource(R.drawable.ic_profile_refresh)
}
startsWith("file") -> {
holder.operation.setImageResource(R.drawable.ic_profile_edit)
}
}
}
}
}
@@ -0,0 +1,36 @@
package com.github.kr328.clash.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.adapter.FormAdapter
import java.lang.Exception
class FormFragment(private val elements: List<FormElement>) : Fragment() {
enum class FormType {
STRING, URL, FILE
}
interface ResultDumper {
class DumpException(message: String) : Exception(message)
fun dump(element: FormElement, data: Any)
}
data class FormElement(val type: FormType, val icon: Int, val title: Int, val hint: Int, val result: ResultDumper)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return RecyclerView(requireActivity()).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
layoutManager = LinearLayoutManager(requireActivity())
adapter = FormAdapter(requireActivity(), elements)
}
}
}
@@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable
data class ClashProfile(
@SerialName("Proxy") val proxies: List<ClashProxy>,
@SerialName("Proxy Group") val proxyGroups: List<ClashProxyGroup>,
@SerialName("Rule") val rules: List<String>
@SerialName("Rule") val rules: List<ClashRule>
)
@@ -0,0 +1,82 @@
package com.github.kr328.clash.model
import com.charleskorn.kaml.YamlException
import kotlinx.serialization.*
import kotlinx.serialization.internal.StringDescriptor
@Serializable(ClashRule.Serializer::class)
data class ClashRule(val matcher: Matcher, val pattern: String, val target: String) {
enum class Matcher {
DOMAIN_SUFFIX,
DOMAIN_KEYWORD,
DOMAIN,
IP_CIDR,
SRC_IP_CIDR,
GEOIP,
DST_PORT,
SRC_PORT,
MATCH;
override fun toString(): String {
return when (this) {
DOMAIN_SUFFIX -> "DOMAIN-SUFFIX"
DOMAIN_KEYWORD -> "DOMAIN-KEYWORD"
DOMAIN -> "DOMAIN"
IP_CIDR -> "IP-CIDR"
SRC_IP_CIDR -> "SRC-IP-CIDR"
GEOIP -> "GEOIP"
DST_PORT -> "DST-PORT"
SRC_PORT -> "SRC-PORT"
MATCH -> "MATCH"
}
}
companion object {
fun fromString(s: String): Matcher {
return when (s) {
"DOMAIN-SUFFIX" -> DOMAIN_SUFFIX
"DOMAIN-KEYWORD" -> DOMAIN_KEYWORD
"DOMAIN" -> DOMAIN
"IP-CIDR" -> IP_CIDR
"SRC-IP-CIDR" -> SRC_IP_CIDR
"GEOIP" -> GEOIP
"DST-PORT" -> DST_PORT
"SRC-PORT" -> SRC_PORT
"MATCH" -> MATCH
else -> throw YamlException("Invalid matcher $s", 0, 0)
}
}
}
}
class Serializer : KSerializer<ClashRule> {
override val descriptor: SerialDescriptor
get() = StringDescriptor.withName("rule")
override fun deserialize(decoder: Decoder): ClashRule {
val rule = decoder.decodeString().split(",")
return when (rule.size) {
3 -> {
ClashRule(Matcher.fromString(rule[0]), rule[1], rule[2])
}
2 -> {
if ( Matcher.fromString(rule[0]) != Matcher.MATCH )
throw YamlException("Invalid rule $rule", 0, 0)
ClashRule(Matcher.MATCH, "", rule[1])
}
else -> throw YamlException("Invalid rule $rule", 0, 0)
}
}
override fun serialize(encoder: Encoder, obj: ClashRule) {
if ( obj.matcher == Matcher.MATCH ) {
encoder.encodeString("${obj.matcher},${obj.target}")
}
else {
encoder.encodeString("${obj.matcher},${obj.pattern},${obj.target}")
}
}
}
}
@@ -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="#FF000000"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>
@@ -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="#FF000000"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
</vector>
@@ -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="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>
@@ -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="#FF000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/activity_new_profile_toolbar"
android:layout_width="match_parent"
android:layout_height="60dp"/>
</com.google.android.material.appbar.AppBarLayout>
<ListView
android:id="@+id/activity_new_profile_list"
android:dividerHeight="0dp"
android:divider="@null"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
+11 -6
View File
@@ -1,13 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/activity_profiles_toolbar"
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="75dp"/>
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/activity_profiles_toolbar"
android:elevation="4dp"
android:layout_width="match_parent"
android:layout_height="60dp" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
@@ -33,11 +39,10 @@
android:contentDescription="@string/app_name"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_width="40dp"
android:layout_height="60dp"/>
android:layout_width="20dp"
android:layout_height="40dp"/>
<TextView
android:text="@string/clash_new_profile"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:gravity="center|start"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="15dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/adapter_from_text_icon"
android:src="@drawable/ic_about"
android:contentDescription="@string/app_name"
android:layout_width="30dp"
android:layout_height="30dp"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/adapter_from_text_text"
android:drawableEnd="@drawable/ic_about"
android:layout_marginStart="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
android:inputType="none"/>
</LinearLayout>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main_clash_profiles"
android:layout_marginTop="10dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="@android:color/white">
<ImageView
android:id="@+id/adapter_new_profile_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_centerVertical="true"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_profiles" />
<TextView
android:id="@+id/adapter_new_profile_title"
android:layout_toEndOf="@id/adapter_new_profile_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:text="@string/clash_profiles" />
<TextView
android:id="@+id/adapter_new_profile_summary"
android:text="@string/clash_profiles_summary_unselected"
android:textColor="@android:color/darker_gray"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:layout_marginTop="2dp"
android:layout_alignStart="@id/adapter_new_profile_title"
android:layout_below="@id/adapter_new_profile_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
@@ -22,11 +22,13 @@
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/adapter_profiles_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:text="@string/app_name"/>
<TextView
android:id="@+id/adapter_profiles_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
@@ -47,6 +49,7 @@
android:background="@color/light_gray"/>
<ImageView
android:id="@+id/adapter_profiles_operation"
android:contentDescription="@string/app_name"
android:layout_width="80dp"
android:layout_height="80dp"
+10 -3
View File
@@ -1,9 +1,9 @@
<resources>
<string name="app_name">Clash</string>
<string name="clash_status_stopped">Clash Stopped</string>
<string name="clash_status_click_to_start">Click to start</string>
<string name="clash_status_started">Clash Started</string>
<string name="clash_status_stopped">Stopped</string>
<string name="clash_status_click_to_start">Click here to start</string>
<string name="clash_status_started">Running</string>
<string name="clash_status_forwarded_traffic">%s Traffic Forwarded</string>
<string name="clash_proxy_manage">Proxy Manage</string>
@@ -19,4 +19,11 @@
<string name="clash_about">About</string>
<string name="clash_new_profile">New Profile</string>
<string name="clash_profile_item_summary">%d Proxies, %d ProxyGroups, %d Rules</string>
<string name="clash_new_profile_file_title">File</string>
<string name="clash_new_profile_file_summary">From Local Storage</string>
<string name="clash_new_profile_url_title">URL</string>
<string name="clash_new_profile_url_summary">From Internet</string>
</resources>
@@ -3,13 +3,12 @@ package com.github.kr328.clash.core
import android.content.Context
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.Uri
import android.util.Log
import org.json.JSONObject
import java.io.*
class Clash(
private val context: Context,
context: Context,
clashDir: File,
private val controllerPath: File,
listener: (ClashProcessStatus) -> Unit
@@ -24,15 +23,12 @@ class Clash(
const val PING_REPLY = 233
}
private val cacheFile = context.cacheDir.resolve("config.yaml")
fun ping(): Boolean {
try {
return runControl(COMMAND_PING) {_, input, _ ->
return runControl(COMMAND_PING) { _, input, _ ->
input.readInt() == PING_REPLY
}
}
catch (e: IOException) {
} catch (e: IOException) {
Log.w(TAG, "Clash ping failure", e)
}
return false
@@ -40,24 +36,15 @@ class Clash(
@Throws(IOException::class)
@Synchronized
fun loadProfile(url: Uri?) {
if ( url == null || url.scheme != "content" )
return
fun loadProfile(file: File) {
runControl(COMMAND_PROFILE_RELOAD) { _, input, output ->
output.writeString(JSONObject().apply {
put("path", file.absolutePath)
}.toString())
synchronized(cacheFile) {
context.contentResolver.openInputStream(url)?.use {
it.copyTo(FileOutputStream(cacheFile))
}
runControl(COMMAND_PROFILE_RELOAD) {_, input, output ->
output.writeString(JSONObject().apply {
put("path", cacheFile.absolutePath)
}.toString())
val result = input.readString()
if (result.isNotEmpty()) {
throw IOException(JSONObject(result).getString("error"))
}
val result = input.readString()
if (result.isNotEmpty()) {
throw IOException(JSONObject(result).getString("error"))
}
}
}
@@ -73,10 +60,18 @@ class Clash(
}
}
private fun <R>runControl(command: Int, block: (LocalSocket, DataInputStream, DataOutputStream) -> R): R {
private fun <R> runControl(
command: Int,
block: (LocalSocket, DataInputStream, DataOutputStream) -> R
): R {
val socket = LocalSocket()
socket.connect(LocalSocketAddress(controllerPath.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
socket.connect(
LocalSocketAddress(
controllerPath.absolutePath,
LocalSocketAddress.Namespace.FILESYSTEM
)
)
val input = DataInputStream(socket.inputStream)
val output = DataOutputStream(socket.outputStream)
@@ -90,8 +85,7 @@ class Clash(
output.close()
socket.close()
}
catch (ignore: IOException) {
} catch (ignore: IOException) {
}
@@ -1,35 +0,0 @@
package com.github.kr328.clash.core
import android.os.Parcel
import android.os.Parcelable
data class ClashStatus(val status: Int) : Parcelable {
companion object {
const val STOPPED = 0
const val IDLE = 1
const val LOADING_PROFILE = 2
@JvmField
val CREATOR = object: Parcelable.Creator<ClashStatus> {
override fun createFromParcel(parcel: Parcel): ClashStatus {
return ClashStatus(parcel)
}
override fun newArray(size: Int): Array<ClashStatus?> {
return arrayOfNulls(size)
}
}
}
constructor(parcel: Parcel) : this(parcel.readInt())
override fun writeToParcel(dest: Parcel?, flags: Int) {
dest?.writeInt(status)
}
override fun describeContents(): Int {
return 0
}
}
@@ -2,7 +2,6 @@ package com.github.kr328.clash.service
import android.app.Service
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.IBinder
import android.os.ParcelFileDescriptor
@@ -12,6 +11,7 @@ import androidx.lifecycle.Observer
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.ClashProcessStatus
import com.github.kr328.clash.service.data.ClashDatabase
import java.io.File
import java.io.IOException
import java.util.concurrent.Executors
@@ -23,7 +23,6 @@ class ClashService : Service() {
private val handler = Handler()
private val executor = Executors.newSingleThreadExecutor()
private lateinit var clash: Clash
private var lastProfile: String? = null
private var status = ClashProcessStatus(ClashProcessStatus.STATUS_STOPPED_INT)
set(value) {
@@ -38,29 +37,17 @@ class ClashService : Service() {
private val observers = mutableMapOf<String, IClashObserver>()
private val defaultProfileObserver = object: Observer<String?> {
override fun onChanged(url: String?) {
override fun onChanged(file: String?) {
executor.submit {
Log.i(TAG, "Loading profile $url")
Log.i(TAG, "Loading profile $file")
if (url == null)
clash.stop()
if (url == lastProfile)
if (file == null)
return@submit
try {
clash.loadProfile(Uri.parse(url))
lastProfile = url
clash.loadProfile(File(file))
} catch (e: IOException) {
Log.w(TAG, "Load profile failure")
if (lastProfile == null) {
clash.stop()
} else {
val p = lastProfile
lastProfile = null
ClashDatabase.getInstance(this@ClashService)
.openClashProfileDao()
.setSelectedProfile(p ?: "")
}
}
}
}
@@ -140,12 +127,12 @@ class ClashService : Service() {
ClashProcessStatus.STATUS_STARTED_INT ->
ClashDatabase.getInstance(this)
.openClashProfileDao()
.observeDefaultProfileUrl()
.observeDefaultProfileCache()
.observeForever(defaultProfileObserver)
ClashProcessStatus.STATUS_STOPPED_INT ->
ClashDatabase.getInstance(this)
.openClashProfileDao()
.observeDefaultProfileUrl()
.observeDefaultProfileCache()
.removeObserver(defaultProfileObserver)
}
}
@@ -6,9 +6,15 @@ import androidx.room.Query
@Dao
interface ClashProfileDao {
@Query("UPDATE profiles SET selected = CASE WHEN url = :url THEN 1 ELSE 0 END")
fun setSelectedProfile(url: String)
@Query("UPDATE profiles SET selected = CASE WHEN id = :id THEN 1 ELSE 0 END")
fun setSelectedProfile(id: Long)
@Query("SELECT url FROM profiles WHERE selected = 1 LIMIT 1")
fun observeDefaultProfileUrl(): LiveData<String?>
@Query("SELECT cache FROM profiles WHERE selected = 1 LIMIT 1")
fun observeDefaultProfileCache(): LiveData<String?>
@Query("SELECT name FROM profiles WHERE selected = 1 LIMIT 1")
fun observeDefaultProfileName(): LiveData<String?>
@Query("SELECT * FROM profiles ORDER BY name")
fun observeProfiles(): LiveData<List<ClashProfileEntity>>
}
@@ -6,9 +6,9 @@ import androidx.room.*
@TypeConverters(ClashProfileEntity.Converters::class)
data class ClashProfileEntity(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "token") val token: String,
@ColumnInfo(name = "cache") val cache: String,
@ColumnInfo(name = "selected") val selected: Boolean,
@ColumnInfo(name = "type") val writable: Type,
@ColumnInfo(name = "proxies") val proxies: Long,
@ColumnInfo(name = "proxy_groups") val proxyGroups: Long,
@ColumnInfo(name = "rules") val rules: Long,