Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da347492d3 | ||
|
|
2ec5d8db3c | ||
|
|
fd9c5040bf | ||
|
|
aa328f0add | ||
|
|
9743d7b87b | ||
|
|
98fb0c433e | ||
|
|
7c9fcd9f43 | ||
|
|
54c76d9968 | ||
|
|
40b3f0fedc | ||
|
|
dcfcf83430 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -25,8 +25,8 @@ jobs:
|
|||||||
id: cache-libtun2socks-restore
|
id: cache-libtun2socks-restore
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/AndroidLibXrayLite/libs
|
path: ${{ github.workspace }}/libs
|
||||||
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/refs/heads/main') }}-${{ hashFiles('.git/modules/libancillary/refs/heads/shadowsocks-android') }}
|
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
||||||
|
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
uses: actions/cache/save@v4
|
uses: actions/cache/save@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/libs
|
path: ${{ github.workspace }}/libs
|
||||||
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/refs/heads/main') }}-${{ hashFiles('.git/modules/libancillary/refs/heads/shadowsocks-android') }}
|
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
||||||
|
|
||||||
- name: Copy libtun2socks
|
- name: Copy libtun2socks
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Submodule AndroidLibXrayLite updated: ec529ceeef...08c4a038f0
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 632
|
versionCode = 636
|
||||||
versionName = "1.9.36"
|
versionName = "1.9.39"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
@@ -157,9 +157,8 @@ dependencies {
|
|||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
|
|
||||||
// Reactive and Utility Libraries
|
// Reactive and Utility Libraries
|
||||||
implementation(libs.rxjava)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.rxandroid)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.rxpermissions)
|
|
||||||
|
|
||||||
// Language and Processing Libraries
|
// Language and Processing Libraries
|
||||||
implementation(libs.language.base)
|
implementation(libs.language.base)
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ import com.v2ray.ang.util.MessageUtil
|
|||||||
import com.v2ray.ang.util.PluginUtil
|
import com.v2ray.ang.util.PluginUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import libv2ray.V2RayPoint
|
import libv2ray.V2RayPoint
|
||||||
@@ -62,7 +63,7 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
private var lastQueryTime = 0L
|
private var lastQueryTime = 0L
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
private var mBuilder: NotificationCompat.Builder? = null
|
||||||
private var mDisposable: Disposable? = null
|
private var speedNotificationJob: Job? = null
|
||||||
private var mNotificationManager: NotificationManager? = null
|
private var mNotificationManager: NotificationManager? = null
|
||||||
|
|
||||||
fun startV2Ray(context: Context) {
|
fun startV2Ray(context: Context) {
|
||||||
@@ -365,8 +366,8 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mBuilder = null
|
mBuilder = null
|
||||||
mDisposable?.dispose()
|
speedNotificationJob?.cancel()
|
||||||
mDisposable = null
|
speedNotificationJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
||||||
@@ -393,7 +394,7 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startSpeedNotification() {
|
private fun startSpeedNotification() {
|
||||||
if (mDisposable == null &&
|
if (speedNotificationJob == null &&
|
||||||
v2rayPoint.isRunning &&
|
v2rayPoint.isRunning &&
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
|
||||||
) {
|
) {
|
||||||
@@ -401,8 +402,8 @@ object V2RayServiceManager {
|
|||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||||
outboundTags?.remove(TAG_DIRECT)
|
outboundTags?.remove(TAG_DIRECT)
|
||||||
|
|
||||||
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
.subscribe {
|
while (isActive) {
|
||||||
val queryTime = System.currentTimeMillis()
|
val queryTime = System.currentTimeMillis()
|
||||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
||||||
var proxyTotal = 0L
|
var proxyTotal = 0L
|
||||||
@@ -430,6 +431,8 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
lastZeroSpeed = zeroSpeed
|
lastZeroSpeed = zeroSpeed
|
||||||
lastQueryTime = queryTime
|
lastQueryTime = queryTime
|
||||||
|
delay(3000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,11 +448,10 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopSpeedNotification() {
|
private fun stopSpeedNotification() {
|
||||||
mDisposable?.let {
|
speedNotificationJob?.let {
|
||||||
it.dispose() //stop queryStats
|
it.cancel()
|
||||||
mDisposable = null
|
speedNotificationJob = null
|
||||||
updateNotification(currentConfig?.remarks, 0, 0)
|
updateNotification(currentConfig?.remarks, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityAboutBinding
|
import com.v2ray.ang.databinding.ActivityAboutBinding
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
import com.v2ray.ang.util.SpeedtestUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.ZipUtil
|
import com.v2ray.ang.util.ZipUtil
|
||||||
@@ -21,11 +22,24 @@ import java.io.File
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
class AboutActivity : BaseActivity() {
|
class AboutActivity : BaseActivity() {
|
||||||
|
|
||||||
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
|
||||||
private val extDir by lazy { File(Utils.backupPath(this)) }
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -33,6 +47,7 @@ class AboutActivity : BaseActivity() {
|
|||||||
title = getString(R.string.title_about)
|
title = getString(R.string.title_about)
|
||||||
|
|
||||||
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
||||||
|
|
||||||
binding.layoutBackup.setOnClickListener {
|
binding.layoutBackup.setOnClickListener {
|
||||||
val ret = backupConfiguration(extDir.absolutePath)
|
val ret = backupConfiguration(extDir.absolutePath)
|
||||||
if (ret.first) {
|
if (ret.first) {
|
||||||
@@ -50,7 +65,8 @@ class AboutActivity : BaseActivity() {
|
|||||||
Intent(Intent.ACTION_SEND).setType("application/zip")
|
Intent(Intent.ACTION_SEND).setType("application/zip")
|
||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.putExtra(
|
.putExtra(
|
||||||
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
|
Intent.EXTRA_STREAM,
|
||||||
|
FileProvider.getUriForFile(
|
||||||
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
||||||
)
|
)
|
||||||
), getString(R.string.title_configuration_share)
|
), getString(R.string.title_configuration_share)
|
||||||
@@ -62,22 +78,21 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutRestore.setOnClickListener {
|
binding.layoutRestore.setOnClickListener {
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permission =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
try {
|
try {
|
||||||
showFileChooser()
|
showFileChooser()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
} else
|
} else {
|
||||||
toast(R.string.toast_permission_denied)
|
requestPermissionLauncher.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +103,15 @@ class AboutActivity : BaseActivity() {
|
|||||||
binding.layoutFeedback.setOnClickListener {
|
binding.layoutFeedback.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutOssLicenses.setOnClickListener {
|
binding.layoutOssLicenses.setOnClickListener {
|
||||||
val webView = android.webkit.WebView(this);
|
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)
|
android.app.AlertDialog.Builder(this)
|
||||||
.setTitle("Open source licenses")
|
.setTitle("Open source licenses")
|
||||||
.setView(webView)
|
.setView(webView)
|
||||||
.setPositiveButton("OK", android.content.DialogInterface.OnClickListener { dialog, whichButton -> dialog.dismiss() }).show()
|
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutTgChannel.setOnClickListener {
|
binding.layoutTgChannel.setOnClickListener {
|
||||||
@@ -157,9 +174,9 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val chooseFile =
|
private val chooseFile =
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val uri = it.data?.data
|
val uri = result.data?.data
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
if (result.resultCode == RESULT_OK && uri != null) {
|
||||||
try {
|
try {
|
||||||
val targetFile =
|
val targetFile =
|
||||||
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
@@ -27,7 +28,6 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
import com.v2ray.ang.AppConfig.VPN
|
||||||
import com.v2ray.ang.R
|
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.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -81,6 +79,60 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
val mainViewModel: MainViewModel by viewModels()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -129,11 +181,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
migrateLegacy()
|
migrateLegacy()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
RxPermissions(this)
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
pendingAction = Action.POST_NOTIFICATIONS
|
||||||
.subscribe {
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
if (!it)
|
|
||||||
toast(R.string.toast_permission_denied_notification)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,9 +276,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
Utils.stopVService(this)
|
||||||
}
|
}
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
lifecycleScope.launch {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
delay(500)
|
||||||
.subscribe {
|
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,38 +511,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
* import config from qrcode
|
* import config from qrcode
|
||||||
*/
|
*/
|
||||||
private fun importQRcode(forConfig: Boolean): Boolean {
|
private fun importQRcode(forConfig: Boolean): Boolean {
|
||||||
// try {
|
val permission = Manifest.permission.CAMERA
|
||||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
if (forConfig) {
|
||||||
// .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))
|
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
else
|
} else {
|
||||||
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
}
|
||||||
// }
|
} else {
|
||||||
|
pendingAction = if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
|
}
|
||||||
return true
|
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
|
* import config from clipboard
|
||||||
*/
|
*/
|
||||||
@@ -614,10 +645,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
private fun importConfigViaSub(): Boolean {
|
private fun importConfigViaSub(): Boolean {
|
||||||
// val dialog = AlertDialog.Builder(this)
|
|
||||||
// .setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
|
||||||
// .setCancelable(false)
|
|
||||||
// .show()
|
|
||||||
binding.pbWaiting.show()
|
binding.pbWaiting.show()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@@ -630,7 +657,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
//dialog.dismiss()
|
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,17 +671,17 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
intent.type = "*/*"
|
intent.type = "*/*"
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
try {
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
} catch (ex: ActivityNotFoundException) {
|
} else {
|
||||||
toast(R.string.toast_require_file_manager)
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
val uri = it.data?.data
|
pendingAction = Action.READ_CONTENT_FROM_URI
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||||
readContentFromUri(uri)
|
} else {
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,10 +694,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
try {
|
try {
|
||||||
contentResolver.openInputStream(uri).use { input ->
|
contentResolver.openInputStream(uri).use { input ->
|
||||||
importCustomizeConfig(input?.bufferedReader()?.readText())
|
importCustomizeConfig(input?.bufferedReader()?.readText())
|
||||||
@@ -679,8 +703,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
} else
|
} else {
|
||||||
toast(R.string.toast_permission_denied)
|
requestPermissionLauncher.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AngApplication.Companion.application
|
import com.v2ray.ang.AngApplication.Companion.application
|
||||||
import com.v2ray.ang.AppConfig
|
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.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.delay
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.*
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -165,10 +166,13 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
Utils.stopVService(mActivity)
|
Utils.stopVService(mActivity)
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
mActivity.lifecycleScope.launch {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
try {
|
||||||
.subscribe {
|
delay(500)
|
||||||
V2RayServiceManager.startV2Ray(mActivity)
|
V2RayServiceManager.startV2Ray(mActivity)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import com.v2ray.ang.extension.v2RayApplication
|
|||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
import com.v2ray.ang.util.Utils
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
|
||||||
class PerAppProxyActivity : BaseActivity() {
|
class PerAppProxyActivity : BaseActivity() {
|
||||||
@@ -43,94 +42,40 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
|
|
||||||
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
lifecycleScope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
try {
|
||||||
.map {
|
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) {
|
if (blacklist != null) {
|
||||||
it.forEach { one ->
|
appsList.forEach { app ->
|
||||||
if (blacklist.contains(one.packageName)) {
|
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
|
||||||
one.isSelected = 1
|
|
||||||
} else {
|
|
||||||
one.isSelected = 0
|
|
||||||
}
|
}
|
||||||
}
|
appsList.sortedWith(Comparator { p1, p2 ->
|
||||||
val comparator = Comparator<AppInfo> { p1, p2 ->
|
|
||||||
when {
|
when {
|
||||||
p1.isSelected > p2.isSelected -> -1
|
p1.isSelected > p2.isSelected -> -1
|
||||||
p1.isSelected == p2.isSelected -> 0
|
p1.isSelected == p2.isSelected -> 0
|
||||||
else -> 1
|
else -> 1
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
it.sortedWith(comparator)
|
|
||||||
} else {
|
} else {
|
||||||
val comparator = object : Comparator<AppInfo> {
|
|
||||||
val collator = Collator.getInstance()
|
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> {
|
appsAll = apps
|
||||||
// val collator = Collator.getInstance()
|
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
|
||||||
// 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)
|
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.pbWaiting.visibility = View.GONE
|
binding.pbWaiting.visibility = View.GONE
|
||||||
}
|
} catch (e: Exception) {
|
||||||
/***
|
binding.pbWaiting.visibility = View.GONE
|
||||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
Log.e(ANG_PACKAGE, "Error loading apps", e)
|
||||||
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 ->
|
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
||||||
}
|
}
|
||||||
@@ -140,36 +85,6 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
||||||
}
|
}
|
||||||
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
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() {
|
override fun onPause() {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
||||||
@@ -40,6 +39,16 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
resources.getStringArray(R.array.preset_rulesets)
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -102,11 +111,9 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do noting
|
//do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
@@ -142,18 +149,10 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.import_rulesets_from_qrcode -> {
|
R.id.import_rulesets_from_qrcode -> {
|
||||||
RxPermissions(this)
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe {
|
|
||||||
if (it)
|
|
||||||
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
R.id.export_rulesets_to_clipboard -> {
|
R.id.export_rulesets_to_clipboard -> {
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
if (rulesetList.isNullOrEmpty()) {
|
if (rulesetList.isNullOrEmpty()) {
|
||||||
@@ -201,5 +200,4 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,23 @@ import android.Manifest
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
|
||||||
class ScScannerActivity : BaseActivity() {
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_none)
|
setContentView(R.layout.activity_none)
|
||||||
@@ -18,19 +28,10 @@ class ScScannerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun importQRcode(): Boolean {
|
fun importQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe { granted ->
|
|
||||||
if (granted) {
|
|
||||||
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
||||||
@@ -46,5 +47,4 @@ class ScScannerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,15 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
@@ -21,6 +23,37 @@ import io.github.g00fy2.quickie.config.ScannerConfig
|
|||||||
class ScannerActivity : BaseActivity() {
|
class ScannerActivity : BaseActivity() {
|
||||||
|
|
||||||
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
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?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -72,14 +105,11 @@ class ScannerActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
.subscribe { granted ->
|
|
||||||
if (granted) {
|
|
||||||
showFileChooser()
|
showFileChooser()
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_permission_denied)
|
requestPermissionLauncher.launch(permission)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -100,26 +130,4 @@ class ScannerActivity : BaseActivity() {
|
|||||||
toast(R.string.toast_require_file_manager)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -50,6 +49,38 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -86,27 +117,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
requestStoragePermissionLauncher.launch(permission)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
@@ -158,14 +169,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun importAssetFromQRcode(): Boolean {
|
private fun importAssetFromQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe {
|
|
||||||
if (it)
|
|
||||||
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import android.content.Context
|
|||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
object AppManagerUtil {
|
object AppManagerUtil {
|
||||||
private fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
suspend fun loadNetworkAppList(context: Context): ArrayList<AppInfo> =
|
||||||
val packageManager = ctx.packageManager
|
withContext(Dispatchers.IO) {
|
||||||
|
val packageManager = context.packageManager
|
||||||
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||||
val apps = ArrayList<AppInfo>()
|
val apps = ArrayList<AppInfo>()
|
||||||
|
|
||||||
@@ -23,17 +25,6 @@ object AppManagerUtil {
|
|||||||
apps.add(appInfo)
|
apps.add(appInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps
|
return@withContext 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
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.8.1"
|
agp = "8.8.2"
|
||||||
desugar_jdk_libs = "2.1.4"
|
desugar_jdk_libs = "2.1.5"
|
||||||
gradleLicensePlugin = "0.9.8"
|
gradleLicensePlugin = "0.9.8"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
@@ -9,14 +9,13 @@ junitVersion = "1.2.1"
|
|||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
activity = "1.10.0"
|
activity = "1.10.1"
|
||||||
constraintlayout = "2.2.0"
|
constraintlayout = "2.2.1"
|
||||||
mmkvStatic = "1.3.11"
|
mmkvStatic = "1.3.12"
|
||||||
gson = "2.11.0"
|
gson = "2.11.0"
|
||||||
quickieFoss = "1.13.1"
|
quickieFoss = "1.13.1"
|
||||||
rxjava = "3.1.10"
|
kotlinx-coroutines-android = "1.10.1"
|
||||||
rxandroid = "3.0.2"
|
kotlinx-coroutines-core = "1.10.1"
|
||||||
rxpermissions = "0.12"
|
|
||||||
swiperefreshlayout = "1.1.0"
|
swiperefreshlayout = "1.1.0"
|
||||||
toastcompat = "1.1.0"
|
toastcompat = "1.1.0"
|
||||||
editorkit = "2.9.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" }
|
mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" }
|
||||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||||
quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
|
quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
|
||||||
rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjava" }
|
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "kotlinx-coroutines-android" }
|
||||||
rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid" }
|
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "kotlinx-coroutines-core" }
|
||||||
rxpermissions = { module = "com.github.tbruyelle:rxpermissions", version.ref = "rxpermissions" }
|
|
||||||
toastcompat = { module = "me.drakeet.support:toastcompat", version.ref = "toastcompat" }
|
toastcompat = { module = "me.drakeet.support:toastcompat", version.ref = "toastcompat" }
|
||||||
editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
|
editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
|
||||||
language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }
|
language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }
|
||||||
|
|||||||
2
badvpn
2
badvpn
Submodule badvpn updated: 3cb49ab810...e68480088a
2
hysteria
2
hysteria
Submodule hysteria updated: 8c05217590...401ed5245d
Reference in New Issue
Block a user