From a72c2d8abb766b6822c7b3adfe6cc96ea9531584 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:18:30 +0800 Subject: [PATCH] url profile update --- .../java/com/github/kr328/clash/Constants.kt | 1 - .../github/kr328/clash/ImportFileActivity.kt | 11 +- .../github/kr328/clash/ImportUrlActivity.kt | 90 ++++++++++++- .../github/kr328/clash/ProfilesActivity.kt | 127 +++++++++++++++++- .../kr328/clash/adapter/ProfileAdapter.kt | 54 +++++--- .../github/kr328/clash/view/RadioFatItem.kt | 4 +- app/src/main/res/drawable/ic_edit.xml | 9 ++ app/src/main/res/drawable/ic_refresh.xml | 9 ++ app/src/main/res/drawable/ic_sync.xml | 9 ++ app/src/main/res/layout/activity_profiles.xml | 1 + .../res/layout/dialog_profile_updating.xml | 15 +++ .../main/res/layout/view_radio_fat_item.xml | 11 +- app/src/main/res/menu/menu_profile_popup.xml | 7 + app/src/main/res/values/strings.xml | 11 +- .../kr328/clash/service/ClashService.kt | 8 +- .../clash/service/data/ClashProfileEntity.kt | 69 +++++----- 16 files changed, 352 insertions(+), 84 deletions(-) create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/drawable/ic_sync.xml create mode 100644 app/src/main/res/layout/dialog_profile_updating.xml create mode 100644 app/src/main/res/menu/menu_profile_popup.xml diff --git a/app/src/main/java/com/github/kr328/clash/Constants.kt b/app/src/main/java/com/github/kr328/clash/Constants.kt index 0919d96..391d6c5 100644 --- a/app/src/main/java/com/github/kr328/clash/Constants.kt +++ b/app/src/main/java/com/github/kr328/clash/Constants.kt @@ -1,6 +1,5 @@ package com.github.kr328.clash object Constants { - const val TAG = "ClashForAndroid" const val PROFILES_DIR = "profiles" } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt index 3b4563a..2fe543e 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt @@ -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() } diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt index 5565182..c908344 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt @@ -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.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) { diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 4d7c879..45b92a6 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -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() + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt index b79b196..962620d 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt @@ -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() { var profiles: Array = 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)) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt b/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt index 2808c80..798a467 100644 --- a/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt +++ b/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt @@ -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) } diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..2ab2fb7 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..8229a9a --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sync.xml b/app/src/main/res/drawable/ic_sync.xml new file mode 100644 index 0000000..ce8796c --- /dev/null +++ b/app/src/main/res/drawable/ic_sync.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_profiles.xml b/app/src/main/res/layout/activity_profiles.xml index e5c2d35..91ff361 100644 --- a/app/src/main/res/layout/activity_profiles.xml +++ b/app/src/main/res/layout/activity_profiles.xml @@ -1,6 +1,7 @@ diff --git a/app/src/main/res/layout/dialog_profile_updating.xml b/app/src/main/res/layout/dialog_profile_updating.xml new file mode 100644 index 0000000..d1e43dd --- /dev/null +++ b/app/src/main/res/layout/dialog_profile_updating.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_radio_fat_item.xml b/app/src/main/res/layout/view_radio_fat_item.xml index 6c8266b..f7b2d98 100644 --- a/app/src/main/res/layout/view_radio_fat_item.xml +++ b/app/src/main/res/layout/view_radio_fat_item.xml @@ -53,11 +53,18 @@ android:layout_alignTop="@id/view_radio_fat_item_text_container" android:layout_alignBottom="@id/view_radio_fat_item_text_container"> + + + android:layout_width="25dp" + android:layout_height="25dp" /> diff --git a/app/src/main/res/menu/menu_profile_popup.xml b/app/src/main/res/menu/menu_profile_popup.xml new file mode 100644 index 0000000..50548d8 --- /dev/null +++ b/app/src/main/res/menu/menu_profile_popup.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64c1420..749ce59 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,9 +20,12 @@ Feedback About - New Profile - %d Proxy, %d Group, %d Rule + %s (File) + %s (URL) + Remove + Profile Updating + New Profile File From Local Storage @@ -46,7 +49,7 @@ Empty Name Empty Path - Invalid Config: %s + Invalid Config: %s %s - %s @@ -85,4 +88,6 @@ Clash Start Service Starting Clash + + Not Implemented diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt index 85c3d4a..3398d8a 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt @@ -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) ) diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 0f1aa03..10b3d54 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -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 { - 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 { - 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 { + override fun createFromParcel(parcel: Parcel): ClashProfileEntity { + return Parcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } } } \ No newline at end of file