This commit is contained in:
hhhkkmk 2025-03-04 10:05:52 +08:00 committed by GitHub
parent 98fb0c433e
commit 9743d7b87b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 318 additions and 355 deletions

View file

@ -157,9 +157,8 @@ dependencies {
implementation(libs.gson)
// Reactive and Utility Libraries
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
// Language and Processing Libraries
implementation(libs.language.base)

View file

@ -31,10 +31,11 @@ import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.Utils
import go.Seq
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import libv2ray.Libv2ray
import libv2ray.V2RayPoint
@ -62,7 +63,7 @@ object V2RayServiceManager {
private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null
private var mDisposable: Disposable? = null
private var speedNotificationJob: Job? = null
private var mNotificationManager: NotificationManager? = null
fun startV2Ray(context: Context) {
@ -365,8 +366,8 @@ object V2RayServiceManager {
}
mBuilder = null
mDisposable?.dispose()
mDisposable = null
speedNotificationJob?.cancel()
speedNotificationJob = null
}
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
@ -393,7 +394,7 @@ object V2RayServiceManager {
}
private fun startSpeedNotification() {
if (mDisposable == null &&
if (speedNotificationJob == null &&
v2rayPoint.isRunning &&
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
) {
@ -401,8 +402,8 @@ object V2RayServiceManager {
val outboundTags = currentConfig?.getAllOutboundTags()
outboundTags?.remove(TAG_DIRECT)
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
.subscribe {
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
val queryTime = System.currentTimeMillis()
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
var proxyTotal = 0L
@ -430,7 +431,9 @@ object V2RayServiceManager {
}
lastZeroSpeed = zeroSpeed
lastQueryTime = queryTime
delay(3000)
}
}
}
}
@ -445,11 +448,10 @@ object V2RayServiceManager {
}
private fun stopSpeedNotification() {
mDisposable?.let {
it.dispose() //stop queryStats
mDisposable = null
speedNotificationJob?.let {
it.cancel()
speedNotificationJob = null
updateNotification(currentConfig?.remarks, 0, 0)
}
}
}

View file

@ -5,15 +5,16 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tbruyelle.rxpermissions3.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil
@ -21,11 +22,24 @@ import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
class AboutActivity : BaseActivity() {
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
private val extDir by lazy { File(Utils.backupPath(this)) }
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
}
} else {
toast(R.string.toast_permission_denied)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@ -33,6 +47,7 @@ class AboutActivity : BaseActivity() {
title = getString(R.string.title_about)
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
binding.layoutBackup.setOnClickListener {
val ret = backupConfiguration(extDir.absolutePath)
if (ret.first) {
@ -50,7 +65,8 @@ class AboutActivity : BaseActivity() {
Intent(Intent.ACTION_SEND).setType("application/zip")
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
Intent.EXTRA_STREAM,
FileProvider.getUriForFile(
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
)
), getString(R.string.title_configuration_share)
@ -62,23 +78,22 @@ class AboutActivity : BaseActivity() {
}
binding.layoutRestore.setOnClickListener {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
}
} else
toast(R.string.toast_permission_denied)
val permission =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
}
} else {
requestPermissionLauncher.launch(permission)
}
}
binding.layoutSoureCcode.setOnClickListener {
@ -88,13 +103,15 @@ class AboutActivity : BaseActivity() {
binding.layoutFeedback.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGIssues)
}
binding.layoutOssLicenses.setOnClickListener{
binding.layoutOssLicenses.setOnClickListener {
val webView = android.webkit.WebView(this);
webView.loadUrl("file:///android_asset/open_source_licenses.html");
webView.loadUrl("file:///android_asset/open_source_licenses.html")
android.app.AlertDialog.Builder(this)
.setTitle("Open source licenses")
.setView(webView)
.setPositiveButton("OK", android.content.DialogInterface.OnClickListener { dialog, whichButton -> dialog.dismiss() }).show()
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.show()
}
binding.layoutTgChannel.setOnClickListener {
@ -157,9 +174,9 @@ class AboutActivity : BaseActivity() {
}
private val chooseFile =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
if (result.resultCode == RESULT_OK && uri != null) {
try {
val targetFile =
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
@ -180,4 +197,7 @@ class AboutActivity : BaseActivity() {
}
}
private fun toast(messageResId: Int) {
Toast.makeText(this, getString(messageResId), Toast.LENGTH_SHORT).show()
}
}

View file

@ -3,6 +3,7 @@ package com.v2ray.ang.ui
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.net.Uri
import android.net.VpnService
@ -27,7 +28,6 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.navigation.NavigationView
import com.google.android.material.tabs.TabLayout
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R
@ -41,8 +41,6 @@ import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -81,6 +79,60 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
private var mItemTouchHelper: ItemTouchHelper? = null
val mainViewModel: MainViewModel by viewModels()
// register activity result for requesting permission
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
when (pendingAction) {
Action.IMPORT_QR_CODE_CONFIG ->
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
Action.IMPORT_QR_CODE_URL ->
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
Action.READ_CONTENT_FROM_URI ->
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
addCategory(Intent.CATEGORY_OPENABLE)
}, getString(R.string.title_file_chooser)))
Action.POST_NOTIFICATIONS -> {}
else -> {}
}
} else {
toast(R.string.toast_permission_denied)
}
pendingAction = Action.NONE
}
private var pendingAction: Action = Action.NONE
enum class Action {
NONE,
IMPORT_QR_CODE_CONFIG,
IMPORT_QR_CODE_URL,
READ_CONTENT_FROM_URI,
POST_NOTIFICATIONS
}
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
readContentFromUri(uri)
}
}
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@ -129,12 +181,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
migrateLegacy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this)
.request(Manifest.permission.POST_NOTIFICATIONS)
.subscribe {
if (!it)
toast(R.string.toast_permission_denied_notification)
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
pendingAction = Action.POST_NOTIFICATIONS
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
@ -226,11 +276,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
if (mainViewModel.isRunning.value == true) {
Utils.stopVService(this)
}
Observable.timer(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
startV2Ray()
}
lifecycleScope.launch {
delay(500)
startV2Ray()
}
}
public override fun onResume() {
@ -462,38 +511,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* import config from qrcode
*/
private fun importQRcode(forConfig: Boolean): Boolean {
// try {
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
// .addCategory(Intent.CATEGORY_DEFAULT)
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
// } catch (e: Exception) {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
if (forConfig)
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
else
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
val permission = Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
if (forConfig) {
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
} else {
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
}
// }
} else {
pendingAction = if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL
requestPermissionLauncher.launch(permission)
}
return true
}
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
}
}
/**
* import config from clipboard
*/
@ -614,10 +645,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* import config from sub
*/
private fun importConfigViaSub(): Boolean {
// val dialog = AlertDialog.Builder(this)
// .setView(LayoutProgressBinding.inflate(layoutInflater).root)
// .setCancelable(false)
// .show()
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
@ -630,7 +657,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} else {
toast(R.string.toast_failure)
}
//dialog.dismiss()
binding.pbWaiting.hide()
}
}
@ -645,17 +671,17 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try {
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
}
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
readContentFromUri(uri)
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
pendingAction = Action.READ_CONTENT_FROM_URI
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} else {
requestPermissionLauncher.launch(permission)
}
}
@ -668,20 +694,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
try {
contentResolver.openInputStream(uri).use { input ->
importCustomizeConfig(input?.bufferedReader()?.readText())
}
} catch (e: Exception) {
e.printStackTrace()
}
} else
toast(R.string.toast_permission_denied)
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
try {
contentResolver.openInputStream(uri).use { input ->
importCustomizeConfig(input?.bufferedReader()?.readText())
}
} catch (e: Exception) {
e.printStackTrace()
}
} else {
requestPermissionLauncher.launch(permission)
}
}
/**
@ -763,4 +787,4 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.drawerLayout.closeDrawer(GravityCompat.START)
return true
}
}
}

View file

@ -8,6 +8,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AngApplication.Companion.application
import com.v2ray.ang.AppConfig
@ -23,9 +24,9 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
companion object {
@ -165,11 +166,14 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) {
Utils.stopVService(mActivity)
Observable.timer(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
mActivity.lifecycleScope.launch {
try {
delay(500)
V2RayServiceManager.startV2Ray(mActivity)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
@ -246,4 +250,4 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
override fun onItemDismiss(position: Int) {
}
}
}

View file

@ -20,10 +20,9 @@ import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.Collator
class PerAppProxyActivity : BaseActivity() {
@ -43,93 +42,39 @@ class PerAppProxyActivity : BaseActivity() {
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
AppManagerUtil.rxLoadNetworkAppList(this)
.subscribeOn(Schedulers.io())
.map {
if (blacklist != null) {
it.forEach { one ->
if (blacklist.contains(one.packageName)) {
one.isSelected = 1
} else {
one.isSelected = 0
lifecycleScope.launch {
try {
binding.pbWaiting.visibility = View.VISIBLE
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val apps = withContext(Dispatchers.IO) {
val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
if (blacklist != null) {
appsList.forEach { app ->
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
}
}
val comparator = Comparator<AppInfo> { p1, p2 ->
when {
p1.isSelected > p2.isSelected -> -1
p1.isSelected == p2.isSelected -> 0
else -> 1
}
}
it.sortedWith(comparator)
} else {
val comparator = object : Comparator<AppInfo> {
appsList.sortedWith(Comparator { p1, p2 ->
when {
p1.isSelected > p2.isSelected -> -1
p1.isSelected == p2.isSelected -> 0
else -> 1
}
})
} else {
val collator = Collator.getInstance()
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
appsList.sortedWith(compareBy(collator) { it.appName })
}
it.sortedWith(comparator)
}
}
// .map {
// val comparator = object : Comparator<AppInfo> {
// val collator = Collator.getInstance()
// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
// }
// it.sortedWith(comparator)
// }
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
appsAll = it
adapter = PerAppProxyAdapter(this, it, blacklist)
appsAll = apps
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
binding.recyclerView.adapter = adapter
binding.pbWaiting.visibility = View.GONE
} catch (e: Exception) {
binding.pbWaiting.visibility = View.GONE
Log.e(ANG_PACKAGE, "Error loading apps", e)
}
/***
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var dst = 0
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
dst += dy
if (dst > threshold) {
header_view.hide()
dst = 0
} else if (dst < -20) {
header_view.show()
dst = 0
}
}
var hiding = false
fun View.hide() {
val target = -height.toFloat()
if (hiding || translationY == target) return
animate()
.translationY(target)
.setInterpolator(AccelerateInterpolator(2F))
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
hiding = false
}
})
hiding = true
}
var showing = false
fun View.show() {
val target = 0f
if (showing || translationY == target) return
animate()
.translationY(target)
.setInterpolator(DecelerateInterpolator(2F))
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
showing = false
}
})
showing = true
}
})
***/
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
@ -140,36 +85,6 @@ class PerAppProxyActivity : BaseActivity() {
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
}
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
/***
et_search.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
//hide
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
val key = v.text.toString().toUpperCase()
val apps = ArrayList<AppInfo>()
if (TextUtils.isEmpty(key)) {
appsAll?.forEach {
apps.add(it)
}
} else {
appsAll?.forEach {
if (it.appName.toUpperCase().indexOf(key) >= 0) {
apps.add(it)
}
}
}
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
recycler_view.adapter = adapter
adapter?.notifyDataSetChanged()
true
} else {
false
}
}
***/
}
override fun onPause() {

View file

@ -12,7 +12,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
@ -39,6 +38,16 @@ class RoutingSettingActivity : BaseActivity() {
private val preset_rulesets: Array<out String> by lazy {
resources.getStringArray(R.array.preset_rulesets)
}
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -102,11 +111,9 @@ class RoutingSettingActivity : BaseActivity() {
e.printStackTrace()
}
}.show()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting
//do nothing
}
.show()
true
@ -142,18 +149,10 @@ class RoutingSettingActivity : BaseActivity() {
}
R.id.import_rulesets_from_qrcode -> {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
}
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
true
}
R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
@ -201,5 +200,4 @@ class RoutingSettingActivity : BaseActivity() {
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
adapter.notifyDataSetChanged()
}
}
}

