Compare commits

...

21 Commits

Author SHA1 Message Date
2dust
da347492d3 up 1.9.39 2025-03-07 14:25:30 +08:00
2dust
2ec5d8db3c Update AndroidLibXrayLite 2025-03-07 14:24:07 +08:00
2dust
fd9c5040bf up 1.9.38 2025-03-04 10:19:39 +08:00
2dust
aa328f0add Update AndroidLibXrayLite 2025-03-04 10:12:46 +08:00
hhhkkmk
9743d7b87b update (#4365) 2025-03-04 10:05:52 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
98fb0c433e fixup! Fix badvpn (#4302) (#4352)
thx
2025-02-25 09:31:11 +08:00
2dust
7c9fcd9f43 Update build.gradle.kts 2025-02-22 17:08:23 +08:00
2dust
54c76d9968 git submodule update --remote 2025-02-22 16:51:24 +08:00
2dust
40b3f0fedc up 1.9.37 2025-02-22 15:17:29 +08:00
2dust
dcfcf83430 Update AndroidLibXrayLite 2025-02-22 14:52:05 +08:00
2dust
e46b354643 up 1.9.36 2025-02-19 18:18:43 +08:00
2dust
f497e4e301 Update AndroidLibXrayLite 2025-02-19 18:13:34 +08:00
2dust
b65e4b3819 Bug fix
https://github.com/2dust/v2rayNG/issues/4329
2025-02-11 10:46:01 +08:00
Hossin Asaadi
d166b036fc Update ServerActivity.kt (#4326) 2025-02-11 10:31:22 +08:00
2dust
ddf5f22037 up 1.9.35 2025-02-09 10:41:09 +08:00
2dust
7d8a9f2b6d Update AndroidLibXrayLite 2025-02-09 10:33:41 +08:00
alphax-hue3682
0a1695e3d7 Update kotlin version to 2.1.10 (#4305)
* Update libs.versions.toml

* Update README.md
2025-02-07 14:31:45 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
4a653d4935 Fix badvpn (#4302)
* copying from df181a3065

* add missing includes of dc99ade18d

* update workflow

* fixup! update workflow
2025-01-31 13:58:59 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
2bc31a10c5 rm AndroidLibV2rayLite (#4303) 2025-01-31 13:54:08 +08:00
alphax-hue3682
e8d2c6214b Update dependencies (#4301)
* Update dependencies

* Update dependencies
2025-01-30 20:46:01 +08:00
alphax-hue3682
3a0f2687e9 Update_Submodules (#4292)
* UpdateSubmodules

* Update _Submodules
2025-01-30 20:20:26 +08:00
24 changed files with 514 additions and 398 deletions

View File

@@ -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/AndroidLibXrayLite/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/AndroidLibXrayLite/modules/libancillary/HEAD') }} 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
@@ -51,7 +51,6 @@ jobs:
- name: Build libtun2socks - name: Build libtun2socks
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true' if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
run: | run: |
cd ${{ github.workspace }}/AndroidLibXrayLite
bash compile-tun2socks.sh bash compile-tun2socks.sh
tar -xvzf libtun2socks.so.tgz tar -xvzf libtun2socks.so.tgz
env: env:
@@ -61,12 +60,12 @@ jobs:
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true' if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4 uses: actions/cache/save@v4
with: with:
path: ${{ github.workspace }}/AndroidLibXrayLite/libs path: ${{ github.workspace }}/libs
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/AndroidLibXrayLite/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/AndroidLibXrayLite/modules/libancillary/HEAD') }} key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
- name: Copy libtun2socks - name: Copy libtun2socks
run: | run: |
cp -r ${{ github.workspace }}/AndroidLibXrayLite/libs ${{ github.workspace }}/V2rayNG/app cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
- name: Fetch AndroidLibXrayLite tag - name: Fetch AndroidLibXrayLite tag
run: | run: |

6
.gitmodules vendored
View File

@@ -4,3 +4,9 @@
[submodule "AndroidLibXrayLite"] [submodule "AndroidLibXrayLite"]
path = AndroidLibXrayLite path = AndroidLibXrayLite
url = https://github.com/2dust/AndroidLibXrayLite url = https://github.com/2dust/AndroidLibXrayLite
[submodule "badvpn"]
path = badvpn
url = https://github.com/XTLS/badvpn
[submodule "libancillary"]
path = libancillary
url = https://github.com/shadowsocks/libancillary

View File

@@ -1,20 +0,0 @@
# AndroidLibV2rayLite
### Preparation
- latest Ubuntu environment
- At lease 30G free space
- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
### Prepare Go
- Go to https://golang.org/doc/install and install latest go
- Make sure `go version` works as expected
### Prepare gomobile
- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile
- export PATH=$PATH:~/go/bin
- Make sure `gomobile init` works as expected
### Prepare NDK
- Go to https://developer.android.com/ndk/downloads and install latest NDK
- export PATH=$PATH:<wherever you ndk is located>
- Make sure `ndk-build -v` works as expected
### Make
- sudo apt install make
- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile)

View File

@@ -3,7 +3,7 @@
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core) A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
[![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop) [![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.0-blue.svg)](https://kotlinlang.org) [![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.10-blue.svg)](https://kotlinlang.org)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng) [![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases) [![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases)

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 630 versionCode = 636
versionName = "1.9.34" 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)

View File

@@ -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,7 +431,9 @@ 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)
} }
} }
} }

