url profile update

This commit is contained in:
Kr328 committed 2019-12-06 14:18:30 +08:00
1 parent 8ad202cc07
commit a72c2d8abb
16 files changed
+352 -84

No files matched your search

@@ -1,6 +1,5 @@
package com.github.kr328.clash
object Constants {
const val TAG = "ClashForAndroid"
const val PROFILES_DIR = "profiles"
}
@@ -99,7 +99,7 @@ class ImportFileActivity : BaseActivity() {
it.readBytes().toString(Charsets.UTF_8)
} ?: throw NullPointerException("Unable to open config file")
val parsed = Yaml(configuration = YamlConfiguration(strictMode = false)).parse(
Yaml(configuration = YamlConfiguration(strictMode = false)).parse(
ClashProfile.serializer(),
data
)
@@ -114,12 +114,9 @@ class ImportFileActivity : BaseActivity() {
it.profileService.addProfile(
ClashProfileEntity(
name = name.content,
token = "file|" + file.content!!.toString(),
cache = cache.absolutePath,
token = ClashProfileEntity.fileToken(file.content!!.toString()),
file = cache.absolutePath,
active = false,
proxies = parsed.proxies.size,
proxyGroups = parsed.proxyGroups.size,
rules = parsed.rules.size,
lastUpdate = System.currentTimeMillis()
)
)
@@ -129,7 +126,7 @@ class ImportFileActivity : BaseActivity() {
} catch (e: Exception) {
Snackbar.make(
activity_import_file_root,
getString(R.string.clash_import_file_invalid, e.toString()),
getString(R.string.clash_profile_invalid, e.toString()),
Snackbar.LENGTH_LONG
).show()
}
@@ -2,14 +2,27 @@ package com.github.kr328.clash
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import com.github.kr328.clash.adapter.FormAdapter
import com.github.kr328.clash.model.ClashProfile
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.utils.FileUtils
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_import_file.*
import kotlinx.android.synthetic.main.activity_import_url.*
import java.io.FileOutputStream
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
import java.net.*
import kotlin.concurrent.thread
class ImportUrlActivity : BaseActivity() {
companion object {
const val DEFAULT_TIMEOUT = 30 * 1000
}
private val elements = listOf<FormAdapter.Type>(
FormAdapter.TextType(
R.drawable.ic_about,
@@ -33,6 +46,10 @@ class ImportUrlActivity : BaseActivity() {
it.layoutManager = LinearLayoutManager(this)
it.adapter = FormAdapter(this, elements)
}
activity_import_url_save.setOnClickListener {
checkAndInsert()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -49,13 +66,74 @@ class ImportUrlActivity : BaseActivity() {
val name = elements[0] as FormAdapter.TextType
val url = elements[1] as FormAdapter.TextType
if ( name.content.isEmpty() ) {
if ( name.content.isBlank() ) {
return
}
URL(url.content).openConnection()
if ( url.content.isBlank() ) {
return
}
activity_import_url_save.visibility = View.GONE
activity_import_url_saving.visibility = View.VISIBLE
runClash {
val httpPort = it.queryGeneral().ports.randomHttp
thread {
try {
val connection = if ( httpPort > 0 )
URL(url.content).openConnection(Proxy(Proxy.Type.HTTP,
InetSocketAddress.createUnresolved("127.0.0.1", httpPort)))
else
URL(url.content).openConnection()
val data = with (connection) {
connectTimeout = DEFAULT_TIMEOUT
connect()
getInputStream().bufferedReader().use {
it.readText()
}
}
Yaml(configuration = YamlConfiguration(strictMode = false)).parse(ClashProfile.serializer(), data)
val cache =
FileUtils.generateRandomFile(filesDir.resolve(Constants.PROFILES_DIR), ".yaml")
FileOutputStream(cache).use { outputStream ->
outputStream.write(data.toByteArray())
}
runClash { clash ->
clash.profileService.addProfile(ClashProfileEntity(name.content,
ClashProfileEntity.urlToken(url.content),
cache.absolutePath,
false,
System.currentTimeMillis()))
}
runOnUiThread {
finish()
}
}
catch (e: Exception) {
runOnUiThread {
Snackbar.make(
activity_import_url_root,
getString(R.string.clash_profile_invalid, e.toString()),
Snackbar.LENGTH_LONG
).show()
activity_import_url_save.visibility = View.VISIBLE
activity_import_url_saving.visibility = View.GONE
}
}
}
}
}
catch (e: Exception) {
@@ -1,15 +1,35 @@
package com.github.kr328.clash
import android.app.AlertDialog
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.PopupMenu
import androidx.appcompat.view.menu.MenuPopupHelper
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import com.github.kr328.clash.adapter.ProfileAdapter
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.event.ProfileChangedEvent
import com.github.kr328.clash.model.ClashProfile
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.utils.FileUtils
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_import_url.*
import kotlinx.android.synthetic.main.activity_profiles.*
import java.io.FileOutputStream
import java.lang.Exception
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
import kotlin.concurrent.thread
class ProfilesActivity : BaseActivity() {
private var dialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profiles)
@@ -24,11 +44,10 @@ class ProfilesActivity : BaseActivity() {
override fun canScrollHorizontally(): Boolean = false
override fun canScrollVertically(): Boolean = false
}
activity_profiles_main_list.adapter = ProfileAdapter(this) { id ->
runClash {
it.profileService.setActiveProfile(id)
}
}
activity_profiles_main_list.adapter = ProfileAdapter(this,
this::onProfileClick,
this::onOperateClick,
this::onProfileLongClick)
}
override fun onStart() {
@@ -84,4 +103,102 @@ class ProfilesActivity : BaseActivity() {
result.dispatchUpdatesTo(adapter)
}
}
private fun onProfileClick(profile: ClashProfileEntity) {
runClash {
it.profileService.setActiveProfile(profile.id)
}
}
private fun onOperateClick(profile: ClashProfileEntity) {
when {
ClashProfileEntity.isUrlToken(profile.token) -> {
dialog?.dismiss()
dialog = AlertDialog.Builder(this)
.setTitle(R.string.clash_profile_updating)
.setView(R.layout.dialog_profile_updating)
.setCancelable(false)
.show()
updateProfile(profile)
}
ClashProfileEntity.isFileToken(profile.token) -> {
Snackbar.make(
activity_profiles_root,
R.string.not_implemented,
Snackbar.LENGTH_LONG
).show()
}
}
}
private fun onProfileLongClick(parent: View, profile: ClashProfileEntity) {
PopupMenu(this, parent).apply {
setOnMenuItemClickListener { removeProfile(profile).run { true } }
inflate(R.menu.menu_profile_popup)
show()
}
}
private fun removeProfile(profile: ClashProfileEntity) {
runClash {
it.profileService.removeProfile(profile.id)
}
}
private fun updateProfile(profile: ClashProfileEntity) {
val url = ClashProfileEntity.getUrl(profile.token)
runClash {
val httpPort = it.queryGeneral().ports.randomHttp
thread {
try {
val connection = if ( httpPort > 0 )
URL(url).openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress.createUnresolved("127.0.0.1", httpPort))
)
else
URL(url).openConnection()
val data = with (connection) {
connectTimeout = ImportUrlActivity.DEFAULT_TIMEOUT
connect()
getInputStream().bufferedReader().use {
it.readText()
}
}
Yaml(configuration = YamlConfiguration(strictMode = false)).parse(
ClashProfile.serializer(), data)
FileOutputStream(profile.file).use { outputStream ->
outputStream.write(data.toByteArray())
}
runClash { clash ->
clash.profileService.addProfile(profile
.copy(lastUpdate = System.currentTimeMillis()))
}
}
catch (e: Exception) {
runOnUiThread {
Snackbar.make(
activity_profiles_root,
getString(R.string.clash_profile_invalid, e.toString()),
Snackbar.LENGTH_LONG
).show()
}
}
runOnUiThread {
dialog?.dismiss()
}
}
}
}
}
@@ -1,13 +1,19 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.view.RadioFatItem
import java.text.SimpleDateFormat
import java.util.*
class ProfileAdapter(private val context: Context, private val listener: (Int) -> Unit) :
class ProfileAdapter(private val context: Context,
private val onClick: (ClashProfileEntity) -> Unit,
private val onOperateClick: (ClashProfileEntity) -> Unit,
private val onLongClicked: (View,ClashProfileEntity) -> Unit) :
RecyclerView.Adapter<ProfileAdapter.ProfileViewHolder>() {
var profiles: Array<ClashProfileEntity> = emptyArray()
@@ -32,26 +38,40 @@ class ProfileAdapter(private val context: Context, private val listener: (Int) -
val current = profiles[position]
holder.view.title = current.name
holder.view.summary = context.getString(
R.string.clash_profile_item_summary,
current.proxies, current.proxyGroups, current.rules
)
holder.view.isChecked = current.active
holder.view.setOnClickListener {
listener(current.id)
onClick(current)
}
holder.view.setOnOperationOnClickListener(View.OnClickListener {
onOperateClick(current)
})
holder.view.setOnLongClickListener {
onLongClicked(it, current).run { true }
}
holder.view.operation = context.getDrawable(R.drawable.ic_vert)
val profileUpdateDate = GregorianCalendar().apply {
timeInMillis = current.lastUpdate
}
val now = Calendar.getInstance()
// with(current.token) {
// when {
// startsWith("http") || startsWith("content") -> {
//
// }
// startsWith("file") -> {
//
// }
// }
// }
val formatter = if ( profileUpdateDate.get(Calendar.YEAR) == now.get(Calendar.YEAR) &&
profileUpdateDate.get(Calendar.MONTH) == now.get(Calendar.MONTH) &&
profileUpdateDate.get(Calendar.DAY_OF_MONTH) == now.get(Calendar.DAY_OF_MONTH) )
SimpleDateFormat("HH:mm:ss", Locale.getDefault())
else
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
when {
ClashProfileEntity.isFileToken(current.token) -> {
holder.view.operation = context.getDrawable(R.drawable.ic_edit)
holder.view.summary = context.getString(R.string.clash_profile_item_summary_file,
formatter.format(profileUpdateDate.time))
}
ClashProfileEntity.isUrlToken(current.token) -> {
holder.view.operation = context.getDrawable(R.drawable.ic_sync)
holder.view.summary = context.getString(R.string.clash_profile_item_summary_url,
formatter.format(profileUpdateDate.time))
}
}
}
}
@@ -61,11 +61,11 @@ class RadioFatItem @JvmOverloads constructor(
clickable.setOnLongClickListener(l)
}
fun setOperationOnClickListener(l: OnClickListener?) {
fun setOnOperationOnClickListener(l: OnClickListener?) {
operationClickable.setOnClickListener(l)
}
fun setOperationOnLongClickListener(l: OnLongClickListener?) {
fun setOnOperationOnLongClickListener(l: OnLongClickListener?) {
operationClickable.setOnLongClickListener(l)
}
+9
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="#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>
+9
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="#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>
+9
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="#FF000000"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>
@@ -1,6 +1,7 @@
<?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:id="@+id/activity_profiles_root"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -0,0 +1,15 @@
<?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="wrap_content">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:indeterminate="true"
android:indeterminateBehavior="repeat" />
</LinearLayout>
@@ -53,11 +53,18 @@
android:layout_alignTop="@id/view_radio_fat_item_text_container"
android:layout_alignBottom="@id/view_radio_fat_item_text_container">
<View
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/lightGray"/>
<View
android:id="@+id/view_radio_fat_item_operation"
android:layout_gravity="center"
android:layout_width="30dp"
android:layout_height="30dp" />
android:layout_width="25dp"
android:layout_height="25dp" />
</FrameLayout>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_pofile_popup_remove"
android:title="@string/clash_profile_remove" />
</menu>
+8 -3
View File
@@ -20,9 +20,12 @@
<string name="clash_feedback">Feedback</string>
<string name="clash_about">About</string>
<string name="clash_new_profile">New Profile</string>
<string name="clash_profile_item_summary">%d Proxy, %d Group, %d Rule</string>
<string name="clash_profile_item_summary_file">%s (File)</string>
<string name="clash_profile_item_summary_url">%s (URL)</string>
<string name="clash_profile_remove">Remove</string>
<string name="clash_profile_updating">Profile Updating</string>
<string name="clash_new_profile">New Profile</string>
<string name="clash_new_profile_file_title">File</string>
<string name="clash_new_profile_file_summary">From Local Storage</string>
@@ -46,7 +49,7 @@
<string name="clash_import_file_empty_name">Empty Name</string>
<string name="clash_import_file_empty_path">Empty Path</string>
<string name="clash_import_file_invalid">Invalid Config: %s</string>
<string name="clash_profile_invalid">Invalid Config: %s</string>
<string name="clash_proxy_group_title">%s - %s</string>
@@ -85,4 +88,6 @@
<string name="clash_start_service_notification_channel">Clash Start Service</string>
<string name="clash_start_service_notification">Starting Clash</string>
<string name="not_implemented">Not Implemented</string>
</resources>
@@ -7,6 +7,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.*
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.ClashProcess
import com.github.kr328.clash.core.event.*
import com.github.kr328.clash.core.model.GeneralPacket
import com.github.kr328.clash.core.model.ProxyPacket
@@ -237,6 +238,9 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master,
private fun reloadProfile() {
executor.submit {
if ( clash.process.getProcessStatus() != ProcessEvent.STARTED)
return@submit
val active = profileService.queryActiveProfile()
if (active == null) {
@@ -244,11 +248,11 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master,
return@submit
}
Log.i("Loading profile ${active.cache}")
Log.i("Loading profile ${active.file}")
try {
val remove = clash.loadProfile(
File(active.cache),
File(active.file),
profileService.queryProfileSelected(active.id)
)
@@ -1,5 +1,6 @@
package com.github.kr328.clash.service.data
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import androidx.room.*
@@ -7,48 +8,15 @@ import com.github.kr328.clash.core.serialization.Parcels
import kotlinx.serialization.Serializable
@Entity(tableName = "profiles")
@TypeConverters(ClashProfileEntity.Converters::class)
@Serializable
data class ClashProfileEntity(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "token") val token: String,
@ColumnInfo(name = "cache") val cache: String,
@ColumnInfo(name = "file") val file: String,
@ColumnInfo(name = "active") val active: Boolean,
@ColumnInfo(name = "proxies") val proxies: Int,
@ColumnInfo(name = "proxy_groups") val proxyGroups: Int,
@ColumnInfo(name = "rules") val rules: Int,
@ColumnInfo(name = "last_update") val lastUpdate: Long,
@ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0
) : Parcelable {
enum class Type {
UNKNOWN, URL, FILE
}
class Converters {
@TypeConverter
fun typeToInt(type: Type?): Int? {
return type?.run {
when (this) {
Type.URL -> 1
Type.FILE -> 2
Type.UNKNOWN -> 0
}
}
}
@TypeConverter
fun intToType(i: Int?): Type? {
return i?.run {
when (this) {
1 -> Type.URL
2 -> Type.FILE
else -> Type.UNKNOWN
}
}
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcels.dump(serializer(), this, parcel)
}
@@ -57,13 +25,36 @@ data class ClashProfileEntity(
return 0
}
companion object CREATOR : Parcelable.Creator<ClashProfileEntity> {
override fun createFromParcel(parcel: Parcel): ClashProfileEntity {
return Parcels.load(serializer(), parcel)
companion object {
fun fileToken(content: String): String {
return "file|$content"
}
override fun newArray(size: Int): Array<ClashProfileEntity?> {
return arrayOfNulls(size)
fun urlToken(content: String): String {
return "url|$content"
}
fun isFileToken(token: String): Boolean {
return token.startsWith("file|")
}
fun isUrlToken(token: String): Boolean {
return token.startsWith("url|")
}
fun getUrl(token: String): String {
return token.removePrefix("url|")
}
@JvmField
val CREATOR = object : Parcelable.Creator<ClashProfileEntity> {
override fun createFromParcel(parcel: Parcel): ClashProfileEntity {
return Parcels.load(serializer(), parcel)
}
override fun newArray(size: Int): Array<ClashProfileEntity?> {
return arrayOfNulls(size)
}
}
}
}