View file

@ -4,13 +4,23 @@ import android.Manifest
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager
class ScScannerActivity : BaseActivity() {
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_none)
@ -18,19 +28,10 @@ class ScScannerActivity : BaseActivity() {
}
fun importQRcode(): Boolean {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe { granted ->
if (granted) {
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
}
}
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
return true
}
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
@ -46,5 +47,4 @@ class ScScannerActivity : BaseActivity() {
}
finish()
}
}
}

View file

@ -2,13 +2,15 @@ package com.v2ray.ang.ui
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
@ -21,6 +23,37 @@ import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : BaseActivity() {
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_decoding_failed)
}
}
}
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
showFileChooser()
} else {
toast(R.string.toast_permission_denied)
}
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -72,15 +105,12 @@ class ScannerActivity : BaseActivity() {
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(permission)
.subscribe { granted ->
if (granted) {
showFileChooser()
} else {
toast(R.string.toast_permission_denied)
}
}
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
showFileChooser()
} else {
requestPermissionLauncher.launch(permission)
}
true
}
@ -100,26 +130,4 @@ class ScannerActivity : BaseActivity() {
toast(R.string.toast_require_file_manager)
}
}
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_decoding_failed)
}
}
}
}
}

View file

@ -19,7 +19,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.R
@ -50,6 +49,38 @@ class UserAssetActivity : BaseActivity() {
val extDir by lazy { File(Utils.userAssetPath(this)) }
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
private val requestStoragePermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try {
chooseFile.launch(
Intent.createChooser(
intent,
getString(R.string.title_file_chooser)
)
)
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
} else {
toast(R.string.toast_permission_denied)
}
}
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -86,27 +117,7 @@ class UserAssetActivity : BaseActivity() {
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try {
chooseFile.launch(
Intent.createChooser(
intent,
getString(R.string.title_file_chooser)
)
)
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
} else
toast(R.string.toast_permission_denied)
}
requestStoragePermissionLauncher.launch(permission)
}
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -158,14 +169,7 @@ class UserAssetActivity : BaseActivity() {
}
private fun importAssetFromQRcode(): Boolean {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
}
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
return true
}
@ -349,4 +353,4 @@ class UserAssetActivity : BaseActivity() {
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
RecyclerView.ViewHolder(itemUserAssetBinding.root)
}
}

