diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index de4dad9bc..9efa35471 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 @@ -104,13 +118,27 @@ class UserAssetActivity : BaseActivity() { } private val chooseFile = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it -> val uri = it.data?.data if (it.resultCode == RESULT_OK && uri != null) { + val assetId = Utils.getUuid() try { + val assetItem = AssetUrlItem( + getCursorName(uri) ?: uri.toString(), + "file" + ) + + // check remarks unique + val assetList = MmkvManager.decodeAssetUrls() + if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) { + toast(R.string.msg_remark_is_duplicate) + return@registerForActivityResult + } + assetStorage?.encode(assetId, Gson().toJson(assetItem)) copyFile(uri) } catch (e: Exception) { toast(R.string.toast_asset_copy_failed) + MmkvManager.removeAssetUrl(assetId) } } } @@ -143,31 +171,33 @@ class UserAssetActivity : BaseActivity() { val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt()) toast(R.string.msg_downloading_content) - geofiles.forEach { + var assets = MmkvManager.decodeAssetUrls() + assets = addBuiltInGeoItems(assets) + + assets.forEach { //toast(getString(R.string.msg_downloading_content) + it) lifecycleScope.launch(Dispatchers.IO) { - val result = downloadGeo(it, 60000, httpPort) + val result = downloadGeo(it.second, 60000, httpPort) launch(Dispatchers.Main) { if (result) { - toast(getString(R.string.toast_success) + " " + it) + toast(getString(R.string.toast_success) + " " + it.second.remarks) binding.recyclerView.adapter?.notifyDataSetChanged() } else { - toast(getString(R.string.toast_failure) + " " + it) + toast(getString(R.string.toast_failure) + " " + it.second.remarks) } } } } } - private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean { - val url = AppConfig.geoUrl + name - val targetTemp = File(extDir, name + "_temp") - val target = File(extDir, name) + private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean { + val targetTemp = File(extDir, item.remarks + "_temp") + val target = File(extDir, item.remarks) var conn: HttpURLConnection? = null //Log.d(AppConfig.ANG_PACKAGE, url) try { - conn = URL(url).openConnection( + conn = URL(item.url).openConnection( Proxy( Proxy.Type.HTTP, InetSocketAddress("127.0.0.1", httpPort) @@ -192,33 +222,74 @@ class UserAssetActivity : BaseActivity() { conn?.disconnect() } } + private fun addBuiltInGeoItems(assets: List>): List> { + val list = mutableListOf>() + builtInGeoFiles.forEach { + list.add(Utils.getUuid() to AssetUrlItem( + it, + AppConfig.geoUrl + it + ) + ) + } + + return list + assets + } inner class UserAssetAdapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { - return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + return UserAssetViewHolder( + ItemRecyclerUserAssetBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false) + ) } @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) { - val file = extDir.listFiles()?.getOrNull(position) ?: return - holder.itemUserAssetBinding.assetName.text = file.name - val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM) - holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}" - if (file.name in geofiles) { + var assets = MmkvManager.decodeAssetUrls(); + assets = addBuiltInGeoItems(assets); + val item = assets.getOrNull(position) ?: return +// file with name == item.second.remarks + val file = extDir.listFiles()?.find { it.name == item.second.remarks } + + holder.itemUserAssetBinding.assetName.text = item.second.remarks + + if (file != null) { + val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM) + holder.itemUserAssetBinding.assetProperties.text = + "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}" + } else { + holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found) + } + + if (item.second.remarks in builtInGeoFiles) { + holder.itemUserAssetBinding.layoutEdit.visibility = GONE holder.itemUserAssetBinding.layoutRemove.visibility = GONE } else { + holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE } holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE } + + holder.itemUserAssetBinding.layoutEdit.setOnClickListener { + val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java) + intent.putExtra("assetId", item.first) + startActivity(intent) + } holder.itemUserAssetBinding.layoutRemove.setOnClickListener { - file.delete() + file?.delete() + MmkvManager.removeAssetUrl(item.first) binding.recyclerView.adapter?.notifyItemRemoved(position) } } override fun getItemCount(): Int { - return extDir.listFiles()?.size ?: 0 + var assets = MmkvManager.decodeAssetUrls(); + assets = addBuiltInGeoItems(assets); + return assets.size } } - class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root) + class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : + RecyclerView.ViewHolder(itemUserAssetBinding.root) } 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 000000000..c032bef3f --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt @@ -0,0 +1,145 @@ +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 +import java.io.File + +class UserAssetUrlActivity : BaseActivity() { + private lateinit var binding: ActivityUserAssetUrlBinding + + var del_config: MenuItem? = null + var save_config: MenuItem? = null + + val extDir by lazy { File(Utils.userAssetPath(this)) } + private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) } + private val editAssetId by lazy { intent.getStringExtra("assetId").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) + + // remove file associated with the asset + val file = extDir.resolve(assetItem.remarks) + if (file.exists()) { + file.delete() + } + } else { + assetId = Utils.getUuid() + assetItem = AssetUrlItem() + } + + assetItem.remarks = binding.etRemarks.text.toString() + assetItem.url = binding.etUrl.text.toString() + + // check remarks unique + val assetList = MmkvManager.decodeAssetUrls() + if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) { + toast(R.string.msg_remark_is_duplicate) + return false + } + + + 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.removeAssetUrl(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 68bd9f42b..f00ab0e84 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 @@ -2,6 +2,7 @@ package com.v2ray.ang.util import com.google.gson.Gson import com.tencent.mmkv.MMKV +import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.ServerAffiliationInfo import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.SubscriptionItem @@ -12,6 +13,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" @@ -20,6 +22,7 @@ object MmkvManager { private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) } private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) } + private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) } fun decodeServerList(): MutableList { val json = mainStorage?.decodeString(KEY_ANG_CONFIGS) @@ -142,6 +145,22 @@ object MmkvManager { removeServerViaSubid(subid) } + fun decodeAssetUrls(): List> { + val assetUrlItems = mutableListOf>() + assetStorage?.allKeys()?.forEach { key -> + val json = assetStorage?.decodeString(key) + if (!json.isNullOrBlank()) { + assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java))) + } + } + assetUrlItems.sortedBy { (_, value) -> value.addedTime } + return assetUrlItems + } + + fun removeAssetUrl(assetid: String) { + assetStorage?.remove(assetid) + } + fun removeAllServer() { mainStorage?.clearAll() serverStorage?.clearAll() 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 000000000..bc3780093 --- /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/layout/item_recycler_user_asset.xml b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml index 7ae979126..7677d3ff1 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml @@ -19,27 +19,61 @@ android:background="@color/colorPrimary" android:foreground="?attr/selectableItemBackground" android:padding="@dimen/nav_header_vertical_spacing"> - + + - - + + + + + + + + + + + + android:title="@string/menu_item_add_asset" + app:showAsAction="ifRoom" > + + + + + المستخدم (اختياري) التشفير التدفق - مفتاح عام - ShortId - SpiderX Reserved (اختياري) العنوان المحلي IPv4(اختياري) Mtu(optional, default 1420) @@ -82,6 +79,9 @@ إضافة ملفات تحميل الملفات + أضف عنوان URL للأصل + لم يتم العثور على الملف + الملاحظات موجودة بالفعل جار التحميل بحث تحديد الكل @@ -221,4 +221,6 @@ VPN الوكيل فقط + يضيف + إضافة رابط diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml index 3d7332e16..6ba420f2f 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -82,6 +82,9 @@ دانلود فایل‌ها + URL را اضافه کنید + فایل پیدا نشد + نام قبلاً وجود دارد بارگذاری جستجو انتخاب همه @@ -253,5 +256,7 @@ VPN فقط پروکسی + افزودن + افزودن لینک diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index b6d9ee5fb..354cac8f4 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -49,8 +49,6 @@ Запрос узла (WS/H2) / Шифрование QUIC Путь (WS/H2) / Ключ QUIC / Сид KCP / Сервис gRPC TLS - Fingerprint - Alpn Разрешать небезопасные SNI Адрес @@ -84,6 +82,9 @@ Загрузить файлы + Добавить URL ресурса + Файл не найден + Замечания уже есть Загрузка… Поиск Выбрать все @@ -256,5 +257,7 @@ VPN Только прокси + Добавлять + Добавить ссылку diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml index 4a186f4be..0b4d8bcdd 100644 --- a/V2rayNG/app/src/main/res/values-vi/strings.xml +++ b/V2rayNG/app/src/main/res/values-vi/strings.xml @@ -82,6 +82,9 @@ Tải xuống tệp tin + Thêm URL nội dung + Không tìm thấy tập tin + Nhận xét đã tồn tại Đang tải... Tìm kiếm Chọn tất cả @@ -255,5 +258,7 @@ Chế độ VPN Chế độ Proxy + Thêm vào + Thêm liên kết diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml index 956c6dba9..00d0f539f 100644 --- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -83,6 +83,9 @@ + 添加资产网址 + 文件未找到 + 备注已经存在 正在加载 搜索 全选 @@ -254,5 +257,7 @@ 订阅导入成功 导入订阅失败 + 添加 + 添加链接 diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml index 1efdf66b3..c81b140e3 100644 --- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml @@ -82,6 +82,9 @@ 下載檔案 + 新增資產網址 + 文件未找到 + 備註已經存在 載入 搜尋 全選 @@ -253,5 +256,7 @@ 訂閱導入成功 導入訂閱失敗 + 添加 + 添加連結 diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index eccbbb56b..a518741ca 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -85,8 +85,15 @@ Config malformed Host(SNI)(Optional) File copy failed, please use File Manager + Add asset Add files + Add URL + URL Download files + Add asset URL + File not found + The remarks already exists + Loading