View File

@@ -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,23 +78,22 @@ class AboutActivity : BaseActivity() {
} }
binding.layoutRestore.setOnClickListener { binding.layoutRestore.setOnClickListener {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val permission =
Manifest.permission.READ_MEDIA_IMAGES if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
} else { Manifest.permission.READ_MEDIA_IMAGES
Manifest.permission.READ_EXTERNAL_STORAGE } 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)
} }
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 { binding.layoutSoureCcode.setOnClickListener {
@@ -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()
}
} }

View File

@@ -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,12 +181,10 @@ 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)
}
} }
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
@@ -226,11 +276,10 @@ 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() }
}
} }
public override fun onResume() { public override fun onResume() {
@@ -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) scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
// } catch (e: Exception) { } else {
RxPermissions(this) scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
.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)
} }
// } } 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,20 +694,18 @@ 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 { try {
if (it) { contentResolver.openInputStream(uri).use { input ->
try { importCustomizeConfig(input?.bufferedReader()?.readText())
contentResolver.openInputStream(uri).use { input -> }
importCustomizeConfig(input?.bufferedReader()?.readText()) } catch (e: Exception) {
} e.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
} else
toast(R.string.toast_permission_denied)
} }
} else {
requestPermissionLauncher.launch(permission)
}
} }
/** /**
@@ -763,4 +787,4 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.drawerLayout.closeDrawer(GravityCompat.START) binding.drawerLayout.closeDrawer(GravityCompat.START)
return true return true
} }
} }

View File

@@ -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,11 +166,14 @@ 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()
} }
}
} }
} }
} }
@@ -246,4 +250,4 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
override fun onItemDismiss(position: Int) { 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.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,93 +42,39 @@ 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
if (blacklist != null) { val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
it.forEach { one -> val apps = withContext(Dispatchers.IO) {
if (blacklist.contains(one.packageName)) { val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
one.isSelected = 1
} else { if (blacklist != null) {
one.isSelected = 0 appsList.forEach { app ->
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 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 }
} })
} } else {
it.sortedWith(comparator)
} 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 { appsAll = apps
// val comparator = object : Comparator<AppInfo> { adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
// 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)
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
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 -> 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() {

View File

@@ -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
@@ -39,6 +38,16 @@ class RoutingSettingActivity : BaseActivity() {
private val preset_rulesets: Array<out String> by lazy { private val preset_rulesets: Array<out String> by lazy {
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)
@@ -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()
} }
}
}

View File

@@ -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()
} }
}
}

View File

@@ -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,15 +105,12 @@ 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 -> showFileChooser()
if (granted) { } else {
showFileChooser() requestPermissionLauncher.launch(permission)
} else { }
toast(R.string.toast_permission_denied)
}
}
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)
}
}
}
}

View File

@@ -356,11 +356,11 @@ class ServerActivity : BaseActivity() {
et_sni?.text = Utils.getEditable(config.sni) et_sni?.text = Utils.getEditable(config.sni)
config.fingerPrint?.let { config.fingerPrint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, it) val utlsIndex = Utils.arrayFind(uTlsItems, it)
sp_stream_fingerprint?.setSelection(utlsIndex) utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) }
} }
config.alpn?.let { config.alpn?.let {
val alpnIndex = Utils.arrayFind(alpns, it) val alpnIndex = Utils.arrayFind(alpns, it)
sp_stream_alpn?.setSelection(alpnIndex) alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
} }
if (config.security == TLS) { if (config.security == TLS) {
container_allow_insecure?.visibility = View.VISIBLE container_allow_insecure?.visibility = View.VISIBLE

View File

@@ -4,10 +4,15 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLDecoder import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() { class UrlSchemeActivity : BaseActivity() {
@@ -66,11 +71,15 @@ class UrlSchemeActivity : BaseActivity() {
decodedUrl += "#${fragment}" decodedUrl += "#${fragment}"
} }
Log.d("UrlScheme-decodedUrl", decodedUrl) Log.d("UrlScheme-decodedUrl", decodedUrl)
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false) lifecycleScope.launch(Dispatchers.IO) {
if (count + countSub > 0) { val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
toast(R.string.import_subscription_success) withContext(Dispatchers.Main) {
} else { if (count + countSub > 0) {
toast(R.string.import_subscription_failure) toast(R.string.import_subscription_success)
} else {
toast(R.string.import_subscription_failure)
}
}
} }
} }
} }

View File

@@ -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
} }
@@ -349,4 +353,4 @@ class UserAssetActivity : BaseActivity() {
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
RecyclerView.ViewHolder(itemUserAssetBinding.root) RecyclerView.ViewHolder(itemUserAssetBinding.root)
} }

View File

@@ -4,36 +4,27 @@ 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 packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) val packageManager = context.packageManager
val apps = ArrayList<AppInfo>() val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
val apps = ArrayList<AppInfo>()
for (pkg in packages) { for (pkg in packages) {
val applicationInfo = pkg.applicationInfo ?: continue val applicationInfo = pkg.applicationInfo ?: continue
val appName = applicationInfo.loadLabel(packageManager).toString() val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0 val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo) 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

@@ -1,22 +1,21 @@
[versions] [versions]
agp = "8.8.0" 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.0" kotlin = "2.1.10"
coreKtx = "1.15.0" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" 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" }

1
badvpn Submodule

Submodule badvpn added at e68480088a

33
compile-tun2socks.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
# Set magic variables for current file & dir
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)"
if [[ ! -d $NDK_HOME ]]; then
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"
exit 1
fi
TMPDIR=$(mktemp -d)
clear_tmp () {
rm -rf $TMPDIR
}
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT
install -m644 $__dir/tun2socks.mk $TMPDIR/
pushd $TMPDIR
ln -s $__dir/badvpn badvpn
ln -s $__dir/libancillary libancillary
$NDK_HOME/ndk-build \
NDK_PROJECT_PATH=. \
APP_BUILD_SCRIPT=./tun2socks.mk \
APP_ABI=all \
APP_PLATFORM=android-19 \
NDK_LIBS_OUT=$TMPDIR/libs \
NDK_OUT=$TMPDIR/tmp \
APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4 \
LOCAL_LDFLAGS=-Wl,--build-id=none
tar cvfz $__dir/libtun2socks.so.tgz libs
popd
rm -rf $TMPDIR

1
libancillary Submodule

Submodule libancillary added at 232d69a5eb

124
tun2socks.mk Normal file
View File

@@ -0,0 +1,124 @@
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
LOCAL_PATH := $(call my-dir)
ROOT_PATH := $(LOCAL_PATH)
########################################################
## libancillary
########################################################
include $(CLEAR_VARS)
ANCILLARY_SOURCE := fd_recv.c fd_send.c
LOCAL_MODULE := libancillary
#LOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libancillary
LOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE))
include $(BUILD_STATIC_LIBRARY)
########################################################
## tun2socks
########################################################
include $(CLEAR_VARS)
LOCAL_CFLAGS := -std=gnu99
LOCAL_CFLAGS += -DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN -D_GNU_SOURCE
LOCAL_CFLAGS += -DBADVPN_USE_SIGNALFD -DBADVPN_USE_EPOLL
LOCAL_CFLAGS += -DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE
LOCAL_CFLAGS += -DNDEBUG -DANDROID
LOCAL_CFLAGS += -I
LOCAL_STATIC_LIBRARIES := libancillary
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/badvpn/libancillary \
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv4 \
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv6 \
$(LOCAL_PATH)/badvpn/lwip/src/include \
$(LOCAL_PATH)/badvpn/lwip/custom \
$(LOCAL_PATH)/badvpn \
$(LOCAL_PATH)/libancillary
TUN2SOCKS_SOURCES := \
base/BLog_syslog.c \
system/BReactor_badvpn.c \
system/BSignal.c \
system/BConnection_common.c \
system/BConnection_unix.c \
system/BTime.c \
system/BUnixSignal.c \
system/BNetwork.c \
system/BDatagram_common.c \
system/BDatagram_unix.c \
flow/StreamRecvInterface.c \
flow/PacketRecvInterface.c \
flow/PacketPassInterface.c \
flow/StreamPassInterface.c \
flow/SinglePacketBuffer.c \
flow/BufferWriter.c \
flow/PacketBuffer.c \
flow/PacketStreamSender.c \
flow/PacketPassConnector.c \
flow/PacketProtoFlow.c \
flow/PacketPassFairQueue.c \
flow/PacketProtoEncoder.c \
flow/PacketProtoDecoder.c \
socksclient/BSocksClient.c \
tuntap/BTap.c \
lwip/src/core/udp.c \
lwip/src/core/memp.c \
lwip/src/core/init.c \
lwip/src/core/pbuf.c \
lwip/src/core/tcp.c \
lwip/src/core/tcp_out.c \
lwip/src/core/netif.c \
lwip/src/core/def.c \
lwip/src/core/ip.c \
lwip/src/core/mem.c \
lwip/src/core/tcp_in.c \
lwip/src/core/stats.c \
lwip/src/core/inet_chksum.c \
lwip/src/core/timeouts.c \
lwip/src/core/ipv4/icmp.c \
lwip/src/core/ipv4/igmp.c \
lwip/src/core/ipv4/ip4_addr.c \
lwip/src/core/ipv4/ip4_frag.c \
lwip/src/core/ipv4/ip4.c \
lwip/src/core/ipv4/autoip.c \
lwip/src/core/ipv6/ethip6.c \
lwip/src/core/ipv6/inet6.c \
lwip/src/core/ipv6/ip6_addr.c \
lwip/src/core/ipv6/mld6.c \
lwip/src/core/ipv6/dhcp6.c \
lwip/src/core/ipv6/icmp6.c \
lwip/src/core/ipv6/ip6.c \
lwip/src/core/ipv6/ip6_frag.c \
lwip/src/core/ipv6/nd6.c \
lwip/custom/sys.c \
tun2socks/tun2socks.c \
base/DebugObject.c \
base/BLog.c \
base/BPending.c \
flowextra/PacketPassInactivityMonitor.c \
tun2socks/SocksUdpGwClient.c \
udpgw_client/UdpGwClient.c \
socks_udp_client/SocksUdpClient.c
LOCAL_MODULE := tun2socks
LOCAL_LDLIBS := -ldl -llog
LOCAL_SRC_FILES := $(addprefix badvpn/, $(TUN2SOCKS_SOURCES))
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
# we are building target objects
my := TARGET_
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk