Skip to content

Commit

Permalink
Merge pull request #2834 from hamidrezahy/thirdparty_asset_update
Browse files Browse the repository at this point in the history
Automatic update of third party dat files - fix #2821
  • Loading branch information
2dust authored Feb 13, 2024
2 parents 5589d10 + a33a698 commit b78b370
Show file tree
Hide file tree
Showing 16 changed files with 446 additions and 37 deletions.
3 changes: 3 additions & 0 deletions V2rayNG/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
<activity
android:exported="false"
android:name=".ui.UserAssetActivity" />
<activity
android:exported="false"
android:name=".ui.UserAssetUrlActivity" />

<activity
android:exported="false"
Expand Down
8 changes: 8 additions & 0 deletions V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AssetUrlItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.v2ray.ang.dto

data class AssetUrlItem(
var remarks: String = "",
var url: String = "",
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ data class SubscriptionItem(
var lastUpdated: Long = -1,
var autoUpdate: Boolean = false,
val updateInterval: Int? = null,
)
)

111 changes: 91 additions & 20 deletions V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
Expand All @@ -39,9 +41,11 @@ import java.util.*
class UserAssetActivity : BaseActivity() {
private lateinit var binding: ActivitySubSettingBinding
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }

val extDir by lazy { File(Utils.userAssetPath(this)) }
val geofiles = arrayOf("geosite.dat", "geoip.dat")
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -55,6 +59,11 @@ class UserAssetActivity : BaseActivity() {
binding.recyclerView.adapter = UserAssetAdapter()
}

override fun onResume() {
super.onResume()
binding.recyclerView.adapter?.notifyDataSetChanged()
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_asset, menu)
return super.onCreateOptionsMenu(menu)
Expand All @@ -66,6 +75,11 @@ class UserAssetActivity : BaseActivity() {
true
}

R.id.add_url -> {
val intent = Intent(this, UserAssetUrlActivity::class.java)
startActivity(intent)
true
}
R.id.download_file -> {
downloadGeoFiles()
true
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -192,33 +222,74 @@ class UserAssetActivity : BaseActivity() {
conn?.disconnect()
}
}
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
val list = mutableListOf<Pair<String, AssetUrlItem>>()
builtInGeoFiles.forEach {
list.add(Utils.getUuid() to AssetUrlItem(
it,
AppConfig.geoUrl + it
)
)
}

return list + assets
}

inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
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)
}
145 changes: 145 additions & 0 deletions V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit b78b370

Please sign in to comment.