View file

@ -4,36 +4,27 @@ import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.v2ray.ang.dto.AppInfo
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
object AppManagerUtil {
private fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
val packageManager = ctx.packageManager
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
val apps = ArrayList<AppInfo>()
suspend fun loadNetworkAppList(context: Context): ArrayList<AppInfo> =
withContext(Dispatchers.IO) {
val packageManager = context.packageManager
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
val apps = ArrayList<AppInfo>()
for (pkg in packages) {
val applicationInfo = pkg.applicationInfo ?: continue
for (pkg in packages) {
val applicationInfo = pkg.applicationInfo ?: continue
val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo)
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo)
}
return@withContext apps
}
return apps
}
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
Observable.unsafeCreate {
it.onNext(loadNetworkAppList(ctx))
}
// val PackageInfo.hasInternetPermission: Boolean
// get() {
// val permissions = requestedPermissions
// return permissions?.any { it == Manifest.permission.INTERNET } ?: false
// }
}
}

View file

@ -11,12 +11,11 @@ appcompat = "1.7.0"
material = "1.12.0"
activity = "1.10.0"
constraintlayout = "2.2.0"
mmkvStatic = "1.3.11"
mmkvStatic = "1.3.12"
gson = "2.11.0"
quickieFoss = "1.13.1"
rxjava = "3.1.10"
rxandroid = "3.0.2"
rxpermissions = "0.12"
kotlinx-coroutines-android = "1.10.1"
kotlinx-coroutines-core = "1.10.1"
swiperefreshlayout = "1.1.0"
toastcompat = "1.1.0"
editorkit = "2.9.0"
@ -43,9 +42,8 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjava" }
rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid" }
rxpermissions = { module = "com.github.tbruyelle:rxpermissions", version.ref = "rxpermissions" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "kotlinx-coroutines-android" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "kotlinx-coroutines-core" }
toastcompat = { module = "me.drakeet.support:toastcompat", version.ref = "toastcompat" }
editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }