diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml
index de4dad9b..9efa3547 100644
--- a/V2rayNG/app/src/main/AndroidManifest.xml
+++ b/V2rayNG/app/src/main/AndroidManifest.xml
@@ -93,6 +93,9 @@
+
{
+ val intent = Intent(this, UserAssetUrlActivity::class.java)
+ startActivity(intent)
+ true
+ }
R.id.download_file -> {
downloadGeoFiles()
true
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt
new file mode 100644
index 00000000..3199227d
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt
@@ -0,0 +1,128 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import androidx.appcompat.app.AlertDialog
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
+import com.v2ray.ang.dto.AssetUrlItem
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+
+class UserAssetUrlActivity : BaseActivity() {
+ private lateinit var binding: ActivityUserAssetUrlBinding
+
+ var del_config: MenuItem? = null
+ var save_config: MenuItem? = null
+
+ private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
+ private val editAssetId by lazy { intent.getStringExtra("assetUrlId").orEmpty() }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityUserAssetUrlBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+ title = getString(R.string.title_user_asset_add_url)
+
+ val json = assetStorage?.decodeString(editAssetId)
+ if (!json.isNullOrBlank()) {
+ bindingAsset(Gson().fromJson(json, AssetUrlItem::class.java))
+ } else {
+ clearAsset()
+ }
+ }
+
+ /**
+ * bingding seleced asset config
+ */
+ private fun bindingAsset(assetItem: AssetUrlItem): Boolean {
+ binding.etRemarks.text = Utils.getEditable(assetItem.remarks)
+ binding.etUrl.text = Utils.getEditable(assetItem.url)
+ return true
+ }
+
+ /**
+ * clear or init asset config
+ */
+ private fun clearAsset(): Boolean {
+ binding.etRemarks.text = null
+ binding.etUrl.text = null
+ return true
+ }
+
+ /**
+ * save asset config
+ */
+ private fun saveServer(): Boolean {
+ val assetItem: AssetUrlItem
+ val json = assetStorage?.decodeString(editAssetId)
+ var assetId = editAssetId
+ if (!json.isNullOrBlank()) {
+ assetItem = Gson().fromJson(json, AssetUrlItem::class.java)
+ } else {
+ assetId = Utils.getUuid()
+ assetItem = AssetUrlItem()
+ }
+
+ assetItem.remarks = binding.etRemarks.text.toString()
+ assetItem.url = binding.etUrl.text.toString()
+
+ if (TextUtils.isEmpty(assetItem.remarks)) {
+ toast(R.string.sub_setting_remarks)
+ return false
+ }
+ if (TextUtils.isEmpty(assetItem.url)) {
+ toast(R.string.title_url)
+ return false
+ }
+
+ assetStorage?.encode(assetId, Gson().toJson(assetItem))
+ toast(R.string.toast_success)
+ finish()
+ return true
+ }
+
+ /**
+ * save server config
+ */
+ private fun deleteServer(): Boolean {
+ if (editAssetId.isNotEmpty()) {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ MmkvManager.removeSubscription(editAssetId)
+ finish()
+ }
+ .show()
+ }
+ return true
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_server, menu)
+ del_config = menu.findItem(R.id.del_config)
+ save_config = menu.findItem(R.id.save_config)
+
+ if (editAssetId.isEmpty()) {
+ del_config?.isVisible = false
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.del_config -> {
+ deleteServer()
+ true
+ }
+ R.id.save_config -> {
+ saveServer()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
index 68bd9f42..9683c6d3 100644
--- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
@@ -12,6 +12,7 @@ object MmkvManager {
const val ID_SERVER_RAW = "SERVER_RAW"
const val ID_SERVER_AFF = "SERVER_AFF"
const val ID_SUB = "SUB"
+ const val ID_ASSET = "ASSET"
const val ID_SETTING = "SETTING"
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
diff --git a/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml
new file mode 100644
index 00000000..bc378009
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/menu_asset.xml b/V2rayNG/app/src/main/res/menu/menu_asset.xml
index 80a7611f..a242dc65 100644
--- a/V2rayNG/app/src/main/res/menu/menu_asset.xml
+++ b/V2rayNG/app/src/main/res/menu/menu_asset.xml
@@ -2,10 +2,20 @@