+ package="com.v2ray.ang">
+ android:anyDensity="true"
+ android:smallScreens="true"
+ android:normalScreens="true"
+ android:largeScreens="true"
+ android:xlargeScreens="true"/>
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
-
+ android:extractNativeLibs="true"
+ android:theme="@style/AppTheme">
+ android:theme="@style/AppTheme.NoActionBar"
+ android:launchMode="singleTask">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ android:name=".ui.ServerActivity"
+ android:windowSoftInputMode="stateUnchanged" />
+ android:name=".ui.ServerCustomConfigActivity"
+ android:windowSoftInputMode="stateUnchanged" />
+
+
+
+
+
+
+
+
+
+
+
@@ -165,107 +95,53 @@
-
-
-
+
-
-
-
+
+ android:name="android.appwidget.provider"
+ android:resource="@xml/app_widget_provider" />
-
-
-
-
-
+ android:name=".service.QSTileService"
+ android:icon="@drawable/ic_v"
+ android:label="@string/app_tile_name"
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
+ android:process=":RunSoLibV2RayDaemon">
-
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name">
-
+
-
-
-
-
-
-
-
-
-
diff --git a/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
new file mode 100644
index 00000000..2a492f78
--- /dev/null
+++ b/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.vending.billing;
+
+import android.os.Bundle;
+
+/**
+ * InAppBillingService is the service that provides in-app billing version 3 and beyond.
+ * This service provides the following features:
+ * 1. Provides a new API to get details of in-app items published for the app including
+ * price, type, title and description.
+ * 2. The purchase flow is synchronous and purchase information is available immediately
+ * after it completes.
+ * 3. Purchase information of in-app purchases is maintained within the Google Play system
+ * till the purchase is consumed.
+ * 4. An API to consume a purchase of an inapp item. All purchases of one-time
+ * in-app items are consumable and thereafter can be purchased again.
+ * 5. An API to get current purchases of the user immediately. This will not contain any
+ * consumed purchases.
+ *
+ * All calls will give a response code with the following possible values
+ * RESULT_OK = 0 - success
+ * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
+ * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
+ * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
+ * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
+ * RESULT_ERROR = 6 - Fatal error during the API action
+ * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
+ * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
+ */
+interface IInAppBillingService {
+ /**
+ * Checks support for the requested billing API version, package and in-app type.
+ * Minimum API version supported by this interface is 3.
+ * @param apiVersion the billing version which the app is using
+ * @param packageName the package name of the calling app
+ * @param type type of the in-app item being purchased "inapp" for one-time purchases
+ * and "subs" for subscription.
+ * @return RESULT_OK(0) on success, corresponding result code on failures
+ */
+ int isBillingSupported(int apiVersion, String packageName, String type);
+
+ /**
+ * Provides details of a list of SKUs
+ * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
+ * with a list JSON strings containing the productId, price, title and description.
+ * This API can be called with a maximum of 20 SKUs.
+ * @param apiVersion billing API version that the Third-party is using
+ * @param packageName the package name of the calling app
+ * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "DETAILS_LIST" with a StringArrayList containing purchase information
+ * in JSON format similar to:
+ * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
+ * "title : "Example Title", "description" : "This is an example description" }'
+ */
+ Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
+
+ /**
+ * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
+ * the type, a unique purchase token and an optional developer payload.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param sku the SKU of the in-app item as published in the developer console
+ * @param type the type of the in-app item ("inapp" for one-time purchases
+ * and "subs" for subscription).
+ * @param developerPayload optional argument to be sent back with the purchase information
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "BUY_INTENT" - PendingIntent to start the purchase flow
+ *
+ * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+ * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+ * If the purchase is successful, the result data will contain the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "INAPP_PURCHASE_DATA" - String in JSON format similar to
+ * '{"orderId":"12999763169054705758.1371079406387615",
+ * "packageName":"com.example.app",
+ * "productId":"exampleSku",
+ * "purchaseTime":1345678900000,
+ * "purchaseToken" : "122333444455555",
+ * "developerPayload":"example developer payload" }'
+ * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+ * was signed with the private key of the developer
+ * TODO: change this to app-specific keys.
+ */
+ Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
+ String developerPayload);
+
+ /**
+ * Returns the current SKUs owned by the user of the type and package name specified along with
+ * purchase information and a signature of the data to be validated.
+ * This will return all SKUs that have been purchased in V3 and managed items purchased using
+ * V1 and V2 that have not been consumed.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param type the type of the in-app items being requested
+ * ("inapp" for one-time purchases and "subs" for subscription).
+ * @param continuationToken to be set as null for the first call, if the number of owned
+ * skus are too many, a continuationToken is returned in the response bundle.
+ * This method can be called again with the continuation token to get the next set of
+ * owned skus.
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
+ * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
+ * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
+ * of the purchase information
+ * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+ * next set of in-app purchases. Only set if the
+ * user has more owned skus than the current list.
+ */
+ Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
+
+ /**
+ * Consume the last purchase of the given SKU. This will result in this item being removed
+ * from all subsequent responses to getPurchases() and allow re-purchase of this item.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param purchaseToken token in the purchase information JSON that identifies the purchase
+ * to be consumed
+ * @return 0 if consumption succeeded. Appropriate error values for failures.
+ */
+ int consumePurchase(int apiVersion, String packageName, String purchaseToken);
+}
diff --git a/V2rayNG/app/src/main/assets/custom_routing_black b/V2rayNG/app/src/main/assets/custom_routing_black
deleted file mode 100644
index f15daacd..00000000
--- a/V2rayNG/app/src/main/assets/custom_routing_black
+++ /dev/null
@@ -1,142 +0,0 @@
-[
- {
- "remarks": "绕过bittorrent",
- "outboundTag": "direct",
- "protocol": [
- "bittorrent"
- ]
- },
- {
- "remarks": "Google cn",
- "outboundTag": "proxy",
- "domain": [
- "domain:googleapis.cn",
- "domain:gstatic.com"
- ]
- },
- {
- "remarks": "阻断udp443",
- "outboundTag": "block",
- "port": "443",
- "network": "udp"
- },
- {
- "remarks": "绕过局域网IP",
- "outboundTag": "direct",
- "ip": [
- "geoip:private"
- ]
- },
- {
- "remarks": "绕过局域网域名",
- "outboundTag": "direct",
- "domain": [
- "geosite:private"
- ]
- },
- {
- "remarks": "代理海外公共DNSIP",
- "outboundTag": "proxy",
- "ip": [
- "1.1.1.1",
- "1.0.0.1",
- "2606:4700:4700::1111",
- "2606:4700:4700::1001",
- "1.1.1.2",
- "1.0.0.2",
- "2606:4700:4700::1112",
- "2606:4700:4700::1002",
- "1.1.1.3",
- "1.0.0.3",
- "2606:4700:4700::1113",
- "2606:4700:4700::1003",
- "8.8.8.8",
- "8.8.4.4",
- "2001:4860:4860::8888",
- "2001:4860:4860::8844",
- "94.140.14.14",
- "94.140.15.15",
- "2a10:50c0::ad1:ff",
- "2a10:50c0::ad2:ff",
- "94.140.14.15",
- "94.140.15.16",
- "2a10:50c0::bad1:ff",
- "2a10:50c0::bad2:ff",
- "94.140.14.140",
- "94.140.14.141",
- "2a10:50c0::1:ff",
- "2a10:50c0::2:ff",
- "208.67.222.222",
- "208.67.220.220",
- "2620:119:35::35",
- "2620:119:53::53",
- "208.67.222.123",
- "208.67.220.123",
- "2620:119:35::123",
- "2620:119:53::123",
- "9.9.9.9",
- "149.112.112.112",
- "2620:fe::9",
- "2620:fe::fe",
- "9.9.9.11",
- "149.112.112.11",
- "2620:fe::11",
- "2620:fe::fe:11",
- "9.9.9.10",
- "149.112.112.10",
- "2620:fe::10",
- "2620:fe::fe:10",
- "77.88.8.8",
- "77.88.8.1",
- "2a02:6b8::feed:0ff",
- "2a02:6b8:0:1::feed:0ff",
- "77.88.8.88",
- "77.88.8.2",
- "2a02:6b8::feed:bad",
- "2a02:6b8:0:1::feed:bad",
- "77.88.8.7",
- "77.88.8.3",
- "2a02:6b8::feed:a11",
- "2a02:6b8:0:1::feed:a11"
- ]
- },
- {
- "remarks": "代理海外公共DNS域名",
- "outboundTag": "proxy",
- "domain": [
- "domain:cloudflare-dns.com",
- "domain:one.one.one.one",
- "domain:dns.google",
- "domain:adguard-dns.com",
- "domain:opendns.com",
- "domain:umbrella.com",
- "domain:quad9.net",
- "domain:yandex.net"
- ]
- },
- {
- "remarks": "代理IP",
- "outboundTag": "proxy",
- "ip": [
- "geoip:facebook",
- "geoip:fastly",
- "geoip:google",
- "geoip:netflix",
- "geoip:telegram",
- "geoip:twitter"
- ]
- },
- {
- "remarks": "代理GFW",
- "outboundTag": "proxy",
- "domain": [
- "geosite:gfw",
- "geosite:greatfire"
- ]
- },
- {
- "remarks": "最终直连",
- "port": "0-65535",
- "outboundTag": "direct"
- }
-]
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/assets/custom_routing_global b/V2rayNG/app/src/main/assets/custom_routing_global
deleted file mode 100644
index 21bfb24d..00000000
--- a/V2rayNG/app/src/main/assets/custom_routing_global
+++ /dev/null
@@ -1,27 +0,0 @@
-[
- {
- "remarks": "阻断udp443",
- "outboundTag": "block",
- "port": "443",
- "network": "udp"
- },
- {
- "remarks": "绕过局域网IP",
- "outboundTag": "direct",
- "ip": [
- "geoip:private"
- ]
- },
- {
- "remarks": "绕过局域网域名",
- "outboundTag": "direct",
- "domain": [
- "geosite:private"
- ]
- },
- {
- "remarks": "最终代理",
- "port": "0-65535",
- "outboundTag": "proxy"
- }
-]
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/assets/custom_routing_white b/V2rayNG/app/src/main/assets/custom_routing_white
deleted file mode 100644
index e3c360c0..00000000
--- a/V2rayNG/app/src/main/assets/custom_routing_white
+++ /dev/null
@@ -1,96 +0,0 @@
-[
- {
- "remarks": "Google cn",
- "outboundTag": "proxy",
- "domain": [
- "domain:googleapis.cn",
- "domain:gstatic.com"
- ]
- },
- {
- "remarks": "阻断udp443",
- "outboundTag": "block",
- "port": "443",
- "network": "udp"
- },
- {
- "remarks": "绕过局域网IP",
- "outboundTag": "direct",
- "ip": [
- "geoip:private"
- ]
- },
- {
- "remarks": "绕过局域网域名",
- "outboundTag": "direct",
- "domain": [
- "geosite:private"
- ]
- },
- {
- "remarks": "绕过中国公共DNSIP",
- "outboundTag": "direct",
- "ip": [
- "223.5.5.5",
- "223.6.6.6",
- "2400:3200::1",
- "2400:3200:baba::1",
- "119.29.29.29",
- "1.12.12.12",
- "120.53.53.53",
- "2402:4e00::",
- "2402:4e00:1::",
- "180.76.76.76",
- "2400:da00::6666",
- "114.114.114.114",
- "114.114.115.115",
- "114.114.114.119",
- "114.114.115.119",
- "114.114.114.110",
- "114.114.115.110",
- "180.184.1.1",
- "180.184.2.2",
- "101.226.4.6",
- "218.30.118.6",
- "123.125.81.6",
- "140.207.198.6",
- "1.2.4.8",
- "210.2.4.8",
- "52.80.66.66",
- "117.50.22.22",
- "2400:7fc0:849e:200::4",
- "2404:c2c0:85d8:901::4",
- "117.50.10.10",
- "52.80.52.52",
- "2400:7fc0:849e:200::8",
- "2404:c2c0:85d8:901::8",
- "117.50.60.30",
- "52.80.60.30"
- ]
- },
- {
- "remarks": "绕过中国公共DNS域名",
- "outboundTag": "direct",
- "domain": [
- "domain:alidns.com",
- "domain:doh.pub",
- "domain:dot.pub",
- "domain:360.cn",
- "domain:onedns.net"
- ]
- },
- {
- "remarks": "绕过中国IP",
- "outboundTag": "direct",
- "ip": [
- "geoip:cn"
- ]
- },
- {
- "remarks": "绕过中国域名",
- "outboundTag": "direct",
- "domain": [
- "geosite:cn"
- ]
- }
-]
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/assets/custom_routing_white_iran b/V2rayNG/app/src/main/assets/custom_routing_white_iran
deleted file mode 100644
index 97ff1227..00000000
--- a/V2rayNG/app/src/main/assets/custom_routing_white_iran
+++ /dev/null
@@ -1,37 +0,0 @@
-[
- {
- "remarks": "Block udp443",
- "outboundTag": "block",
- "port": "443",
- "network": "udp"
- },
- {
- "remarks": "Direct LAN IP",
- "outboundTag": "direct",
- "ip": [
- "geoip:private"
- ]
- },
- {
- "remarks": "Direct LAN domains",
- "outboundTag": "direct",
- "domain": [
- "geosite:private"
- ]
- },
- {
- "remarks": "Bypass Iran domains",
- "outboundTag": "direct",
- "domain": [
- "domain:ir",
- "geosite:category-ir"
- ]
- },
- {
- "remarks": "Bypass Iran IP",
- "outboundTag": "direct",
- "ip": [
- "geoip:ir"
- ]
- }
-]
diff --git a/V2rayNG/app/src/main/assets/open_source_licenses.html b/V2rayNG/app/src/main/assets/open_source_licenses.html
deleted file mode 100644
index 0cfd1daa..00000000
--- a/V2rayNG/app/src/main/assets/open_source_licenses.html
+++ /dev/null
@@ -1,1285 +0,0 @@
-
-
-
-
- Open source licenses
-
-
- Notice for packages:
-
- - Camera Core
-
- - Copyright © 2019 The Android Open Source Project
-
-
-
-
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- 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.
-
-
- BSD License
-https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/README.chromium
-
-
-
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- 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.
-
-
-
-
- - MMKV
-
- - Copyright © 20xx Tencent Wechat, Inc.
-
-
-
-
-
- BSD 3-Clause License
-
-Copyright (c) [year], [fullname]
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-
-
-
- Android Software Development Kit License
-https://developer.android.com/studio/terms.html
-
-
-
-
- ML Kit Terms of Service
-https://developers.google.com/ml-kit/terms
-
-
-
-
- Apache 2.0 License
-https://github.com/massivemadness/EditorKit/blob/master/LICENSE
-
-
-
-
- MIT-0
-https://spdx.org/licenses/MIT-0.html
-
-
-
-
- MIT License
-
-Copyright (c) [year] [fullname]
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-
-
-
-
diff --git a/V2rayNG/app/src/main/assets/proxy_packagename.txt b/V2rayNG/app/src/main/assets/proxy_packagename.txt
index 12abf7db..ecac8251 100644
--- a/V2rayNG/app/src/main/assets/proxy_packagename.txt
+++ b/V2rayNG/app/src/main/assets/proxy_packagename.txt
@@ -4,7 +4,6 @@ au.com.shiftyjelly.pocketcasts
bbc.mobile.news.ww
be.mygod.vpnhotspot
ch.protonmail.android
-cm.aptoide.pt
co.wanqu.android
com.alphainventor.filemanager
com.amazon.kindle
@@ -35,9 +34,7 @@ com.chrome.canary
com.chrome.dev
com.cl.newt66y
com.cradle.iitc_mobile
-org.exarhteam.iitc_mobile
com.cygames.shadowverse
-com.dcard.freedom
com.devhd.feedly
com.devolver.reigns2
com.discord
@@ -111,7 +108,6 @@ com.ifttt.ifttt
com.imgur.mobile
com.innologica.inoreader
com.instagram.android
-com.instagram.lite
com.instapaper.android
com.jarvanh.vpntether
com.kapp.youtube.final
@@ -119,7 +115,6 @@ com.klinker.android.twitter_l
com.lastpass.lpandroid
com.linecorp.linelite
com.lingodeer
-com.ltnnews.news
com.mediapods.tumbpods
com.mgoogle.android.gms
com.microsoft.emmx
@@ -164,7 +159,6 @@ com.slack
com.snaptube.premium
com.sololearn
com.sonelli.juicessh
-com.sparkslab.dcardreader
com.spotify.music
com.tencent.huatuo
com.termux
@@ -179,13 +173,10 @@ com.twitter.android
com.u91porn
com.u9porn
com.ubisoft.dance.justdance2015companion
-com.udn.news
com.utopia.pxview
+com.valvesoftware.android.steam.communimunity
com.valvesoftware.android.steam.community
-com.vanced.manager
com.vanced.android.youtube
-com.vanced.android.apps.youtube.music
-com.mgoogle.android.gms
com.vimeo.android.videoapp
com.vivaldi.browser
com.vivaldi.browser.snapshot
@@ -195,12 +186,10 @@ com.wire
com.wuxiangai.refactor
com.xda.labs
com.xvideos.app
-com.yahoo.mobile.client.android.superapp
com.yandex.browser
com.yandex.browser.beta
com.yandex.browser.alpha
com.z28j.feel
-com.zhiliaoapp.musically
con.medium.reader
de.apkgrabber
de.robv.android.xposed.installer
@@ -221,7 +210,6 @@ jp.bokete.app.android
jp.naver.line.android
jp.pxv.android
luo.speedometergpspro
-m.cna.com.tw.App
mark.via.gp
me.tshine.easymark
net.teeha.android.url_shortener
@@ -238,7 +226,6 @@ org.mozilla.firefox_beta
org.mozilla.focus
org.schabi.newpipe
org.telegram.messenger
-org.telegram.messenger.web
org.telegram.multi
org.telegram.plus
org.thunderdog.challegram
@@ -252,162 +239,3 @@ tw.com.gamer.android.activecenter
videodownloader.downloadvideo.downloader
uk.co.bbc.learningenglish
com.ted.android
-de.danoeh.antennapod
-com.kiwibrowser.browser
-nekox.messenger
-com.nextcloud.client
-com.aurora.store
-com.aurora.adroid
-chat.simplex.app
-im.vector.app
-network.loki.messenger
-eu.siacs.conversations
-xyz.nextalone.nagram
-net.programmierecke.radiodroid2
-im.fdx.v2ex
-ml.docilealligator.infinityforreddit
-com.bytemyth.ama
-app.vanadium.browser
-com.cakewallet.cake_wallet
-org.purplei2p.i2pd
-dk.tacit.android.foldersync.lite
-com.nononsenseapps.feeder
-com.m2049r.xmrwallet
-com.paypal.android.p2pmobile
-com.google.android.apps.googlevoice
-com.readdle.spark
-org.torproject.torbrowser
-com.deepl.mobiletranslator
-com.microsoft.bing
-com.keylesspalace.tusky
-com.ottplay.ottplay
-ru.iptvremote.android.iptv.pro
-jp.naver.line.android
-com.xmflsct.app.tooot
-com.forem.android
-app.revanced.android.youtube
-com.mgoogle.android.gms
-com.pionex.client
-vip.mytokenpocket
-im.token.app
-com.linekong.mars24
-com.feixiaohao
-com.aicoin.appandroid
-com.binance.dev
-com.kraken.trade
-com.okinc.okex.gp
-com.authy.authy
-air.com.rosettastone.mobile.CoursePlayer
-com.blizzard.bma
-com.amazon.kindle
-com.google.android.apps.fitness
-net.tsapps.appsales
-com.wemesh.android
-com.google.android.apps.googleassistant
-allen.town.focus.reader
-me.hyliu.fluent_reader_lite
-com.aljazeera.mobile
-com.ft.news
-de.marmaro.krt.ffupdater
-myradio.radio.fmradio.liveradio.radiostation
-com.google.earth
-eu.kanade.tachiyomi.j2k
-com.audials
-com.microsoft.skydrive
-com.mb.android.tg
-com.melodis.midomiMusicIdentifier.freemium
-com.foxnews.android
-ch.threema.app
-com.briarproject.briar.android
-foundation.e.apps
-com.valvesoftware.android.steam.friendsui
-com.imback.yeetalk
-so.onekey.app.wallet
-com.xc3fff0e.xmanager
-meditofoundation.medito
-com.picol.client
-com.streetwriters.notesnook
-shanghai.panewsApp.com
-org.coursera.android
-com.positron_it.zlib
-com.blizzard.messenger
-com.javdb.javrocket
-com.picacomic.fregata
-com.fxl.chacha
-me.proton.android.drive
-com.lastpass.lpandroid
-com.tradingview.tradingviewapp
-com.deviantart.android.damobile
-com.fusionmedia.investing
-com.ewa.ewaapp
-com.duolingo
-com.hellotalk
-io.github.huskydg.magisk
-com.jsy.xpgbox
-com.hostloc.app.hostloc
-com.dena.pokota
-com.vitorpamplona.amethyst
-com.zhiliaoapp.musically
-us.spotco.fennec_dos
-com.fongmi.android.tv
-com.pocketprep.android.itcybersecurity
-com.cloudtv
-com.glassdoor.app
-com.indeed.android.jobsearch
-com.linkedin.android
-com.github.tvbox.osc.bh
-com.example.douban
-com.sipnetic.app
-com.microsoft.rdc.androidx
-org.zwanoo.android.speedtest
-com.sonelli.juicessh
-com.scmp.newspulse
-org.lsposed.manager
-mnn.Android
-com.thomsonretuers.reuters
-com.guardian
-com.ttxapps.onesyncv2
-org.fcitx.fcitx5.android.updater
-com.tailscale.ipn
-tw.nekomimi.nekogram
-com.nexon.kartdrift
-io.syncapps.lemmy_sync
-com.seazon.feedme
-com.readwise
-de.spiritcroc.riotx
-com.openai.chatgpt
-io.changenow.changenow
-com.poe.android
-com.twingate
-com.blinkslabs.blinkist.android
-com.ichi2.anki
-md.obsidian
-com.musixmatch.android.lyrify
-com.cyber.turbo
-com.offsec.nethunter
-me.ghui.v2er
-com.samruston.twitter
-org.adaway
-org.swiftapps.swiftbackup
-com.zerotier.one
-com.quietmobile
-com.instagram.barcelona
-im.molly.app
-com.rvx.android.youtube
-com.deepl.mobiletranslator
-com.qingsong.yingmi
-com.lemurbrowser.exts
-com.silverdev.dnartdroid
-me.ash.reader
-de.tutao.tutanota
-dev.imranr.obtainium
-com.getsomeheadspace.android
-org.cromite.cromite
-com.nutomic.syncthingandroid
-com.bumble.app
-com.cnn.mobile.android.phone
-com.google.android.apps.authenticator2
-com.microsoft.copilot
-com.netflix.NGP.Storyteller
-com.Slack
-com.server.auditor.ssh.client
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/assets/v2ray_config.json b/V2rayNG/app/src/main/assets/v2ray_config.json
index 4f8c3d7e..5e009dc8 100644
--- a/V2rayNG/app/src/main/assets/v2ray_config.json
+++ b/V2rayNG/app/src/main/assets/v2ray_config.json
@@ -54,7 +54,6 @@
"users": [
{
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
- "alterId": 0,
"security": "auto",
"level": 8
}
@@ -81,9 +80,7 @@
},
{
"protocol": "freedom",
- "settings": {
- "domainStrategy": "UseIP"
- },
+ "settings": {},
"tag": "direct"
},
{
@@ -97,7 +94,7 @@
}
],
"routing": {
- "domainStrategy": "AsIs",
+ "domainStrategy": "IPIfNonMatch",
"rules": []
},
"dns": {
diff --git a/V2rayNG/app/src/main/ic_launcher-web.png b/V2rayNG/app/src/main/ic_launcher-web.png
index 03a4ce8a..f6fc3b8c 100644
Binary files a/V2rayNG/app/src/main/ic_launcher-web.png and b/V2rayNG/app/src/main/ic_launcher-web.png differ
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt
deleted file mode 100644
index 44f680b3..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.v2ray.ang
-
-import android.content.Context
-import androidx.multidex.MultiDexApplication
-import androidx.work.Configuration
-import androidx.work.WorkManager
-import com.tencent.mmkv.MMKV
-import com.v2ray.ang.AppConfig.ANG_PACKAGE
-import com.v2ray.ang.handler.SettingsManager
-
-class AngApplication : MultiDexApplication() {
- companion object {
- lateinit var application: AngApplication
- }
-
- /**
- * Attaches the base context to the application.
- * @param base The base context.
- */
- override fun attachBaseContext(base: Context?) {
- super.attachBaseContext(base)
- application = this
- }
-
- private val workManagerConfiguration: Configuration = Configuration.Builder()
- .setDefaultProcessName("${ANG_PACKAGE}:bg")
- .build()
-
- /**
- * Initializes the application.
- */
- override fun onCreate() {
- super.onCreate()
-
- MMKV.initialize(this)
-
- SettingsManager.setNightMode()
- // Initialize WorkManager with the custom configuration
- WorkManager.initialize(this, workManagerConfiguration)
-
- SettingsManager.initRoutingRulesets(this)
-
- es.dmoral.toasty.Toasty.Config.getInstance()
- .setGravity(android.view.Gravity.BOTTOM, 0, 200)
- .apply()
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
deleted file mode 100644
index 09e3a9d5..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
+++ /dev/null
@@ -1,248 +0,0 @@
-package com.v2ray.ang
-
-
-object AppConfig {
-
- /** The application's package name. */
- const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
- const val TAG = BuildConfig.APPLICATION_ID
-
- /** Directory names used in the app's file system. */
- const val DIR_ASSETS = "assets"
- const val DIR_BACKUPS = "backups"
-
- /** Legacy configuration keys. */
- const val ANG_CONFIG = "ang_config"
-
- /** Preferences mapped to MMKV storage. */
- const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
- const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
- const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
- const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
- const val PREF_BYPASS_APPS = "pref_bypass_apps"
- const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
- const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
- const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
- const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
- const val PREF_VPN_DNS = "pref_vpn_dns"
- const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
- const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
- const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
- const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
- const val PREF_MUX_ENABLED = "pref_mux_enabled"
- const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
- const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
- const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
- const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
- const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
- const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
- const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
- const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
- const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
- const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
- const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
- const val PREF_SPEED_ENABLED = "pref_speed_enabled"
- const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
- const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
- const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display"
- const val PREF_LANGUAGE = "pref_language"
- const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
- const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
- const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
- const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
- const val PREF_SOCKS_PORT = "pref_socks_port"
- const val PREF_REMOTE_DNS = "pref_remote_dns"
- const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
- const val PREF_DNS_HOSTS = "pref_dns_hosts"
- const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
- const val PREF_LOGLEVEL = "pref_core_loglevel"
- const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
- const val PREF_MODE = "pref_mode"
- const val PREF_IS_BOOTED = "pref_is_booted"
- const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
- const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources"
-
- /** Cache keys. */
- const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
- const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
-
- /** Protocol identifiers. */
- const val PROTOCOL_FREEDOM = "freedom"
-
- /** Broadcast actions. */
- const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
- const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
- const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
-
- /** Tasker extras. */
- const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
- const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
- const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
- const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
- const val TASKER_DEFAULT_GUID = "Default"
-
- /** Tags for different proxy modes. */
- const val TAG_PROXY = "proxy"
- const val TAG_DIRECT = "direct"
- const val TAG_BLOCKED = "block"
- const val TAG_FRAGMENT = "fragment"
-
- /** Network-related constants. */
- const val UPLINK = "uplink"
- const val DOWNLINK = "downlink"
-
- /** URLs for various resources. */
- const val GITHUB_URL = "https://github.com"
- const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
- const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
- const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
- const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
- const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
- const val APP_ISSUES_URL = "$APP_URL/issues"
- const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
- const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
- const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
- const val TG_CHANNEL_URL = "https://t.me/github_2dust"
- const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
- const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
- const val IP_API_URL = "https://speed.cloudflare.com/meta"
-
- /** DNS server addresses. */
- const val DNS_PROXY = "1.1.1.1"
- const val DNS_DIRECT = "223.5.5.5"
- const val DNS_VPN = "1.1.1.1"
- const val GEOSITE_PRIVATE = "geosite:private"
- const val GEOSITE_CN = "geosite:cn"
- const val GEOIP_PRIVATE = "geoip:private"
- const val GEOIP_CN = "geoip:cn"
-
- /** Ports and addresses for various services. */
- const val PORT_LOCAL_DNS = "10853"
- const val PORT_SOCKS = "10808"
- const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
- const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
- const val WIREGUARD_LOCAL_MTU = "1420"
- const val LOOPBACK = "127.0.0.1"
-
- /** Message constants for communication. */
- const val MSG_REGISTER_CLIENT = 1
- const val MSG_STATE_RUNNING = 11
- const val MSG_STATE_NOT_RUNNING = 12
- const val MSG_UNREGISTER_CLIENT = 2
- const val MSG_STATE_START = 3
- const val MSG_STATE_START_SUCCESS = 31
- const val MSG_STATE_START_FAILURE = 32
- const val MSG_STATE_STOP = 4
- const val MSG_STATE_STOP_SUCCESS = 41
- const val MSG_STATE_RESTART = 5
- const val MSG_MEASURE_DELAY = 6
- const val MSG_MEASURE_DELAY_SUCCESS = 61
- const val MSG_MEASURE_CONFIG = 7
- const val MSG_MEASURE_CONFIG_SUCCESS = 71
- const val MSG_MEASURE_CONFIG_CANCEL = 72
-
- /** Notification channel IDs and names. */
- const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
- const val RAY_NG_CHANNEL_NAME = "v2rayNG Background Service"
- const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
- const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
-
- /** Protocols Scheme **/
- const val VMESS = "vmess://"
- const val CUSTOM = ""
- const val SHADOWSOCKS = "ss://"
- const val SOCKS = "socks://"
- const val HTTP = "http://"
- const val VLESS = "vless://"
- const val TROJAN = "trojan://"
- const val WIREGUARD = "wireguard://"
- const val TUIC = "tuic://"
- const val HYSTERIA2 = "hysteria2://"
- const val HY2 = "hy2://"
-
- /** Give a good name to this, IDK*/
- const val VPN = "VPN"
-
- // Google API rule constants
- const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
- const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
-
- // Android Private DNS constants
- const val DNS_DNSPOD_DOMAIN = "dot.pub"
- const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
- const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
- const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
- const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
- const val DNS_GOOGLE_DOMAIN = "dns.google"
- const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
- const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
-
- const val DEFAULT_PORT = 443
- const val DEFAULT_SECURITY = "auto"
- const val DEFAULT_LEVEL = 8
- const val DEFAULT_NETWORK = "tcp"
- const val TLS = "tls"
- const val REALITY = "reality"
- const val HEADER_TYPE_HTTP = "http"
-
- val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
- val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
- val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
- val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9")
- val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
- val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
- val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
- val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
-
- //minimum list https://serverfault.com/a/304791
- val ROUTED_IP_LIST = arrayListOf(
- "0.0.0.0/5",
- "8.0.0.0/7",
- "11.0.0.0/8",
- "12.0.0.0/6",
- "16.0.0.0/4",
- "32.0.0.0/3",
- "64.0.0.0/2",
- "128.0.0.0/3",
- "160.0.0.0/5",
- "168.0.0.0/6",
- "172.0.0.0/12",
- "172.32.0.0/11",
- "172.64.0.0/10",
- "172.128.0.0/9",
- "173.0.0.0/8",
- "174.0.0.0/7",
- "176.0.0.0/4",
- "192.0.0.0/9",
- "192.128.0.0/11",
- "192.160.0.0/13",
- "192.169.0.0/16",
- "192.170.0.0/15",
- "192.172.0.0/14",
- "192.176.0.0/12",
- "192.192.0.0/10",
- "193.0.0.0/8",
- "194.0.0.0/7",
- "196.0.0.0/6",
- "200.0.0.0/5",
- "208.0.0.0/4",
- "240.0.0.0/4"
- )
-
- val PRIVATE_IP_LIST = arrayListOf(
- "0.0.0.0/8",
- "10.0.0.0/8",
- "127.0.0.0/8",
- "172.16.0.0/12",
- "192.168.0.0/16",
- "169.254.0.0/16",
- "224.0.0.0/4"
- )
-
- val GEO_FILES_SOURCES = arrayListOf(
- "Loyalsoldier/v2ray-rules-dat",
- "runetfreedom/russia-v2ray-rules-dat",
- "Chocolate4U/Iran-v2ray-rules"
- )
-
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt
deleted file mode 100644
index 219e35e9..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.v2ray.ang.dto
-
-import android.graphics.drawable.Drawable
-
-data class AppInfo(
- val appName: String,
- val packageName: String,
- val appIcon: Drawable,
- val isSystemApp: Boolean,
- var isSelected: Int
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt
deleted file mode 100644
index 5a8d1e60..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.v2ray.ang.dto
-
-data class AssetUrlItem(
- var remarks: String = "",
- var url: String = "",
- val addedTime: Long = System.currentTimeMillis(),
- var lastUpdated: Long = -1,
- var locked: Boolean? = false,
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt
deleted file mode 100644
index be4f62e5..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.v2ray.ang.dto
-
-data class CheckUpdateResult(
- val hasUpdate: Boolean,
- val latestVersion: String? = null,
- val releaseNotes: String? = null,
- val downloadUrl: String? = null,
- val error: String? = null,
- val isPreRelease: Boolean = false
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt
deleted file mode 100644
index c8870248..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.v2ray.ang.dto
-
-data class ConfigResult(
- var status: Boolean,
- var guid: String? = null,
- var content: String = "",
- var socksPort: Int? = null,
-)
-
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt
deleted file mode 100644
index ed7cad3b..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.v2ray.ang.dto
-
-import com.v2ray.ang.AppConfig
-
-
-enum class EConfigType(val value: Int, val protocolScheme: String) {
- VMESS(1, AppConfig.VMESS),
- CUSTOM(2, AppConfig.CUSTOM),
- SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
- SOCKS(4, AppConfig.SOCKS),
- VLESS(5, AppConfig.VLESS),
- TROJAN(6, AppConfig.TROJAN),
- WIREGUARD(7, AppConfig.WIREGUARD),
-
- // TUIC(8, AppConfig.TUIC),
- HYSTERIA2(9, AppConfig.HYSTERIA2),
- HTTP(10, AppConfig.HTTP);
-
- companion object {
- fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
deleted file mode 100644
index 0a7dce56..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.v2ray.ang.dto
-
-import com.google.gson.annotations.SerializedName
-
-data class GitHubRelease(
- @SerializedName("tag_name")
- val tagName: String,
- @SerializedName("body")
- val body: String,
- @SerializedName("assets")
- val assets: List,
- @SerializedName("prerelease")
- val prerelease: Boolean = false,
- @SerializedName("published_at")
- val publishedAt: String = ""
-) {
- data class Asset(
- @SerializedName("name")
- val name: String,
- @SerializedName("browser_download_url")
- val browserDownloadUrl: String
- )
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt
deleted file mode 100644
index c4fc6582..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.v2ray.ang.dto
-
-data class Hysteria2Bean(
- val server: String?,
- val auth: String?,
- val lazy: Boolean? = true,
- val obfs: ObfsBean? = null,
- val socks5: Socks5Bean? = null,
- val http: Socks5Bean? = null,
- val tls: TlsBean? = null,
- val transport: TransportBean? = null,
- val bandwidth: BandwidthBean? = null,
-) {
- data class ObfsBean(
- val type: String?,
- val salamander: SalamanderBean?
- ) {
- data class SalamanderBean(
- val password: String?,
- )
- }
-
- data class Socks5Bean(
- val listen: String?,
- )
-
- data class TlsBean(
- val sni: String?,
- val insecure: Boolean?,
- val pinSHA256: String?,
- )
-
- data class TransportBean(
- val type: String?,
- val udp: TransportUdpBean?
- ) {
- data class TransportUdpBean(
- val hopInterval: String?,
- )
- }
-
- data class BandwidthBean(
- val down: String?,
- val up: String?,
- )
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
deleted file mode 100644
index 97814fbb..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.v2ray.ang.dto
-
-data class IPAPIInfo(
- var ip: String? = null,
- var clientIp: String? = null,
- var ip_addr: String? = null,
- var query: String? = null,
- var country: String? = null,
- var country_name: String? = null,
- var country_code: String? = null,
- var countryCode: String? = null
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt
deleted file mode 100644
index 46333d06..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.v2ray.ang.dto
-
-enum class Language(val code: String) {
- AUTO("auto"),
- ENGLISH("en"),
- CHINA("zh-rCN"),
- TRADITIONAL_CHINESE("zh-rTW"),
- VIETNAMESE("vi"),
- RUSSIAN("ru"),
- PERSIAN("fa"),
- ARABIC("ar"),
- BANGLA("bn"),
- BAKHTIARI("bqi-rIR");
-
- companion object {
- fun fromCode(code: String): Language {
- return entries.find { it.code == code } ?: AUTO
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
deleted file mode 100644
index 0e8091b9..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.v2ray.ang.dto
-
-enum class NetworkType(val type: String) {
- TCP("tcp"),
- KCP("kcp"),
- WS("ws"),
- HTTP_UPGRADE("httpupgrade"),
- XHTTP("xhttp"),
- HTTP("http"),
- H2("h2"),
-
- //QUIC("quic"),
- GRPC("grpc");
-
- companion object {
- fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt
deleted file mode 100644
index 7a1f7346..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.v2ray.ang.dto
-
-import com.v2ray.ang.AppConfig.LOOPBACK
-import com.v2ray.ang.AppConfig.PORT_SOCKS
-import com.v2ray.ang.AppConfig.TAG_BLOCKED
-import com.v2ray.ang.AppConfig.TAG_DIRECT
-import com.v2ray.ang.AppConfig.TAG_PROXY
-import com.v2ray.ang.util.Utils
-
-data class ProfileItem(
- val configVersion: Int = 4,
- val configType: EConfigType,
- var subscriptionId: String = "",
- var addedTime: Long = System.currentTimeMillis(),
-
- var remarks: String = "",
- var server: String? = null,
- var serverPort: String? = null,
-
- var password: String? = null,
- var method: String? = null,
- var flow: String? = null,
- var username: String? = null,
-
- var network: String? = null,
- var headerType: String? = null,
- var host: String? = null,
- var path: String? = null,
- var seed: String? = null,
- var quicSecurity: String? = null,
- var quicKey: String? = null,
- var mode: String? = null,
- var serviceName: String? = null,
- var authority: String? = null,
- var xhttpMode: String? = null,
- var xhttpExtra: String? = null,
-
- var security: String? = null,
- var sni: String? = null,
- var alpn: String? = null,
- var fingerPrint: String? = null,
- var insecure: Boolean? = null,
-
- var publicKey: String? = null,
- var shortId: String? = null,
- var spiderX: String? = null,
-
- var secretKey: String? = null,
- var preSharedKey: String? = null,
- var localAddress: String? = null,
- var reserved: String? = null,
- var mtu: Int? = null,
-
- var obfsPassword: String? = null,
- var portHopping: String? = null,
- var portHoppingInterval: String? = null,
- var pinSHA256: String? = null,
- var bandwidthDown: String? = null,
- var bandwidthUp: String? = null,
-
- ) {
- companion object {
- fun create(configType: EConfigType): ProfileItem {
- return ProfileItem(configType = configType)
- }
- }
-
- fun getAllOutboundTags(): MutableList {
- return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
- }
-
- fun getServerAddressAndPort(): String {
- if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
- return "$LOOPBACK:$PORT_SOCKS"
- }
- return Utils.getIpv6Address(server) + ":" + serverPort
- }
-
- override fun equals(other: Any?): Boolean {
- if (other == null) return false
- val obj = other as ProfileItem
-
- return (this.server == obj.server
- && this.serverPort == obj.serverPort
- && this.password == obj.password
- && this.method == obj.method
- && this.flow == obj.flow
- && this.username == obj.username
-
- && this.network == obj.network
- && this.headerType == obj.headerType
- && this.host == obj.host
- && this.path == obj.path
- && this.seed == obj.seed
- && this.quicSecurity == obj.quicSecurity
- && this.quicKey == obj.quicKey
- && this.mode == obj.mode
- && this.serviceName == obj.serviceName
- && this.authority == obj.authority
- && this.xhttpMode == obj.xhttpMode
-
- && this.security == obj.security
- && this.sni == obj.sni
- && this.alpn == obj.alpn
- && this.fingerPrint == obj.fingerPrint
- && this.publicKey == obj.publicKey
- && this.shortId == obj.shortId
-
- && this.secretKey == obj.secretKey
- && this.localAddress == obj.localAddress
- && this.reserved == obj.reserved
- && this.mtu == obj.mtu
-
- && this.obfsPassword == obj.obfsPassword
- && this.portHopping == obj.portHopping
- && this.portHoppingInterval == obj.portHoppingInterval
- && this.pinSHA256 == obj.pinSHA256
- )
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt
deleted file mode 100644
index a98ac167..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.v2ray.ang.dto
-
-enum class RoutingType(val fileName: String) {
- WHITE("custom_routing_white"),
- BLACK("custom_routing_black"),
- GLOBAL("custom_routing_global"),
- WHITE_IRAN("custom_routing_white_iran");
-
- companion object {
- fun fromIndex(index: Int): RoutingType {
- return when (index) {
- 0 -> WHITE
- 1 -> BLACK
- 2 -> GLOBAL
- 3 -> WHITE_IRAN
- else -> WHITE
- }
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt
deleted file mode 100644
index 9005f559..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.v2ray.ang.dto
-
-data class RulesetItem(
- var remarks: String? = "",
- var ip: List? = null,
- var domain: List? = null,
- var outboundTag: String = "",
- var port: String? = null,
- var network: String? = null,
- var protocol: List? = null,
- var enabled: Boolean = true,
- var locked: Boolean? = false,
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt
deleted file mode 100644
index baa1fe48..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.v2ray.ang.dto
-
-import com.v2ray.ang.AppConfig.TAG_BLOCKED
-import com.v2ray.ang.AppConfig.TAG_DIRECT
-import com.v2ray.ang.AppConfig.TAG_PROXY
-
-data class ServerConfig(
- val configVersion: Int = 3,
- val configType: EConfigType,
- var subscriptionId: String = "",
- val addedTime: Long = System.currentTimeMillis(),
- var remarks: String = "",
- val outboundBean: V2rayConfig.OutboundBean? = null,
- var fullConfig: V2rayConfig? = null
-) {
- companion object {
- fun create(configType: EConfigType): ServerConfig {
- when (configType) {
- EConfigType.VMESS,
- EConfigType.VLESS ->
- return ServerConfig(
- configType = configType,
- outboundBean = V2rayConfig.OutboundBean(
- protocol = configType.name.lowercase(),
- settings = V2rayConfig.OutboundBean.OutSettingsBean(
- vnext = listOf(
- V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
- users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())
- )
- )
- ),
- streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
- )
- )
-
- EConfigType.CUSTOM ->
- return ServerConfig(configType = configType)
-
- EConfigType.SHADOWSOCKS,
- EConfigType.SOCKS,
- EConfigType.HTTP,
- EConfigType.TROJAN,
- EConfigType.HYSTERIA2 ->
- return ServerConfig(
- configType = configType,
- outboundBean = V2rayConfig.OutboundBean(
- protocol = configType.name.lowercase(),
- settings = V2rayConfig.OutboundBean.OutSettingsBean(
- servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())
- ),
- streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
- )
- )
-
- EConfigType.WIREGUARD ->
- return ServerConfig(
- configType = configType,
- outboundBean = V2rayConfig.OutboundBean(
- protocol = configType.name.lowercase(),
- settings = V2rayConfig.OutboundBean.OutSettingsBean(
- secretKey = "",
- peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
- )
- )
- )
- }
- }
- }
-
- fun getProxyOutbound(): V2rayConfig.OutboundBean? {
- if (configType != EConfigType.CUSTOM) {
- return outboundBean
- }
- return fullConfig?.getProxyOutbound()
- }
-
- fun getAllOutboundTags(): MutableList {
- if (configType != EConfigType.CUSTOM) {
- return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
- }
- fullConfig?.let { config ->
- return config.outbounds.map { it.tag }.toMutableList()
- }
- return mutableListOf()
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt
deleted file mode 100644
index 7ea3e6a3..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.v2ray.ang.dto
-
-data class ServersCache(
- val guid: String,
- val profile: ProfileItem
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt
deleted file mode 100644
index 8957df78..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.v2ray.ang.dto
-
-data class SubscriptionItem(
- var remarks: String = "",
- var url: String = "",
- var enabled: Boolean = true,
- val addedTime: Long = System.currentTimeMillis(),
- var lastUpdated: Long = -1,
- var autoUpdate: Boolean = false,
- val updateInterval: Int? = null,
- var prevProfile: String? = null,
- var nextProfile: String? = null,
- var filter: String? = null,
- var allowInsecureUrl: Boolean = false,
-)
-
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
deleted file mode 100644
index 155be104..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
+++ /dev/null
@@ -1,556 +0,0 @@
-package com.v2ray.ang.dto
-
-import com.google.gson.annotations.SerializedName
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.util.Utils
-
-data class V2rayConfig(
- var remarks: String? = null,
- var stats: Any? = null,
- val log: LogBean,
- var policy: PolicyBean? = null,
- val inbounds: ArrayList,
- var outbounds: ArrayList,
- var dns: DnsBean? = null,
- val routing: RoutingBean,
- val api: Any? = null,
- val transport: Any? = null,
- val reverse: Any? = null,
- var fakedns: Any? = null,
- val browserForwarder: Any? = null,
- var observatory: Any? = null,
- var burstObservatory: Any? = null
-) {
-
- data class LogBean(
- val access: String? = null,
- val error: String? = null,
- var loglevel: String? = null,
- val dnsLog: Boolean? = null
- )
-
- data class InboundBean(
- var tag: String,
- var port: Int,
- var protocol: String,
- var listen: String? = null,
- val settings: Any? = null,
- val sniffing: SniffingBean? = null,
- val streamSettings: Any? = null,
- val allocate: Any? = null
- ) {
-
- data class InSettingsBean(
- val auth: String? = null,
- val udp: Boolean? = null,
- val userLevel: Int? = null,
- val address: String? = null,
- val port: Int? = null,
- val network: String? = null
- )
-
- data class SniffingBean(
- var enabled: Boolean,
- val destOverride: ArrayList,
- val metadataOnly: Boolean? = null,
- var routeOnly: Boolean? = null
- )
- }
-
- data class OutboundBean(
- var tag: String = "proxy",
- var protocol: String,
- var settings: OutSettingsBean? = null,
- var streamSettings: StreamSettingsBean? = null,
- val proxySettings: Any? = null,
- val sendThrough: String? = null,
- var mux: MuxBean? = MuxBean(false)
- ) {
- data class OutSettingsBean(
- var vnext: List? = null,
- var fragment: FragmentBean? = null,
- var noises: List? = null,
- var servers: List? = null,
- /*Blackhole*/
- var response: Response? = null,
- /*DNS*/
- val network: String? = null,
- var address: Any? = null,
- val port: Int? = null,
- /*Freedom*/
- var domainStrategy: String? = null,
- val redirect: String? = null,
- val userLevel: Int? = null,
- /*Loopback*/
- val inboundTag: String? = null,
- /*Wireguard*/
- var secretKey: String? = null,
- val peers: List? = null,
- var reserved: List? = null,
- var mtu: Int? = null,
- var obfsPassword: String? = null,
- ) {
-
- data class VnextBean(
- var address: String = "",
- var port: Int = AppConfig.DEFAULT_PORT,
- var users: List
- ) {
-
- data class UsersBean(
- var id: String = "",
- var alterId: Int? = null,
- var security: String? = null,
- var level: Int = AppConfig.DEFAULT_LEVEL,
- var encryption: String? = null,
- var flow: String? = null
- )
- }
-
- data class FragmentBean(
- var packets: String? = null,
- var length: String? = null,
- var interval: String? = null
- )
-
- data class NoiseBean(
- var type: String? = null,
- var packet: String? = null,
- var delay: String? = null
- )
-
- data class ServersBean(
- var address: String = "",
- var method: String? = null,
- var ota: Boolean = false,
- var password: String? = null,
- var port: Int = AppConfig.DEFAULT_PORT,
- var level: Int = AppConfig.DEFAULT_LEVEL,
- val email: String? = null,
- var flow: String? = null,
- val ivCheck: Boolean? = null,
- var users: List? = null
- ) {
- data class SocksUsersBean(
- var user: String = "",
- var pass: String = "",
- var level: Int = AppConfig.DEFAULT_LEVEL
- )
- }
-
- data class Response(var type: String)
-
- data class WireGuardBean(
- var publicKey: String = "",
- var preSharedKey: String? = null,
- var endpoint: String = ""
- )
- }
-
- data class StreamSettingsBean(
- var network: String = AppConfig.DEFAULT_NETWORK,
- var security: String? = null,
- var tcpSettings: TcpSettingsBean? = null,
- var kcpSettings: KcpSettingsBean? = null,
- var wsSettings: WsSettingsBean? = null,
- var httpupgradeSettings: HttpupgradeSettingsBean? = null,
- var xhttpSettings: XhttpSettingsBean? = null,
- var httpSettings: HttpSettingsBean? = null,
- var tlsSettings: TlsSettingsBean? = null,
- var quicSettings: QuicSettingBean? = null,
- var realitySettings: TlsSettingsBean? = null,
- var grpcSettings: GrpcSettingsBean? = null,
- var hy2steriaSettings: Hy2steriaSettingsBean? = null,
- val dsSettings: Any? = null,
- var sockopt: SockoptBean? = null
- ) {
-
- data class TcpSettingsBean(
- var header: HeaderBean = HeaderBean(),
- val acceptProxyProtocol: Boolean? = null
- ) {
- data class HeaderBean(
- var type: String = "none",
- var request: RequestBean? = null,
- var response: Any? = null
- ) {
- data class RequestBean(
- var path: List = ArrayList(),
- var headers: HeadersBean = HeadersBean(),
- val version: String? = null,
- val method: String? = null
- ) {
- data class HeadersBean(
- var Host: List? = ArrayList(),
- @SerializedName("User-Agent")
- val userAgent: List? = null,
- @SerializedName("Accept-Encoding")
- val acceptEncoding: List? = null,
- val Connection: List? = null,
- val Pragma: String? = null
- )
- }
- }
- }
-
- data class KcpSettingsBean(
- var mtu: Int = 1350,
- var tti: Int = 50,
- var uplinkCapacity: Int = 12,
- var downlinkCapacity: Int = 100,
- var congestion: Boolean = false,
- var readBufferSize: Int = 1,
- var writeBufferSize: Int = 1,
- var header: HeaderBean = HeaderBean(),
- var seed: String? = null
- ) {
- data class HeaderBean(
- var type: String = "none",
- var domain: String? = null
- )
- }
-
- data class WsSettingsBean(
- var path: String? = null,
- var headers: HeadersBean = HeadersBean(),
- val maxEarlyData: Int? = null,
- val useBrowserForwarding: Boolean? = null,
- val acceptProxyProtocol: Boolean? = null
- ) {
- data class HeadersBean(var Host: String = "")
- }
-
- data class HttpupgradeSettingsBean(
- var path: String? = null,
- var host: String? = null,
- val acceptProxyProtocol: Boolean? = null
- )
-
- data class XhttpSettingsBean(
- var path: String? = null,
- var host: String? = null,
- var mode: String? = null,
- var extra: Any? = null,
- )
-
- data class HttpSettingsBean(
- var host: List = ArrayList(),
- var path: String? = null
- )
-
- data class SockoptBean(
- var TcpNoDelay: Boolean? = null,
- var tcpKeepAliveIdle: Int? = null,
- var tcpFastOpen: Boolean? = null,
- var tproxy: String? = null,
- var mark: Int? = null,
- var dialerProxy: String? = null,
- var domainStrategy: String? = null
- )
-
- data class TlsSettingsBean(
- var allowInsecure: Boolean = false,
- var serverName: String? = null,
- val alpn: List? = null,
- val minVersion: String? = null,
- val maxVersion: String? = null,
- val preferServerCipherSuites: Boolean? = null,
- val cipherSuites: String? = null,
- val fingerprint: String? = null,
- val certificates: List? = null,
- val disableSystemRoot: Boolean? = null,
- val enableSessionResumption: Boolean? = null,
- // REALITY settings
- val show: Boolean = false,
- var publicKey: String? = null,
- var shortId: String? = null,
- var spiderX: String? = null
- )
-
- data class QuicSettingBean(
- var security: String = "none",
- var key: String = "",
- var header: HeaderBean = HeaderBean()
- ) {
- data class HeaderBean(var type: String = "none")
- }
-
- data class GrpcSettingsBean(
- var serviceName: String = "",
- var authority: String? = null,
- var multiMode: Boolean? = null,
- var idle_timeout: Int? = null,
- var health_check_timeout: Int? = null
- )
-
- data class Hy2steriaSettingsBean(
- var password: String? = null,
- var use_udp_extension: Boolean? = true,
- var congestion: Hy2CongestionBean? = null
- ) {
- data class Hy2CongestionBean(
- var type: String? = "bbr",
- var up_mbps: Int? = null,
- var down_mbps: Int? = null,
- )
- }
-
- }
-
- data class MuxBean(
- var enabled: Boolean,
- var concurrency: Int? = null,
- var xudpConcurrency: Int? = null,
- var xudpProxyUDP443: String? = null,
- )
-
- fun getServerAddress(): String? {
- if (protocol.equals(EConfigType.VMESS.name, true)
- || protocol.equals(EConfigType.VLESS.name, true)
- ) {
- return settings?.vnext?.first()?.address
- } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
- || protocol.equals(EConfigType.SOCKS.name, true)
- || protocol.equals(EConfigType.HTTP.name, true)
- || protocol.equals(EConfigType.TROJAN.name, true)
- || protocol.equals(EConfigType.HYSTERIA2.name, true)
- ) {
- return settings?.servers?.first()?.address
- } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
- return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
- }
- return null
- }
-
- fun getServerPort(): Int? {
- if (protocol.equals(EConfigType.VMESS.name, true)
- || protocol.equals(EConfigType.VLESS.name, true)
- ) {
- return settings?.vnext?.first()?.port
- } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
- || protocol.equals(EConfigType.SOCKS.name, true)
- || protocol.equals(EConfigType.HTTP.name, true)
- || protocol.equals(EConfigType.TROJAN.name, true)
- || protocol.equals(EConfigType.HYSTERIA2.name, true)
- ) {
- return settings?.servers?.first()?.port
- } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
- return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
- }
- return null
- }
-
- fun getServerAddressAndPort(): String {
- val address = getServerAddress().orEmpty()
- val port = getServerPort()
- return Utils.getIpv6Address(address) + ":" + port
- }
-
- fun getPassword(): String? {
- if (protocol.equals(EConfigType.VMESS.name, true)
- || protocol.equals(EConfigType.VLESS.name, true)
- ) {
- return settings?.vnext?.first()?.users?.first()?.id
- } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
- || protocol.equals(EConfigType.TROJAN.name, true)
- || protocol.equals(EConfigType.HYSTERIA2.name, true)
- ) {
- return settings?.servers?.first()?.password
- } else if (protocol.equals(EConfigType.SOCKS.name, true)
- || protocol.equals(EConfigType.HTTP.name, true)
- ) {
- return settings?.servers?.first()?.users?.first()?.pass
- } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
- return settings?.secretKey
- }
- return null
- }
-
- fun getSecurityEncryption(): String? {
- return when {
- protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
- protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
- protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
- else -> null
- }
- }
-
- fun getTransportSettingDetails(): List? {
- if (protocol.equals(EConfigType.VMESS.name, true)
- || protocol.equals(EConfigType.VLESS.name, true)
- || protocol.equals(EConfigType.TROJAN.name, true)
- || protocol.equals(EConfigType.SHADOWSOCKS.name, true)
- ) {
- val transport = streamSettings?.network ?: return null
- return when (transport) {
- NetworkType.TCP.type -> {
- val tcpSetting = streamSettings?.tcpSettings ?: return null
- listOf(
- tcpSetting.header.type,
- tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(),
- tcpSetting.header.request?.path?.joinToString(",").orEmpty()
- )
- }
-
- NetworkType.KCP.type -> {
- val kcpSetting = streamSettings?.kcpSettings ?: return null
- listOf(
- kcpSetting.header.type,
- "",
- kcpSetting.seed.orEmpty()
- )
- }
-
- NetworkType.WS.type -> {
- val wsSetting = streamSettings?.wsSettings ?: return null
- listOf(
- "",
- wsSetting.headers.Host,
- wsSetting.path
- )
- }
-
- NetworkType.HTTP_UPGRADE.type -> {
- val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
- listOf(
- "",
- httpupgradeSetting.host,
- httpupgradeSetting.path
- )
- }
-
- NetworkType.XHTTP.type -> {
- val xhttpSettings = streamSettings?.xhttpSettings ?: return null
- listOf(
- "",
- xhttpSettings.host,
- xhttpSettings.path
- )
- }
-
- NetworkType.H2.type -> {
- val h2Setting = streamSettings?.httpSettings ?: return null
- listOf(
- "",
- h2Setting.host.joinToString(","),
- h2Setting.path
- )
- }
-
-// "quic" -> {
-// val quicSetting = streamSettings?.quicSettings ?: return null
-// listOf(
-// quicSetting.header.type,
-// quicSetting.security,
-// quicSetting.key
-// )
-// }
-
- NetworkType.GRPC.type -> {
- val grpcSetting = streamSettings?.grpcSettings ?: return null
- listOf(
- if (grpcSetting.multiMode == true) "multi" else "gun",
- grpcSetting.authority.orEmpty(),
- grpcSetting.serviceName
- )
- }
-
- else -> null
- }
- }
- return null
- }
-
- fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean {
- val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also {
- streamSettings = it
- }
-
- val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also {
- stream.sockopt = it
- }
-
- return sockopt
- }
- }
-
- data class DnsBean(
- var servers: ArrayList? = null,
- var hosts: Map? = null,
- val clientIp: String? = null,
- val disableCache: Boolean? = null,
- val queryStrategy: String? = null,
- val tag: String? = null
- ) {
- data class ServersBean(
- var address: String = "",
- var port: Int? = null,
- var domains: List? = null,
- var expectIPs: List? = null,
- val clientIp: String? = null,
- val skipFallback: Boolean? = null,
- )
- }
-
- data class RoutingBean(
- var domainStrategy: String,
- var domainMatcher: String? = null,
- var rules: ArrayList,
- val balancers: List? = null
- ) {
-
- data class RulesBean(
- var type: String = "field",
- var ip: ArrayList? = null,
- var domain: ArrayList? = null,
- var outboundTag: String = "",
- var balancerTag: String? = null,
- var port: String? = null,
- val sourcePort: String? = null,
- val network: String? = null,
- val source: List? = null,
- val user: List? = null,
- var inboundTag: List? = null,
- val protocol: List? = null,
- val attrs: String? = null,
- val domainMatcher: String? = null
- )
- }
-
- data class PolicyBean(
- var levels: Map,
- var system: Any? = null
- ) {
- data class LevelBean(
- var handshake: Int? = null,
- var connIdle: Int? = null,
- var uplinkOnly: Int? = null,
- var downlinkOnly: Int? = null,
- val statsUserUplink: Boolean? = null,
- val statsUserDownlink: Boolean? = null,
- var bufferSize: Int? = null
- )
- }
-
- data class FakednsBean(
- var ipPool: String = "198.18.0.0/15",
- var poolSize: Int = 10000
- ) // roughly 10 times smaller than total ip pool
-
- fun getProxyOutbound(): OutboundBean? {
- outbounds.forEach { outbound ->
- EConfigType.entries.forEach {
- if (outbound.protocol.equals(it.name, true)) {
- return outbound
- }
- }
- }
- return null
- }
-
- fun getAllProxyOutbound(): List {
- return outbounds.filter { outbound ->
- EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt
deleted file mode 100644
index 779c7e3c..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.v2ray.ang.dto
-
-data class VmessQRCode(
- var v: String = "",
- var ps: String = "",
- var add: String = "",
- var port: String = "",
- var id: String = "",
- var aid: String = "0",
- var scy: String = "",
- var net: String = "",
- var type: String = "",
- var host: String = "",
- var path: String = "",
- var tls: String = "",
- var sni: String = "",
- var alpn: String = "",
- var fp: String = ""
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt
deleted file mode 100644
index 6b7bc379..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.v2ray.ang.dto
-
-/**
- * VPN interface address configuration enum class
- * Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration.
- * Each option provides client and router addresses to establish point-to-point VPN tunnels.
- */
-enum class VpnInterfaceAddressConfig(
- val displayName: String,
- val ipv4Client: String,
- val ipv4Router: String,
- val ipv6Client: String,
- val ipv6Router: String
-) {
- OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"),
- OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"),
- OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"),
- OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"),
- OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"),
- OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"),
- OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2");
-
- companion object {
- /**
- * Retrieves the VPN interface address configuration based on the specified index.
- *
- * @param index The configuration index (0-based) corresponding to user selection
- * @return The VpnInterfaceAddressConfig instance at the specified index,
- * or OPTION_1 (default) if the index is out of bounds
- */
- fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig {
- return if (index in values().indices) {
- values()[index]
- } else {
- OPTION_1 // Default to the first configuration
- }
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt
deleted file mode 100644
index 6e5c6bb4..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt
+++ /dev/null
@@ -1,212 +0,0 @@
-package com.v2ray.ang.extension
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.os.Build
-import android.os.Bundle
-import android.widget.Toast
-import com.v2ray.ang.AngApplication
-import es.dmoral.toasty.Toasty
-import org.json.JSONObject
-import java.io.Serializable
-import java.net.URI
-import java.net.URLConnection
-
-val Context.v2RayApplication: AngApplication?
- get() = applicationContext as? AngApplication
-
-/**
- * Shows a toast message with the given resource ID.
- *
- * @param message The resource ID of the message to show.
- */
-fun Context.toast(message: Int) {
- Toasty.normal(this, message).show()
-}
-
-/**
- * Shows a toast message with the given text.
- *
- * @param message The text of the message to show.
- */
-fun Context.toast(message: CharSequence) {
- Toasty.normal(this, message).show()
-}
-
-/**
- * Shows a toast message with the given resource ID.
- *
- * @param message The resource ID of the message to show.
- */
-fun Context.toastSuccess(message: Int) {
- Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
-}
-
-/**
- * Shows a toast message with the given text.
- *
- * @param message The text of the message to show.
- */
-fun Context.toastSuccess(message: CharSequence) {
- Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
-}
-
-/**
- * Shows a toast message with the given resource ID.
- *
- * @param message The resource ID of the message to show.
- */
-fun Context.toastError(message: Int) {
- Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
-}
-
-/**
- * Shows a toast message with the given text.
- *
- * @param message The text of the message to show.
- */
-fun Context.toastError(message: CharSequence) {
- Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
-}
-
-
-/**
- * Puts a key-value pair into the JSONObject.
- *
- * @param pair The key-value pair to put.
- */
-fun JSONObject.putOpt(pair: Pair) {
- put(pair.first, pair.second)
-}
-
-/**
- * Puts multiple key-value pairs into the JSONObject.
- *
- * @param pairs The map of key-value pairs to put.
- */
-fun JSONObject.putOpt(pairs: Map) {
- pairs.forEach { put(it.key, it.value) }
-}
-
-const val THRESHOLD = 1000L
-const val DIVISOR = 1024.0
-
-/**
- * Converts a Long value to a speed string.
- *
- * @return The speed string.
- */
-fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
-
-/**
- * Converts a Long value to a traffic string.
- *
- * @return The traffic string.
- */
-fun Long.toTrafficString(): String {
- val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
- var size = this.toDouble()
- var unitIndex = 0
- while (size >= THRESHOLD && unitIndex < units.size - 1) {
- size /= DIVISOR
- unitIndex++
- }
- return String.format("%.1f %s", size, units[unitIndex])
-}
-
-val URLConnection.responseLength: Long
- get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- contentLengthLong
- } else {
- contentLength.toLong()
- }
-
-val URI.idnHost: String
- get() = host?.replace("[", "")?.replace("]", "").orEmpty()
-
-/**
- * Removes all whitespace from the string.
- *
- * @return The string without whitespace.
- */
-fun String?.removeWhiteSpace(): String? = this?.replace(" ", "")
-
-/**
- * Converts the string to a Long value, or returns 0 if the conversion fails.
- *
- * @return The Long value.
- */
-fun String.toLongEx(): Long = toLongOrNull() ?: 0
-
-/**
- * Listens for package changes and executes a callback when a change occurs.
- *
- * @param onetime Whether to unregister the receiver after the first callback.
- * @param callback The callback to execute when a package change occurs.
- * @return The BroadcastReceiver that was registered.
- */
-fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- callback()
- if (onetime) context.unregisterReceiver(this)
- }
- }.apply {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- registerReceiver(this, IntentFilter().apply {
- addAction(Intent.ACTION_PACKAGE_ADDED)
- addAction(Intent.ACTION_PACKAGE_REMOVED)
- addDataScheme("package")
- }, Context.RECEIVER_EXPORTED)
- } else {
- registerReceiver(this, IntentFilter().apply {
- addAction(Intent.ACTION_PACKAGE_ADDED)
- addAction(Intent.ACTION_PACKAGE_REMOVED)
- addDataScheme("package")
- })
- }
- }
-
-/**
- * Retrieves a serializable object from the Bundle.
- *
- * @param key The key of the serializable object.
- * @return The serializable object, or null if not found.
- */
-inline fun Bundle.serializable(key: String): T? = when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
- else -> @Suppress("DEPRECATION") getSerializable(key) as? T
-}
-
-/**
- * Retrieves a serializable object from the Intent.
- *
- * @param key The key of the serializable object.
- * @return The serializable object, or null if not found.
- */
-inline fun Intent.serializable(key: String): T? = when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
- else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
-}
-
-/**
- * Checks if the CharSequence is not null and not empty.
- *
- * @return True if the CharSequence is not null and not empty, false otherwise.
- */
-fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
-
-fun String.concatUrl(vararg paths: String): String {
- val builder = StringBuilder(this.trimEnd('/'))
-
- paths.forEach { path ->
- val trimmedPath = path.trim('/')
- if (trimmedPath.isNotEmpty()) {
- builder.append('/').append(trimmedPath)
- }
- }
-
- return builder.toString()
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt
deleted file mode 100644
index 3bc20927..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig
-import com.v2ray.ang.util.JsonUtil
-
-object CustomFmt : FmtBase() {
- /**
- * Parses a JSON string into a ProfileItem object.
- *
- * @param str the JSON string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- val config = ProfileItem.create(EConfigType.CUSTOM)
-
- val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
- val outbound = fullConfig.getProxyOutbound()
-
- config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString()
- config.server = outbound?.getServerAddress()
- config.serverPort = outbound?.getServerPort().toString()
-
- return config
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt
deleted file mode 100644
index 73cdf958..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.NetworkType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.extension.isNotNullEmpty
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.util.HttpUtil
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-open class FmtBase {
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @param userInfo the user information to include in the URI
- * @param dicQuery the query parameters to include in the URI
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap?): String {
- val query = if (dicQuery != null)
- "?" + dicQuery.toList().joinToString(
- separator = "&",
- transform = { it.first + "=" + Utils.urlEncode(it.second) })
- else ""
-
- val url = String.format(
- "%s@%s:%s",
- Utils.urlEncode(userInfo ?: ""),
- Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
- config.serverPort
- )
-
- return "${url}${query}#${Utils.urlEncode(config.remarks)}"
- }
-
- /**
- * Extracts query parameters from a URI.
- *
- * @param uri the URI to extract query parameters from
- * @return a map of query parameters
- */
- fun getQueryParam(uri: URI): Map {
- return uri.rawQuery.split("&")
- .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
- }
-
- /**
- * Populates a ProfileItem object with values from query parameters.
- *
- * @param config the ProfileItem object to populate
- * @param queryParam the query parameters to use for populating the ProfileItem
- * @param allowInsecure whether to allow insecure connections
- */
- fun getItemFormQuery(config: ProfileItem, queryParam: Map, allowInsecure: Boolean) {
- config.network = queryParam["type"] ?: NetworkType.TCP.type
- config.headerType = queryParam["headerType"]
- config.host = queryParam["host"]
- config.path = queryParam["path"]
-
- config.seed = queryParam["seed"]
- config.quicSecurity = queryParam["quicSecurity"]
- config.quicKey = queryParam["key"]
- config.mode = queryParam["mode"]
- config.serviceName = queryParam["serviceName"]
- config.authority = queryParam["authority"]
- config.xhttpMode = queryParam["mode"]
- config.xhttpExtra = queryParam["extra"]
-
- config.security = queryParam["security"]
- if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
- config.security = null
- }
- config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
- allowInsecure
- } else {
- queryParam["allowInsecure"].orEmpty() == "1"
- }
- config.sni = queryParam["sni"]
- config.fingerPrint = queryParam["fp"]
- config.alpn = queryParam["alpn"]
- config.publicKey = queryParam["pbk"]
- config.shortId = queryParam["sid"]
- config.spiderX = queryParam["spx"]
- config.flow = queryParam["flow"]
- }
-
- /**
- * Creates a map of query parameters from a ProfileItem object.
- *
- * @param config the ProfileItem object to create query parameters from
- * @return a map of query parameters
- */
- fun getQueryDic(config: ProfileItem): HashMap {
- val dicQuery = HashMap()
- dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
- config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
- config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
- config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
- config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
- config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
- config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
- config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
-
- val networkType = NetworkType.fromString(config.network)
- dicQuery["type"] = networkType.type
-
- when (networkType) {
- NetworkType.TCP -> {
- dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
- config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
- }
-
- NetworkType.KCP -> {
- dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
- config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
- }
-
- NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
- config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
- config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
- }
-
- NetworkType.XHTTP -> {
- config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
- config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
- config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
- config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
- }
-
- NetworkType.HTTP, NetworkType.H2 -> {
- dicQuery["type"] = "http"
- config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
- config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
- }
-
-// NetworkType.QUIC -> {
-// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
-// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
-// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
-// }
-
- NetworkType.GRPC -> {
- config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
- config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
- config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
- }
- }
-
- return dicQuery
- }
-
- fun getServerAddress(profileItem: ProfileItem): String {
- if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
- return profileItem.server.orEmpty()
- }
-
- val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
- if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
- return domain
- }
- //Resolve and replace domain
- val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
- if (resolvedIps.isNullOrEmpty()) {
- return domain
- }
- return resolvedIps.first()
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt
deleted file mode 100644
index 8c641f24..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.extension.isNotNullEmpty
-import com.v2ray.ang.handler.V2rayConfigManager
-
-object HttpFmt : FmtBase() {
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
-
- outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = getServerAddress(profileItem)
- server.port = profileItem.serverPort.orEmpty().toInt()
- if (profileItem.username.isNotNullEmpty()) {
- val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
- socksUsersBean.user = profileItem.username.orEmpty()
- socksUsersBean.pass = profileItem.password.orEmpty()
- server.users = listOf(socksUsersBean)
- }
- }
-
- return outboundBean
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt
deleted file mode 100644
index 3b3dc88c..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt
+++ /dev/null
@@ -1,151 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.LOOPBACK
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.Hysteria2Bean
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.extension.idnHost
-import com.v2ray.ang.extension.isNotNullEmpty
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object Hysteria2Fmt : FmtBase() {
- /**
- * Parses a Hysteria2 URI string into a ProfileItem object.
- *
- * @param str the Hysteria2 URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
- val config = ProfileItem.create(EConfigType.HYSTERIA2)
-
- val uri = URI(Utils.fixIllegalUrl(str))
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
- config.server = uri.idnHost
- config.serverPort = uri.port.toString()
- config.password = uri.userInfo
- config.security = AppConfig.TLS
-
- if (!uri.rawQuery.isNullOrEmpty()) {
- val queryParam = getQueryParam(uri)
-
- config.security = queryParam["security"] ?: AppConfig.TLS
- config.insecure = if (queryParam["insecure"].isNullOrEmpty()) {
- allowInsecure
- } else {
- queryParam["insecure"].orEmpty() == "1"
- }
- config.sni = queryParam["sni"]
- config.alpn = queryParam["alpn"]
-
- config.obfsPassword = queryParam["obfs-password"]
- config.portHopping = queryParam["mport"]
- config.pinSHA256 = queryParam["pinSHA256"]
-
- }
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem): String {
- val dicQuery = HashMap()
-
- config.security.let { if (it != null) dicQuery["security"] = it }
- config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
- config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
- config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" }
-
- if (config.obfsPassword.isNotNullEmpty()) {
- dicQuery["obfs"] = "salamander"
- dicQuery["obfs-password"] = config.obfsPassword.orEmpty()
- }
- if (config.portHopping.isNotNullEmpty()) {
- dicQuery["mport"] = config.portHopping.orEmpty()
- }
- if (config.pinSHA256.isNotNullEmpty()) {
- dicQuery["pinSHA256"] = config.pinSHA256.orEmpty()
- }
-
- return toUri(config, config.password, dicQuery)
- }
-
- /**
- * Converts a ProfileItem object to a Hysteria2Bean object.
- *
- * @param config the ProfileItem object to convert
- * @param socksPort the port number for the socks5 proxy
- * @return the converted Hysteria2Bean object, or null if conversion fails
- */
- fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
-
- val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
- Hysteria2Bean.ObfsBean(
- type = "salamander",
- salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
- password = config.obfsPassword
- )
- )
-
- val transport = if (config.portHopping.isNullOrEmpty()) null else
- Hysteria2Bean.TransportBean(
- type = "udp",
- udp = Hysteria2Bean.TransportBean.TransportUdpBean(
- hopInterval = (config.portHoppingInterval ?: "30") + "s"
- )
- )
-
- val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else
- Hysteria2Bean.BandwidthBean(
- down = config.bandwidthDown,
- up = config.bandwidthUp,
- )
-
- val server =
- if (config.portHopping.isNullOrEmpty())
- config.getServerAddressAndPort()
- else
- Utils.getIpv6Address(config.server) + ":" + config.portHopping
-
- val bean = Hysteria2Bean(
- server = server,
- auth = config.password,
- obfs = obfs,
- transport = transport,
- bandwidth = bandwidth,
- socks5 = Hysteria2Bean.Socks5Bean(
- listen = "$LOOPBACK:${socksPort}",
- ),
- http = Hysteria2Bean.Socks5Bean(
- listen = "$LOOPBACK:${socksPort}",
- ),
- tls = Hysteria2Bean.TlsBean(
- sni = config.sni ?: config.server,
- insecure = config.insecure,
- pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256
- )
- )
- return bean
- }
-
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
- return outboundBean
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
deleted file mode 100644
index 87ba74f8..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.v2ray.ang.fmt
-
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.NetworkType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.extension.idnHost
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object ShadowsocksFmt : FmtBase() {
- /**
- * Parses a Shadowsocks URI string into a ProfileItem object.
- *
- * @param str the Shadowsocks URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- return parseSip002(str) ?: parseLegacy(str)
- }
-
- /**
- * Parses a SIP002 Shadowsocks URI string into a ProfileItem object.
- *
- * @param str the SIP002 Shadowsocks URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parseSip002(str: String): ProfileItem? {
- val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
-
- val uri = URI(Utils.fixIllegalUrl(str))
- if (uri.idnHost.isEmpty()) return null
- if (uri.port <= 0) return null
- if (uri.userInfo.isNullOrEmpty()) return null
-
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
- config.server = uri.idnHost
- config.serverPort = uri.port.toString()
-
- val result = if (uri.userInfo.contains(":")) {
- uri.userInfo.split(":", limit = 2)
- } else {
- Utils.decode(uri.userInfo).split(":", limit = 2)
- }
- if (result.count() == 2) {
- config.method = result.first()
- config.password = result.last()
- }
-
- if (!uri.rawQuery.isNullOrEmpty()) {
- val queryParam = getQueryParam(uri)
- if (queryParam["plugin"]?.contains("obfs=http") == true) {
- val queryPairs = HashMap()
- for (pair in queryParam["plugin"]?.split(";") ?: listOf()) {
- val idx = pair.split("=")
- if (idx.count() == 2) {
- queryPairs.put(idx.first(), idx.last())
- }
- }
- config.network = NetworkType.TCP.type
- config.headerType = "http"
- config.host = queryPairs["obfs-host"]
- config.path = queryPairs["path"]
- }
- }
-
- return config
- }
-
- /**
- * Parses a legacy Shadowsocks URI string into a ProfileItem object.
- *
- * @param str the legacy Shadowsocks URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parseLegacy(str: String): ProfileItem? {
- val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
- var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
- val indexSplit = result.indexOf("#")
- if (indexSplit > 0) {
- try {
- config.remarks =
- Utils.urlDecode(result.substring(indexSplit + 1, result.length))
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
- }
-
- result = result.substring(0, indexSplit)
- }
-
- //part decode
- val indexS = result.indexOf("@")
- result = if (indexS > 0) {
- Utils.decode(result.substring(0, indexS)) + result.substring(
- indexS,
- result.length
- )
- } else {
- Utils.decode(result)
- }
-
- val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
- val match = legacyPattern.matchEntire(result) ?: return null
-
- config.server = match.groupValues[3].removeSurrounding("[", "]")
- config.serverPort = match.groupValues[4]
- config.password = match.groupValues[2]
- config.method = match.groupValues[1].lowercase()
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem): String {
- val pw = "${config.method}:${config.password}"
-
- return toUri(config, Utils.encode(pw), null)
- }
-
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
-
- outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = getServerAddress(profileItem)
- server.port = profileItem.serverPort.orEmpty().toInt()
- server.password = profileItem.password
- server.method = profileItem.method
- }
-
- val sni = outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTransportSettings(it, profileItem)
- }
-
- outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
- }
-
- return outboundBean
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt
deleted file mode 100644
index 30bc08e4..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.extension.idnHost
-import com.v2ray.ang.extension.isNotNullEmpty
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object SocksFmt : FmtBase() {
- /**
- * Parses a Socks URI string into a ProfileItem object.
- *
- * @param str the Socks URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- val config = ProfileItem.create(EConfigType.SOCKS)
-
- val uri = URI(Utils.fixIllegalUrl(str))
- if (uri.idnHost.isEmpty()) return null
- if (uri.port <= 0) return null
-
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
- config.server = uri.idnHost
- config.serverPort = uri.port.toString()
-
- if (uri.userInfo?.isEmpty() == false) {
- val result = Utils.decode(uri.userInfo).split(":", limit = 2)
- if (result.count() == 2) {
- config.username = result.first()
- config.password = result.last()
- }
- }
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem): String {
- val pw =
- if (config.username.isNotNullEmpty())
- "${config.username}:${config.password}"
- else
- ":"
-
- return toUri(config, Utils.encode(pw), null)
- }
-
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
-
- outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = getServerAddress(profileItem)
- server.port = profileItem.serverPort.orEmpty().toInt()
- if (profileItem.username.isNotNullEmpty()) {
- val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
- socksUsersBean.user = profileItem.username.orEmpty()
- socksUsersBean.pass = profileItem.password.orEmpty()
- server.users = listOf(socksUsersBean)
- }
- }
-
- return outboundBean
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt
deleted file mode 100644
index 446ef99c..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.NetworkType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.extension.idnHost
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object TrojanFmt : FmtBase() {
- /**
- * Parses a Trojan URI string into a ProfileItem object.
- *
- * @param str the Trojan URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
- val config = ProfileItem.create(EConfigType.TROJAN)
-
- val uri = URI(Utils.fixIllegalUrl(str))
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
- config.server = uri.idnHost
- config.serverPort = uri.port.toString()
- config.password = uri.userInfo
-
- if (uri.rawQuery.isNullOrEmpty()) {
- config.network = NetworkType.TCP.type
- config.security = AppConfig.TLS
- config.insecure = allowInsecure
- } else {
- val queryParam = getQueryParam(uri)
-
- getItemFormQuery(config, queryParam, allowInsecure)
- config.security = queryParam["security"] ?: AppConfig.TLS
- }
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem): String {
- val dicQuery = getQueryDic(config)
-
- return toUri(config, config.password, dicQuery)
- }
-
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
-
- outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = getServerAddress(profileItem)
- server.port = profileItem.serverPort.orEmpty().toInt()
- server.password = profileItem.password
- server.flow = profileItem.flow
- }
-
- val sni = outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTransportSettings(it, profileItem)
- }
-
- outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
- }
-
- return outboundBean
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt
deleted file mode 100644
index 9242f0ec..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.extension.idnHost
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object VlessFmt : FmtBase() {
-
- /**
- * Parses a Vless URI string into a ProfileItem object.
- *
- * @param str the Vless URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
- val config = ProfileItem.create(EConfigType.VLESS)
-
- val uri = URI(Utils.fixIllegalUrl(str))
- if (uri.rawQuery.isNullOrEmpty()) return null
- val queryParam = getQueryParam(uri)
-
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
- config.server = uri.idnHost
- config.serverPort = uri.port.toString()
- config.password = uri.userInfo
- config.method = queryParam["encryption"] ?: "none"
-
- getItemFormQuery(config, queryParam, allowInsecure)
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem): String {
- val dicQuery = getQueryDic(config)
- dicQuery["encryption"] = config.method ?: "none"
-
- return toUri(config, config.password, dicQuery)
- }
-
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
-
- outboundBean?.settings?.vnext?.first()?.let { vnext ->
- vnext.address = getServerAddress(profileItem)
- vnext.port = profileItem.serverPort.orEmpty().toInt()
- vnext.users[0].id = profileItem.password.orEmpty()
- vnext.users[0].encryption = profileItem.method
- vnext.users[0].flow = profileItem.flow
- }
-
- val sni = outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTransportSettings(it, profileItem)
- }
-
- outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
- }
-
- return outboundBean
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt
deleted file mode 100644
index 4201f4dc..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package com.v2ray.ang.fmt
-
-import android.text.TextUtils
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.NetworkType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.dto.VmessQRCode
-import com.v2ray.ang.extension.idnHost
-import com.v2ray.ang.extension.isNotNullEmpty
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.JsonUtil
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object VmessFmt : FmtBase() {
- /**
- * Parses a Vmess string into a ProfileItem object.
- *
- * @param str the Vmess string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
- return parseVmessStd(str)
- }
-
- var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
- val config = ProfileItem.create(EConfigType.VMESS)
-
- var result = str.replace(EConfigType.VMESS.protocolScheme, "")
- result = Utils.decode(result)
- if (TextUtils.isEmpty(result)) {
- Log.w(AppConfig.TAG, "Toast decoding failed")
- return null
- }
- val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
- // Although VmessQRCode fields are non null, looks like Gson may still create null fields
- if (TextUtils.isEmpty(vmessQRCode.add)
- || TextUtils.isEmpty(vmessQRCode.port)
- || TextUtils.isEmpty(vmessQRCode.id)
- || TextUtils.isEmpty(vmessQRCode.net)
- ) {
- Log.w(AppConfig.TAG, "Toast incorrect protocol")
- return null
- }
-
- config.remarks = vmessQRCode.ps
- config.server = vmessQRCode.add
- config.serverPort = vmessQRCode.port
- config.password = vmessQRCode.id
- config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
-
- config.network = vmessQRCode.net ?: NetworkType.TCP.type
- config.headerType = vmessQRCode.type
- config.host = vmessQRCode.host
- config.path = vmessQRCode.path
-
- when (NetworkType.fromString(config.network)) {
- NetworkType.KCP -> {
- config.seed = vmessQRCode.path
- }
-
-// NetworkType.QUIC -> {
-// config.quicSecurity = vmessQRCode.host
-// config.quicKey = vmessQRCode.path
-// }
-
- NetworkType.GRPC -> {
- config.mode = vmessQRCode.type
- config.serviceName = vmessQRCode.path
- config.authority = vmessQRCode.host
- }
-
- else -> {}
- }
-
- config.security = vmessQRCode.tls
- config.insecure = allowInsecure
- config.sni = vmessQRCode.sni
- config.fingerPrint = vmessQRCode.fp
- config.alpn = vmessQRCode.alpn
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem): String {
- val vmessQRCode = VmessQRCode()
-
- vmessQRCode.v = "2"
- vmessQRCode.ps = config.remarks
- vmessQRCode.add = config.server.orEmpty()
- vmessQRCode.port = config.serverPort.orEmpty()
- vmessQRCode.id = config.password.orEmpty()
- vmessQRCode.scy = config.method.orEmpty()
- vmessQRCode.aid = "0"
-
- vmessQRCode.net = config.network.orEmpty()
- vmessQRCode.type = config.headerType.orEmpty()
- when (NetworkType.fromString(config.network)) {
- NetworkType.KCP -> {
- vmessQRCode.path = config.seed.orEmpty()
- }
-
-// NetworkType.QUIC -> {
-// vmessQRCode.host = config.quicSecurity.orEmpty()
-// vmessQRCode.path = config.quicKey.orEmpty()
-// }
-
- NetworkType.GRPC -> {
- vmessQRCode.type = config.mode.orEmpty()
- vmessQRCode.path = config.serviceName.orEmpty()
- vmessQRCode.host = config.authority.orEmpty()
- }
-
- else -> {}
- }
-
- config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
- config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
-
- vmessQRCode.tls = config.security.orEmpty()
- vmessQRCode.sni = config.sni.orEmpty()
- vmessQRCode.fp = config.fingerPrint.orEmpty()
- vmessQRCode.alpn = config.alpn.orEmpty()
-
- val json = JsonUtil.toJson(vmessQRCode)
- return Utils.encode(json)
- }
-
- /**
- * Parses a standard Vmess URI string into a ProfileItem object.
- *
- * @param str the standard Vmess URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parseVmessStd(str: String): ProfileItem? {
- val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
- val config = ProfileItem.create(EConfigType.VMESS)
-
- val uri = URI(Utils.fixIllegalUrl(str))
- if (uri.rawQuery.isNullOrEmpty()) return null
- val queryParam = getQueryParam(uri)
-
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
- config.server = uri.idnHost
- config.serverPort = uri.port.toString()
- config.password = uri.userInfo
- config.method = AppConfig.DEFAULT_SECURITY
-
- getItemFormQuery(config, queryParam, allowInsecure)
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
-
- outboundBean?.settings?.vnext?.first()?.let { vnext ->
- vnext.address = getServerAddress(profileItem)
- vnext.port = profileItem.serverPort.orEmpty().toInt()
- vnext.users[0].id = profileItem.password.orEmpty()
- vnext.users[0].security = profileItem.method
- }
-
- val sni = outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTransportSettings(it, profileItem)
- }
-
- outboundBean?.streamSettings?.let {
- V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
- }
-
- return outboundBean
- }
-
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt
deleted file mode 100644
index 8f1cec84..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-package com.v2ray.ang.fmt
-
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.extension.idnHost
-import com.v2ray.ang.extension.removeWhiteSpace
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object WireguardFmt : FmtBase() {
- /**
- * Parses a URI string into a ProfileItem object.
- *
- * @param str the URI string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parse(str: String): ProfileItem? {
- val config = ProfileItem.create(EConfigType.WIREGUARD)
-
- val uri = URI(Utils.fixIllegalUrl(str))
- if (uri.rawQuery.isNullOrEmpty()) return null
- val queryParam = getQueryParam(uri)
-
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
- config.server = uri.idnHost
- config.serverPort = uri.port.toString()
-
- config.secretKey = uri.userInfo.orEmpty()
- config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
- config.publicKey = queryParam["publickey"].orEmpty()
- config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
- config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
- config.reserved = queryParam["reserved"] ?: "0,0,0"
-
- return config
- }
-
- /**
- * Parses a Wireguard configuration file string into a ProfileItem object.
- *
- * @param str the Wireguard configuration file string to parse
- * @return the parsed ProfileItem object, or null if parsing fails
- */
- fun parseWireguardConfFile(str: String): ProfileItem? {
- val config = ProfileItem.create(EConfigType.WIREGUARD)
-
- val interfaceParams: MutableMap = mutableMapOf()
- val peerParams: MutableMap = mutableMapOf()
-
- var currentSection: String? = null
-
- str.lines().forEach { line ->
- val trimmedLine = line.trim()
-
- if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
- return@forEach
- }
-
- when {
- trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
- trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
- else -> {
- if (currentSection != null) {
- val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
- if (parts.size == 2) {
- val key = parts[0].lowercase()
- val value = parts[1]
- when (currentSection) {
- "Interface" -> interfaceParams[key] = value
- "Peer" -> peerParams[key] = value
- }
- }
- }
- }
- }
- }
-
- config.secretKey = interfaceParams["privatekey"].orEmpty()
- config.remarks = System.currentTimeMillis().toString()
- config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
- config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
- config.publicKey = peerParams["publickey"].orEmpty()
- config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
- val endpoint = peerParams["endpoint"].orEmpty()
- val endpointParts = endpoint.split(":", limit = 2)
- if (endpointParts.size == 2) {
- config.server = endpointParts[0]
- config.serverPort = endpointParts[1]
- } else {
- config.server = endpoint
- config.serverPort = ""
- }
- config.reserved = peerParams["reserved"] ?: "0,0,0"
-
- return config
- }
-
- /**
- * Converts a ProfileItem object to an OutboundBean object.
- *
- * @param profileItem the ProfileItem object to convert
- * @return the converted OutboundBean object, or null if conversion fails
- */
- fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
-
- outboundBean?.settings?.let { wireguard ->
- wireguard.secretKey = profileItem.secretKey
- wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
- wireguard.peers?.firstOrNull()?.let { peer ->
- peer.publicKey = profileItem.publicKey.orEmpty()
- peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
- peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
- }
- wireguard.mtu = profileItem.mtu
- wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
- }
-
- return outboundBean
- }
-
- /**
- * Converts a ProfileItem object to a URI string.
- *
- * @param config the ProfileItem object to convert
- * @return the converted URI string
- */
- fun toUri(config: ProfileItem): String {
- val dicQuery = HashMap()
-
- dicQuery["publickey"] = config.publicKey.orEmpty()
- if (config.reserved != null) {
- dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty()
- }
- dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty()
- if (config.mtu != null) {
- dicQuery["mtu"] = config.mtu.toString()
- }
- if (config.preSharedKey != null) {
- dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty()
- }
-
- return toUri(config, config.secretKey, dicQuery)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt
deleted file mode 100644
index d24ae0c2..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt
+++ /dev/null
@@ -1,493 +0,0 @@
-package com.v2ray.ang.handler
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.text.TextUtils
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.HY2
-import com.v2ray.ang.R
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.SubscriptionItem
-import com.v2ray.ang.fmt.CustomFmt
-import com.v2ray.ang.fmt.Hysteria2Fmt
-import com.v2ray.ang.fmt.ShadowsocksFmt
-import com.v2ray.ang.fmt.SocksFmt
-import com.v2ray.ang.fmt.TrojanFmt
-import com.v2ray.ang.fmt.VlessFmt
-import com.v2ray.ang.fmt.VmessFmt
-import com.v2ray.ang.fmt.WireguardFmt
-import com.v2ray.ang.util.HttpUtil
-import com.v2ray.ang.util.JsonUtil
-import com.v2ray.ang.util.QRCodeDecoder
-import com.v2ray.ang.util.Utils
-import java.net.URI
-
-object AngConfigManager {
-
-
- /**
- * Shares the configuration to the clipboard.
- *
- * @param context The context.
- * @param guid The GUID of the configuration.
- * @return The result code.
- */
- fun share2Clipboard(context: Context, guid: String): Int {
- try {
- val conf = shareConfig(guid)
- if (TextUtils.isEmpty(conf)) {
- return -1
- }
-
- Utils.setClipboard(context, conf)
-
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
- return -1
- }
- return 0
- }
-
- /**
- * Shares non-custom configurations to the clipboard.
- *
- * @param context The context.
- * @param serverList The list of server GUIDs.
- * @return The number of configurations shared.
- */
- fun shareNonCustomConfigsToClipboard(context: Context, serverList: List): Int {
- try {
- val sb = StringBuilder()
- for (guid in serverList) {
- val url = shareConfig(guid)
- if (TextUtils.isEmpty(url)) {
- continue
- }
- sb.append(url)
- sb.appendLine()
- }
- if (sb.count() > 0) {
- Utils.setClipboard(context, sb.toString())
- }
- return sb.lines().count()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
- return -1
- }
- }
-
- /**
- * Shares the configuration as a QR code.
- *
- * @param guid The GUID of the configuration.
- * @return The QR code bitmap.
- */
- fun share2QRCode(guid: String): Bitmap? {
- try {
- val conf = shareConfig(guid)
- if (TextUtils.isEmpty(conf)) {
- return null
- }
- return QRCodeDecoder.createQRCode(conf)
-
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
- return null
- }
- }
-
- /**
- * Shares the full content of the configuration to the clipboard.
- *
- * @param context The context.
- * @param guid The GUID of the configuration.
- * @return The result code.
- */
- fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
- try {
- if (guid == null) return -1
- val result = V2rayConfigManager.getV2rayConfig(context, guid)
- if (result.status) {
- val config = MmkvManager.decodeServerConfig(guid)
- if (config?.configType == EConfigType.HYSTERIA2) {
- val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
- val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort)
- Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content)
- return 0
- }
- Utils.setClipboard(context, result.content)
- } else {
- return -1
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
- return -1
- }
- return 0
- }
-
- /**
- * Shares the configuration.
- *
- * @param guid The GUID of the configuration.
- * @return The configuration string.
- */
- private fun shareConfig(guid: String): String {
- try {
- val config = MmkvManager.decodeServerConfig(guid) ?: return ""
-
- return config.configType.protocolScheme + when (config.configType) {
- EConfigType.VMESS -> VmessFmt.toUri(config)
- EConfigType.CUSTOM -> ""
- EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
- EConfigType.SOCKS -> SocksFmt.toUri(config)
- EConfigType.HTTP -> ""
- EConfigType.VLESS -> VlessFmt.toUri(config)
- EConfigType.TROJAN -> TrojanFmt.toUri(config)
- EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
- EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
- return ""
- }
- }
-
- /**
- * Imports a batch of configurations.
- *
- * @param server The server string.
- * @param subid The subscription ID.
- * @param append Whether to append the configurations.
- * @return A pair containing the number of configurations and subscriptions imported.
- */
- fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair {
- var count = parseBatchConfig(Utils.decode(server), subid, append)
- if (count <= 0) {
- count = parseBatchConfig(server, subid, append)
- }
- if (count <= 0) {
- count = parseCustomConfigServer(server, subid)
- }
-
- var countSub = parseBatchSubscription(server)
- if (countSub <= 0) {
- countSub = parseBatchSubscription(Utils.decode(server))
- }
- if (countSub > 0) {
- updateConfigViaSubAll()
- }
-
- return count to countSub
- }
-
- /**
- * Parses a batch of subscriptions.
- *
- * @param servers The servers string.
- * @return The number of subscriptions parsed.
- */
- private fun parseBatchSubscription(servers: String?): Int {
- try {
- if (servers == null) {
- return 0
- }
-
- var count = 0
- servers.lines()
- .distinct()
- .forEach { str ->
- if (Utils.isValidSubUrl(str)) {
- count += importUrlAsSubscription(str)
- }
- }
- return count
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
- }
- return 0
- }
-
- /**
- * Parses a batch of configurations.
- *
- * @param servers The servers string.
- * @param subid The subscription ID.
- * @param append Whether to append the configurations.
- * @return The number of configurations parsed.
- */
- private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
- try {
- if (servers == null) {
- return 0
- }
- val removedSelectedServer =
- if (!TextUtils.isEmpty(subid) && !append) {
- MmkvManager.decodeServerConfig(
- MmkvManager.getSelectServer().orEmpty()
- )?.let {
- if (it.subscriptionId == subid) {
- return@let it
- }
- return@let null
- }
- } else {
- null
- }
- if (!append) {
- MmkvManager.removeServerViaSubid(subid)
- }
-
- val subItem = MmkvManager.decodeSubscription(subid)
- var count = 0
- servers.lines()
- .distinct()
- .reversed()
- .forEach {
- val resId = parseConfig(it, subid, subItem, removedSelectedServer)
- if (resId == 0) {
- count++
- }
- }
- return count
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to parse batch config", e)
- }
- return 0
- }
-
- /**
- * Parses a custom configuration server.
- *
- * @param server The server string.
- * @param subid The subscription ID.
- * @return The number of configurations parsed.
- */
- private fun parseCustomConfigServer(server: String?, subid: String): Int {
- if (server == null) {
- return 0
- }
- if (server.contains("inbounds")
- && server.contains("outbounds")
- && server.contains("routing")
- ) {
- try {
- val serverList: Array =
- JsonUtil.fromJson(server, Array::class.java)
-
- if (serverList.isNotEmpty()) {
- var count = 0
- for (srv in serverList.reversed()) {
- val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
- config.subscriptionId = subid
- val key = MmkvManager.encodeServerConfig("", config)
- MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
- count += 1
- }
- return count
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
- }
-
- try {
- // For compatibility
- val config = CustomFmt.parse(server) ?: return 0
- config.subscriptionId = subid
- val key = MmkvManager.encodeServerConfig("", config)
- MmkvManager.encodeServerRaw(key, server)
- return 1
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
- }
- return 0
- } else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
- try {
- val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
- val key = MmkvManager.encodeServerConfig("", config)
- MmkvManager.encodeServerRaw(key, server)
- return 1
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
- }
- return 0
- } else {
- return 0
- }
- }
-
- /**
- * Parses the configuration from a QR code or string.
- *
- * @param str The configuration string.
- * @param subid The subscription ID.
- * @param subItem The subscription item.
- * @param removedSelectedServer The removed selected server.
- * @return The result code.
- */
- private fun parseConfig(
- str: String?,
- subid: String,
- subItem: SubscriptionItem?,
- removedSelectedServer: ProfileItem?
- ): Int {
- try {
- if (str == null || TextUtils.isEmpty(str)) {
- return R.string.toast_none_data
- }
-
- val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
- VmessFmt.parse(str)
- } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
- ShadowsocksFmt.parse(str)
- } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
- SocksFmt.parse(str)
- } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
- TrojanFmt.parse(str)
- } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
- VlessFmt.parse(str)
- } else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
- WireguardFmt.parse(str)
- } else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
- Hysteria2Fmt.parse(str)
- } else {
- null
- }
-
- if (config == null) {
- return R.string.toast_incorrect_protocol
- }
- //filter
- if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) {
- val matched = Regex(pattern = subItem.filter ?: "")
- .containsMatchIn(input = config.remarks)
- if (!matched) return -1
- }
-
- config.subscriptionId = subid
- val guid = MmkvManager.encodeServerConfig("", config)
- if (removedSelectedServer != null &&
- config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort
- ) {
- MmkvManager.setSelectServer(guid)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to parse config", e)
- return -1
- }
- return 0
- }
-
- /**
- * Updates the configuration via all subscriptions.
- *
- * @return The number of configurations updated.
- */
- fun updateConfigViaSubAll(): Int {
- var count = 0
- try {
- MmkvManager.decodeSubscriptions().forEach {
- count += updateConfigViaSub(it)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
- return 0
- }
- return count
- }
-
- /**
- * Updates the configuration via a subscription.
- *
- * @param it The subscription item.
- * @return The number of configurations updated.
- */
- fun updateConfigViaSub(it: Pair): Int {
- try {
- if (TextUtils.isEmpty(it.first)
- || TextUtils.isEmpty(it.second.remarks)
- || TextUtils.isEmpty(it.second.url)
- ) {
- return 0
- }
- if (!it.second.enabled) {
- return 0
- }
- val url = HttpUtil.toIdnUrl(it.second.url)
- if (!Utils.isValidUrl(url)) {
- return 0
- }
- if (!it.second.allowInsecureUrl) {
- if (!Utils.isValidSubUrl(url)) {
- return 0
- }
- }
- Log.i(AppConfig.TAG, url)
-
- var configText = try {
- val httpPort = SettingsManager.getHttpPort()
- HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort)
- } catch (e: Exception) {
- Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
- ""
- }
- if (configText.isEmpty()) {
- configText = try {
- HttpUtil.getUrlContentWithUserAgent(url)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
- ""
- }
- }
- if (configText.isEmpty()) {
- return 0
- }
- return parseConfigViaSub(configText, it.first, false)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
- return 0
- }
- }
-
- /**
- * Parses the configuration via a subscription.
- *
- * @param server The server string.
- * @param subid The subscription ID.
- * @param append Whether to append the configurations.
- * @return The number of configurations parsed.
- */
- private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
- var count = parseBatchConfig(Utils.decode(server), subid, append)
- if (count <= 0) {
- count = parseBatchConfig(server, subid, append)
- }
- if (count <= 0) {
- count = parseCustomConfigServer(server, subid)
- }
- return count
- }
-
- /**
- * Imports a URL as a subscription.
- *
- * @param url The URL.
- * @return The number of subscriptions imported.
- */
- private fun importUrlAsSubscription(url: String): Int {
- val subscriptions = MmkvManager.decodeSubscriptions()
- subscriptions.forEach {
- if (it.second.url == url) {
- return 0
- }
- }
- val uri = URI(Utils.fixIllegalUrl(url))
- val subItem = SubscriptionItem()
- subItem.remarks = uri.fragment ?: "import sub"
- subItem.url = url
- MmkvManager.encodeSubscription("", subItem)
- return 1
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt
deleted file mode 100644
index 9dd90c76..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-package com.v2ray.ang.handler
-
-import android.util.Log
-import com.tencent.mmkv.MMKV
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.NetworkType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.ServerConfig
-import com.v2ray.ang.extension.removeWhiteSpace
-import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
-import com.v2ray.ang.util.JsonUtil
-
-object MigrateManager {
- private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
- private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
-
- /**
- * Migrates server configurations to profile items.
- *
- * @return True if migration was successful, false otherwise.
- */
- fun migrateServerConfig2Profile(): Boolean {
- if (serverStorage.count().toInt() == 0) {
- return false
- }
- val serverList = serverStorage.allKeys() ?: return false
- Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + serverList.count())
-
- for (guid in serverList) {
- var configOld = decodeServerConfigOld(guid) ?: continue
- var config = decodeServerConfig(guid)
- if (config != null) {
- serverStorage.remove(guid)
- continue
- }
- config = migrateServerConfig2ProfileSub(configOld) ?: continue
- config.subscriptionId = configOld.subscriptionId
-
- MmkvManager.encodeServerConfig(guid, config)
-
- //check and remove old
- decodeServerConfig(guid) ?: continue
- serverStorage.remove(guid)
- Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks)
- }
- Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end")
- return true
- }
-
- /**
- * Migrates a server configuration to a profile item.
- *
- * @param configOld The old server configuration.
- * @return The profile item.
- */
- private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
- return when (configOld.getProxyOutbound()?.protocol) {
- EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
- EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld)
- EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld)
- EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld)
-
- EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld)
- EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld)
- EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld)
- EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld)
-
- EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld)
-
- else -> null
- }
- }
-
- /**
- * Migrates a common server configuration to a profile item.
- *
- * @param configOld The old server configuration.
- * @return The profile item.
- */
- private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
- val config = ProfileItem.create(configOld.configType)
-
- val outbound = configOld.getProxyOutbound() ?: return null
- config.remarks = configOld.remarks
- config.server = outbound.getServerAddress()
- config.serverPort = outbound.getServerPort().toString()
- config.method = outbound.getSecurityEncryption()
- config.password = outbound.getPassword()
- config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
-
- config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
- outbound.getTransportSettingDetails()?.let { transportDetails ->
- config.headerType = transportDetails[0].orEmpty()
- config.host = transportDetails[1].orEmpty()
- config.path = transportDetails[2].orEmpty()
- }
-
- config.seed = outbound?.streamSettings?.kcpSettings?.seed
- config.quicSecurity = outbound?.streamSettings?.quicSettings?.security
- config.quicKey = outbound?.streamSettings?.quicSettings?.key
- config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun"
- config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName
- config.authority = outbound?.streamSettings?.grpcSettings?.authority
-
- config.security = outbound.streamSettings?.security
- val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings
- config.insecure = tlsSettings?.allowInsecure
- config.sni = tlsSettings?.serverName
- config.fingerPrint = tlsSettings?.fingerprint
- config.alpn = tlsSettings?.alpn?.joinToString(",").removeWhiteSpace().toString()
-
- config.publicKey = tlsSettings?.publicKey
- config.shortId = tlsSettings?.shortId
- config.spiderX = tlsSettings?.spiderX
-
- return config
- }
-
- /**
- * Migrates a SOCKS server configuration to a profile item.
- *
- * @param configOld The old server configuration.
- * @return The profile item.
- */
- private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
- val config = ProfileItem.create(EConfigType.SOCKS)
-
- val outbound = configOld.getProxyOutbound() ?: return null
- config.remarks = configOld.remarks
- config.server = outbound.getServerAddress()
- config.serverPort = outbound.getServerPort().toString()
- config.username = outbound.settings?.servers?.first()?.users?.first()?.user
- config.password = outbound.getPassword()
-
- return config
- }
-
- /**
- * Migrates an HTTP server configuration to a profile item.
- *
- * @param configOld The old server configuration.
- * @return The profile item.
- */
- private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
- val config = ProfileItem.create(EConfigType.HTTP)
-
- val outbound = configOld.getProxyOutbound() ?: return null
- config.remarks = configOld.remarks
- config.server = outbound.getServerAddress()
- config.serverPort = outbound.getServerPort().toString()
- config.username = outbound.settings?.servers?.first()?.users?.first()?.user
- config.password = outbound.getPassword()
-
- return config
- }
-
- /**
- * Migrates a WireGuard server configuration to a profile item.
- *
- * @param configOld The old server configuration.
- * @return The profile item.
- */
- private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
- val config = ProfileItem.create(EConfigType.WIREGUARD)
-
- val outbound = configOld.getProxyOutbound() ?: return null
- config.remarks = configOld.remarks
- config.server = outbound.getServerAddress()
- config.serverPort = outbound.getServerPort().toString()
-
- outbound.settings?.let { wireguard ->
- config.secretKey = wireguard.secretKey
- config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString()
- config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
- config.mtu = wireguard.mtu
- config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()
- }
- return config
- }
-
- /**
- * Migrates a Hysteria2 server configuration to a profile item.
- *
- * @param configOld The old server configuration.
- * @return The profile item.
- */
- private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
- val config = ProfileItem.create(EConfigType.HYSTERIA2)
-
- val outbound = configOld.getProxyOutbound() ?: return null
- config.remarks = configOld.remarks
- config.server = outbound.getServerAddress()
- config.serverPort = outbound.getServerPort().toString()
- config.password = outbound.getPassword()
-
- config.security = AppConfig.TLS
- outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
- config.insecure = tlsSetting.allowInsecure
- config.sni = tlsSetting.serverName
- config.alpn = tlsSetting.alpn?.joinToString(",").removeWhiteSpace().orEmpty()
-
- }
- config.obfsPassword = outbound.settings?.obfsPassword
-
- return config
- }
-
- /**
- * Migrates a custom server configuration to a profile item.
- *
- * @param configOld The old server configuration.
- * @return The profile item.
- */
- private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
- val config = ProfileItem.create(EConfigType.CUSTOM)
-
- val outbound = configOld.getProxyOutbound() ?: return null
- config.remarks = configOld.remarks
- config.server = outbound.getServerAddress()
- config.serverPort = outbound.getServerPort().toString()
-
- return config
- }
-
- /**
- * Decodes the old server configuration.
- *
- * @param guid The server GUID.
- * @return The old server configuration.
- */
- private fun decodeServerConfigOld(guid: String): ServerConfig? {
- if (guid.isBlank()) {
- return null
- }
- val json = serverStorage.decodeString(guid)
- if (json.isNullOrBlank()) {
- return null
- }
- return JsonUtil.fromJson(json, ServerConfig::class.java)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt
deleted file mode 100644
index 9b589dd0..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt
+++ /dev/null
@@ -1,588 +0,0 @@
-package com.v2ray.ang.handler
-
-import com.tencent.mmkv.MMKV
-import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
-import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
-import com.v2ray.ang.dto.AssetUrlItem
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.RulesetItem
-import com.v2ray.ang.dto.ServerAffiliationInfo
-import com.v2ray.ang.dto.SubscriptionItem
-import com.v2ray.ang.util.JsonUtil
-import com.v2ray.ang.util.Utils
-
-object MmkvManager {
-
- //region private
-
- //private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
- private const val ID_MAIN = "MAIN"
- private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
- private const val ID_SERVER_RAW = "SERVER_RAW"
- private const val ID_SERVER_AFF = "SERVER_AFF"
- private const val ID_SUB = "SUB"
- private const val ID_ASSET = "ASSET"
- private const val ID_SETTING = "SETTING"
- private const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
- private const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
- private const val KEY_SUB_IDS = "SUB_IDS"
-
- //private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
- private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
- private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
- private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
- private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
- private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
- private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
- private val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
-
- //endregion
-
- //region Server
-
- /**
- * Gets the selected server GUID.
- *
- * @return The selected server GUID.
- */
- fun getSelectServer(): String? {
- return mainStorage.decodeString(KEY_SELECTED_SERVER)
- }
-
- /**
- * Sets the selected server GUID.
- *
- * @param guid The server GUID.
- */
- fun setSelectServer(guid: String) {
- mainStorage.encode(KEY_SELECTED_SERVER, guid)
- }
-
- /**
- * Encodes the server list.
- *
- * @param serverList The list of server GUIDs.
- */
- fun encodeServerList(serverList: MutableList) {
- mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
- }
-
- /**
- * Decodes the server list.
- *
- * @return The list of server GUIDs.
- */
- fun decodeServerList(): MutableList {
- val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
- return if (json.isNullOrBlank()) {
- mutableListOf()
- } else {
- JsonUtil.fromJson(json, Array::class.java).toMutableList()
- }
- }
-
- /**
- * Decodes the server configuration.
- *
- * @param guid The server GUID.
- * @return The server configuration.
- */
- fun decodeServerConfig(guid: String): ProfileItem? {
- if (guid.isBlank()) {
- return null
- }
- val json = profileFullStorage.decodeString(guid)
- if (json.isNullOrBlank()) {
- return null
- }
- return JsonUtil.fromJson(json, ProfileItem::class.java)
- }
-
-// fun decodeProfileConfig(guid: String): ProfileLiteItem? {
-// if (guid.isBlank()) {
-// return null
-// }
-// val json = profileStorage.decodeString(guid)
-// if (json.isNullOrBlank()) {
-// return null
-// }
-// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
-// }
-
- /**
- * Encodes the server configuration.
- *
- * @param guid The server GUID.
- * @param config The server configuration.
- * @return The server GUID.
- */
- fun encodeServerConfig(guid: String, config: ProfileItem): String {
- val key = guid.ifBlank { Utils.getUuid() }
- profileFullStorage.encode(key, JsonUtil.toJson(config))
- val serverList = decodeServerList()
- if (!serverList.contains(key)) {
- serverList.add(0, key)
- encodeServerList(serverList)
- if (getSelectServer().isNullOrBlank()) {
- mainStorage.encode(KEY_SELECTED_SERVER, key)
- }
- }
-// val profile = ProfileLiteItem(
-// configType = config.configType,
-// subscriptionId = config.subscriptionId,
-// remarks = config.remarks,
-// server = config.getProxyOutbound()?.getServerAddress(),
-// serverPort = config.getProxyOutbound()?.getServerPort(),
-// )
-// profileStorage.encode(key, JsonUtil.toJson(profile))
- return key
- }
-
- /**
- * Removes the server configuration.
- *
- * @param guid The server GUID.
- */
- fun removeServer(guid: String) {
- if (guid.isBlank()) {
- return
- }
- if (getSelectServer() == guid) {
- mainStorage.remove(KEY_SELECTED_SERVER)
- }
- val serverList = decodeServerList()
- serverList.remove(guid)
- encodeServerList(serverList)
- profileFullStorage.remove(guid)
- //profileStorage.remove(guid)
- serverAffStorage.remove(guid)
- }
-
- /**
- * Removes the server configurations via subscription ID.
- *
- * @param subid The subscription ID.
- */
- fun removeServerViaSubid(subid: String) {
- if (subid.isBlank()) {
- return
- }
- profileFullStorage.allKeys()?.forEach { key ->
- decodeServerConfig(key)?.let { config ->
- if (config.subscriptionId == subid) {
- removeServer(key)
- }
- }
- }
- }
-
- /**
- * Decodes the server affiliation information.
- *
- * @param guid The server GUID.
- * @return The server affiliation information.
- */
- fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
- if (guid.isBlank()) {
- return null
- }
- val json = serverAffStorage.decodeString(guid)
- if (json.isNullOrBlank()) {
- return null
- }
- return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
- }
-
- /**
- * Encodes the server test delay in milliseconds.
- *
- * @param guid The server GUID.
- * @param testResult The test delay in milliseconds.
- */
- fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
- if (guid.isBlank()) {
- return
- }
- val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
- aff.testDelayMillis = testResult
- serverAffStorage.encode(guid, JsonUtil.toJson(aff))
- }
-
- /**
- * Clears all test delay results.
- *
- * @param keys The list of server GUIDs.
- */
- fun clearAllTestDelayResults(keys: List?) {
- keys?.forEach { key ->
- decodeServerAffiliationInfo(key)?.let { aff ->
- aff.testDelayMillis = 0
- serverAffStorage.encode(key, JsonUtil.toJson(aff))
- }
- }
- }
-
- /**
- * Removes all server configurations.
- *
- * @return The number of server configurations removed.
- */
- fun removeAllServer(): Int {
- val count = profileFullStorage.allKeys()?.count() ?: 0
- mainStorage.clearAll()
- profileFullStorage.clearAll()
- //profileStorage.clearAll()
- serverAffStorage.clearAll()
- return count
- }
-
- /**
- * Removes invalid server configurations.
- *
- * @param guid The server GUID.
- * @return The number of server configurations removed.
- */
- fun removeInvalidServer(guid: String): Int {
- var count = 0
- if (guid.isNotEmpty()) {
- decodeServerAffiliationInfo(guid)?.let { aff ->
- if (aff.testDelayMillis < 0L) {
- removeServer(guid)
- count++
- }
- }
- } else {
- serverAffStorage.allKeys()?.forEach { key ->
- decodeServerAffiliationInfo(key)?.let { aff ->
- if (aff.testDelayMillis < 0L) {
- removeServer(key)
- count++
- }
- }
- }
- }
- return count
- }
-
- /**
- * Encodes the raw server configuration.
- *
- * @param guid The server GUID.
- * @param config The raw server configuration.
- */
- fun encodeServerRaw(guid: String, config: String) {
- serverRawStorage.encode(guid, config)
- }
-
- /**
- * Decodes the raw server configuration.
- *
- * @param guid The server GUID.
- * @return The raw server configuration.
- */
- fun decodeServerRaw(guid: String): String? {
- return serverRawStorage.decodeString(guid)
- }
-
- //endregion
-
- //region Subscriptions
-
- /**
- * Initializes the subscription list.
- */
- private fun initSubsList() {
- val subsList = decodeSubsList()
- if (subsList.isNotEmpty()) {
- return
- }
- subStorage.allKeys()?.forEach { key ->
- subsList.add(key)
- }
- encodeSubsList(subsList)
- }
-
- /**
- * Decodes the subscriptions.
- *
- * @return The list of subscriptions.
- */
- fun decodeSubscriptions(): List> {
- initSubsList()
-
- val subscriptions = mutableListOf>()
- decodeSubsList().forEach { key ->
- val json = subStorage.decodeString(key)
- if (!json.isNullOrBlank()) {
- subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java)))
- }
- }
- return subscriptions
- }
-
- /**
- * Removes the subscription.
- *
- * @param subid The subscription ID.
- */
- fun removeSubscription(subid: String) {
- subStorage.remove(subid)
- val subsList = decodeSubsList()
- subsList.remove(subid)
- encodeSubsList(subsList)
-
- removeServerViaSubid(subid)
- }
-
- /**
- * Encodes the subscription.
- *
- * @param guid The subscription GUID.
- * @param subItem The subscription item.
- */
- fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
- val key = guid.ifBlank { Utils.getUuid() }
- subStorage.encode(key, JsonUtil.toJson(subItem))
-
- val subsList = decodeSubsList()
- if (!subsList.contains(key)) {
- subsList.add(key)
- encodeSubsList(subsList)
- }
- }
-
- /**
- * Decodes the subscription.
- *
- * @param subscriptionId The subscription ID.
- * @return The subscription item.
- */
- fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
- val json = subStorage.decodeString(subscriptionId) ?: return null
- return JsonUtil.fromJson(json, SubscriptionItem::class.java)
- }
-
- /**
- * Encodes the subscription list.
- *
- * @param subsList The list of subscription IDs.
- */
- fun encodeSubsList(subsList: MutableList) {
- mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
- }
-
- /**
- * Decodes the subscription list.
- *
- * @return The list of subscription IDs.
- */
- fun decodeSubsList(): MutableList {
- val json = mainStorage.decodeString(KEY_SUB_IDS)
- return if (json.isNullOrBlank()) {
- mutableListOf()
- } else {
- JsonUtil.fromJson(json, Array::class.java).toMutableList()
- }
- }
-
- //endregion
-
- //region Asset
-
- /**
- * Decodes the asset URLs.
- *
- * @return The list of asset URLs.
- */
- fun decodeAssetUrls(): List> {
- val assetUrlItems = mutableListOf>()
- assetStorage.allKeys()?.forEach { key ->
- val json = assetStorage.decodeString(key)
- if (!json.isNullOrBlank()) {
- assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java)))
- }
- }
- return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
- }
-
- /**
- * Removes the asset URL.
- *
- * @param assetid The asset ID.
- */
- fun removeAssetUrl(assetid: String) {
- assetStorage.remove(assetid)
- }
-
- /**
- * Encodes the asset.
- *
- * @param assetid The asset ID.
- * @param assetItem The asset item.
- */
- fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
- val key = assetid.ifBlank { Utils.getUuid() }
- assetStorage.encode(key, JsonUtil.toJson(assetItem))
- }
-
- /**
- * Decodes the asset.
- *
- * @param assetid The asset ID.
- * @return The asset item.
- */
- fun decodeAsset(assetid: String): AssetUrlItem? {
- val json = assetStorage.decodeString(assetid) ?: return null
- return JsonUtil.fromJson(json, AssetUrlItem::class.java)
- }
-
- //endregion
-
- //region Routing
-
- /**
- * Decodes the routing rulesets.
- *
- * @return The list of routing rulesets.
- */
- fun decodeRoutingRulesets(): MutableList? {
- val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
- if (ruleset.isNullOrEmpty()) return null
- return JsonUtil.fromJson(ruleset, Array::class.java).toMutableList()
- }
-
- /**
- * Encodes the routing rulesets.
- *
- * @param rulesetList The list of routing rulesets.
- */
- fun encodeRoutingRulesets(rulesetList: MutableList?) {
- if (rulesetList.isNullOrEmpty())
- encodeSettings(PREF_ROUTING_RULESET, "")
- else
- encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
- }
-
- //endregion
-
- /**
- * Encodes the settings.
- *
- * @param key The settings key.
- * @param value The settings value.
- * @return Whether the encoding was successful.
- */
- fun encodeSettings(key: String, value: String?): Boolean {
- return settingsStorage.encode(key, value)
- }
-
- /**
- * Encodes the settings.
- *
- * @param key The settings key.
- * @param value The settings value.
- * @return Whether the encoding was successful.
- */
- fun encodeSettings(key: String, value: Int): Boolean {
- return settingsStorage.encode(key, value)
- }
-
- /**
- * Encodes the settings.
- *
- * @param key The settings key.
- * @param value The settings value.
- * @return Whether the encoding was successful.
- */
- fun encodeSettings(key: String, value: Boolean): Boolean {
- return settingsStorage.encode(key, value)
- }
-
- /**
- * Encodes the settings.
- *
- * @param key The settings key.
- * @param value The settings value.
- * @return Whether the encoding was successful.
- */
- fun encodeSettings(key: String, value: MutableSet): Boolean {
- return settingsStorage.encode(key, value)
- }
-
- /**
- * Decodes the settings string.
- *
- * @param key The settings key.
- * @return The settings value.
- */
- fun decodeSettingsString(key: String): String? {
- return settingsStorage.decodeString(key)
- }
-
- /**
- * Decodes the settings string.
- *
- * @param key The settings key.
- * @param defaultValue The default value.
- * @return The settings value.
- */
- fun decodeSettingsString(key: String, defaultValue: String?): String? {
- return settingsStorage.decodeString(key, defaultValue)
- }
-
- /**
- * Decodes the settings boolean.
- *
- * @param key The settings key.
- * @return The settings value.
- */
- fun decodeSettingsBool(key: String): Boolean {
- return settingsStorage.decodeBool(key, false)
- }
-
- /**
- * Decodes the settings boolean.
- *
- * @param key The settings key.
- * @param defaultValue The default value.
- * @return The settings value.
- */
- fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
- return settingsStorage.decodeBool(key, defaultValue)
- }
-
- /**
- * Decodes the settings string set.
- *
- * @param key The settings key.
- * @return The settings value.
- */
- fun decodeSettingsStringSet(key: String): MutableSet? {
- return settingsStorage.decodeStringSet(key)
- }
-
- //endregion
-
- //region Others
-
- /**
- * Encodes the start on boot setting.
- *
- * @param startOnBoot Whether to start on boot.
- */
- fun encodeStartOnBoot(startOnBoot: Boolean) {
- encodeSettings(PREF_IS_BOOTED, startOnBoot)
- }
-
- /**
- * Decodes the start on boot setting.
- *
- * @return Whether to start on boot.
- */
- fun decodeStartOnBoot(): Boolean {
- return decodeSettingsBool(PREF_IS_BOOTED, false)
- }
-
- //endregion
-
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt
deleted file mode 100644
index b2e23f7f..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt
+++ /dev/null
@@ -1,373 +0,0 @@
-package com.v2ray.ang.handler
-
-import android.content.Context
-import android.content.res.AssetManager
-import android.text.TextUtils
-import android.util.Log
-import androidx.appcompat.app.AppCompatDelegate
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.ANG_PACKAGE
-import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
-import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
-import com.v2ray.ang.AppConfig.TAG_DIRECT
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.Language
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.RoutingType
-import com.v2ray.ang.dto.RulesetItem
-import com.v2ray.ang.dto.V2rayConfig
-import com.v2ray.ang.dto.VpnInterfaceAddressConfig
-import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
-import com.v2ray.ang.handler.MmkvManager.decodeServerList
-import com.v2ray.ang.util.JsonUtil
-import com.v2ray.ang.util.Utils
-import java.io.File
-import java.io.FileOutputStream
-import java.util.Collections
-import java.util.Locale
-
-object SettingsManager {
-
- /**
- * Initialize routing rulesets.
- * @param context The application context.
- */
- fun initRoutingRulesets(context: Context) {
- val exist = MmkvManager.decodeRoutingRulesets()
- if (exist.isNullOrEmpty()) {
- val rulesetList = getPresetRoutingRulesets(context)
- MmkvManager.encodeRoutingRulesets(rulesetList)
- }
- }
-
- /**
- * Get preset routing rulesets.
- * @param context The application context.
- * @param index The index of the routing type.
- * @return A mutable list of RulesetItem.
- */
- private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList? {
- val fileName = RoutingType.fromIndex(index).fileName
- val assets = Utils.readTextFromAssets(context, fileName)
- if (TextUtils.isEmpty(assets)) {
- return null
- }
-
- return JsonUtil.fromJson(assets, Array::class.java).toMutableList()
- }
-
- /**
- * Reset routing rulesets from presets.
- * @param context The application context.
- * @param index The index of the routing type.
- */
- fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
- val rulesetList = getPresetRoutingRulesets(context, index) ?: return
- resetRoutingRulesetsCommon(rulesetList)
- }
-
- /**
- * Reset routing rulesets.
- * @param content The content of the rulesets.
- * @return True if successful, false otherwise.
- */
- fun resetRoutingRulesets(content: String?): Boolean {
- if (content.isNullOrEmpty()) {
- return false
- }
-
- try {
- val rulesetList = JsonUtil.fromJson(content, Array::class.java).toMutableList()
- if (rulesetList.isNullOrEmpty()) {
- return false
- }
-
- resetRoutingRulesetsCommon(rulesetList)
- return true
- } catch (e: Exception) {
- Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
- return false
- }
- }
-
- /**
- * Common method to reset routing rulesets.
- * @param rulesetList The list of rulesets.
- */
- private fun resetRoutingRulesetsCommon(rulesetList: MutableList) {
- val rulesetNew: MutableList = mutableListOf()
- MmkvManager.decodeRoutingRulesets()?.forEach { key ->
- if (key.locked == true) {
- rulesetNew.add(key)
- }
- }
-
- rulesetNew.addAll(rulesetList)
- MmkvManager.encodeRoutingRulesets(rulesetNew)
- }
-
- /**
- * Get a routing ruleset by index.
- * @param index The index of the ruleset.
- * @return The RulesetItem.
- */
- fun getRoutingRuleset(index: Int): RulesetItem? {
- if (index < 0) return null
-
- val rulesetList = MmkvManager.decodeRoutingRulesets()
- if (rulesetList.isNullOrEmpty()) return null
-
- return rulesetList[index]
- }
-
- /**
- * Save a routing ruleset.
- * @param index The index of the ruleset.
- * @param ruleset The RulesetItem to save.
- */
- fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
- if (ruleset == null) return
-
- var rulesetList = MmkvManager.decodeRoutingRulesets()
- if (rulesetList.isNullOrEmpty()) {
- rulesetList = mutableListOf()
- }
-
- if (index < 0 || index >= rulesetList.count()) {
- rulesetList.add(0, ruleset)
- } else {
- rulesetList[index] = ruleset
- }
- MmkvManager.encodeRoutingRulesets(rulesetList)
- }
-
- /**
- * Remove a routing ruleset by index.
- * @param index The index of the ruleset.
- */
- fun removeRoutingRuleset(index: Int) {
- if (index < 0) return
-
- val rulesetList = MmkvManager.decodeRoutingRulesets()
- if (rulesetList.isNullOrEmpty()) return
-
- rulesetList.removeAt(index)
- MmkvManager.encodeRoutingRulesets(rulesetList)
- }
-
- /**
- * Check if routing rulesets bypass LAN.
- * @return True if bypassing LAN, false otherwise.
- */
- fun routingRulesetsBypassLan(): Boolean {
- val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1"
- if (vpnBypassLan == "1") {
- return true
- } else if (vpnBypassLan == "2") {
- return false
- }
-
- val guid = MmkvManager.getSelectServer() ?: return false
- val config = decodeServerConfig(guid) ?: return false
- if (config.configType == EConfigType.CUSTOM) {
- val raw = MmkvManager.decodeServerRaw(guid) ?: return false
- val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
- val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }.any {
- it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
- }
- return exist == true
- }
-
- val rulesetItems = MmkvManager.decodeRoutingRulesets()
- val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
- it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
- }
- return exist == true
- }
-
- /**
- * Swap routing rulesets.
- * @param fromPosition The position to swap from.
- * @param toPosition The position to swap to.
- */
- fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
- val rulesetList = MmkvManager.decodeRoutingRulesets()
- if (rulesetList.isNullOrEmpty()) return
-
- Collections.swap(rulesetList, fromPosition, toPosition)
- MmkvManager.encodeRoutingRulesets(rulesetList)
- }
-
- /**
- * Swap subscriptions.
- * @param fromPosition The position to swap from.
- * @param toPosition The position to swap to.
- */
- fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
- val subsList = MmkvManager.decodeSubsList()
- if (subsList.isNullOrEmpty()) return
-
- Collections.swap(subsList, fromPosition, toPosition)
- MmkvManager.encodeSubsList(subsList)
- }
-
- /**
- * Get server via remarks.
- * @param remarks The remarks of the server.
- * @return The ProfileItem.
- */
- fun getServerViaRemarks(remarks: String?): ProfileItem? {
- if (remarks.isNullOrEmpty()) {
- return null
- }
- val serverList = decodeServerList()
- for (guid in serverList) {
- val profile = decodeServerConfig(guid)
- if (profile != null && profile.remarks == remarks) {
- return profile
- }
- }
- return null
- }
-
- /**
- * Get the SOCKS port.
- * @return The SOCKS port.
- */
- fun getSocksPort(): Int {
- return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
- }
-
- /**
- * Get the HTTP port.
- * @return The HTTP port.
- */
- fun getHttpPort(): Int {
- return getSocksPort() + if (Utils.isXray()) 0 else 1
- }
-
- /**
- * Initialize assets.
- * @param context The application context.
- * @param assets The AssetManager.
- */
- fun initAssets(context: Context, assets: AssetManager) {
- val extFolder = Utils.userAssetPath(context)
-
- try {
- val geo = arrayOf("geosite.dat", "geoip.dat")
- assets.list("")
- ?.filter { geo.contains(it) }
- ?.filter { !File(extFolder, it).exists() }
- ?.forEach {
- val target = File(extFolder, it)
- assets.open(it).use { input ->
- FileOutputStream(target).use { output ->
- input.copyTo(output)
- }
- }
- Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
- }
- } catch (e: Exception) {
- Log.e(ANG_PACKAGE, "asset copy failed", e)
- }
- }
-
- /**
- * Get domestic DNS servers from preference.
- * @return A list of domestic DNS servers.
- */
- fun getDomesticDnsServers(): List {
- val domesticDns =
- MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
- val ret = domesticDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
- if (ret.isEmpty()) {
- return listOf(AppConfig.DNS_DIRECT)
- }
- return ret
- }
-
- /**
- * Get remote DNS servers from preference.
- * @return A list of remote DNS servers.
- */
- fun getRemoteDnsServers(): List {
- val remoteDns =
- MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
- val ret = remoteDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
- if (ret.isEmpty()) {
- return listOf(AppConfig.DNS_PROXY)
- }
- return ret
- }
-
- /**
- * Get VPN DNS servers from preference.
- * @return A list of VPN DNS servers.
- */
- fun getVpnDnsServers(): List {
- val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
- return vpnDns.split(",").filter { Utils.isPureIpAddress(it) }
- }
-
- /**
- * Get delay test URL.
- * @param second Whether to use the second URL.
- * @return The delay test URL.
- */
- fun getDelayTestUrl(second: Boolean = false): String {
- return if (second) {
- AppConfig.DELAY_TEST_URL2
- } else {
- MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
- ?: AppConfig.DELAY_TEST_URL
- }
- }
-
- /**
- * Get the locale.
- * @return The locale.
- */
- fun getLocale(): Locale {
- val langCode =
- MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
- val language = Language.fromCode(langCode)
-
- return when (language) {
- Language.AUTO -> Utils.getSysLocale()
- Language.ENGLISH -> Locale.ENGLISH
- Language.CHINA -> Locale.CHINA
- Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
- Language.VIETNAMESE -> Locale("vi")
- Language.RUSSIAN -> Locale("ru")
- Language.PERSIAN -> Locale("fa")
- Language.ARABIC -> Locale("ar")
- Language.BANGLA -> Locale("bn")
- Language.BAKHTIARI -> Locale("bqi", "IR")
- }
- }
-
- /**
- * Set night mode.
- */
- fun setNightMode() {
- when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
- "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
- "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- }
- }
-
- /**
- * Retrieves the currently selected VPN interface address configuration.
- * This method reads the user's preference for VPN interface addressing and returns
- * the corresponding configuration containing IPv4 and IPv6 addresses.
- *
- * @return The selected VpnInterfaceAddressConfig instance, or the default configuration
- * if no valid selection is found or if the stored index is invalid.
- */
- fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig {
- val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt()
- return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt
deleted file mode 100644
index e547c378..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.v2ray.ang.handler
-
-import android.content.Context
-import android.os.SystemClock
-import android.text.TextUtils
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.dto.IPAPIInfo
-import com.v2ray.ang.extension.responseLength
-import com.v2ray.ang.util.HttpUtil
-import com.v2ray.ang.util.JsonUtil
-import kotlinx.coroutines.isActive
-import libv2ray.Libv2ray
-import java.io.IOException
-import java.net.InetSocketAddress
-import java.net.Socket
-import java.net.UnknownHostException
-import kotlin.coroutines.coroutineContext
-
-object SpeedtestManager {
-
- private val tcpTestingSockets = ArrayList()
-
- /**
- * Measures the TCP connection time to a given URL and port.
- *
- * @param url The URL to connect to.
- * @param port The port to connect to.
- * @return The connection time in milliseconds, or -1 if the connection failed.
- */
- suspend fun tcping(url: String, port: Int): Long {
- var time = -1L
- for (k in 0 until 2) {
- val one = socketConnectTime(url, port)
- if (!coroutineContext.isActive) {
- break
- }
- if (one != -1L && (time == -1L || one < time)) {
- time = one
- }
- }
- return time
- }
-
- /**
- * Measures the real ping time using the V2Ray library.
- *
- * @param config The configuration string for the V2Ray library.
- * @return The ping time in milliseconds, or -1 if the ping failed.
- */
- fun realPing(config: String): Long {
- return try {
- Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
- -1L
- }
- }
-
- /**
- * Measures the ping time to a given URL using the system ping command.
- *
- * @param url The URL to ping.
- * @return The ping time in milliseconds as a string, or "-1ms" if the ping failed.
- */
- fun ping(url: String): String {
- try {
- val command = "/system/bin/ping -c 3 $url"
- val process = Runtime.getRuntime().exec(command)
- val allText = process.inputStream.bufferedReader().use { it.readText() }
- if (!TextUtils.isEmpty(allText)) {
- val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
- val temps =
- tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
- if (temps.count() > 0 && temps[0].length < 10) {
- return temps[0].toFloat().toInt().toString() + "ms"
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
- }
- return "-1ms"
- }
-
- /**
- * Measures the time taken to establish a TCP connection to a given URL and port.
- *
- * @param url The URL to connect to.
- * @param port The port to connect to.
- * @return The connection time in milliseconds, or -1 if the connection failed.
- */
- fun socketConnectTime(url: String, port: Int): Long {
- try {
- val socket = Socket()
- synchronized(this) {
- tcpTestingSockets.add(socket)
- }
- val start = System.currentTimeMillis()
- socket.connect(InetSocketAddress(url, port), 3000)
- val time = System.currentTimeMillis() - start
- synchronized(this) {
- tcpTestingSockets.remove(socket)
- }
- socket.close()
- return time
- } catch (e: UnknownHostException) {
- Log.e(AppConfig.TAG, "Unknown host: $url", e)
- } catch (e: IOException) {
- Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
- }
- return -1
- }
-
- /**
- * Closes all TCP sockets that are currently being tested.
- */
- fun closeAllTcpSockets() {
- synchronized(this) {
- tcpTestingSockets.forEach {
- it?.close()
- }
- tcpTestingSockets.clear()
- }
- }
-
- /**
- * Tests the connection to a given URL and port.
- *
- * @param context The Context in which the test is running.
- * @param port The port to connect to.
- * @return A pair containing the elapsed time in milliseconds and the result message.
- */
- fun testConnection(context: Context, port: Int): Pair {
- var result: String
- var elapsed = -1L
-
- val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
- try {
- val start = SystemClock.elapsedRealtime()
- val code = conn.responseCode
- elapsed = SystemClock.elapsedRealtime() - start
-
- if (code == 204 || code == 200 && conn.responseLength == 0L) {
- result = context.getString(R.string.connection_test_available, elapsed)
- } else {
- throw IOException(
- context.getString(
- R.string.connection_test_error_status_code,
- code
- )
- )
- }
- } catch (e: IOException) {
- Log.e(AppConfig.TAG, "Connection test IOException", e)
- result = context.getString(R.string.connection_test_error, e.message)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Connection test Exception", e)
- result = context.getString(R.string.connection_test_error, e.message)
- } finally {
- conn.disconnect()
- }
-
- return Pair(elapsed, result)
- }
-
- fun getRemoteIPInfo(): String? {
- val httpPort = SettingsManager.getHttpPort()
- var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null
-
- var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
- var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query
- var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode
-
- return "(${country ?: "unknown"}) $ip"
- }
-
- /**
- * Gets the version of the V2Ray library.
- *
- * @return The version of the V2Ray library.
- */
- fun getLibVersion(): String {
- return Libv2ray.checkVersionX()
- }
-
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt
deleted file mode 100644
index 37b55c2e..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.v2ray.ang.handler
-
-import android.content.Context
-import android.os.Build
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.BuildConfig
-import com.v2ray.ang.dto.CheckUpdateResult
-import com.v2ray.ang.dto.GitHubRelease
-import com.v2ray.ang.extension.concatUrl
-import com.v2ray.ang.util.HttpUtil
-import com.v2ray.ang.util.JsonUtil
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.io.FileOutputStream
-
-object UpdateCheckerManager {
- suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
- val url = if (includePreRelease) {
- AppConfig.APP_API_URL
- } else {
- AppConfig.APP_API_URL.concatUrl("latest")
- }
-
- var response = HttpUtil.getUrlContent(url, 5000)
- if (response.isNullOrEmpty()) {
- val httpPort = SettingsManager.getHttpPort()
- response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response")
- }
-
- val latestRelease = if (includePreRelease) {
- JsonUtil.fromJson(response, Array::class.java)
- .firstOrNull()
- ?: throw IllegalStateException("No pre-release found")
- } else {
- JsonUtil.fromJson(response, GitHubRelease::class.java)
- }
-
- val latestVersion = latestRelease.tagName.removePrefix("v")
- Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})")
-
- return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) {
- val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0])
- CheckUpdateResult(
- hasUpdate = true,
- latestVersion = latestVersion,
- releaseNotes = latestRelease.body,
- downloadUrl = downloadUrl,
- isPreRelease = latestRelease.prerelease
- )
- } else {
- CheckUpdateResult(hasUpdate = false)
- }
- }
-
- suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
- try {
- val httpPort = SettingsManager.getHttpPort()
- val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
- ?: throw IllegalStateException("Failed to create connection")
-
- try {
- val apkFile = File(context.cacheDir, "update.apk")
- Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
-
- FileOutputStream(apkFile).use { outputStream ->
- connection.inputStream.use { inputStream ->
- inputStream.copyTo(outputStream)
- }
- }
- Log.i(AppConfig.TAG, "APK download completed")
- return@withContext apkFile
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
- return@withContext null
- } finally {
- try {
- connection.disconnect()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
- return@withContext null
- }
- }
-
- private fun compareVersions(version1: String, version2: String): Int {
- val v1 = version1.split(".")
- val v2 = version2.split(".")
-
- for (i in 0 until maxOf(v1.size, v2.size)) {
- val num1 = if (i < v1.size) v1[i].toInt() else 0
- val num2 = if (i < v2.size) v2[i].toInt() else 0
- if (num1 != num2) return num1 - num2
- }
- return 0
- }
-
- private fun getDownloadUrl(release: GitHubRelease, abi: String): String {
- return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl
- ?: release.assets.firstOrNull()?.browserDownloadUrl
- ?: throw IllegalStateException("No compatible APK found")
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt
deleted file mode 100644
index f53697bb..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt
+++ /dev/null
@@ -1,1091 +0,0 @@
-package com.v2ray.ang.handler
-
-import android.content.Context
-import android.text.TextUtils
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.ConfigResult
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.NetworkType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.RulesetItem
-import com.v2ray.ang.dto.V2rayConfig
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
-import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
-import com.v2ray.ang.extension.isNotNullEmpty
-import com.v2ray.ang.fmt.HttpFmt
-import com.v2ray.ang.fmt.ShadowsocksFmt
-import com.v2ray.ang.fmt.SocksFmt
-import com.v2ray.ang.fmt.TrojanFmt
-import com.v2ray.ang.fmt.VlessFmt
-import com.v2ray.ang.fmt.VmessFmt
-import com.v2ray.ang.fmt.WireguardFmt
-import com.v2ray.ang.util.HttpUtil
-import com.v2ray.ang.util.JsonUtil
-import com.v2ray.ang.util.Utils
-
-object V2rayConfigManager {
- private var initConfigCache: String? = null
-
- //region get config function
-
- /**
- * Retrieves the V2ray configuration for the given GUID.
- *
- * @param context The context of the caller.
- * @param guid The unique identifier for the V2ray configuration.
- * @return A ConfigResult object containing the configuration details or indicating failure.
- */
- fun getV2rayConfig(context: Context, guid: String): ConfigResult {
- try {
- val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
- return if (config.configType == EConfigType.CUSTOM) {
- getV2rayCustomConfig(guid, config)
- } else {
- getV2rayNormalConfig(context, guid, config)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get V2ray config", e)
- return ConfigResult(false)
- }
- }
-
- /**
- * Retrieves the speedtest V2ray configuration for the given GUID.
- *
- * @param context The context of the caller.
- * @param guid The unique identifier for the V2ray configuration.
- * @return A ConfigResult object containing the configuration details or indicating failure.
- */
- fun getV2rayConfig4Speedtest(context: Context, guid: String): ConfigResult {
- try {
- val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
- return if (config.configType == EConfigType.CUSTOM) {
- getV2rayCustomConfig(guid, config)
- } else {
- getV2rayNormalConfig4Speedtest(context, guid, config)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
- return ConfigResult(false)
- }
- }
-
- /**
- * Retrieves the custom V2ray configuration.
- *
- * @param guid The unique identifier for the V2ray configuration.
- * @param config The profile item containing the configuration details.
- * @return A ConfigResult object containing the result of the configuration retrieval.
- */
- private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult {
- val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
- return ConfigResult(true, guid, raw)
- }
-
- /**
- * Retrieves the normal V2ray configuration.
- *
- * @param context The context in which the function is called.
- * @param guid The unique identifier for the V2ray configuration.
- * @param config The profile item containing the configuration details.
- * @return A ConfigResult object containing the result of the configuration retrieval.
- */
- private fun getV2rayNormalConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
- val result = ConfigResult(false)
-
- val address = config.server ?: return result
- if (!Utils.isPureIpAddress(address)) {
- if (!Utils.isValidUrl(address)) {
- Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
- return result
- }
- }
-
- val v2rayConfig = initV2rayConfig(context) ?: return result
- v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
- v2rayConfig.remarks = config.remarks
-
- getInbounds(v2rayConfig)
-
- if (config.configType == EConfigType.HYSTERIA2) {
- result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
- } else {
- getOutbounds(v2rayConfig, config) ?: return result
- getMoreOutbounds(v2rayConfig, config.subscriptionId)
- }
-
- getRouting(v2rayConfig)
-
- getFakeDns(v2rayConfig)
-
- getDns(v2rayConfig)
-
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
- getCustomLocalDns(v2rayConfig)
- }
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
- v2rayConfig.stats = null
- v2rayConfig.policy = null
- }
-
- //Resolve and add to DNS Hosts
- if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") {
- resolveOutboundDomainsToHosts(v2rayConfig)
- }
-
- result.status = true
- result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
- result.guid = guid
- return result
- }
-
- /**
- * Retrieves the normal V2ray configuration for speedtest.
- *
- * @param context The context in which the function is called.
- * @param guid The unique identifier for the V2ray configuration.
- * @param config The profile item containing the configuration details.
- * @return A ConfigResult object containing the result of the configuration retrieval.
- */
- private fun getV2rayNormalConfig4Speedtest(context: Context, guid: String, config: ProfileItem): ConfigResult {
- val result = ConfigResult(false)
-
- val address = config.server ?: return result
- if (!Utils.isPureIpAddress(address)) {
- if (!Utils.isValidUrl(address)) {
- Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
- return result
- }
- }
-
- val v2rayConfig = initV2rayConfig(context) ?: return result
-
- if (config.configType == EConfigType.HYSTERIA2) {
- result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
- } else {
- getOutbounds(v2rayConfig, config) ?: return result
- getMoreOutbounds(v2rayConfig, config.subscriptionId)
- }
-
- v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
- v2rayConfig.inbounds.clear()
- v2rayConfig.routing.rules.clear()
- v2rayConfig.dns = null
- v2rayConfig.fakedns = null
- v2rayConfig.stats = null
- v2rayConfig.policy = null
-
- v2rayConfig.outbounds.forEach { key ->
- key.mux = null
- }
-
- result.status = true
- result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
- result.guid = guid
- return result
- }
-
- /**
- * Initializes V2ray configuration.
- *
- * This function loads the V2ray configuration from assets or from a cached value.
- * It first attempts to use the cached configuration if available, otherwise reads
- * the configuration from the "v2ray_config.json" asset file.
- *
- * @param context Android context used to access application assets
- * @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty
- */
- private fun initV2rayConfig(context: Context): V2rayConfig? {
- val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
- if (TextUtils.isEmpty(assets)) {
- return null
- }
- initConfigCache = assets
- val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
- return config
- }
-
-
- //endregion
-
-
- //region some sub function
-
- /**
- * Configures the inbound settings for V2ray.
- *
- * This function sets up the listening ports, sniffing options, and other inbound-related configurations.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @return true if inbound configuration was successful, false otherwise
- */
- private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
- try {
- val socksPort = SettingsManager.getSocksPort()
-
- v2rayConfig.inbounds.forEach { curInbound ->
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
- //bind all inbounds to localhost if the user requests
- curInbound.listen = AppConfig.LOOPBACK
- }
- }
- v2rayConfig.inbounds[0].port = socksPort
- val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
- val sniffAllTlsAndHttp =
- MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
- v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
- v2rayConfig.inbounds[0].sniffing?.routeOnly =
- MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
- if (!sniffAllTlsAndHttp) {
- v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
- }
- if (fakedns) {
- v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
- }
-
- if (Utils.isXray()) {
- v2rayConfig.inbounds.removeAt(1)
- } else {
- val httpPort = SettingsManager.getHttpPort()
- v2rayConfig.inbounds[1].port = httpPort
- }
-
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
- return false
- }
- return true
- }
-
- /**
- * Configures the fake DNS settings if enabled.
- *
- * Adds FakeDNS configuration to v2rayConfig if both local DNS and fake DNS are enabled.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- */
- private fun getFakeDns(v2rayConfig: V2rayConfig) {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
- && MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
- ) {
- v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
- }
- }
-
- /**
- * Configures routing settings for V2ray.
- *
- * Sets up the domain strategy and adds routing rules from saved rulesets.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @return true if routing configuration was successful, false otherwise
- */
- private fun getRouting(v2rayConfig: V2rayConfig): Boolean {
- try {
-
- v2rayConfig.routing.domainStrategy =
- MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
- ?: "AsIs"
-
- val rulesetItems = MmkvManager.decodeRoutingRulesets()
- rulesetItems?.forEach { key ->
- getRoutingUserRule(key, v2rayConfig)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure routing", e)
- return false
- }
- return true
- }
-
- /**
- * Adds a specific ruleset item to the routing configuration.
- *
- * @param item The ruleset item to add
- * @param v2rayConfig The V2ray configuration object to be modified
- */
- private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
- try {
- if (item == null || !item.enabled) {
- return
- }
-
- val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
-
- v2rayConfig.routing.rules.add(rule)
-
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to apply routing user rule", e)
- }
- }
-
- /**
- * Retrieves domain rules for a specific outbound tag.
- *
- * Searches through all rulesets to find domains targeting the specified tag.
- *
- * @param tag The outbound tag to search for
- * @return ArrayList of domain rules matching the tag
- */
- private fun getUserRule2Domain(tag: String): ArrayList {
- val domain = ArrayList()
-
- val rulesetItems = MmkvManager.decodeRoutingRulesets()
- rulesetItems?.forEach { key ->
- if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
- key.domain?.forEach {
- if (it != AppConfig.GEOSITE_PRIVATE
- && (it.startsWith("geosite:") || it.startsWith("domain:"))
- ) {
- domain.add(it)
- }
- }
- }
- }
-
- return domain
- }
-
- /**
- * Configures custom local DNS settings.
- *
- * Sets up DNS inbound, outbound, and routing rules for local DNS resolution.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @return true if custom local DNS configuration was successful, false otherwise
- */
- private fun getCustomLocalDns(v2rayConfig: V2rayConfig): Boolean {
- try {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
- val geositeCn = arrayListOf(AppConfig.GEOSITE_CN)
- val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
- val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
- // fakedns with all domains to make it always top priority
- v2rayConfig.dns?.servers?.add(
- 0,
- V2rayConfig.DnsBean.ServersBean(
- address = "fakedns",
- domains = geositeCn.plus(proxyDomain).plus(directDomain)
- )
- )
- }
-
- // DNS inbound
- val remoteDns = SettingsManager.getRemoteDnsServers()
- if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
- val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
- address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
- port = 53,
- network = "tcp,udp"
- )
-
- val localDnsPort = Utils.parseInt(
- MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT),
- AppConfig.PORT_LOCAL_DNS.toInt()
- )
- v2rayConfig.inbounds.add(
- V2rayConfig.InboundBean(
- tag = "dns-in",
- port = localDnsPort,
- listen = AppConfig.LOOPBACK,
- protocol = "dokodemo-door",
- settings = dnsInboundSettings,
- sniffing = null
- )
- )
- }
-
- // DNS outbound
- if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
- v2rayConfig.outbounds.add(
- V2rayConfig.OutboundBean(
- protocol = "dns",
- tag = "dns-out",
- settings = null,
- streamSettings = null,
- mux = null
- )
- )
- }
-
- // DNS routing tag
- v2rayConfig.routing.rules.add(
- 0, RulesBean(
- inboundTag = arrayListOf("dns-in"),
- outboundTag = "dns-out",
- domain = null
- )
- )
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure custom local DNS", e)
- return false
- }
- return true
- }
-
- /**
- * Configures the DNS settings for V2ray.
- *
- * Sets up DNS servers, hosts, and routing rules for DNS resolution.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @return true if DNS configuration was successful, false otherwise
- */
- private fun getDns(v2rayConfig: V2rayConfig): Boolean {
- try {
- val hosts = mutableMapOf()
- val servers = ArrayList()
-
- //remote Dns
- val remoteDns = SettingsManager.getRemoteDnsServers()
- val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
- remoteDns.forEach {
- servers.add(it)
- }
- if (proxyDomain.isNotEmpty()) {
- servers.add(
- V2rayConfig.DnsBean.ServersBean(
- address = remoteDns.first(),
- domains = proxyDomain,
- )
- )
- }
-
- // domestic DNS
- val domesticDns = SettingsManager.getDomesticDnsServers()
- val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
- val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
- val geoipCn = arrayListOf(AppConfig.GEOIP_CN)
- if (directDomain.isNotEmpty()) {
- servers.add(
- V2rayConfig.DnsBean.ServersBean(
- address = domesticDns.first(),
- domains = directDomain,
- expectIPs = if (isCnRoutingMode) geoipCn else null,
- skipFallback = true
- )
- )
- }
-
- if (Utils.isPureIpAddress(domesticDns.first())) {
- v2rayConfig.routing.rules.add(
- 0, RulesBean(
- outboundTag = AppConfig.TAG_DIRECT,
- port = "53",
- ip = arrayListOf(domesticDns.first()),
- domain = null
- )
- )
- }
-
- //block dns
- val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
- if (blkDomain.isNotEmpty()) {
- hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
- }
-
- // hardcode googleapi rule to fix play store problems
- hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
-
- // hardcode popular Android Private DNS rule to fix localhost DNS problem
- hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
- hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES
- hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES
- hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES
- hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
- hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
- hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
- hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
-
- //User DNS hosts
- try {
- val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
- if (userHosts.isNotNullEmpty()) {
- var userHostsMap = userHosts?.split(",")
- ?.filter { it.isNotEmpty() }
- ?.filter { it.contains(":") }
- ?.associate { it.split(":").let { (k, v) -> k to v } }
- if (userHostsMap != null) hosts.putAll(userHostsMap)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
- }
-
- // DNS dns
- v2rayConfig.dns = V2rayConfig.DnsBean(
- servers = servers,
- hosts = hosts
- )
-
- // DNS routing
- if (Utils.isPureIpAddress(remoteDns.first())) {
- v2rayConfig.routing.rules.add(
- 0, RulesBean(
- outboundTag = AppConfig.TAG_PROXY,
- port = "53",
- ip = arrayListOf(remoteDns.first()),
- domain = null
- )
- )
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure DNS", e)
- return false
- }
- return true
- }
-
-
- //endregion
-
-
- //region outbound related functions
-
- /**
- * Configures the primary outbound connection.
- *
- * Converts the profile to an outbound configuration and applies global settings.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @param config The profile item containing connection details
- * @return true if outbound configuration was successful, null if there was an error
- */
- private fun getOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Boolean? {
- val outbound = convertProfile2Outbound(config) ?: return null
- val ret = updateOutboundWithGlobalSettings(outbound)
- if (!ret) return null
-
- if (v2rayConfig.outbounds.isNotEmpty()) {
- v2rayConfig.outbounds[0] = outbound
- } else {
- v2rayConfig.outbounds.add(outbound)
- }
-
- updateOutboundFragment(v2rayConfig)
- return true
- }
-
- /**
- * Configures special outbound settings for Hysteria2 protocol.
- *
- * Creates a SOCKS outbound connection on a free port for protocols requiring special handling.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @param config The profile item containing connection details
- * @return The port number for the SOCKS connection, or null if there was an error
- */
- private fun getPlusOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Int? {
- try {
- val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
-
- val outboundNew = OutboundBean(
- mux = null,
- protocol = EConfigType.SOCKS.name.lowercase(),
- settings = OutSettingsBean(
- servers = listOf(
- OutSettingsBean.ServersBean(
- address = AppConfig.LOOPBACK,
- port = socksPort
- )
- )
- )
- )
- if (v2rayConfig.outbounds.isNotEmpty()) {
- v2rayConfig.outbounds[0] = outboundNew
- } else {
- v2rayConfig.outbounds.add(outboundNew)
- }
-
- return socksPort
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure plusOutbound", e)
- return null
- }
- }
-
- /**
- * Configures additional outbound connections for proxy chaining.
- *
- * Sets up previous and next proxies in a subscription for advanced routing capabilities.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @param subscriptionId The subscription ID to look up related proxies
- * @return true if additional outbounds were configured successfully, false otherwise
- */
- private fun getMoreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String): Boolean {
- //fragment proxy
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
- return false
- }
-
- if (subscriptionId.isEmpty()) {
- return false
- }
- try {
- val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return false
-
- //current proxy
- val outbound = v2rayConfig.outbounds[0]
-
- //Previous proxy
- val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
- if (prevNode != null) {
- val prevOutbound = convertProfile2Outbound(prevNode)
- if (prevOutbound != null) {
- updateOutboundWithGlobalSettings(prevOutbound)
- prevOutbound.tag = AppConfig.TAG_PROXY + "2"
- v2rayConfig.outbounds.add(prevOutbound)
- outbound.ensureSockopt().dialerProxy = prevOutbound.tag
- }
- }
-
- //Next proxy
- val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
- if (nextNode != null) {
- val nextOutbound = convertProfile2Outbound(nextNode)
- if (nextOutbound != null) {
- updateOutboundWithGlobalSettings(nextOutbound)
- nextOutbound.tag = AppConfig.TAG_PROXY
- v2rayConfig.outbounds.add(0, nextOutbound)
- outbound.tag = AppConfig.TAG_PROXY + "1"
- nextOutbound.ensureSockopt().dialerProxy = outbound.tag
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
- return false
- }
-
- return true
- }
-
- /**
- * Updates outbound settings based on global preferences.
- *
- * Applies multiplexing and protocol-specific settings to an outbound connection.
- *
- * @param outbound The outbound connection to update
- * @return true if the update was successful, false otherwise
- */
- private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
- try {
- var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
- val protocol = outbound.protocol
- if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
- || protocol.equals(EConfigType.SOCKS.name, true)
- || protocol.equals(EConfigType.HTTP.name, true)
- || protocol.equals(EConfigType.TROJAN.name, true)
- || protocol.equals(EConfigType.WIREGUARD.name, true)
- || protocol.equals(EConfigType.HYSTERIA2.name, true)
- ) {
- muxEnabled = false
- } else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
- muxEnabled = false
- }
-
- if (muxEnabled == true) {
- outbound.mux?.enabled = true
- outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt()
- outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt()
- outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC, "reject")
- if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) {
- outbound.mux?.concurrency = -1
- }
- } else {
- outbound.mux?.enabled = false
- outbound.mux?.concurrency = -1
- }
-
- if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
- var localTunAddr = if (outbound.settings?.address == null) {
- listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
- } else {
- outbound.settings?.address as List<*>
- }
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) {
- localTunAddr = listOf(localTunAddr.first())
- }
- outbound.settings?.address = localTunAddr
- }
-
- if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK
- && outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP
- ) {
- val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
- val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
-
- val requestString: String by lazy {
- """{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
- }
- outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
- requestString,
- StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
- )
- outbound.streamSettings?.tcpSettings?.header?.request?.path =
- if (path.isNullOrEmpty()) {
- listOf("/")
- } else {
- path
- }
- outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
- }
-
-
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
- return false
- }
- return true
- }
-
- /**
- * Updates the outbound with fragment settings for traffic optimization.
- *
- * Configures packet fragmentation for TLS and REALITY protocols if enabled.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- * @return true if fragment configuration was successful, false otherwise
- */
- private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
- try {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
- return true
- }
- if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS
- && v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY
- ) {
- return true
- }
-
- val fragmentOutbound =
- V2rayConfig.OutboundBean(
- protocol = AppConfig.PROTOCOL_FREEDOM,
- tag = AppConfig.TAG_FRAGMENT,
- mux = null
- )
-
- var packets =
- MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
- if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY
- && packets == "tlshello"
- ) {
- packets = "1-3"
- } else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS
- && packets != "tlshello"
- ) {
- packets = "tlshello"
- }
-
- fragmentOutbound.settings = OutboundBean.OutSettingsBean(
- fragment = OutboundBean.OutSettingsBean.FragmentBean(
- packets = packets,
- length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
- ?: "50-100",
- interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
- ?: "10-20"
- ),
- noises = listOf(
- OutboundBean.OutSettingsBean.NoiseBean(
- type = "rand",
- packet = "10-20",
- delay = "10-16",
- )
- ),
- )
- fragmentOutbound.streamSettings = StreamSettingsBean(
- sockopt = StreamSettingsBean.SockoptBean(
- TcpNoDelay = true,
- mark = 255
- )
- )
- v2rayConfig.outbounds.add(fragmentOutbound)
-
- //proxy chain
- v2rayConfig.outbounds[0].streamSettings?.sockopt =
- StreamSettingsBean.SockoptBean(
- dialerProxy = AppConfig.TAG_FRAGMENT
- )
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
- return false
- }
- return true
- }
-
- /**
- * Resolves domain names to IP addresses in outbound connections.
- *
- * Pre-resolves domains to improve connection speed and reliability.
- *
- * @param v2rayConfig The V2ray configuration object to be modified
- */
- private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) {
- val proxyOutboundList = v2rayConfig.getAllProxyOutbound()
- val dns = v2rayConfig.dns ?: return
- val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf()
- val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true
-
- for (item in proxyOutboundList) {
- val domain = item.getServerAddress()
- if (domain.isNullOrEmpty()) continue
-
- if (newHosts.containsKey(domain)) {
- item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
- continue
- }
-
- val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
- if (resolvedIps.isNullOrEmpty()) continue
-
- item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
- newHosts[domain] = if (resolvedIps.size == 1) {
- resolvedIps[0]
- } else {
- resolvedIps
- }
- }
-
- dns.hosts = newHosts
- }
-
- /**
- * Converts a profile item to an outbound configuration.
- *
- * Creates appropriate outbound settings based on the protocol type.
- *
- * @param profileItem The profile item to convert
- * @return OutboundBean configuration for the profile, or null if not supported
- */
- private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
- return when (profileItem.configType) {
- EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
- EConfigType.CUSTOM -> null
- EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toOutbound(profileItem)
- EConfigType.SOCKS -> SocksFmt.toOutbound(profileItem)
- EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
- EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
- EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
- EConfigType.HYSTERIA2 -> null
- EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
- }
- }
-
- /**
- * Creates an initial outbound configuration for a specific protocol type.
- *
- * Provides a template configuration for different protocol types.
- *
- * @param configType The type of configuration to create
- * @return An initial OutboundBean for the specified configuration type, or null for custom types
- */
- fun createInitOutbound(configType: EConfigType): OutboundBean? {
- return when (configType) {
- EConfigType.VMESS,
- EConfigType.VLESS ->
- return OutboundBean(
- protocol = configType.name.lowercase(),
- settings = OutSettingsBean(
- vnext = listOf(
- OutSettingsBean.VnextBean(
- users = listOf(OutSettingsBean.VnextBean.UsersBean())
- )
- )
- ),
- streamSettings = StreamSettingsBean()
- )
-
- EConfigType.SHADOWSOCKS,
- EConfigType.SOCKS,
- EConfigType.HTTP,
- EConfigType.TROJAN,
- EConfigType.HYSTERIA2 ->
- return OutboundBean(
- protocol = configType.name.lowercase(),
- settings = OutSettingsBean(
- servers = listOf(OutSettingsBean.ServersBean())
- ),
- streamSettings = StreamSettingsBean()
- )
-
- EConfigType.WIREGUARD ->
- return OutboundBean(
- protocol = configType.name.lowercase(),
- settings = OutSettingsBean(
- secretKey = "",
- peers = listOf(OutSettingsBean.WireGuardBean())
- )
- )
-
- EConfigType.CUSTOM -> null
- }
- }
-
- /**
- * Configures transport settings for an outbound connection.
- *
- * Sets up protocol-specific transport options based on the profile settings.
- *
- * @param streamSettings The stream settings to configure
- * @param profileItem The profile containing transport configuration
- * @return The Server Name Indication (SNI) value to use, or null if not applicable
- */
- fun populateTransportSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem): String? {
- val transport = profileItem.network.orEmpty()
- val headerType = profileItem.headerType
- val host = profileItem.host
- val path = profileItem.path
- val seed = profileItem.seed
-// val quicSecurity = profileItem.quicSecurity
-// val key = profileItem.quicKey
- val mode = profileItem.mode
- val serviceName = profileItem.serviceName
- val authority = profileItem.authority
- val xhttpMode = profileItem.xhttpMode
- val xhttpExtra = profileItem.xhttpExtra
-
- var sni: String? = null
- streamSettings.network = if (transport.isEmpty()) NetworkType.TCP.type else transport
- when (streamSettings.network) {
- NetworkType.TCP.type -> {
- val tcpSetting = StreamSettingsBean.TcpSettingsBean()
- if (headerType == AppConfig.HEADER_TYPE_HTTP) {
- tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
- if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
- val requestObj = StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
- requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
- requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
- tcpSetting.header.request = requestObj
- sni = requestObj.headers.Host?.getOrNull(0)
- }
- } else {
- tcpSetting.header.type = "none"
- sni = host
- }
- streamSettings.tcpSettings = tcpSetting
- }
-
- NetworkType.KCP.type -> {
- val kcpsetting = StreamSettingsBean.KcpSettingsBean()
- kcpsetting.header.type = headerType ?: "none"
- if (seed.isNullOrEmpty()) {
- kcpsetting.seed = null
- } else {
- kcpsetting.seed = seed
- }
- if (host.isNullOrEmpty()) {
- kcpsetting.header.domain = null
- } else {
- kcpsetting.header.domain = host
- }
- streamSettings.kcpSettings = kcpsetting
- }
-
- NetworkType.WS.type -> {
- val wssetting = StreamSettingsBean.WsSettingsBean()
- wssetting.headers.Host = host.orEmpty()
- sni = host
- wssetting.path = path ?: "/"
- streamSettings.wsSettings = wssetting
- }
-
- NetworkType.HTTP_UPGRADE.type -> {
- val httpupgradeSetting = StreamSettingsBean.HttpupgradeSettingsBean()
- httpupgradeSetting.host = host.orEmpty()
- sni = host
- httpupgradeSetting.path = path ?: "/"
- streamSettings.httpupgradeSettings = httpupgradeSetting
- }
-
- NetworkType.XHTTP.type -> {
- val xhttpSetting = StreamSettingsBean.XhttpSettingsBean()
- xhttpSetting.host = host.orEmpty()
- sni = host
- xhttpSetting.path = path ?: "/"
- xhttpSetting.mode = xhttpMode
- xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
- streamSettings.xhttpSettings = xhttpSetting
- }
-
- NetworkType.H2.type, NetworkType.HTTP.type -> {
- streamSettings.network = NetworkType.H2.type
- val h2Setting = StreamSettingsBean.HttpSettingsBean()
- h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
- sni = h2Setting.host.getOrNull(0)
- h2Setting.path = path ?: "/"
- streamSettings.httpSettings = h2Setting
- }
-
-// "quic" -> {
-// val quicsetting = QuicSettingBean()
-// quicsetting.security = quicSecurity ?: "none"
-// quicsetting.key = key.orEmpty()
-// quicsetting.header.type = headerType ?: "none"
-// quicSettings = quicsetting
-// }
-
- NetworkType.GRPC.type -> {
- val grpcSetting = StreamSettingsBean.GrpcSettingsBean()
- grpcSetting.multiMode = mode == "multi"
- grpcSetting.serviceName = serviceName.orEmpty()
- grpcSetting.authority = authority.orEmpty()
- grpcSetting.idle_timeout = 60
- grpcSetting.health_check_timeout = 20
- sni = authority
- streamSettings.grpcSettings = grpcSetting
- }
- }
- return sni
- }
-
- /**
- * Configures TLS or REALITY security settings for an outbound connection.
- *
- * Sets up security-related parameters like certificates, fingerprints, and SNI.
- *
- * @param streamSettings The stream settings to configure
- * @param profileItem The profile containing security configuration
- * @param sniExt An external SNI value to use if the profile doesn't specify one
- */
- fun populateTlsSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
- val streamSecurity = profileItem.security.orEmpty()
- val allowInsecure = profileItem.insecure == true
- val sni = if (profileItem.sni.isNullOrEmpty()) {
- when {
- sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
- profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server
- else -> sniExt
- }
- } else {
- profileItem.sni
- }
- val fingerprint = profileItem.fingerPrint
- val alpns = profileItem.alpn
- val publicKey = profileItem.publicKey
- val shortId = profileItem.shortId
- val spiderX = profileItem.spiderX
-
- streamSettings.security = if (streamSecurity.isEmpty()) null else streamSecurity
- if (streamSettings.security == null) return
- val tlsSetting = StreamSettingsBean.TlsSettingsBean(
- allowInsecure = allowInsecure,
- serverName = if (sni.isNullOrEmpty()) null else sni,
- fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
- alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
- publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
- shortId = if (shortId.isNullOrEmpty()) null else shortId,
- spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
- )
- if (streamSettings.security == AppConfig.TLS) {
- streamSettings.tlsSettings = tlsSetting
- streamSettings.realitySettings = null
- } else if (streamSettings.security == AppConfig.REALITY) {
- streamSettings.tlsSettings = null
- streamSettings.realitySettings = tlsSetting
- }
- }
-
- //endregion
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt
deleted file mode 100644
index 0f6d37c0..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.v2ray.ang.helper
-
-import android.graphics.Canvas
-import android.graphics.Rect
-import android.graphics.drawable.Drawable
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-
-class CustomDividerItemDecoration(
- private val divider: Drawable,
- private val orientation: Int
-) : RecyclerView.ItemDecoration() {
-
- override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
- if (orientation == RecyclerView.VERTICAL) {
- drawVerticalDividers(canvas, parent)
- } else {
- drawHorizontalDividers(canvas, parent)
- }
- }
-
- private fun drawVerticalDividers(canvas: Canvas, parent: RecyclerView) {
- val left = parent.paddingLeft
- val right = parent.width - parent.paddingRight
-
- val childCount = parent.childCount
- for (i in 0 until childCount - 1) {
- val child = parent.getChildAt(i)
- val params = child.layoutParams as RecyclerView.LayoutParams
-
- val top = child.bottom + params.bottomMargin
- val bottom = top + divider.intrinsicHeight
-
- divider.setBounds(left, top, right, bottom)
- divider.draw(canvas)
- }
- }
-
- private fun drawHorizontalDividers(canvas: Canvas, parent: RecyclerView) {
- val top = parent.paddingTop
- val bottom = parent.height - parent.paddingBottom
-
- val childCount = parent.childCount
- for (i in 0 until childCount - 1) {
- val child = parent.getChildAt(i)
- val params = child.layoutParams as RecyclerView.LayoutParams
-
- val left = child.right + params.rightMargin
- val right = left + divider.intrinsicWidth
-
- divider.setBounds(left, top, right, bottom)
- divider.draw(canvas)
- }
- }
-
- override fun getItemOffsets(
- outRect: Rect,
- view: View,
- parent: RecyclerView,
- state: RecyclerView.State
- ) {
- if (orientation == RecyclerView.VERTICAL) {
- outRect.set(0, 0, 0, divider.intrinsicHeight)
- } else {
- outRect.set(0, 0, divider.intrinsicWidth, 0)
- }
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java
similarity index 53%
rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt
rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java
index 8707d16f..2de0c74a 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java
@@ -13,41 +13,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.v2ray.ang.helper
+
+package com.v2ray.ang.helper;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.ItemTouchHelper;
/**
- * Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
+ * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
*
* @author Paul Burke (ipaulpro)
*/
-interface ItemTouchHelperAdapter {
+public interface ItemTouchHelperAdapter {
+
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
- * an item is shifted, and **not** at the end of a "drop" event.
- *
- * Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
+ * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item.
* @return True if the item was moved to the new adapter position.
- * @see RecyclerView.getAdapterPositionFor
- * @see RecyclerView.ViewHolder.getAdapterPosition
+ *
+ * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+ * @see RecyclerView.ViewHolder#getAdapterPosition()
*/
- fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
+ boolean onItemMove(int fromPosition, int toPosition);
- fun onItemMoveCompleted()
+ void onItemMoveCompleted();
/**
- * Called when an item has been dismissed by a swipe.
- *
- * Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
+ * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
- * @see RecyclerView.getAdapterPositionFor
- * @see RecyclerView.ViewHolder.getAdapterPosition
+ *
+ * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+ * @see RecyclerView.ViewHolder#getAdapterPosition()
*/
- fun onItemDismiss(position: Int)
+ void onItemDismiss(int position);
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java
similarity index 67%
rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt
rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java
index 75655b0c..149768fc 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java
@@ -13,26 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.v2ray.ang.helper
-import androidx.recyclerview.widget.ItemTouchHelper
+package com.v2ray.ang.helper;
+
+import androidx.recyclerview.widget.ItemTouchHelper;
/**
- * Interface to notify an item ViewHolder of relevant callbacks from [ ].
+ * Interface to notify an item ViewHolder of relevant callbacks from {@link
+ * ItemTouchHelper.Callback}.
*
* @author Paul Burke (ipaulpro)
*/
-interface ItemTouchHelperViewHolder {
+public interface ItemTouchHelperViewHolder {
+
/**
- * Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
+ * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
* Implementations should update the item view to indicate it's active state.
*/
- fun onItemSelected()
+ void onItemSelected();
/**
- * Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
+ * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
* state should be cleared.
*/
- fun onItemClear()
+ void onItemClear();
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java
new file mode 100644
index 00000000..a6407b6f
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * 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.
+ */
+
+package com.v2ray.ang.helper;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Listener for manual initiation of a drag.
+ */
+public interface OnStartDragListener {
+
+ /**
+ * Called when a view is requesting a start of a drag.
+ *
+ * @param viewHolder The holder of the view to drag.
+ */
+ void onStartDrag(RecyclerView.ViewHolder viewHolder);
+
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java
new file mode 100644
index 00000000..97fceab7
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * 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.
+ */
+
+package com.v2ray.ang.helper;
+
+import android.graphics.Canvas;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.ItemTouchHelper;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
+ * swipe-to-dismiss. Drag events are automatically started by an item long-press.
+ *
+ * Expects the RecyclerView.Adapter
to listen for {@link
+ * ItemTouchHelperAdapter} callbacks and the RecyclerView.ViewHolder
to implement
+ * {@link ItemTouchHelperViewHolder}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
+
+ public static final float ALPHA_FULL = 1.0f;
+
+ private final ItemTouchHelperAdapter mAdapter;
+
+ public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled() {
+ return false;
+ }
+
+ @Override
+ public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
+ // Set movement flags based on the layout manager
+ if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
+ final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
+ final int swipeFlags = 0;
+ return makeMovementFlags(dragFlags, swipeFlags);
+ } else {
+ final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
+ final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
+ return makeMovementFlags(dragFlags, swipeFlags);
+ }
+ }
+
+ @Override
+ public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
+ if (source.getItemViewType() != target.getItemViewType()) {
+ return false;
+ }
+
+ // Notify the adapter of the move
+ mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
+ return true;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
+ // Notify the adapter of the dismissal
+ mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
+ }
+
+ @Override
+ public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX,
+ float dY, int actionState, boolean isCurrentlyActive) {
+ if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
+ // Fade out the view as it is swiped out of the parent's bounds
+ final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
+ viewHolder.itemView.setAlpha(alpha);
+ viewHolder.itemView.setTranslationX(dX);
+ } else {
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
+ }
+ }
+
+ @Override
+ public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
+ // We only want the active item to change
+ if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
+ if (viewHolder instanceof ItemTouchHelperViewHolder) {
+ // Let the view holder know that this item is being moved or dragged
+ ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
+ itemViewHolder.onItemSelected();
+ }
+ }
+
+ super.onSelectedChanged(viewHolder, actionState);
+ }
+
+ @Override
+ public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
+ super.clearView(recyclerView, viewHolder);
+
+ mAdapter.onItemMoveCompleted();
+
+ viewHolder.itemView.setAlpha(ALPHA_FULL);
+
+ if (viewHolder instanceof ItemTouchHelperViewHolder) {
+ // Tell the view holder it's time to restore the idle state
+ ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
+ itemViewHolder.onItemClear();
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt
deleted file mode 100644
index b98129a7..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2015 Paul Burke
- *
- * 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.
- */
-package com.v2ray.ang.helper
-
-import android.animation.ValueAnimator
-import android.graphics.Canvas
-import android.view.animation.DecelerateInterpolator
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.RecyclerView
-import kotlin.math.abs
-import kotlin.math.min
-import kotlin.math.sign
-
-/**
- * An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and
- * swipe-to-dismiss. Drag events are automatically started by an item long-press.
- *
- * Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement
- * [ItemTouchHelperViewHolder].
- *
- * @author Paul Burke (ipaulpro)
- */
-class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
- private var mReturnAnimator: ValueAnimator? = null
-
- override fun isLongPressDragEnabled(): Boolean = true
-
- override fun isItemViewSwipeEnabled(): Boolean = true
-
- override fun getMovementFlags(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder
- ): Int {
- val dragFlags: Int
- val swipeFlags: Int
- if (recyclerView.layoutManager is GridLayoutManager) {
- dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
- swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
- } else {
- dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
- swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
- }
- return makeMovementFlags(dragFlags, swipeFlags)
- }
-
- override fun onMove(
- recyclerView: RecyclerView,
- source: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
- ): Boolean {
- return if (source.itemViewType != target.itemViewType) {
- false
- } else {
- mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
- true
- }
- }
-
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- // Do not delete; simply return item to original position
- returnViewToOriginalPosition(viewHolder)
- }
-
- override fun onChildDraw(
- c: Canvas, recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
- ) {
- if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
- val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD
- val swipeAmount = abs(dX)
- val direction = sign(dX)
-
- // Limit maximum swipe distance
- val translationX = min(swipeAmount, maxSwipeDistance) * direction
- val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance
-
- viewHolder.itemView.translationX = translationX
- viewHolder.itemView.alpha = alpha
-
- if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
- returnViewToOriginalPosition(viewHolder)
- }
- } else {
- super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
- }
- }
-
- private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) {
- mReturnAnimator?.takeIf { it.isRunning }?.cancel()
-
- mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply {
- addUpdateListener { animation ->
- val value = animation.animatedValue as Float
- viewHolder.itemView.translationX = value
- viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)
- }
- interpolator = DecelerateInterpolator()
- duration = ANIMATION_DURATION
- start()
- }
- }
-
- override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
- if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) {
- viewHolder.onItemSelected()
- }
- super.onSelectedChanged(viewHolder, actionState)
- }
-
- override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
- super.clearView(recyclerView, viewHolder)
- viewHolder.itemView.alpha = ALPHA_FULL
- if (viewHolder is ItemTouchHelperViewHolder) {
- viewHolder.onItemClear()
- }
- mAdapter.onItemMoveCompleted()
- }
-
- override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
- return 1.1f // Set a value greater than 1 to prevent default swipe delete
- }
-
- override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
- return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger
- }
-
- companion object {
- private const val ALPHA_FULL = 1.0f
- private const val SWIPE_THRESHOLD = 0.25f
- private const val ANIMATION_DURATION: Long = 200
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt
deleted file mode 100644
index 7a072cd0..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai *
- * Copyright (C) 2021 by Max Lv *
- * Copyright (C) 2021 by Mygod Studio *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- * *
- ******************************************************************************/
-
-package com.v2ray.ang.plugin
-
-import android.content.pm.ResolveInfo
-
-class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
- init {
- check(resolveInfo.providerInfo != null)
- }
-
- override val componentInfo get() = resolveInfo.providerInfo!!
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt
deleted file mode 100644
index 04294ac6..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai *
- * Copyright (C) 2021 by Max Lv *
- * Copyright (C) 2021 by Mygod Studio *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- * *
- ******************************************************************************/
-
-package com.v2ray.ang.plugin
-
-import android.graphics.drawable.Drawable
-
-abstract class Plugin {
- abstract val id: String
- abstract val label: CharSequence
- abstract val version: Int
- abstract val versionName: String
- open val icon: Drawable? get() = null
- open val defaultConfig: String? get() = null
- open val packageName: String get() = ""
- open val directBootAware: Boolean get() = true
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
- return id == (other as Plugin).id
- }
-
- override fun hashCode() = id.hashCode()
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt
deleted file mode 100644
index 5aa253f8..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- * *
- ******************************************************************************/
-
-package com.v2ray.ang.plugin
-
-object PluginContract {
-
- const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
- const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
- const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
- const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path"
- const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
-
- const val COLUMN_PATH = "path"
- const val COLUMN_MODE = "mode"
- const val SCHEME = "plugin"
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt
deleted file mode 100644
index 2495eb45..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai *
- * Copyright (C) 2021 by Max Lv *
- * Copyright (C) 2021 by Mygod Studio *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- * *
- ******************************************************************************/
-
-package com.v2ray.ang.plugin
-
-import android.content.Intent
-import android.content.pm.PackageManager
-import com.v2ray.ang.AngApplication
-
-class PluginList : ArrayList() {
- init {
- addAll(
- AngApplication.application.packageManager.queryIntentContentProviders(
- Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
- )
- .filter { it.providerInfo.exported }.map { NativePlugin(it) })
- }
-
- val lookup = mutableMapOf().apply {
- for (plugin in this@PluginList.toList()) {
- fun check(old: Plugin?) {
- if (old != null && old != plugin) {
- this@PluginList.remove(old)
- }
- /* if (old != null && old !== plugin) {
- val packages = this@PluginList.filter { it.id == plugin.id }
- .joinToString { it.packageName }
- val message = "Conflicting plugins found from: $packages"
- Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
- throw IllegalStateException(message)
- }*/
- }
- check(put(plugin.id, plugin))
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt
deleted file mode 100644
index e7e8457a..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt
+++ /dev/null
@@ -1,233 +0,0 @@
-/******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai *
- * Copyright (C) 2021 by Max Lv *
- * Copyright (C) 2021 by Mygod Studio *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- * *
- ******************************************************************************/
-
-package com.v2ray.ang.plugin
-
-import android.annotation.SuppressLint
-import android.content.BroadcastReceiver
-import android.content.ContentResolver
-import android.content.Intent
-import android.content.pm.ComponentInfo
-import android.content.pm.PackageManager
-import android.content.pm.ProviderInfo
-import android.database.Cursor
-import android.net.Uri
-import android.os.Build
-import android.system.Os
-import androidx.core.os.bundleOf
-import com.v2ray.ang.AngApplication
-import com.v2ray.ang.extension.listenForPackageChanges
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
-import java.io.File
-import java.io.FileNotFoundException
-
-object PluginManager {
-
- class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin)
-
- private var receiver: BroadcastReceiver? = null
- private var cachedPlugins: PluginList? = null
- fun fetchPlugins() = synchronized(this) {
- if (receiver == null) receiver = AngApplication.application.listenForPackageChanges {
- synchronized(this) {
- receiver = null
- cachedPlugins = null
- }
- }
- if (cachedPlugins == null) cachedPlugins = PluginList()
- cachedPlugins!!
- }
-
- private fun buildUri(id: String, authority: String) = Uri.Builder()
- .scheme(PluginContract.SCHEME)
- .authority(authority)
- .path("/$id")
- .build()
-
- data class InitResult(
- val path: String,
- )
-
- @Throws(Throwable::class)
- fun init(pluginId: String): InitResult? {
- if (pluginId.isEmpty()) return null
- var throwable: Throwable? = null
-
- try {
- val result = initNative(pluginId)
- if (result != null) return result
- } catch (t: Throwable) {
- if (throwable == null) throwable = t //Logs.w(t)
- }
-
- throw throwable ?: PluginNotFoundException(pluginId)
- }
-
- private fun initNative(pluginId: String): InitResult? {
- var flags = PackageManager.GET_META_DATA
- if (Build.VERSION.SDK_INT >= 24) {
- flags =
- flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
- }
- var providers = AngApplication.application.packageManager.queryIntentContentProviders(
- Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags
- )
- .filter { it.providerInfo.exported }
- if (providers.isEmpty()) {
- providers = AngApplication.application.packageManager.queryIntentContentProviders(
- Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags
- )
- .filter { it.providerInfo.exported }
- }
- if (providers.isEmpty()) {
- providers = AngApplication.application.packageManager.queryIntentContentProviders(
- Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags
- )
- .filter { it.providerInfo.exported }
- }
- if (providers.isEmpty()) {
- providers = AngApplication.application.packageManager.queryIntentContentProviders(
- Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags
- )
- .filter { it.providerInfo.exported }
- }
- if (providers.isEmpty()) {
- providers = AngApplication.application.packageManager.queryIntentContentProviders(
- Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
- ).filter {
- it.providerInfo.exported &&
- it.providerInfo.metaData.containsKey(METADATA_KEY_ID) &&
- it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId
- }
- if (providers.size > 1) {
- providers = listOf(providers[0]) // What if there is more than one?
- }
- }
- if (providers.isEmpty()) return null
- if (providers.size > 1) {
- val message =
- "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
- AngApplication.application.toast(message)
- throw IllegalStateException(message)
- }
- val provider = providers.single().providerInfo
- var failure: Throwable? = null
- try {
- initNativeFaster(provider)?.also { return InitResult(it) }
- } catch (t: Throwable) {
- // Logs.w("Initializing native plugin faster mode failed")
- failure = t
- }
-
- val uri = Uri.Builder().apply {
- scheme(ContentResolver.SCHEME_CONTENT)
- authority(provider.authority)
- }.build()
- try {
- return initNativeFast(
- AngApplication.application.contentResolver,
- pluginId,
- uri
- )?.let { InitResult(it) }
- } catch (t: Throwable) {
- // Logs.w("Initializing native plugin fast mode failed")
- failure?.also { t.addSuppressed(it) }
- failure = t
- }
-
- try {
- return initNativeSlow(
- AngApplication.application.contentResolver,
- pluginId,
- uri
- )?.let { InitResult(it) }
- } catch (t: Throwable) {
- failure?.also { t.addSuppressed(it) }
- throw t
- }
- }
-
- private fun initNativeFaster(provider: ProviderInfo): String? {
- return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)
- ?.let { relativePath ->
- File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
- check(canExecute())
- }.absolutePath
- }
- }
-
- private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {
- return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())
- ?.getString(PluginContract.EXTRA_ENTRY)?.also {
- check(File(it).canExecute())
- }
- }
-
- @SuppressLint("Recycle")
- private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {
- var initialized = false
- fun entryNotFound(): Nothing =
- throw IndexOutOfBoundsException("Plugin entry binary not found")
-
- val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
- (cr.query(
- uri,
- arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
- null,
- null,
- null
- )
- ?: return null).use { cursor ->
- if (!cursor.moveToFirst()) entryNotFound()
- pluginDir.deleteRecursively()
- if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
- val pluginDirPath = pluginDir.absolutePath + '/'
- do {
- val path = cursor.getString(0)
- val file = File(pluginDir, path)
- check(file.absolutePath.startsWith(pluginDirPath))
- cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
- file.outputStream().use { outStream -> inStream.copyTo(outStream) }
- }
- Os.chmod(
- file.absolutePath, when (cursor.getType(1)) {
- Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
- Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
- else -> throw IllegalArgumentException("File mode should be of type int")
- }
- )
- if (path == pluginId) initialized = true
- } while (cursor.moveToNext())
- }
- if (!initialized) entryNotFound()
- return File(pluginDir, pluginId).absolutePath
- }
-
- fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
- is String -> value
-// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
-// .getString(value)
-
- null -> null
- else -> error("meta-data $key has invalid type ${value.javaClass}")
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt
deleted file mode 100644
index 6e6861cf..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/******************************************************************************
- * *
- * Copyright (C) 2021 by nekohasekai *
- * Copyright (C) 2021 by Max Lv *
- * Copyright (C) 2021 by Mygod Studio *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * This program is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU General Public License for more details. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- * *
- ******************************************************************************/
-
-package com.v2ray.ang.plugin
-
-import android.content.pm.ComponentInfo
-import android.content.pm.PackageManager
-import android.content.pm.ResolveInfo
-import android.graphics.drawable.Drawable
-import android.os.Build
-import com.v2ray.ang.AngApplication
-import com.v2ray.ang.plugin.PluginManager.loadString
-
-abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
- protected abstract val componentInfo: ComponentInfo
-
- override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
- override val version by lazy {
- getPackageInfo(componentInfo.packageName).versionCode
- }
- override val versionName: String by lazy {
- getPackageInfo(componentInfo.packageName).versionName!!
- }
- override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
- override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
- override val packageName: String get() = componentInfo.packageName
- override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
-
- fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo(
- packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
- else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
- )!!
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt
deleted file mode 100644
index ae15e76c..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.v2ray.ang.receiver
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.service.V2RayServiceManager
-
-class BootReceiver : BroadcastReceiver() {
- /**
- * This method is called when the BroadcastReceiver is receiving an Intent broadcast.
- * It checks if the context is not null and the action is ACTION_BOOT_COMPLETED.
- * If the conditions are met, it starts the V2Ray service.
- *
- * @param context The Context in which the receiver is running.
- * @param intent The Intent being received.
- */
- override fun onReceive(context: Context?, intent: Intent?) {
- if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
- if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
- V2RayServiceManager.startVService(context)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt
deleted file mode 100644
index 99d9bda2..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.v2ray.ang.receiver
-
-import android.app.PendingIntent
-import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProvider
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.widget.RemoteViews
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.service.V2RayServiceManager
-
-class WidgetProvider : AppWidgetProvider() {
- /**
- * This method is called every time the widget is updated.
- * It updates the widget background based on the V2Ray service running state.
- *
- * @param context The Context in which the receiver is running.
- * @param appWidgetManager The AppWidgetManager instance.
- * @param appWidgetIds The appWidgetIds for which an update is needed.
- */
- override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
- super.onUpdate(context, appWidgetManager, appWidgetIds)
- updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.isRunning())
- }
-
- /**
- * Updates the widget background based on whether the V2Ray service is running.
- *
- * @param context The Context in which the receiver is running.
- * @param appWidgetManager The AppWidgetManager instance.
- * @param appWidgetIds The appWidgetIds for which an update is needed.
- * @param isRunning Boolean indicating if the V2Ray service is running.
- */
- private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
- val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
- val intent = Intent(context, WidgetProvider::class.java)
- intent.action = AppConfig.BROADCAST_ACTION_WIDGET_CLICK
- val pendingIntent = PendingIntent.getBroadcast(
- context,
- R.id.layout_switch,
- intent,
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
- )
- remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
- if (isRunning) {
- remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
- remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_active)
- } else {
- remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_play_24dp)
- remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_inactive)
- }
-
- for (appWidgetId in appWidgetIds) {
- appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
- }
- }
-
- /**
- * This method is called when the BroadcastReceiver is receiving an Intent broadcast.
- * It handles widget click actions and updates the widget background based on the V2Ray service state.
- *
- * @param context The Context in which the receiver is running.
- * @param intent The Intent being received.
- */
- override fun onReceive(context: Context, intent: Intent) {
- super.onReceive(context, intent)
- if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
- if (V2RayServiceManager.isRunning()) {
- V2RayServiceManager.stopVService(context)
- } else {
- V2RayServiceManager.startVServiceFromToggle(context)
- }
- } else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
- AppWidgetManager.getInstance(context)?.let { manager ->
- when (intent.getIntExtra("key", 0)) {
- AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
- updateWidgetBackground(
- context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
- true
- )
- }
-
- AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
- updateWidgetBackground(
- context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
- false
- )
- }
- }
- }
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt
deleted file mode 100644
index 92c551a6..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-package com.v2ray.ang.service
-
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.graphics.Color
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.core.app.NotificationCompat
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.ANG_PACKAGE
-import com.v2ray.ang.AppConfig.TAG_DIRECT
-import com.v2ray.ang.R
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.extension.toSpeedString
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.ui.MainActivity
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-import kotlin.math.min
-
-object NotificationService {
- private const val NOTIFICATION_ID = 1
- private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
- private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
- private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
- private const val NOTIFICATION_ICON_THRESHOLD = 3000
-
- private var lastQueryTime = 0L
- private var mBuilder: NotificationCompat.Builder? = null
- private var speedNotificationJob: Job? = null
- private var mNotificationManager: NotificationManager? = null
-
- /**
- * Starts the speed notification.
- * @param currentConfig The current profile configuration.
- */
- fun startSpeedNotification(currentConfig: ProfileItem?) {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
- if (speedNotificationJob != null || V2RayServiceManager.isRunning() == false) return
-
- lastQueryTime = System.currentTimeMillis()
- var lastZeroSpeed = false
- val outboundTags = currentConfig?.getAllOutboundTags()
- outboundTags?.remove(TAG_DIRECT)
-
- speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
- while (isActive) {
- val queryTime = System.currentTimeMillis()
- val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
- var proxyTotal = 0L
- val text = StringBuilder()
- outboundTags?.forEach {
- val up = V2RayServiceManager.queryStats(it, AppConfig.UPLINK)
- val down = V2RayServiceManager.queryStats(it, AppConfig.DOWNLINK)
- if (up + down > 0) {
- appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
- proxyTotal += up + down
- }
- }
- val directUplink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.UPLINK)
- val directDownlink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
- val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
- if (!zeroSpeed || !lastZeroSpeed) {
- if (proxyTotal == 0L) {
- appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
- }
- appendSpeedString(
- text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
- directDownlink / sinceLastQueryInSeconds
- )
- updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
- }
- lastZeroSpeed = zeroSpeed
- lastQueryTime = queryTime
- delay(3000)
- }
- }
- }
-
- /**
- * Shows the notification.
- * @param currentConfig The current profile configuration.
- */
- fun showNotification(currentConfig: ProfileItem?) {
- val service = getService() ?: return
- val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
-
- val startMainIntent = Intent(service, MainActivity::class.java)
- val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
-
- val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
- stopV2RayIntent.`package` = ANG_PACKAGE
- stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
- val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
-
- val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
- restartV2RayIntent.`package` = ANG_PACKAGE
- restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
- val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
-
- val channelId =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- createNotificationChannel()
- } else {
- // If earlier version channel ID is not used
- // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
- ""
- }
-
- mBuilder = NotificationCompat.Builder(service, channelId)
- .setSmallIcon(R.drawable.ic_stat_name)
- .setContentTitle(currentConfig?.remarks)
- .setPriority(NotificationCompat.PRIORITY_MIN)
- .setOngoing(true)
- .setShowWhen(false)
- .setOnlyAlertOnce(true)
- .setContentIntent(contentPendingIntent)
- .addAction(
- R.drawable.ic_delete_24dp,
- service.getString(R.string.notification_action_stop_v2ray),
- stopV2RayPendingIntent
- )
- .addAction(
- R.drawable.ic_delete_24dp,
- service.getString(R.string.title_service_restart),
- restartV2RayPendingIntent
- )
-
- //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
-
- service.startForeground(NOTIFICATION_ID, mBuilder?.build())
- }
-
- /**
- * Cancels the notification.
- */
- fun cancelNotification() {
- val service = getService() ?: return
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
- } else {
- service.stopForeground(true)
- }
-
- mBuilder = null
- speedNotificationJob?.cancel()
- speedNotificationJob = null
- mNotificationManager = null
- }
-
- /**
- * Stops the speed notification.
- * @param currentConfig The current profile configuration.
- */
- fun stopSpeedNotification(currentConfig: ProfileItem?) {
- speedNotificationJob?.let {
- it.cancel()
- speedNotificationJob = null
- updateNotification(currentConfig?.remarks, 0, 0)
- }
- }
-
- /**
- * Creates a notification channel for Android O and above.
- * @return The channel ID.
- */
- @RequiresApi(Build.VERSION_CODES.O)
- private fun createNotificationChannel(): String {
- val channelId = AppConfig.RAY_NG_CHANNEL_ID
- val channelName = AppConfig.RAY_NG_CHANNEL_NAME
- val chan = NotificationChannel(
- channelId,
- channelName, NotificationManager.IMPORTANCE_HIGH
- )
- chan.lightColor = Color.DKGRAY
- chan.importance = NotificationManager.IMPORTANCE_NONE
- chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
- getNotificationManager()?.createNotificationChannel(chan)
- return channelId
- }
-
- /**
- * Updates the notification with the given content text and traffic data.
- * @param contentText The content text.
- * @param proxyTraffic The proxy traffic.
- * @param directTraffic The direct traffic.
- */
- private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
- if (mBuilder != null) {
- if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
- mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
- } else if (proxyTraffic > directTraffic) {
- mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
- } else {
- mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
- }
- mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
- mBuilder?.setContentText(contentText)
- getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
- }
- }
-
- /**
- * Gets the notification manager.
- * @return The notification manager.
- */
- private fun getNotificationManager(): NotificationManager? {
- if (mNotificationManager == null) {
- val service = getService() ?: return null
- mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- }
- return mNotificationManager
- }
-
- /**
- * Appends the speed string to the given text.
- * @param text The text to append to.
- * @param name The name of the tag.
- * @param up The uplink speed.
- * @param down The downlink speed.
- */
- private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
- var n = name ?: "no tag"
- n = n.substring(0, min(n.length, 6))
- text.append(n)
- for (i in n.length..6 step 2) {
- text.append("\t")
- }
- text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
- }
-
- /**
- * Gets the service instance.
- * @return The service instance.
- */
- private fun getService(): Service? {
- return V2RayServiceManager.serviceControl?.get()?.getService()
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt
deleted file mode 100644
index e81e3615..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.v2ray.ang.service
-
-import android.content.Context
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class ProcessService {
- private var process: Process? = null
-
- /**
- * Runs a process with the given command.
- * @param context The context.
- * @param cmd The command to run.
- */
- fun runProcess(context: Context, cmd: MutableList) {
- Log.i(AppConfig.TAG, cmd.toString())
-
- try {
- val proBuilder = ProcessBuilder(cmd)
- proBuilder.redirectErrorStream(true)
- process = proBuilder
- .directory(context.filesDir)
- .start()
-
- CoroutineScope(Dispatchers.IO).launch {
- Thread.sleep(50L)
- Log.i(AppConfig.TAG, "runProcess check")
- process?.waitFor()
- Log.i(AppConfig.TAG, "runProcess exited")
- }
- Log.i(AppConfig.TAG, process.toString())
-
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, e.toString(), e)
- }
- }
-
- /**
- * Stops the running process.
- */
- fun stopProcess() {
- try {
- Log.i(AppConfig.TAG, "runProcess destroy")
- process?.destroy()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to destroy process", e)
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt
deleted file mode 100644
index 085c72eb..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.v2ray.ang.service
-
-import android.app.Service
-
-interface ServiceControl {
- /**
- * Gets the service instance.
- * @return The service instance.
- */
- fun getService(): Service
-
- /**
- * Starts the service.
- */
- fun startService()
-
- /**
- * Stops the service.
- */
- fun stopService()
-
- /**
- * Protects the VPN socket.
- * @param socket The socket to protect.
- * @return True if the socket is protected, false otherwise.
- */
- fun vpnProtect(socket: Int): Boolean
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt
deleted file mode 100644
index 5f3f8172..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.v2ray.ang.service
-
-import android.annotation.SuppressLint
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.content.Context
-import android.os.Build
-import android.util.Log
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.work.CoroutineWorker
-import androidx.work.WorkerParameters
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
-import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
-import com.v2ray.ang.R
-import com.v2ray.ang.handler.AngConfigManager.updateConfigViaSub
-import com.v2ray.ang.handler.MmkvManager
-
-object SubscriptionUpdater {
-
- class UpdateTask(context: Context, params: WorkerParameters) :
- CoroutineWorker(context, params) {
-
- private val notificationManager = NotificationManagerCompat.from(applicationContext)
- private val notification =
- NotificationCompat.Builder(applicationContext, SUBSCRIPTION_UPDATE_CHANNEL)
- .setWhen(0)
- .setTicker("Update")
- .setContentTitle(context.getString(R.string.title_pref_auto_update_subscription))
- .setSmallIcon(R.drawable.ic_stat_name)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
-
- /**
- * Performs the subscription update work.
- * @return The result of the work.
- */
- @SuppressLint("MissingPermission")
- override suspend fun doWork(): Result {
- Log.i(AppConfig.TAG, "subscription automatic update starting")
-
- val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
-
- for (sub in subs) {
- val subItem = sub.second
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
- val channel =
- NotificationChannel(
- SUBSCRIPTION_UPDATE_CHANNEL,
- SUBSCRIPTION_UPDATE_CHANNEL_NAME,
- NotificationManager.IMPORTANCE_MIN
- )
- notificationManager.createNotificationChannel(channel)
- }
- notificationManager.notify(3, notification.build())
- Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
- updateConfigViaSub(Pair(sub.first, subItem))
- notification.setContentText("Updating ${subItem.remarks}")
- }
- notificationManager.cancel(3)
- return Result.success()
- }
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt
deleted file mode 100644
index 25fcd1a6..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.v2ray.ang.service
-
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.IBinder
-import androidx.annotation.RequiresApi
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.util.MyContextWrapper
-import java.lang.ref.SoftReference
-
-class V2RayProxyOnlyService : Service(), ServiceControl {
- /**
- * Initializes the service.
- */
- override fun onCreate() {
- super.onCreate()
- V2RayServiceManager.serviceControl = SoftReference(this)
- }
-
- /**
- * Handles the start command for the service.
- * @param intent The intent.
- * @param flags The flags.
- * @param startId The start ID.
- * @return The start mode.
- */
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- V2RayServiceManager.startCoreLoop()
- return START_STICKY
- }
-
- /**
- * Destroys the service.
- */
- override fun onDestroy() {
- super.onDestroy()
- V2RayServiceManager.stopCoreLoop()
- }
-
- /**
- * Gets the service instance.
- * @return The service instance.
- */
- override fun getService(): Service {
- return this
- }
-
- /**
- * Starts the service.
- */
- override fun startService() {
- // do nothing
- }
-
- /**
- * Stops the service.
- */
- override fun stopService() {
- stopSelf()
- }
-
- /**
- * Protects the VPN socket.
- * @param socket The socket to protect.
- * @return True if the socket is protected, false otherwise.
- */
- override fun vpnProtect(socket: Int): Boolean {
- return true
- }
-
- /**
- * Binds the service.
- * @param intent The intent.
- * @return The binder.
- */
- override fun onBind(intent: Intent?): IBinder? {
- return null
- }
-
- /**
- * Attaches the base context to the service.
- * @param newBase The new base context.
- */
- @RequiresApi(Build.VERSION_CODES.N)
- override fun attachBaseContext(newBase: Context?) {
- val context = newBase?.let {
- MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
- }
- super.attachBaseContext(context)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt
deleted file mode 100644
index 4f42ca23..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt
+++ /dev/null
@@ -1,377 +0,0 @@
-package com.v2ray.ang.service
-
-import android.app.Service
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.os.Build
-import android.util.Log
-import androidx.core.content.ContextCompat
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.handler.SpeedtestManager
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.MessageUtil
-import com.v2ray.ang.util.PluginUtil
-import com.v2ray.ang.util.Utils
-import go.Seq
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import libv2ray.CoreCallbackHandler
-import libv2ray.CoreController
-import libv2ray.Libv2ray
-import java.lang.ref.SoftReference
-
-object V2RayServiceManager {
-
- private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback())
- private val mMsgReceive = ReceiveMessageHandler()
- private var currentConfig: ProfileItem? = null
-
- var serviceControl: SoftReference? = null
- set(value) {
- field = value
- Seq.setContext(value?.get()?.getService()?.applicationContext)
- Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
- }
-
- /**
- * Starts the V2Ray service from a toggle action.
- * @param context The context from which the service is started.
- * @return True if the service was started successfully, false otherwise.
- */
- fun startVServiceFromToggle(context: Context): Boolean {
- if (MmkvManager.getSelectServer().isNullOrEmpty()) {
- context.toast(R.string.app_tile_first_use)
- return false
- }
- startContextService(context)
- return true
- }
-
- /**
- * Starts the V2Ray service.
- * @param context The context from which the service is started.
- * @param guid The GUID of the server configuration to use (optional).
- */
- fun startVService(context: Context, guid: String? = null) {
- if (guid != null) {
- MmkvManager.setSelectServer(guid)
- }
- startContextService(context)
- }
-
- /**
- * Stops the V2Ray service.
- * @param context The context from which the service is stopped.
- */
- fun stopVService(context: Context) {
- context.toast(R.string.toast_services_stop)
- MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
- }
-
- /**
- * Checks if the V2Ray service is running.
- * @return True if the service is running, false otherwise.
- */
- fun isRunning() = coreController.isRunning
-
- /**
- * Gets the name of the currently running server.
- * @return The name of the running server.
- */
- fun getRunningServerName() = currentConfig?.remarks.orEmpty()
-
- /**
- * Starts the context service for V2Ray.
- * Chooses between VPN service or Proxy-only service based on user settings.
- * @param context The context from which the service is started.
- */
- private fun startContextService(context: Context) {
- if (coreController.isRunning) {
- return
- }
- val guid = MmkvManager.getSelectServer() ?: return
- val config = MmkvManager.decodeServerConfig(guid) ?: return
- if (config.configType != EConfigType.CUSTOM
- && !Utils.isValidUrl(config.server)
- && !Utils.isPureIpAddress(config.server.orEmpty())
- ) return
-// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
-// if (!result.status) return
-
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) {
- context.toast(R.string.toast_warning_pref_proxysharing_short)
- } else {
- context.toast(R.string.toast_services_start)
- }
- val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
- Intent(context.applicationContext, V2RayVpnService::class.java)
- } else {
- Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
- }
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
- context.startForegroundService(intent)
- } else {
- context.startService(intent)
- }
- }
-
- /**
- * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
- * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
- * Starts the V2Ray core service.
- */
- fun startCoreLoop(): Boolean {
- if (coreController.isRunning) {
- return false
- }
-
- val service = getService() ?: return false
- val guid = MmkvManager.getSelectServer() ?: return false
- val config = MmkvManager.decodeServerConfig(guid) ?: return false
- val result = V2rayConfigManager.getV2rayConfig(service, guid)
- if (!result.status)
- return false
-
- try {
- val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
- mFilter.addAction(Intent.ACTION_SCREEN_ON)
- mFilter.addAction(Intent.ACTION_SCREEN_OFF)
- mFilter.addAction(Intent.ACTION_USER_PRESENT)
- ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
- return false
- }
-
- currentConfig = config
-
- try {
- coreController.startLoop(result.content)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to start Core loop", e)
- return false
- }
-
- if (coreController.isRunning == false) {
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
- NotificationService.cancelNotification()
- return false
- }
-
- try {
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
- NotificationService.showNotification(currentConfig)
- NotificationService.startSpeedNotification(currentConfig)
-
- PluginUtil.runPlugin(service, config, result.socksPort)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to startup service", e)
- return false
- }
- return true
- }
-
- /**
- * Stops the V2Ray core service.
- * Unregisters broadcast receivers, stops notifications, and shuts down plugins.
- * @return True if the core was stopped successfully, false otherwise.
- */
- fun stopCoreLoop(): Boolean {
- val service = getService() ?: return false
-
- if (coreController.isRunning) {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- coreController.stopLoop()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
- }
- }
- }
-
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
- NotificationService.cancelNotification()
-
- try {
- service.unregisterReceiver(mMsgReceive)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
- }
- PluginUtil.stopPlugin()
-
- return true
- }
-
- /**
- * Queries the statistics for a given tag and link.
- * @param tag The tag to query.
- * @param link The link to query.
- * @return The statistics value.
- */
- fun queryStats(tag: String, link: String): Long {
- return coreController.queryStats(tag, link)
- }
-
- /**
- * Measures the connection delay for the current V2Ray configuration.
- * Tests with primary URL first, then falls back to alternative URL if needed.
- * Also fetches remote IP information if the delay test was successful.
- */
- private fun measureV2rayDelay() {
- if (coreController.isRunning == false) {
- return
- }
-
- CoroutineScope(Dispatchers.IO).launch {
- val service = getService() ?: return@launch
- var time = -1L
- var errorStr = ""
-
- try {
- time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
- errorStr = e.message?.substringAfter("\":") ?: "empty message"
- }
- if (time == -1L) {
- try {
- time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
- errorStr = e.message?.substringAfter("\":") ?: "empty message"
- }
- }
-
- val result = if (time >= 0) {
- service.getString(R.string.connection_test_available, time)
- } else {
- service.getString(R.string.connection_test_error, errorStr)
- }
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
-
- // Only fetch IP info if the delay test was successful
- if (time >= 0) {
- SpeedtestManager.getRemoteIPInfo()?.let { ip ->
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
- }
- }
- }
- }
-
- /**
- * Gets the current service instance.
- * @return The current service instance, or null if not available.
- */
- private fun getService(): Service? {
- return serviceControl?.get()?.getService()
- }
-
- /**
- * Core callback handler implementation for handling V2Ray core events.
- * Handles startup, shutdown, socket protection, and status emission.
- */
- private class CoreCallback : CoreCallbackHandler {
- /**
- * Called when V2Ray core starts up.
- * @return 0 for success, any other value for failure.
- */
- override fun startup(): Long {
- return 0
- }
-
- /**
- * Called when V2Ray core shuts down.
- * @return 0 for success, any other value for failure.
- */
- override fun shutdown(): Long {
- val serviceControl = serviceControl?.get() ?: return -1
- return try {
- serviceControl.stopService()
- 0
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
- -1
- }
- }
-
- /**
- * Called when V2Ray core emits status information.
- * @param l Status code.
- * @param s Status message.
- * @return Always returns 0.
- */
- override fun onEmitStatus(l: Long, s: String?): Long {
- return 0
- }
- }
-
- /**
- * Broadcast receiver for handling messages sent to the service.
- * Handles registration, service control, and screen events.
- */
- private class ReceiveMessageHandler : BroadcastReceiver() {
- /**
- * Handles received broadcast messages.
- * Processes service control messages and screen state changes.
- * @param ctx The context in which the receiver is running.
- * @param intent The intent being received.
- */
- override fun onReceive(ctx: Context?, intent: Intent?) {
- val serviceControl = serviceControl?.get() ?: return
- when (intent?.getIntExtra("key", 0)) {
- AppConfig.MSG_REGISTER_CLIENT -> {
- if (coreController.isRunning) {
- MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
- } else {
- MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
- }
- }
-
- AppConfig.MSG_UNREGISTER_CLIENT -> {
- // nothing to do
- }
-
- AppConfig.MSG_STATE_START -> {
- // nothing to do
- }
-
- AppConfig.MSG_STATE_STOP -> {
- Log.i(AppConfig.TAG, "Stop Service")
- serviceControl.stopService()
- }
-
- AppConfig.MSG_STATE_RESTART -> {
- Log.i(AppConfig.TAG, "Restart Service")
- serviceControl.stopService()
- Thread.sleep(500L)
- startVService(serviceControl.getService())
- }
-
- AppConfig.MSG_MEASURE_DELAY -> {
- measureV2rayDelay()
- }
- }
-
- when (intent?.action) {
- Intent.ACTION_SCREEN_OFF -> {
- Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
- NotificationService.stopSpeedNotification(currentConfig)
- }
-
- Intent.ACTION_SCREEN_ON -> {
- Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
- NotificationService.startSpeedNotification(currentConfig)
- }
- }
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt
deleted file mode 100644
index 3fef1ae1..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package com.v2ray.ang.service
-
-import android.app.Service
-import android.content.Intent
-import android.os.IBinder
-import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
-import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
-import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.extension.serializable
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SpeedtestManager
-import com.v2ray.ang.handler.V2rayConfigManager
-import com.v2ray.ang.util.MessageUtil
-import com.v2ray.ang.util.PluginUtil
-import com.v2ray.ang.util.Utils
-import go.Seq
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.cancelChildren
-import kotlinx.coroutines.launch
-import libv2ray.Libv2ray
-import java.util.concurrent.Executors
-
-class V2RayTestService : Service() {
- private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
-
- /**
- * Initializes the V2Ray environment.
- */
- override fun onCreate() {
- super.onCreate()
- Seq.setContext(this)
- Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
- }
-
- /**
- * Handles the start command for the service.
- * @param intent The intent.
- * @param flags The flags.
- * @param startId The start ID.
- * @return The start mode.
- */
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- when (intent?.getIntExtra("key", 0)) {
- MSG_MEASURE_CONFIG -> {
- val guid = intent.serializable("content") ?: ""
- realTestScope.launch {
- val result = startRealPing(guid)
- MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
- }
- }
-
- MSG_MEASURE_CONFIG_CANCEL -> {
- realTestScope.coroutineContext[Job]?.cancelChildren()
- }
- }
- return super.onStartCommand(intent, flags, startId)
- }
-
- /**
- * Binds the service.
- * @param intent The intent.
- * @return The binder.
- */
- override fun onBind(intent: Intent?): IBinder? {
- return null
- }
-
- /**
- * Starts the real ping test.
- * @param guid The GUID of the configuration.
- * @return The ping result.
- */
- private fun startRealPing(guid: String): Long {
- val retFailure = -1L
-
- val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
- if (config.configType == EConfigType.HYSTERIA2) {
- val delay = PluginUtil.realPingHy2(this, config)
- return delay
- } else {
- val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
- if (!configResult.status) {
- return retFailure
- }
- return SpeedtestManager.realPing(configResult.content)
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt
deleted file mode 100644
index d734c299..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt
+++ /dev/null
@@ -1,375 +0,0 @@
-package com.v2ray.ang.service
-
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.net.ConnectivityManager
-import android.net.LocalSocket
-import android.net.LocalSocketAddress
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import android.net.ProxyInfo
-import android.net.VpnService
-import android.os.Build
-import android.os.ParcelFileDescriptor
-import android.os.StrictMode
-import android.util.Log
-import androidx.annotation.RequiresApi
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.LOOPBACK
-import com.v2ray.ang.BuildConfig
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.util.MyContextWrapper
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import java.io.File
-import java.lang.ref.SoftReference
-
-class V2RayVpnService : VpnService(), ServiceControl {
- companion object {
- private const val VPN_MTU = 1500
- private const val TUN2SOCKS = "libtun2socks.so"
- }
-
- private lateinit var mInterface: ParcelFileDescriptor
- private var isRunning = false
- private lateinit var process: Process
-
- /**destroy
- * Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
- *
- * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
- * satisfies default network capabilities but only THE default network. Unfortunately we need to have
- * android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
- *
- * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
- */
- @delegate:RequiresApi(Build.VERSION_CODES.P)
- private val defaultNetworkRequest by lazy {
- NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .build()
- }
-
- private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager }
-
- @delegate:RequiresApi(Build.VERSION_CODES.P)
- private val defaultNetworkCallback by lazy {
- object : ConnectivityManager.NetworkCallback() {
- override fun onAvailable(network: Network) {
- setUnderlyingNetworks(arrayOf(network))
- }
-
- override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
- // it's a good idea to refresh capabilities
- setUnderlyingNetworks(arrayOf(network))
- }
-
- override fun onLost(network: Network) {
- setUnderlyingNetworks(null)
- }
- }
- }
-
- override fun onCreate() {
- super.onCreate()
- val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
- StrictMode.setThreadPolicy(policy)
- V2RayServiceManager.serviceControl = SoftReference(this)
- }
-
- override fun onRevoke() {
- stopV2Ray()
- }
-
-// override fun onLowMemory() {
-// stopV2Ray()
-// super.onLowMemory()
-// }
-
- override fun onDestroy() {
- super.onDestroy()
- NotificationService.cancelNotification()
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (V2RayServiceManager.startCoreLoop()) {
- startService()
- }
- return START_STICKY
- //return super.onStartCommand(intent, flags, startId)
- }
-
- override fun getService(): Service {
- return this
- }
-
- override fun startService() {
- setup()
- }
-
- override fun stopService() {
- stopV2Ray(true)
- }
-
- override fun vpnProtect(socket: Int): Boolean {
- return protect(socket)
- }
-
- @RequiresApi(Build.VERSION_CODES.N)
- override fun attachBaseContext(newBase: Context?) {
- val context = newBase?.let {
- MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
- }
- super.attachBaseContext(context)
- }
-
- /**
- * Sets up the VPN service.
- * Prepares the VPN and configures it if preparation is successful.
- */
- private fun setup() {
- val prepare = prepare(this)
- if (prepare != null) {
- return
- }
-
- if (setupVpnService() != true) {
- return
- }
-
- runTun2socks()
- }
-
- /**
- * Configures the VPN service.
- * @return True if the VPN service was configured successfully, false otherwise.
- */
- private fun setupVpnService(): Boolean {
- // If the old interface has exactly the same parameters, use it!
- // Configure a builder while parsing the parameters.
- val builder = Builder()
- val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
- //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
-
- builder.setMtu(VPN_MTU)
- builder.addAddress(vpnConfig.ipv4Client, 30)
- //builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
- val bypassLan = SettingsManager.routingRulesetsBypassLan()
- if (bypassLan) {
- AppConfig.ROUTED_IP_LIST.forEach {
- val addr = it.split('/')
- builder.addRoute(addr[0], addr[1].toInt())
- }
- } else {
- builder.addRoute("0.0.0.0", 0)
- }
-
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
- builder.addAddress(vpnConfig.ipv6Client, 126)
- if (bypassLan) {
- builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
- builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool
- } else {
- builder.addRoute("::", 0)
- }
- }
-
-// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
-// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
-// } else {
- SettingsManager.getVpnDnsServers()
- .forEach {
- if (Utils.isPureIpAddress(it)) {
- builder.addDnsServer(it)
- }
- }
-// }
-
- builder.setSession(V2RayServiceManager.getRunningServerName())
-
- val selfPackageName = BuildConfig.APPLICATION_ID
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
- val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
- val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
- //process self package
- if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
- apps?.forEach {
- try {
- if (bypassApps)
- builder.addDisallowedApplication(it)
- else
- builder.addAllowedApplication(it)
- } catch (e: PackageManager.NameNotFoundException) {
- Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e)
- }
- }
- } else {
- builder.addDisallowedApplication(selfPackageName)
- }
-
- // Close the old interface since the parameters have been changed.
- try {
- mInterface.close()
- } catch (ignored: Exception) {
- // ignored
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- try {
- connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to request default network", e)
- }
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- builder.setMetered(false)
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY)) {
- builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOOPBACK, SettingsManager.getHttpPort()))
- }
- }
-
- // Create a new interface using the builder and save the parameters.
- try {
- mInterface = builder.establish()!!
- isRunning = true
- return true
- } catch (e: Exception) {
- // non-nullable lateinit var
- Log.e(AppConfig.TAG, "Failed to establish VPN interface", e)
- stopV2Ray()
- }
- return false
- }
-
- /**
- * Runs the tun2socks process.
- * Starts the tun2socks process with the appropriate parameters.
- */
- private fun runTun2socks() {
- Log.i(AppConfig.TAG, "Start run $TUN2SOCKS")
- val socksPort = SettingsManager.getSocksPort()
- val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
- val cmd = arrayListOf(
- File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
- "--netif-ipaddr", vpnConfig.ipv4Router,
- "--netif-netmask", "255.255.255.252",
- "--socks-server-addr", "$LOOPBACK:${socksPort}",
- "--tunmtu", VPN_MTU.toString(),
- "--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
- "--enable-udprelay",
- "--loglevel", "notice"
- )
-
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
- cmd.add("--netif-ip6addr")
- cmd.add(vpnConfig.ipv6Router)
- }
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
- val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
- cmd.add("--dnsgw")
- cmd.add("$LOOPBACK:${localDnsPort}")
- }
- Log.i(AppConfig.TAG, cmd.toString())
-
- try {
- val proBuilder = ProcessBuilder(cmd)
- proBuilder.redirectErrorStream(true)
- process = proBuilder
- .directory(applicationContext.filesDir)
- .start()
- Thread {
- Log.i(AppConfig.TAG, "$TUN2SOCKS check")
- process.waitFor()
- Log.i(AppConfig.TAG, "$TUN2SOCKS exited")
- if (isRunning) {
- Log.i(AppConfig.TAG, "$TUN2SOCKS restart")
- runTun2socks()
- }
- }.start()
- Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}")
-
- sendFd()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e)
- }
- }
-
- /**
- * Sends the file descriptor to the tun2socks process.
- * Attempts to send the file descriptor multiple times if necessary.
- */
- private fun sendFd() {
- val fd = mInterface.fileDescriptor
- val path = File(applicationContext.filesDir, "sock_path").absolutePath
- Log.i(AppConfig.TAG, "LocalSocket path : $path")
-
- CoroutineScope(Dispatchers.IO).launch {
- var tries = 0
- while (true) try {
- Thread.sleep(50L shl tries)
- Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries")
- LocalSocket().use { localSocket ->
- localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
- localSocket.setFileDescriptorsForSend(arrayOf(fd))
- localSocket.outputStream.write(42)
- }
- break
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e)
- if (tries > 5) break
- tries += 1
- }
- }
- }
-
- /**
- * Stops the V2Ray service.
- * @param isForced Whether to force stop the service.
- */
- private fun stopV2Ray(isForced: Boolean = true) {
-// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
-// val emptyInfo = VpnNetworkInfo()
-// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
-// saveVpnNetworkInfo(configName, info)
- isRunning = false
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- try {
- connectivity.unregisterNetworkCallback(defaultNetworkCallback)
- } catch (ignored: Exception) {
- // ignored
- }
- }
-
- try {
- Log.i(AppConfig.TAG, "$TUN2SOCKS destroy")
- process.destroy()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e)
- }
-
- V2RayServiceManager.stopCoreLoop()
-
- if (isForced) {
- //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
- //It's strage but true.
- //This can be verified by putting stopself() behind and call stopLoop and startLoop
- //in a row for several times. You will find that later created v2ray core report port in use
- //which means the first v2ray core somehow failed to stop and release the port.
- stopSelf()
-
- try {
- mInterface.close()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to close VPN interface", e)
- }
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt
deleted file mode 100644
index 1931cb45..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.Manifest
-import android.content.Intent
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.content.ContextCompat
-import androidx.core.content.FileProvider
-import com.tencent.mmkv.MMKV
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.BuildConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityAboutBinding
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SpeedtestManager
-import com.v2ray.ang.util.Utils
-import com.v2ray.ang.util.ZipUtil
-import java.io.File
-import java.text.SimpleDateFormat
-import java.util.Locale
-
-class AboutActivity : BaseActivity() {
-
- private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
- private val extDir by lazy { File(Utils.backupPath(this)) }
-
- private val requestPermissionLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
- if (isGranted) {
- try {
- showFileChooser()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to show file chooser", e)
- }
- } else {
- toast(R.string.toast_permission_denied)
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- title = getString(R.string.title_about)
-
- binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
-
- binding.layoutBackup.setOnClickListener {
- val ret = backupConfiguration(extDir.absolutePath)
- if (ret.first) {
- toastSuccess(R.string.toast_success)
- } else {
- toastError(R.string.toast_failure)
- }
- }
-
- binding.layoutShare.setOnClickListener {
- val ret = backupConfiguration(cacheDir.absolutePath)
- if (ret.first) {
- startActivity(
- Intent.createChooser(
- Intent(Intent.ACTION_SEND).setType("application/zip")
- .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- .putExtra(
- Intent.EXTRA_STREAM,
- FileProvider.getUriForFile(
- this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
- )
- ), getString(R.string.title_configuration_share)
- )
- )
- } else {
- toastError(R.string.toast_failure)
- }
- }
-
- binding.layoutRestore.setOnClickListener {
- val permission =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- Manifest.permission.READ_MEDIA_IMAGES
- } else {
- Manifest.permission.READ_EXTERNAL_STORAGE
- }
-
- if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
- try {
- showFileChooser()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to show file chooser", e)
- }
- } else {
- requestPermissionLauncher.launch(permission)
- }
- }
-
- binding.layoutSoureCcode.setOnClickListener {
- Utils.openUri(this, AppConfig.APP_URL)
- }
-
- binding.layoutFeedback.setOnClickListener {
- Utils.openUri(this, AppConfig.APP_ISSUES_URL)
- }
-
- binding.layoutOssLicenses.setOnClickListener {
- val webView = android.webkit.WebView(this)
- webView.loadUrl("file:///android_asset/open_source_licenses.html")
- android.app.AlertDialog.Builder(this)
- .setTitle("Open source licenses")
- .setView(webView)
- .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
- .show()
- }
-
- binding.layoutTgChannel.setOnClickListener {
- Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
- }
-
- binding.layoutPrivacyPolicy.setOnClickListener {
- Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
- }
-
- "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
- binding.tvVersion.text = it
- }
- }
-
- private fun backupConfiguration(outputZipFilePos: String): Pair {
- val dateFormated = SimpleDateFormat(
- "yyyy-MM-dd-HH-mm-ss",
- Locale.getDefault()
- ).format(System.currentTimeMillis())
- val folderName = "${getString(R.string.app_name)}_${dateFormated}"
- val backupDir = this.cacheDir.absolutePath + "/$folderName"
- val outputZipFilePath = "$outputZipFilePos/$folderName.zip"
-
- val count = MMKV.backupAllToDirectory(backupDir)
- if (count <= 0) {
- return Pair(false, "")
- }
-
- if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) {
- return Pair(true, outputZipFilePath)
- } else {
- return Pair(false, "")
- }
- }
-
- private fun restoreConfiguration(zipFile: File): Boolean {
- val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
-
- if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
- return false
- }
-
- val count = MMKV.restoreAllFromDirectory(backupDir)
- return count > 0
- }
-
- private fun showFileChooser() {
- val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
- type = "*/*"
- addCategory(Intent.CATEGORY_OPENABLE)
- }
-
- try {
- chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
- } catch (ex: android.content.ActivityNotFoundException) {
- Log.e(AppConfig.TAG, "File chooser activity not found", ex)
- toast(R.string.toast_require_file_manager)
- }
- }
-
- private val chooseFile =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- val uri = result.data?.data
- if (result.resultCode == RESULT_OK && uri != null) {
- try {
- val targetFile =
- File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
- contentResolver.openInputStream(uri).use { input ->
- targetFile.outputStream().use { fileOut ->
- input?.copyTo(fileOut)
- }
- }
- if (restoreConfiguration(targetFile)) {
- toastSuccess(R.string.toast_success)
- } else {
- toastError(R.string.toast_failure)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Error during file restore", e)
- toastError(R.string.toast_failure)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt
deleted file mode 100644
index 0cd7f647..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.content.Context
-import android.os.Build
-import android.os.Bundle
-import android.view.MenuItem
-import androidx.annotation.RequiresApi
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.ContextCompat
-import androidx.core.view.WindowCompat
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.RecyclerView
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.helper.CustomDividerItemDecoration
-import com.v2ray.ang.util.MyContextWrapper
-import com.v2ray.ang.util.Utils
-
-
-abstract class BaseActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- if (!Utils.getDarkModeStatus(this)) {
- WindowCompat.getInsetsController(window, window.decorView).apply {
- isAppearanceLightStatusBars = true
- }
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- android.R.id.home -> {
- // Handles the home button press by delegating to the onBackPressedDispatcher.
- // This ensures consistent back navigation behavior.
- onBackPressedDispatcher.onBackPressed()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-
- @RequiresApi(Build.VERSION_CODES.N)
- override fun attachBaseContext(newBase: Context?) {
- super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale()))
- }
-
- /**
- * Adds a custom divider to a RecyclerView.
- *
- * @param recyclerView The target RecyclerView to which the divider will be added.
- * @param context The context used to access resources.
- * @param drawableResId The resource ID of the drawable to be used as the divider.
- * @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
- */
- fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, context: Context?, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
- // Get the drawable from resources
- val drawable = ContextCompat.getDrawable(context!!, drawableResId)
- requireNotNull(drawable) { "Drawable resource not found" }
-
- // Create a DividerItemDecoration with the specified orientation
- val dividerItemDecoration = CustomDividerItemDecoration(drawable, orientation)
-
- // Add the divider to the RecyclerView
- recyclerView.addItemDecoration(dividerItemDecoration)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt
deleted file mode 100644
index a9b698c5..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.os.Bundle
-import android.util.Log
-import androidx.appcompat.app.AlertDialog
-import androidx.lifecycle.lifecycleScope
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.BuildConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityCheckUpdateBinding
-import com.v2ray.ang.dto.CheckUpdateResult
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SpeedtestManager
-import com.v2ray.ang.handler.UpdateCheckerManager
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.launch
-
-class CheckUpdateActivity : BaseActivity() {
-
- private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- title = getString(R.string.update_check_for_update)
-
- binding.layoutCheckUpdate.setOnClickListener {
- checkForUpdates(binding.checkPreRelease.isChecked)
- }
-
- binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
- MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
- }
- binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
-
- "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
- binding.tvVersion.text = it
- }
-
- checkForUpdates(binding.checkPreRelease.isChecked)
- }
-
- private fun checkForUpdates(includePreRelease: Boolean) {
- toast(R.string.update_checking_for_update)
-
- lifecycleScope.launch {
- try {
- val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
- if (result.hasUpdate) {
- showUpdateDialog(result)
- } else {
- toastSuccess(R.string.update_already_latest_version)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
- toastError(e.message ?: getString(R.string.toast_failure))
- }
- }
- }
-
- private fun showUpdateDialog(result: CheckUpdateResult) {
- AlertDialog.Builder(this)
- .setTitle(getString(R.string.update_new_version_found, result.latestVersion))
- .setMessage(result.releaseNotes)
- .setPositiveButton(R.string.update_now) { _, _ ->
- result.downloadUrl?.let {
- Utils.openUri(this, it)
- }
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt
deleted file mode 100644
index 436f5913..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.util.Log
-import android.view.Menu
-import android.view.MenuItem
-import androidx.appcompat.widget.SearchView
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.ANG_PACKAGE
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityLogcatBinding
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.IOException
-
-
-class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
- private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
-
- private var logsetsAll: MutableList = mutableListOf()
- var logsets: MutableList = mutableListOf()
- private val adapter by lazy { LogcatRecyclerAdapter(this) }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- title = getString(R.string.title_logcat)
-
- binding.recyclerView.setHasFixedSize(true)
- binding.recyclerView.layoutManager = LinearLayoutManager(this)
- addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
- binding.recyclerView.adapter = adapter
-
- binding.refreshLayout.setOnRefreshListener(this)
-
- logsets.add(getString(R.string.pull_down_to_refresh))
- }
-
- private fun getLogcat() {
-
- try {
- binding.refreshLayout.isRefreshing = true
-
- lifecycleScope.launch(Dispatchers.Default) {
- val lst = LinkedHashSet()
- lst.add("logcat")
- lst.add("-d")
- lst.add("-v")
- lst.add("time")
- lst.add("-s")
- lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
- val process = withContext(Dispatchers.IO) {
- Runtime.getRuntime().exec(lst.toTypedArray())
- }
-
- val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
- launch(Dispatchers.Main) {
- logsetsAll = allText.toMutableList()
- logsets = allText.toMutableList()
- refreshData()
- binding.refreshLayout.isRefreshing = false
- }
- }
- } catch (e: IOException) {
- Log.e(AppConfig.TAG, "Failed to get logcat", e)
- }
- }
-
- private fun clearLogcat() {
- try {
- lifecycleScope.launch(Dispatchers.Default) {
- val lst = LinkedHashSet()
- lst.add("logcat")
- lst.add("-c")
- withContext(Dispatchers.IO) {
- val process = Runtime.getRuntime().exec(lst.toTypedArray())
- process.waitFor()
- }
- launch(Dispatchers.Main) {
- logsetsAll.clear()
- logsets.clear()
- refreshData()
- }
- }
- } catch (e: IOException) {
- Log.e(AppConfig.TAG, "Failed to clear logcat", e)
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_logcat, menu)
-
- val searchItem = menu.findItem(R.id.search_view)
- if (searchItem != null) {
- val searchView = searchItem.actionView as SearchView
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String?): Boolean = false
-
- override fun onQueryTextChange(newText: String?): Boolean {
- filterLogs(newText)
- return false
- }
- })
- searchView.setOnCloseListener {
- filterLogs("")
- false
- }
- }
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.copy_all -> {
- Utils.setClipboard(this, logsets.joinToString("\n"))
- toastSuccess(R.string.toast_success)
- true
- }
-
- R.id.clear_all -> {
- clearLogcat()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-
- private fun filterLogs(content: String?): Boolean {
- val key = content?.trim()
- logsets = if (key.isNullOrEmpty()) {
- logsetsAll.toMutableList()
- } else {
- logsetsAll.filter { it.contains(key) }.toMutableList()
- }
-
- refreshData()
- return true
- }
-
- override fun onRefresh() {
- getLogcat()
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun refreshData() {
- adapter.notifyDataSetChanged()
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt
deleted file mode 100644
index e83714d5..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
-
-class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter() {
- private var mActivity: LogcatActivity = activity
-
-
- override fun getItemCount() = mActivity.logsets.size
-
- override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
- try {
- val log = mActivity.logsets[position]
- if (log.isEmpty()) {
- holder.itemSubSettingBinding.logTag.text = ""
- holder.itemSubSettingBinding.logContent.text = ""
- } else {
- val content = log.split("):", limit = 2)
- holder.itemSubSettingBinding.logTag.text = content.first().split("(", limit = 2).first().trim()
- holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Error binding log view data", e)
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
- return MainViewHolder(
- ItemRecyclerLogcatBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- )
- }
-
- class MainViewHolder(val itemSubSettingBinding: ItemRecyclerLogcatBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
-
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt
deleted file mode 100644
index 0c7584d8..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt
+++ /dev/null
@@ -1,695 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.content.res.ColorStateList
-import android.net.Uri
-import android.net.VpnService
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import android.view.KeyEvent
-import android.view.Menu
-import android.view.MenuItem
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.viewModels
-import androidx.appcompat.app.ActionBarDrawerToggle
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.widget.SearchView
-import androidx.core.content.ContextCompat
-import androidx.core.view.GravityCompat
-import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.ItemTouchHelper
-import com.google.android.material.navigation.NavigationView
-import com.google.android.material.tabs.TabLayout
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.VPN
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityMainBinding
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.handler.AngConfigManager
-import com.v2ray.ang.handler.MigrateManager
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
-import com.v2ray.ang.service.V2RayServiceManager
-import com.v2ray.ang.util.Utils
-import com.v2ray.ang.viewmodel.MainViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
- private val binding by lazy {
- ActivityMainBinding.inflate(layoutInflater)
- }
-
- private val adapter by lazy { MainRecyclerAdapter(this) }
- private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- startV2Ray()
- }
- }
- private val requestSubSettingActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- initGroupTab()
- }
- private val tabGroupListener = object : TabLayout.OnTabSelectedListener {
- override fun onTabSelected(tab: TabLayout.Tab?) {
- val selectId = tab?.tag.toString()
- if (selectId != mainViewModel.subscriptionId) {
- mainViewModel.subscriptionIdChanged(selectId)
- }
- }
-
- override fun onTabUnselected(tab: TabLayout.Tab?) {
- }
-
- override fun onTabReselected(tab: TabLayout.Tab?) {
- }
- }
- private var mItemTouchHelper: ItemTouchHelper? = null
- val mainViewModel: MainViewModel by viewModels()
-
- // register activity result for requesting permission
- private val requestPermissionLauncher =
- registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- if (isGranted) {
- when (pendingAction) {
- Action.IMPORT_QR_CODE_CONFIG ->
- scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
-
- Action.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,
- 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"))
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- title = getString(R.string.title_server)
- setSupportActionBar(binding.toolbar)
-
- binding.fab.setOnClickListener {
- if (mainViewModel.isRunning.value == true) {
- V2RayServiceManager.stopVService(this)
- } else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
- val intent = VpnService.prepare(this)
- if (intent == null) {
- startV2Ray()
- } else {
- requestVpnPermission.launch(intent)
- }
- } else {
- startV2Ray()
- }
- }
- binding.layoutTest.setOnClickListener {
- if (mainViewModel.isRunning.value == true) {
- setTestState(getString(R.string.connection_test_testing))
- mainViewModel.testCurrentServerRealPing()
- } else {
-// tv_test_state.text = getString(R.string.connection_test_fail)
- }
- }
-
- binding.recyclerView.setHasFixedSize(true)
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) {
- binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
- } else {
- binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
- }
- addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
- binding.recyclerView.adapter = adapter
-
- mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
- mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
-
- val toggle = ActionBarDrawerToggle(
- this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
- )
- binding.drawerLayout.addDrawerListener(toggle)
- toggle.syncState()
- binding.navView.setNavigationItemSelectedListener(this)
-
- initGroupTab()
- setupViewModel()
- migrateLegacy()
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
- pendingAction = Action.POST_NOTIFICATIONS
- requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- }
- }
-
- onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
- binding.drawerLayout.closeDrawer(GravityCompat.START)
- } else {
- isEnabled = false
- onBackPressedDispatcher.onBackPressed()
- isEnabled = true
- }
- }
- })
- }
-
- @SuppressLint("NotifyDataSetChanged")
- private fun setupViewModel() {
- mainViewModel.updateListAction.observe(this) { index ->
- if (index >= 0) {
- adapter.notifyItemChanged(index)
- } else {
- adapter.notifyDataSetChanged()
- }
- }
- mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
- mainViewModel.isRunning.observe(this) { isRunning ->
- adapter.isRunning = isRunning
- if (isRunning) {
- binding.fab.setImageResource(R.drawable.ic_stop_24dp)
- binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
- setTestState(getString(R.string.connection_connected))
- binding.layoutTest.isFocusable = true
- } else {
- binding.fab.setImageResource(R.drawable.ic_play_24dp)
- binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
- setTestState(getString(R.string.connection_not_connected))
- binding.layoutTest.isFocusable = false
- }
- }
- mainViewModel.startListenBroadcast()
- mainViewModel.initAssets(assets)
- }
-
- private fun migrateLegacy() {
- lifecycleScope.launch(Dispatchers.IO) {
- val result = MigrateManager.migrateServerConfig2Profile()
- launch(Dispatchers.Main) {
- if (result) {
- toast(getString(R.string.migration_success))
- mainViewModel.reloadServerList()
- } else {
- //toast(getString(R.string.migration_fail))
- }
- }
-
- }
- }
-
- private fun initGroupTab() {
- binding.tabGroup.removeOnTabSelectedListener(tabGroupListener)
- binding.tabGroup.removeAllTabs()
- binding.tabGroup.isVisible = false
-
- val (listId, listRemarks) = mainViewModel.getSubscriptions(this)
- if (listId == null || listRemarks == null) {
- return
- }
-
- for (it in listRemarks.indices) {
- val tab = binding.tabGroup.newTab()
- tab.text = listRemarks[it]
- tab.tag = listId[it]
- binding.tabGroup.addTab(tab)
- }
- val selectIndex =
- listId.indexOf(mainViewModel.subscriptionId).takeIf { it >= 0 } ?: (listId.count() - 1)
- binding.tabGroup.selectTab(binding.tabGroup.getTabAt(selectIndex))
- binding.tabGroup.addOnTabSelectedListener(tabGroupListener)
- binding.tabGroup.isVisible = true
- }
-
- private fun startV2Ray() {
- if (MmkvManager.getSelectServer().isNullOrEmpty()) {
- toast(R.string.title_file_chooser)
- return
- }
- V2RayServiceManager.startVService(this)
- }
-
- private fun restartV2Ray() {
- if (mainViewModel.isRunning.value == true) {
- V2RayServiceManager.stopVService(this)
- }
- lifecycleScope.launch {
- delay(500)
- startV2Ray()
- }
- }
-
- public override fun onResume() {
- super.onResume()
- mainViewModel.reloadServerList()
- }
-
- public override fun onPause() {
- super.onPause()
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_main, menu)
-
- val searchItem = menu.findItem(R.id.search_view)
- if (searchItem != null) {
- val searchView = searchItem.actionView as SearchView
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String?): Boolean = false
-
- override fun onQueryTextChange(newText: String?): Boolean {
- mainViewModel.filterConfig(newText.orEmpty())
- return false
- }
- })
-
- searchView.setOnCloseListener {
- mainViewModel.filterConfig("")
- false
- }
- }
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.import_qrcode -> {
- importQRcode()
- true
- }
-
- R.id.import_clipboard -> {
- importClipboard()
- true
- }
-
- R.id.import_local -> {
- importConfigLocal()
- true
- }
-
- R.id.import_manually_vmess -> {
- importManually(EConfigType.VMESS.value)
- true
- }
-
- R.id.import_manually_vless -> {
- importManually(EConfigType.VLESS.value)
- true
- }
-
- R.id.import_manually_ss -> {
- importManually(EConfigType.SHADOWSOCKS.value)
- true
- }
-
- R.id.import_manually_socks -> {
- importManually(EConfigType.SOCKS.value)
- true
- }
-
- R.id.import_manually_http -> {
- importManually(EConfigType.HTTP.value)
- true
- }
-
- R.id.import_manually_trojan -> {
- importManually(EConfigType.TROJAN.value)
- true
- }
-
- R.id.import_manually_wireguard -> {
- importManually(EConfigType.WIREGUARD.value)
- true
- }
-
- R.id.import_manually_hysteria2 -> {
- importManually(EConfigType.HYSTERIA2.value)
- true
- }
-
- R.id.export_all -> {
- exportAll()
- true
- }
-
- R.id.ping_all -> {
- toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
- mainViewModel.testAllTcping()
- true
- }
-
- R.id.real_ping_all -> {
- toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
- mainViewModel.testAllRealPing()
- true
- }
-
- R.id.service_restart -> {
- restartV2Ray()
- true
- }
-
- R.id.del_all_config -> {
- delAllConfig()
- true
- }
-
- R.id.del_duplicate_config -> {
- delDuplicateConfig()
- true
- }
-
- R.id.del_invalid_config -> {
- delInvalidConfig()
- true
- }
-
- R.id.sort_by_test_results -> {
- sortByTestResults()
- true
- }
-
- R.id.sub_update -> {
- importConfigViaSub()
- true
- }
-
-
- else -> super.onOptionsItemSelected(item)
- }
-
- private fun importManually(createConfigType: Int) {
- startActivity(
- Intent()
- .putExtra("createConfigType", createConfigType)
- .putExtra("subscriptionId", mainViewModel.subscriptionId)
- .setClass(this, ServerActivity::class.java)
- )
- }
-
- /**
- * import config from qrcode
- */
- private fun importQRcode(): Boolean {
- val permission = Manifest.permission.CAMERA
- if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
- scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
- } else {
- pendingAction = Action.IMPORT_QR_CODE_CONFIG
- requestPermissionLauncher.launch(permission)
- }
- return true
- }
-
- /**
- * import config from clipboard
- */
- private fun importClipboard()
- : Boolean {
- try {
- val clipboard = Utils.getClipboard(this)
- importBatchConfig(clipboard)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
- return false
- }
- return true
- }
-
- private fun importBatchConfig(server: String?) {
- binding.pbWaiting.show()
-
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
- delay(500L)
- withContext(Dispatchers.Main) {
- when {
- count > 0 -> {
- toast(getString(R.string.title_import_config_count, count))
- mainViewModel.reloadServerList()
- }
-
- countSub > 0 -> initGroupTab()
- else -> toastError(R.string.toast_failure)
- }
- binding.pbWaiting.hide()
- }
- } catch (e: Exception) {
- withContext(Dispatchers.Main) {
- toastError(R.string.toast_failure)
- binding.pbWaiting.hide()
- }
- Log.e(AppConfig.TAG, "Failed to import batch config", e)
- }
- }
- }
-
- /**
- * import config from local config file
- */
- private fun importConfigLocal(): Boolean {
- try {
- showFileChooser()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to import config from local file", e)
- return false
- }
- return true
- }
-
-
- /**
- * import config from sub
- */
- private fun importConfigViaSub(): Boolean {
- binding.pbWaiting.show()
-
- lifecycleScope.launch(Dispatchers.IO) {
- val count = mainViewModel.updateConfigViaSubAll()
- delay(500L)
- launch(Dispatchers.Main) {
- if (count > 0) {
- toast(getString(R.string.title_update_config_count, count))
- mainViewModel.reloadServerList()
- } else {
- toastError(R.string.toast_failure)
- }
- binding.pbWaiting.hide()
- }
- }
- return true
- }
-
- private fun exportAll() {
- binding.pbWaiting.show()
- lifecycleScope.launch(Dispatchers.IO) {
- val ret = mainViewModel.exportAllServer()
- launch(Dispatchers.Main) {
- if (ret > 0)
- toast(getString(R.string.title_export_config_count, ret))
- else
- toastError(R.string.toast_failure)
- binding.pbWaiting.hide()
- }
- }
- }
-
- private fun delAllConfig() {
- AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- binding.pbWaiting.show()
- lifecycleScope.launch(Dispatchers.IO) {
- val ret = mainViewModel.removeAllServer()
- launch(Dispatchers.Main) {
- mainViewModel.reloadServerList()
- toast(getString(R.string.title_del_config_count, ret))
- binding.pbWaiting.hide()
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do noting
- }
- .show()
- }
-
- private fun delDuplicateConfig() {
- AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- binding.pbWaiting.show()
- lifecycleScope.launch(Dispatchers.IO) {
- val ret = mainViewModel.removeDuplicateServer()
- launch(Dispatchers.Main) {
- mainViewModel.reloadServerList()
- toast(getString(R.string.title_del_duplicate_config_count, ret))
- binding.pbWaiting.hide()
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do noting
- }
- .show()
- }
-
- private fun delInvalidConfig() {
- AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- binding.pbWaiting.show()
- lifecycleScope.launch(Dispatchers.IO) {
- val ret = mainViewModel.removeInvalidServer()
- launch(Dispatchers.Main) {
- mainViewModel.reloadServerList()
- toast(getString(R.string.title_del_config_count, ret))
- binding.pbWaiting.hide()
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do noting
- }
- .show()
- }
-
- private fun sortByTestResults() {
- binding.pbWaiting.show()
- lifecycleScope.launch(Dispatchers.IO) {
- mainViewModel.sortByTestResults()
- launch(Dispatchers.Main) {
- mainViewModel.reloadServerList()
- binding.pbWaiting.hide()
- }
- }
- }
-
- /**
- * show file chooser
- */
- private fun showFileChooser() {
- val intent = Intent(Intent.ACTION_GET_CONTENT)
- intent.type = "*/*"
- intent.addCategory(Intent.CATEGORY_OPENABLE)
-
- val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- Manifest.permission.READ_MEDIA_IMAGES
- } else {
- Manifest.permission.READ_EXTERNAL_STORAGE
- }
-
- if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
- pendingAction = Action.READ_CONTENT_FROM_URI
- chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
- } else {
- requestPermissionLauncher.launch(permission)
- }
- }
-
- /**
- * read content from uri
- */
- private fun readContentFromUri(uri: Uri) {
- val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- Manifest.permission.READ_MEDIA_IMAGES
- } else {
- Manifest.permission.READ_EXTERNAL_STORAGE
- }
-
- if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
- try {
- contentResolver.openInputStream(uri).use { input ->
- importBatchConfig(input?.bufferedReader()?.readText())
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to read content from URI", e)
- }
- } else {
- requestPermissionLauncher.launch(permission)
- }
- }
-
- private fun setTestState(content: String?) {
- binding.tvTestState.text = content
- }
-
-// val mConnection = object : ServiceConnection {
-// override fun onServiceDisconnected(name: ComponentName?) {
-// }
-//
-// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
-// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "")
-// }
-// }
-
- override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
- if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
- moveTaskToBack(false)
- return true
- }
- return super.onKeyDown(keyCode, event)
- }
-
-
- override fun onNavigationItemSelected(item: MenuItem): Boolean {
- // Handle navigation view item clicks here.
- when (item.itemId) {
- R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
- R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java))
- R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
- R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java))
- R.id.settings -> startActivity(
- Intent(this, SettingsActivity::class.java)
- .putExtra("isRunning", mainViewModel.isRunning.value == true)
- )
-
- R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
- R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
- R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java))
- R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
- }
-
- binding.drawerLayout.closeDrawer(GravityCompat.START)
- return true
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt
deleted file mode 100644
index e7ea6211..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt
+++ /dev/null
@@ -1,362 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.content.Intent
-import android.graphics.Color
-import android.text.TextUtils
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.app.AlertDialog
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.RecyclerView
-import com.v2ray.ang.AngApplication.Companion.application
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ItemQrcodeBinding
-import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
-import com.v2ray.ang.databinding.ItemRecyclerMainBinding
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.AngConfigManager
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.helper.ItemTouchHelperAdapter
-import com.v2ray.ang.helper.ItemTouchHelperViewHolder
-import com.v2ray.ang.service.V2RayServiceManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter {
- companion object {
- private const val VIEW_TYPE_ITEM = 1
- private const val VIEW_TYPE_FOOTER = 2
- }
-
- private var mActivity: MainActivity = activity
- private val share_method: Array by lazy {
- mActivity.resources.getStringArray(R.array.share_method)
- }
- private val share_method_more: Array by lazy {
- mActivity.resources.getStringArray(R.array.share_method_more)
- }
- var isRunning = false
- private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
-
- /**
- * Gets the total number of items in the adapter (servers count + footer view)
- * @return The total item count
- */
- override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
-
- override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
- if (holder is MainViewHolder) {
- val guid = mActivity.mainViewModel.serversCache[position].guid
- val profile = mActivity.mainViewModel.serversCache[position].profile
- val isCustom = profile.configType == EConfigType.CUSTOM
-
- holder.itemView.setBackgroundColor(Color.TRANSPARENT)
-
- //Name address
- holder.itemMainBinding.tvName.text = profile.remarks
- holder.itemMainBinding.tvStatistics.text = getAddress(profile)
- holder.itemMainBinding.tvType.text = profile.configType.name
-
- //TestResult
- val aff = MmkvManager.decodeServerAffiliationInfo(guid)
- holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
- if ((aff?.testDelayMillis ?: 0L) < 0L) {
- holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
- } else {
- holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
- }
-
- //layoutIndicator
- if (guid == MmkvManager.getSelectServer()) {
- holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
- } else {
- holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
- }
-
- //subscription remarks
- val subRemarks = getSubscriptionRemarks(profile)
- holder.itemMainBinding.tvSubscription.text = subRemarks
- holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
-
- //layout
- if (doubleColumnDisplay) {
- holder.itemMainBinding.layoutShare.visibility = View.GONE
- holder.itemMainBinding.layoutEdit.visibility = View.GONE
- holder.itemMainBinding.layoutRemove.visibility = View.GONE
- holder.itemMainBinding.layoutMore.visibility = View.VISIBLE
-
- //share method
- val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
-
- holder.itemMainBinding.layoutMore.setOnClickListener {
- shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
- }
- } else {
- holder.itemMainBinding.layoutShare.visibility = View.VISIBLE
- holder.itemMainBinding.layoutEdit.visibility = View.VISIBLE
- holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE
- holder.itemMainBinding.layoutMore.visibility = View.GONE
-
- //share method
- val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList()
-
- holder.itemMainBinding.layoutShare.setOnClickListener {
- shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
- }
-
- holder.itemMainBinding.layoutEdit.setOnClickListener {
- editServer(guid, profile)
- }
- holder.itemMainBinding.layoutRemove.setOnClickListener {
- removeServer(guid, position)
- }
- }
-
- holder.itemMainBinding.infoContainer.setOnClickListener {
- setSelectServer(guid)
- }
- }
-// if (holder is FooterViewHolder) {
-// if (true) {
-// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
-// } else {
-// holder.itemFooterBinding.layoutEdit.setOnClickListener {
-// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
-// }
-// }
-// }
- }
-
- /**
- * Gets the server address information
- * Hides part of IP or domain information for privacy protection
- * @param profile The server configuration
- * @return Formatted address string
- */
- private fun getAddress(profile: ProfileItem): String {
- // Hide xxx:xxx:***/xxx.xxx.xxx.***
- return "${
- profile.server?.let {
- if (it.contains(":"))
- it.split(":").take(2).joinToString(":", postfix = ":***")
- else
- it.split('.').dropLast(1).joinToString(".", postfix = ".***")
- }
- } : ${profile.serverPort}"
- }
-
- /**
- * Gets the subscription remarks information
- * @param profile The server configuration
- * @return Subscription remarks string, or empty string if none
- */
- private fun getSubscriptionRemarks(profile: ProfileItem): String {
- val subRemarks =
- if (mActivity.mainViewModel.subscriptionId.isEmpty())
- MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
- else
- null
- return subRemarks?.toString() ?: ""
- }
-
- /**
- * Shares server configuration
- * Displays a dialog with sharing options and executes the selected action
- * @param guid The server unique identifier
- * @param profile The server configuration
- * @param position The position in the list
- * @param shareOptions The list of share options
- * @param skip The number of options to skip
- */
- private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List, skip: Int) {
- AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
- try {
- when (i + skip) {
- 0 -> showQRCode(guid)
- 1 -> share2Clipboard(guid)
- 2 -> shareFullContent(guid)
- 3 -> editServer(guid, profile)
- 4 -> removeServer(guid, position)
- else -> mActivity.toast("else")
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Error when sharing server", e)
- }
- }.show()
- }
-
- /**
- * Displays QR code for the server configuration
- * @param guid The server unique identifier
- */
- private fun showQRCode(guid: String) {
- val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
- ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
- AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
- }
-
- /**
- * Shares server configuration to clipboard
- * @param guid The server unique identifier
- */
- private fun share2Clipboard(guid: String) {
- if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
- mActivity.toastSuccess(R.string.toast_success)
- } else {
- mActivity.toastError(R.string.toast_failure)
- }
- }
-
- /**
- * Shares full server configuration content to clipboard
- * @param guid The server unique identifier
- */
- private fun shareFullContent(guid: String) {
- mActivity.lifecycleScope.launch(Dispatchers.IO) {
- val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
- launch(Dispatchers.Main) {
- if (result == 0) {
- mActivity.toastSuccess(R.string.toast_success)
- } else {
- mActivity.toastError(R.string.toast_failure)
- }
- }
- }
- }
-
- /**
- * Edits server configuration
- * Opens appropriate editing interface based on configuration type
- * @param guid The server unique identifier
- * @param profile The server configuration
- */
- private fun editServer(guid: String, profile: ProfileItem) {
- val intent = Intent().putExtra("guid", guid)
- .putExtra("isRunning", isRunning)
- .putExtra("createConfigType", profile.configType.value)
- if (profile.configType == EConfigType.CUSTOM) {
- mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
- } else {
- mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
- }
- }
-
- /**
- * Removes server configuration
- * Handles confirmation dialog and related checks
- * @param guid The server unique identifier
- * @param position The position in the list
- */
- private fun removeServer(guid: String, position: Int) {
- if (guid != MmkvManager.getSelectServer()) {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
- AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- removeServerSub(guid, position)
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do noting
- }
- .show()
- } else {
- removeServerSub(guid, position)
- }
- } else {
- application.toast(R.string.toast_action_not_allowed)
- }
- }
-
- /**
- * Executes the actual server removal process
- * @param guid The server unique identifier
- * @param position The position in the list
- */
- private fun removeServerSub(guid: String, position: Int) {
- mActivity.mainViewModel.removeServer(guid)
- notifyItemRemoved(position)
- notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
- }
-
- /**
- * Sets the selected server
- * Updates UI and restarts service if needed
- * @param guid The server unique identifier to select
- */
- private fun setSelectServer(guid: String) {
- val selected = MmkvManager.getSelectServer()
- if (guid != selected) {
- MmkvManager.setSelectServer(guid)
- if (!TextUtils.isEmpty(selected)) {
- notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
- }
- notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
- if (isRunning) {
- V2RayServiceManager.stopVService(mActivity)
- mActivity.lifecycleScope.launch {
- try {
- delay(500)
- V2RayServiceManager.startVService(mActivity)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
- }
- }
- }
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
- return when (viewType) {
- VIEW_TYPE_ITEM ->
- MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
-
- else ->
- FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
- }
-
- override fun getItemViewType(position: Int): Int {
- return if (position == mActivity.mainViewModel.serversCache.size) {
- VIEW_TYPE_FOOTER
- } else {
- VIEW_TYPE_ITEM
- }
- }
-
- open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
- fun onItemSelected() {
- itemView.setBackgroundColor(Color.LTGRAY)
- }
-
- fun onItemClear() {
- itemView.setBackgroundColor(0)
- }
- }
-
- class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
- BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
-
- class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
- BaseViewHolder(itemFooterBinding.root)
-
- override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
- mActivity.mainViewModel.swapServer(fromPosition, toPosition)
- notifyItemMoved(fromPosition, toPosition)
- return true
- }
-
- override fun onItemMoveCompleted() {
- // do nothing
- }
-
- override fun onItemDismiss(position: Int) {
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt
deleted file mode 100644
index b0df22e5..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt
+++ /dev/null
@@ -1,279 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.text.TextUtils
-import android.util.Log
-import android.view.Menu
-import android.view.MenuItem
-import android.widget.Toast
-import androidx.appcompat.widget.SearchView
-import androidx.lifecycle.lifecycleScope
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.ANG_PACKAGE
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityBypassListBinding
-import com.v2ray.ang.dto.AppInfo
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.extension.v2RayApplication
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.util.AppManagerUtil
-import com.v2ray.ang.util.HttpUtil
-import com.v2ray.ang.util.Utils
-import es.dmoral.toasty.Toasty
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.text.Collator
-
-class PerAppProxyActivity : BaseActivity() {
- private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) }
-
- private var adapter: PerAppProxyAdapter? = null
- private var appsAll: List? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- title = getString(R.string.per_app_proxy_settings)
-
- addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
-
- lifecycleScope.launch {
- try {
- binding.pbWaiting.show()
- val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
- val apps = withContext(Dispatchers.IO) {
- val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
-
- if (blacklist != null) {
- appsList.forEach { app ->
- app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
- }
- appsList.sortedWith { p1, p2 ->
- when {
- p1.isSelected > p2.isSelected -> -1
- p1.isSelected == p2.isSelected -> 0
- else -> 1
- }
- }
- } else {
- val collator = Collator.getInstance()
- appsList.sortedWith(compareBy(collator) { it.appName })
- }
- }
-
- appsAll = apps
- adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
- binding.recyclerView.adapter = adapter
- binding.pbWaiting.hide()
- } catch (e: Exception) {
- binding.pbWaiting.hide()
- Log.e(ANG_PACKAGE, "Error loading apps", e)
- }
- }
-
- binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
- MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
- }
- binding.switchPerAppProxy.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
-
- binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
- MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
- }
- binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
-
- binding.layoutSwitchBypassAppsTips.setOnClickListener {
- Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show()
- }
- }
-
- override fun onPause() {
- super.onPause()
- adapter?.let {
- MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_bypass_list, menu)
-
- val searchItem = menu.findItem(R.id.search_view)
- if (searchItem != null) {
- val searchView = searchItem.actionView as SearchView
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String?): Boolean = false
-
- override fun onQueryTextChange(newText: String?): Boolean {
- filterProxyApp(newText.orEmpty())
- return false
- }
- })
- }
-
-
- return super.onCreateOptionsMenu(menu)
- }
-
-
- @SuppressLint("NotifyDataSetChanged")
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.select_all -> adapter?.let { it ->
- val pkgNames = it.apps.map { it.packageName }
- if (it.blacklist.containsAll(pkgNames)) {
- it.apps.forEach {
- val packageName = it.packageName
- adapter?.blacklist?.remove(packageName)
- }
- } else {
- it.apps.forEach {
- val packageName = it.packageName
- adapter?.blacklist?.add(packageName)
- }
- }
- it.notifyDataSetChanged()
- true
- } == true
-
- R.id.select_proxy_app -> {
- selectProxyApp()
- true
- }
-
- R.id.import_proxy_app -> {
- importProxyApp()
- true
- }
-
- R.id.export_proxy_app -> {
- exportProxyApp()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-
- private fun selectProxyApp() {
- toast(R.string.msg_downloading_content)
- binding.pbWaiting.show()
-
- val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
- lifecycleScope.launch(Dispatchers.IO) {
- var content = HttpUtil.getUrlContent(url, 5000)
- if (content.isNullOrEmpty()) {
- val httpPort = SettingsManager.getHttpPort()
- content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
- }
- launch(Dispatchers.Main) {
- Log.i(AppConfig.TAG, content)
- selectProxyApp(content, true)
- toastSuccess(R.string.toast_success)
- binding.pbWaiting.hide()
- }
- }
- }
-
- private fun importProxyApp() {
- val content = Utils.getClipboard(applicationContext)
- if (TextUtils.isEmpty(content)) return
- selectProxyApp(content, false)
- toastSuccess(R.string.toast_success)
- }
-
- private fun exportProxyApp() {
- var lst = binding.switchBypassApps.isChecked.toString()
-
- adapter?.blacklist?.forEach block@{
- lst = lst + System.getProperty("line.separator") + it
- }
- Utils.setClipboard(applicationContext, lst)
- toastSuccess(R.string.toast_success)
- }
-
- @SuppressLint("NotifyDataSetChanged")
- private fun selectProxyApp(content: String, force: Boolean): Boolean {
- try {
- val proxyApps = if (TextUtils.isEmpty(content)) {
- Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
- } else {
- content
- }
- if (TextUtils.isEmpty(proxyApps)) return false
-
- adapter?.blacklist?.clear()
-
- if (binding.switchBypassApps.isChecked) {
- adapter?.let { it ->
- it.apps.forEach block@{
- val packageName = it.packageName
- Log.i(AppConfig.TAG, packageName)
- if (!inProxyApps(proxyApps, packageName, force)) {
- adapter?.blacklist?.add(packageName)
- println(packageName)
- return@block
- }
- }
- it.notifyDataSetChanged()
- }
- } else {
- adapter?.let { it ->
- it.apps.forEach block@{
- val packageName = it.packageName
- Log.i(AppConfig.TAG, packageName)
- if (inProxyApps(proxyApps, packageName, force)) {
- adapter?.blacklist?.add(packageName)
- println(packageName)
- return@block
- }
- }
- it.notifyDataSetChanged()
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Error selecting proxy app", e)
- return false
- }
- return true
- }
-
- private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean {
- if (force) {
- if (packageName == "com.google.android.webview") return false
- if (packageName.startsWith("com.google")) return true
- }
-
- return proxyApps.indexOf(packageName) >= 0
- }
-
- private fun filterProxyApp(content: String): Boolean {
- val apps = ArrayList()
-
- val key = content.uppercase()
- if (key.isNotEmpty()) {
- appsAll?.forEach {
- if (it.appName.uppercase().indexOf(key) >= 0
- || it.packageName.uppercase().indexOf(key) >= 0
- ) {
- apps.add(it)
- }
- }
- } else {
- appsAll?.forEach {
- apps.add(it)
- }
- }
-
- adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
- binding.recyclerView.adapter = adapter
- refreshData()
- return true
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun refreshData() {
- adapter?.notifyDataSetChanged()
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt
deleted file mode 100644
index bb0ee572..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.appcompat.app.AlertDialog
-import androidx.lifecycle.lifecycleScope
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityRoutingEditBinding
-import com.v2ray.ang.dto.RulesetItem
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class RoutingEditActivity : BaseActivity() {
- private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
- private val position by lazy { intent.getIntExtra("position", -1) }
-
- private val outbound_tag: Array by lazy {
- resources.getStringArray(R.array.outbound_tag)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- title = getString(R.string.routing_settings_rule_title)
-
- val rulesetItem = SettingsManager.getRoutingRuleset(position)
- if (rulesetItem != null) {
- bindingServer(rulesetItem)
- } else {
- clearServer()
- }
- }
-
- private fun bindingServer(rulesetItem: RulesetItem): Boolean {
- binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
- binding.chkLocked.isChecked = rulesetItem.locked == true
- binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
- binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
- binding.etPort.text = Utils.getEditable(rulesetItem.port)
- binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
- binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
- val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag)
- binding.spOutboundTag.setSelection(outbound)
-
- return true
- }
-
- private fun clearServer(): Boolean {
- binding.etRemarks.text = null
- binding.spOutboundTag.setSelection(0)
- return true
- }
-
- private fun saveServer(): Boolean {
- val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem()
-
- rulesetItem.apply {
- remarks = binding.etRemarks.text.toString()
- locked = binding.chkLocked.isChecked
- domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() }
- ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
- ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() }
- ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
- protocol = binding.etProtocol.text.toString().takeIf { it.isNotEmpty() }
- ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
- port = binding.etPort.text.toString().takeIf { it.isNotEmpty() }
- network = binding.etNetwork.text.toString().takeIf { it.isNotEmpty() }
- outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
- }
-
- if (rulesetItem.remarks.isNullOrEmpty()) {
- toast(R.string.sub_setting_remarks)
- return false
- }
-
- SettingsManager.saveRoutingRuleset(position, rulesetItem)
- toastSuccess(R.string.toast_success)
- finish()
- return true
- }
-
-
- private fun deleteServer(): Boolean {
- if (position >= 0) {
- AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- lifecycleScope.launch(Dispatchers.IO) {
- SettingsManager.removeRoutingRuleset(position)
- launch(Dispatchers.Main) {
- finish()
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- // do nothing
- }
- .show()
- }
- return true
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.action_server, menu)
- val del_config = menu.findItem(R.id.del_config)
-
- if (position < 0) {
- del_config?.isVisible = false
- }
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.del_config -> {
- deleteServer()
- true
- }
-
- R.id.save_config -> {
- saveServer()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt
deleted file mode 100644
index e585cff8..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt
+++ /dev/null
@@ -1,204 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.os.Bundle
-import android.util.Log
-import android.view.Menu
-import android.view.MenuItem
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
-import com.v2ray.ang.dto.RulesetItem
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
-import com.v2ray.ang.util.JsonUtil
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class RoutingSettingActivity : BaseActivity() {
- private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
-
- var rulesets: MutableList = mutableListOf()
- private val adapter by lazy { RoutingSettingRecyclerAdapter(this) }
- private var mItemTouchHelper: ItemTouchHelper? = null
- private val routing_domain_strategy: Array by lazy {
- resources.getStringArray(R.array.routing_domain_strategy)
- }
- private val preset_rulesets: Array by lazy {
- resources.getStringArray(R.array.preset_rulesets)
- }
-
- private val requestCameraPermissionLauncher = registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- if (isGranted) {
- scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
- } else {
- toast(R.string.toast_permission_denied)
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- title = getString(R.string.routing_settings_title)
-
- binding.recyclerView.setHasFixedSize(true)
- binding.recyclerView.layoutManager = LinearLayoutManager(this)
- addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
- binding.recyclerView.adapter = adapter
-
- mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
- mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
-
- binding.tvDomainStrategySummary.text = getDomainStrategy()
- binding.layoutDomainStrategy.setOnClickListener {
- setDomainStrategy()
- }
- }
-
- override fun onResume() {
- super.onResume()
- refreshData()
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_routing_setting, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
- R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
- R.id.import_predefined_rulesets -> importPredefined().let { true }
- R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true }
- R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true }
- R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true }
- else -> super.onOptionsItemSelected(item)
- }
-
- private fun getDomainStrategy(): String {
- return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first()
- }
-
- private fun setDomainStrategy() {
- android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i ->
- try {
- val value = routing_domain_strategy[i]
- MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
- binding.tvDomainStrategySummary.text = value
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to set domain strategy", e)
- }
- }.show()
- }
-
- private fun importPredefined() {
- AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
- AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- try {
- lifecycleScope.launch(Dispatchers.IO) {
- SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
- launch(Dispatchers.Main) {
- refreshData()
- toastSuccess(R.string.toast_success)
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do nothing
- }
- .show()
- }.show()
- }
-
- private fun importFromClipboard() {
- AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- val clipboard = try {
- Utils.getClipboard(this)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
- toastError(R.string.toast_failure)
- return@setPositiveButton
- }
- lifecycleScope.launch(Dispatchers.IO) {
- val result = SettingsManager.resetRoutingRulesets(clipboard)
- withContext(Dispatchers.Main) {
- if (result) {
- refreshData()
- toastSuccess(R.string.toast_success)
- } else {
- toastError(R.string.toast_failure)
- }
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do nothing
- }
- .show()
- }
-
- private fun export2Clipboard() {
- val rulesetList = MmkvManager.decodeRoutingRulesets()
- if (rulesetList.isNullOrEmpty()) {
- toastError(R.string.toast_failure)
- } else {
- Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
- toastSuccess(R.string.toast_success)
- }
- }
-
- private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- importRulesetsFromQRcode(it.data?.getStringExtra("SCAN_RESULT"))
- }
- }
-
- private fun importRulesetsFromQRcode(qrcode: String?): Boolean {
- AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- lifecycleScope.launch(Dispatchers.IO) {
- val result = SettingsManager.resetRoutingRulesets(qrcode)
- withContext(Dispatchers.Main) {
- if (result) {
- refreshData()
- toastSuccess(R.string.toast_success)
- } else {
- toastError(R.string.toast_failure)
- }
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do nothing
- }
- .show()
- return true
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun refreshData() {
- rulesets.clear()
- rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
- adapter.notifyDataSetChanged()
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt
deleted file mode 100644
index bd48ea59..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.content.Intent
-import android.graphics.Color
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import com.v2ray.ang.databinding.ItemRecyclerRoutingSettingBinding
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.helper.ItemTouchHelperAdapter
-import com.v2ray.ang.helper.ItemTouchHelperViewHolder
-
-class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : RecyclerView.Adapter(),
- ItemTouchHelperAdapter {
-
- private var mActivity: RoutingSettingActivity = activity
- override fun getItemCount() = mActivity.rulesets.size
-
- override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
- val ruleset = mActivity.rulesets[position]
-
- holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
- holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
- holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
- holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
- holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
- holder.itemView.setBackgroundColor(Color.TRANSPARENT)
-
- holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
- mActivity.startActivity(
- Intent(mActivity, RoutingEditActivity::class.java)
- .putExtra("position", position)
- )
- }
-
- holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
- if (!it.isPressed) return@setOnCheckedChangeListener
- ruleset.enabled = isChecked
- SettingsManager.saveRoutingRuleset(position, ruleset)
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
- return MainViewHolder(
- ItemRecyclerRoutingSettingBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- )
- }
-
- class MainViewHolder(val itemRoutingSettingBinding: ItemRecyclerRoutingSettingBinding) :
- BaseViewHolder(itemRoutingSettingBinding.root), ItemTouchHelperViewHolder
-
- open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
- fun onItemSelected() {
- itemView.setBackgroundColor(Color.LTGRAY)
- }
-
- fun onItemClear() {
- itemView.setBackgroundColor(0)
- }
- }
-
- override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
- SettingsManager.swapRoutingRuleset(fromPosition, toPosition)
- notifyItemMoved(fromPosition, toPosition)
- return true
- }
-
- override fun onItemMoveCompleted() {
- mActivity.refreshData()
- }
-
- override fun onItemDismiss(position: Int) {
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt
deleted file mode 100644
index 9a972f93..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.Manifest
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.result.contract.ActivityResultContracts
-import com.v2ray.ang.R
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.AngConfigManager
-
-class ScScannerActivity : BaseActivity() {
-
- private val requestCameraPermissionLauncher = registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- if (isGranted) {
- scanQRCode.launch(Intent(this, ScannerActivity::class.java))
- } else {
- toast(R.string.toast_permission_denied)
- finish()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_none)
- importQRcode()
- }
-
- private fun importQRcode(): Boolean {
- requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
- return true
- }
-
- private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
- val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
-
- if (count + countSub > 0) {
- toastSuccess(R.string.toast_success)
- } else {
- toastError(R.string.toast_failure)
- }
-
- startActivity(Intent(this, MainActivity::class.java))
- }
- finish()
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt
deleted file mode 100644
index 2a971ac0..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.Manifest
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.graphics.BitmapFactory
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import android.view.Menu
-import android.view.MenuItem
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.content.ContextCompat
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.util.QRCodeDecoder
-import io.github.g00fy2.quickie.QRResult
-import io.github.g00fy2.quickie.ScanCustomCode
-import io.github.g00fy2.quickie.config.ScannerConfig
-
-class ScannerActivity : BaseActivity() {
-
-
- private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
- private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- val uri = it.data?.data
- if (it.resultCode == RESULT_OK && uri != null) {
- try {
- val inputStream = contentResolver.openInputStream(uri)
- val bitmap = BitmapFactory.decodeStream(inputStream)
- inputStream?.close()
-
- val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
- if (text.isNullOrEmpty()) {
- toast(R.string.toast_decoding_failed)
- } else {
- finished(text)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
- toast(R.string.toast_decoding_failed)
- }
- }
- }
-
- private val requestPermissionLauncher =
- registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- if (isGranted) {
- showFileChooser()
- } else {
- toast(R.string.toast_permission_denied)
- }
- }
-
- public override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
- launchScan()
- }
- }
-
- private fun launchScan() {
- scanQrCode.launch(
- ScannerConfig.build {
- setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
- setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button
- setShowCloseButton(true) // show or hide (default) close button
- }
- )
- }
-
- private fun handleResult(result: QRResult) {
- if (result is QRResult.QRSuccess) {
- finished(result.content.rawValue.orEmpty())
- } else {
- finish()
- }
- }
-
- private fun finished(text: String) {
- val intent = Intent()
- intent.putExtra("SCAN_RESULT", text)
- setResult(RESULT_OK, intent)
- finish()
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_scanner, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.scan_code -> {
- launchScan()
- true
- }
-
- R.id.select_photo -> {
- val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- Manifest.permission.READ_MEDIA_IMAGES
- } else {
- Manifest.permission.READ_EXTERNAL_STORAGE
- }
-
- if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
- showFileChooser()
- } else {
- requestPermissionLauncher.launch(permission)
- }
- true
- }
-
-
- else -> super.onOptionsItemSelected(item)
- }
-
- private fun showFileChooser() {
- val intent = Intent(Intent.ACTION_GET_CONTENT)
- intent.type = "image/*"
- intent.addCategory(Intent.CATEGORY_OPENABLE)
- //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
-
- try {
- chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
- } catch (ex: android.content.ActivityNotFoundException) {
- toast(R.string.toast_require_file_manager)
- }
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt
deleted file mode 100644
index e9bdad66..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt
+++ /dev/null
@@ -1,652 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.os.Bundle
-import android.text.TextUtils
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.EditText
-import android.widget.LinearLayout
-import android.widget.Spinner
-import android.widget.TextView
-import androidx.appcompat.app.AlertDialog
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.DEFAULT_PORT
-import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
-import com.v2ray.ang.AppConfig.REALITY
-import com.v2ray.ang.AppConfig.TLS
-import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
-import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
-import com.v2ray.ang.R
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.NetworkType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.extension.isNotNullEmpty
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.util.JsonUtil
-import com.v2ray.ang.util.Utils
-
-class ServerActivity : BaseActivity() {
-
- private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
- private val isRunning by lazy {
- intent.getBooleanExtra("isRunning", false)
- && editGuid.isNotEmpty()
- && editGuid == MmkvManager.getSelectServer()
- }
- private val createConfigType by lazy {
- EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value))
- ?: EConfigType.VMESS
- }
- private val subscriptionId by lazy {
- intent.getStringExtra("subscriptionId")
- }
-
- private val securitys: Array by lazy {
- resources.getStringArray(R.array.securitys)
- }
- private val shadowsocksSecuritys: Array by lazy {
- resources.getStringArray(R.array.ss_securitys)
- }
- private val flows: Array by lazy {
- resources.getStringArray(R.array.flows)
- }
- private val networks: Array by lazy {
- resources.getStringArray(R.array.networks)
- }
- private val tcpTypes: Array by lazy {
- resources.getStringArray(R.array.header_type_tcp)
- }
- private val kcpAndQuicTypes: Array by lazy {
- resources.getStringArray(R.array.header_type_kcp_and_quic)
- }
- private val grpcModes: Array by lazy {
- resources.getStringArray(R.array.mode_type_grpc)
- }
- private val streamSecuritys: Array by lazy {
- resources.getStringArray(R.array.streamsecurityxs)
- }
- private val allowinsecures: Array by lazy {
- resources.getStringArray(R.array.allowinsecures)
- }
- private val uTlsItems: Array by lazy {
- resources.getStringArray(R.array.streamsecurity_utls)
- }
- private val alpns: Array by lazy {
- resources.getStringArray(R.array.streamsecurity_alpn)
- }
- private val xhttpMode: Array by lazy {
- resources.getStringArray(R.array.xhttp_mode)
- }
-
-
- // Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
- // We don't use AndroidViewBinding because, it is better to share similar logics for different
- // protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
- private val et_remarks: EditText by lazy { findViewById(R.id.et_remarks) }
- private val et_address: EditText by lazy { findViewById(R.id.et_address) }
- private val et_port: EditText by lazy { findViewById(R.id.et_port) }
- private val et_id: EditText by lazy { findViewById(R.id.et_id) }
- private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
- private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
- private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
- private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
- private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
- private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.lay_allow_insecure) }
- private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
- private val container_sni: LinearLayout? by lazy { findViewById(R.id.lay_sni) }
- private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
- private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.lay_stream_fingerprint) }
- private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
- private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
- private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
- private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) }
- private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
- private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
- private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
- private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
- private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
- private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
- private val et_preshared_key: EditText? by lazy { findViewById(R.id.et_preshared_key) }
- private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
- private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
- private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
- private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
- private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.lay_spider_x) }
- private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
- private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
- private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
- private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
- private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
- private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
- private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
- private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
- private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
- private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
- private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
-
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- title = getString(R.string.title_server)
-
- val config = MmkvManager.decodeServerConfig(editGuid)
- when (config?.configType ?: createConfigType) {
- EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
- EConfigType.CUSTOM -> return
- EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
- EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
- EConfigType.HTTP -> setContentView(R.layout.activity_server_socks)
- EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
- EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
- EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
- EConfigType.HYSTERIA2 -> setContentView(R.layout.activity_server_hysteria2)
- }
- sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(
- parent: AdapterView<*>?,
- view: View?,
- position: Int,
- id: Long,
- ) {
- val types = transportTypes(networks[position])
- sp_header_type?.isEnabled = types.size > 1
- val adapter =
- ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
- sp_header_type?.adapter = adapter
- sp_header_type_title?.text =
- when (networks[position]) {
- NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type)
- NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
- else -> getString(R.string.server_lab_head_type)
- }.orEmpty()
- sp_header_type?.setSelection(
- Utils.arrayFind(
- types,
- when (networks[position]) {
- NetworkType.GRPC.type -> config?.mode
- NetworkType.XHTTP.type -> config?.xhttpMode
- else -> config?.headerType
- }.orEmpty()
- )
- )
-
- et_request_host?.text = Utils.getEditable(
- when (networks[position]) {
- //"quic" -> config?.quicSecurity
- NetworkType.GRPC.type -> config?.authority
- else -> config?.host
- }.orEmpty()
- )
- et_path?.text = Utils.getEditable(
- when (networks[position]) {
- NetworkType.KCP.type -> config?.seed
- //"quic" -> config?.quicKey
- NetworkType.GRPC.type -> config?.serviceName
- else -> config?.path
- }.orEmpty()
- )
-
- tv_request_host?.text = Utils.getEditable(
- getString(
- when (networks[position]) {
- NetworkType.TCP.type -> R.string.server_lab_request_host_http
- NetworkType.WS.type -> R.string.server_lab_request_host_ws
- NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade
- NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp
- NetworkType.H2.type -> R.string.server_lab_request_host_h2
- //"quic" -> R.string.server_lab_request_host_quic
- NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
- else -> R.string.server_lab_request_host
- }
- )
- )
-
- tv_path?.text = Utils.getEditable(
- getString(
- when (networks[position]) {
- NetworkType.KCP.type -> R.string.server_lab_path_kcp
- NetworkType.WS.type -> R.string.server_lab_path_ws
- NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
- NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
- NetworkType.H2.type -> R.string.server_lab_path_h2
- //"quic" -> R.string.server_lab_path_quic
- NetworkType.GRPC.type -> R.string.server_lab_path_grpc
- else -> R.string.server_lab_path
- }
- )
- )
- et_extra?.text = Utils.getEditable(
- when (networks[position]) {
- NetworkType.XHTTP.type -> config?.xhttpExtra
- else -> null
- }.orEmpty()
- )
-
- layout_extra?.visibility =
- when (networks[position]) {
- NetworkType.XHTTP.type -> View.VISIBLE
- else -> View.GONE
- }
- }
-
- override fun onNothingSelected(parent: AdapterView<*>?) {
- // do nothing
- }
- }
- sp_stream_security?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(
- parent: AdapterView<*>?,
- view: View?,
- position: Int,
- id: Long,
- ) {
- val isBlank = streamSecuritys[position].isBlank()
- val isTLS = streamSecuritys[position] == TLS
-
- when {
- // Case 1: Null or blank
- isBlank -> {
- listOf(
- container_sni, container_fingerprint, container_alpn,
- container_allow_insecure, container_public_key,
- container_short_id, container_spider_x
- ).forEach { it?.visibility = View.GONE }
- }
-
- // Case 2: TLS value
- isTLS -> {
- listOf(
- container_sni,
- container_fingerprint,
- container_alpn
- ).forEach { it?.visibility = View.VISIBLE }
- container_allow_insecure?.visibility = View.VISIBLE
- listOf(
- container_public_key,
- container_short_id,
- container_spider_x
- ).forEach { it?.visibility = View.GONE }
- }
-
- // Case 3: Other reality values
- else -> {
- listOf(container_sni, container_fingerprint).forEach {
- it?.visibility = View.VISIBLE
- }
- container_alpn?.visibility = View.GONE
- container_allow_insecure?.visibility = View.GONE
- listOf(
- container_public_key,
- container_short_id,
- container_spider_x
- ).forEach { it?.visibility = View.VISIBLE }
- }
- }
- }
-
- override fun onNothingSelected(p0: AdapterView<*>?) {
- // do nothing
- }
- }
- if (config != null) {
- bindingServer(config)
- } else {
- clearServer()
- }
- }
-
- /**
- * binding selected server config
- */
- private fun bindingServer(config: ProfileItem): Boolean {
-
- et_remarks.text = Utils.getEditable(config.remarks)
- et_address.text = Utils.getEditable(config.server.orEmpty())
- et_port.text = Utils.getEditable(config.serverPort ?: DEFAULT_PORT.toString())
- et_id.text = Utils.getEditable(config.password.orEmpty())
-
- if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
- et_security?.text = Utils.getEditable(config.username.orEmpty())
- } else if (config.configType == EConfigType.VLESS) {
- et_security?.text = Utils.getEditable(config.method.orEmpty())
- val flow = Utils.arrayFind(flows, config.flow.orEmpty())
- if (flow >= 0) {
- sp_flow?.setSelection(flow)
- }
- } else if (config.configType == EConfigType.WIREGUARD) {
- et_id.text = Utils.getEditable(config.secretKey.orEmpty())
- et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
- et_preshared_key?.visibility = View.VISIBLE
- et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
- et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
- et_local_address?.text = Utils.getEditable(
- config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4
- )
- et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
- } else if (config.configType == EConfigType.HYSTERIA2) {
- et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
- et_port_hop?.text = Utils.getEditable(config.portHopping)
- et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
- et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
- et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown)
- et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp)
- }
- val securityEncryptions =
- if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
- val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
- if (security >= 0) {
- sp_security?.setSelection(security)
- }
-
- val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty())
- if (streamSecurity >= 0) {
- sp_stream_security?.setSelection(streamSecurity)
- container_sni?.visibility = View.VISIBLE
- container_fingerprint?.visibility = View.VISIBLE
- container_alpn?.visibility = View.VISIBLE
-
- et_sni?.text = Utils.getEditable(config.sni)
- config.fingerPrint?.let { it ->
- val utlsIndex = Utils.arrayFind(uTlsItems, it)
- utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) }
- }
- config.alpn?.let { it ->
- val alpnIndex = Utils.arrayFind(alpns, it)
- alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
- }
- if (config.security == TLS) {
- container_allow_insecure?.visibility = View.VISIBLE
- val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.toString())
- if (allowinsecure >= 0) {
- sp_allow_insecure?.setSelection(allowinsecure)
- }
- container_public_key?.visibility = View.GONE
- container_short_id?.visibility = View.GONE
- container_spider_x?.visibility = View.GONE
- } else if (config.security == REALITY) {
- container_public_key?.visibility = View.VISIBLE
- et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
- container_short_id?.visibility = View.VISIBLE
- et_short_id?.text = Utils.getEditable(config.shortId.orEmpty())
- container_spider_x?.visibility = View.VISIBLE
- et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty())
- container_allow_insecure?.visibility = View.GONE
- }
- }
-
- if (config.security.isNullOrEmpty()) {
- container_sni?.visibility = View.GONE
- container_fingerprint?.visibility = View.GONE
- container_alpn?.visibility = View.GONE
- container_allow_insecure?.visibility = View.GONE
- container_public_key?.visibility = View.GONE
- container_short_id?.visibility = View.GONE
- container_spider_x?.visibility = View.GONE
- }
- val network = Utils.arrayFind(networks, config.network.orEmpty())
- if (network >= 0) {
- sp_network?.setSelection(network)
- }
- return true
- }
-
- /**
- * clear or init server config
- */
- private fun clearServer(): Boolean {
- et_remarks.text = null
- et_address.text = null
- et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
- et_id.text = null
- sp_security?.setSelection(0)
- sp_network?.setSelection(0)
-
- sp_header_type?.setSelection(0)
- et_request_host?.text = null
- et_path?.text = null
- sp_stream_security?.setSelection(0)
- sp_allow_insecure?.setSelection(0)
- et_sni?.text = null
-
- //et_security.text = null
- sp_flow?.setSelection(0)
- et_public_key?.text = null
- et_reserved1?.text = Utils.getEditable("0,0,0")
- et_local_address?.text =
- Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4)
- et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
- return true
- }
-
- /**
- * save server config
- */
- private fun saveServer(): Boolean {
- if (TextUtils.isEmpty(et_remarks.text.toString())) {
- toast(R.string.server_lab_remarks)
- return false
- }
- if (TextUtils.isEmpty(et_address.text.toString())) {
- toast(R.string.server_lab_address)
- return false
- }
- if (createConfigType != EConfigType.HYSTERIA2) {
- if (Utils.parseInt(et_port.text.toString()) <= 0) {
- toast(R.string.server_lab_port)
- return false
- }
- }
- val config =
- MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
- if (config.configType != EConfigType.SOCKS
- && config.configType != EConfigType.HTTP
- && TextUtils.isEmpty(et_id.text.toString())
- ) {
- if (config.configType == EConfigType.TROJAN
- || config.configType == EConfigType.SHADOWSOCKS
- || config.configType == EConfigType.HYSTERIA2
- ) {
- toast(R.string.server_lab_id3)
- } else {
- toast(R.string.server_lab_id)
- }
- return false
- }
- sp_stream_security?.let {
- if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) {
- toast(R.string.server_lab_stream_security)
- return false
- }
- }
- if (et_extra?.text?.toString().isNotNullEmpty()) {
- if (JsonUtil.parseString(et_extra?.text?.toString()) == null) {
- toast(R.string.server_lab_xhttp_extra)
- return false
- }
- }
-
- saveCommon(config)
- saveStreamSettings(config)
- saveTls(config)
-
- if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
- config.subscriptionId = subscriptionId.orEmpty()
- }
- //Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
- MmkvManager.encodeServerConfig(editGuid, config)
- toastSuccess(R.string.toast_success)
- finish()
- return true
- }
-
- private fun saveCommon(config: ProfileItem) {
- config.remarks = et_remarks.text.toString().trim()
- config.server = et_address.text.toString().trim()
- config.serverPort = et_port.text.toString().trim()
- config.password = et_id.text.toString().trim()
-
- if (config.configType == EConfigType.VMESS) {
- config.method = securitys[sp_security?.selectedItemPosition ?: 0]
- } else if (config.configType == EConfigType.VLESS) {
- config.method = et_security?.text.toString().trim()
- config.flow = flows[sp_flow?.selectedItemPosition ?: 0]
- } else if (config.configType == EConfigType.SHADOWSOCKS) {
- config.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
- } else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
- if (!TextUtils.isEmpty(et_security?.text) || !TextUtils.isEmpty(et_id.text)) {
- config.username = et_security?.text.toString().trim()
- }
- } else if (config.configType == EConfigType.TROJAN) {
- } else if (config.configType == EConfigType.WIREGUARD) {
- config.secretKey = et_id.text.toString().trim()
- config.publicKey = et_public_key?.text.toString().trim()
- config.preSharedKey = et_preshared_key?.text.toString().trim()
- config.reserved = et_reserved1?.text.toString().trim()
- config.localAddress = et_local_address?.text.toString().trim()
- config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
- } else if (config.configType == EConfigType.HYSTERIA2) {
- config.obfsPassword = et_obfs_password?.text?.toString()
- config.portHopping = et_port_hop?.text?.toString()
- config.portHoppingInterval = et_port_hop_interval?.text?.toString()
- config.pinSHA256 = et_pinsha256?.text?.toString()
- config.bandwidthDown = et_bandwidth_down?.text?.toString()
- config.bandwidthUp = et_bandwidth_up?.text?.toString()
- }
- }
-
-
- private fun saveStreamSettings(profileItem: ProfileItem) {
- val network = sp_network?.selectedItemPosition ?: return
- val type = sp_header_type?.selectedItemPosition ?: return
- val requestHost = et_request_host?.text?.toString()?.trim() ?: return
- val path = et_path?.text?.toString()?.trim() ?: return
-
- profileItem.network = networks[network]
- profileItem.headerType = transportTypes(networks[network])[type]
- profileItem.host = requestHost
- profileItem.path = path
- profileItem.seed = path
- profileItem.quicSecurity = requestHost
- profileItem.quicKey = path
- profileItem.mode = transportTypes(networks[network])[type]
- profileItem.serviceName = path
- profileItem.authority = requestHost
- profileItem.xhttpMode = transportTypes(networks[network])[type]
- profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
- }
-
- private fun saveTls(config: ProfileItem) {
- val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
- val sniField = et_sni?.text?.toString()?.trim()
- val allowInsecureField = sp_allow_insecure?.selectedItemPosition
- val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: 0
- val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 0
- val publicKey = et_public_key?.text?.toString()
- val shortId = et_short_id?.text?.toString()
- val spiderX = et_spider_x?.text?.toString()
-
- val allowInsecure =
- if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
- MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
- } else {
- allowinsecures[allowInsecureField].toBoolean()
- }
-
- config.security = streamSecuritys[streamSecurity]
- config.insecure = allowInsecure
- config.sni = sniField
- config.fingerPrint = uTlsItems[utlsIndex]
- config.alpn = alpns[alpnIndex]
- config.publicKey = publicKey
- config.shortId = shortId
- config.spiderX = spiderX
- }
-
- private fun transportTypes(network: String?): Array {
- return when (network) {
- NetworkType.TCP.type -> {
- tcpTypes
- }
-
- NetworkType.KCP.type -> {
- kcpAndQuicTypes
- }
-
- NetworkType.GRPC.type -> {
- grpcModes
- }
-
- NetworkType.XHTTP.type -> {
- xhttpMode
- }
-
- else -> {
- arrayOf("---")
- }
- }
- }
-
- /**
- * delete server config
- */
- private fun deleteServer(): Boolean {
- if (editGuid.isNotEmpty()) {
- if (editGuid != MmkvManager.getSelectServer()) {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
- AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- MmkvManager.removeServer(editGuid)
- finish()
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- // do nothing
- }
- .show()
- } else {
- MmkvManager.removeServer(editGuid)
- finish()
- }
- } else {
- application.toast(R.string.toast_action_not_allowed)
- }
- }
- return true
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.action_server, menu)
- val delButton = menu.findItem(R.id.del_config)
- val saveButton = menu.findItem(R.id.save_config)
-
- if (editGuid.isNotEmpty()) {
- if (isRunning) {
- delButton?.isVisible = false
- saveButton?.isVisible = false
- }
- } else {
- delButton?.isVisible = false
- }
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.del_config -> {
- deleteServer()
- true
- }
-
- R.id.save_config -> {
- saveServer()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt
deleted file mode 100644
index 6af64e3a..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt
+++ /dev/null
@@ -1,372 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.content.Intent
-import android.os.Bundle
-import android.text.TextUtils
-import android.view.View
-import androidx.activity.viewModels
-import androidx.preference.CheckBoxPreference
-import androidx.preference.EditTextPreference
-import androidx.preference.ListPreference
-import androidx.preference.PreferenceFragmentCompat
-import androidx.work.ExistingPeriodicWorkPolicy
-import androidx.work.PeriodicWorkRequest
-import androidx.work.multiprocess.RemoteWorkManager
-import com.v2ray.ang.AngApplication
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.VPN
-import com.v2ray.ang.R
-import com.v2ray.ang.extension.toLongEx
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.service.SubscriptionUpdater
-import com.v2ray.ang.util.Utils
-import com.v2ray.ang.viewmodel.SettingsViewModel
-import java.util.concurrent.TimeUnit
-
-class SettingsActivity : BaseActivity() {
- private val settingsViewModel: SettingsViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_settings)
-
- title = getString(R.string.title_settings)
-
- settingsViewModel.startListenPreferenceChange()
- }
-
- class SettingsFragment : PreferenceFragmentCompat() {
-
- private val perAppProxy by lazy { findPreference(AppConfig.PREF_PER_APP_PROXY) }
- private val localDns by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_ENABLED) }
- private val fakeDns by lazy { findPreference(AppConfig.PREF_FAKE_DNS_ENABLED) }
- private val appendHttpProxy by lazy { findPreference(AppConfig.PREF_APPEND_HTTP_PROXY) }
- private val localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) }
- private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) }
- private val vpnBypassLan by lazy { findPreference(AppConfig.PREF_VPN_BYPASS_LAN) }
- private val vpnInterfaceAddress by lazy { findPreference(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
-
- private val mux by lazy { findPreference(AppConfig.PREF_MUX_ENABLED) }
- private val muxConcurrency by lazy { findPreference(AppConfig.PREF_MUX_CONCURRENCY) }
- private val muxXudpConcurrency by lazy { findPreference(AppConfig.PREF_MUX_XUDP_CONCURRENCY) }
- private val muxXudpQuic by lazy { findPreference(AppConfig.PREF_MUX_XUDP_QUIC) }
-
- private val fragment by lazy { findPreference(AppConfig.PREF_FRAGMENT_ENABLED) }
- private val fragmentPackets by lazy { findPreference(AppConfig.PREF_FRAGMENT_PACKETS) }
- private val fragmentLength by lazy { findPreference(AppConfig.PREF_FRAGMENT_LENGTH) }
- private val fragmentInterval by lazy { findPreference(AppConfig.PREF_FRAGMENT_INTERVAL) }
-
- private val autoUpdateCheck by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
- private val autoUpdateInterval by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
-
- private val socksPort by lazy { findPreference(AppConfig.PREF_SOCKS_PORT) }
- private val remoteDns by lazy { findPreference(AppConfig.PREF_REMOTE_DNS) }
- private val domesticDns by lazy { findPreference(AppConfig.PREF_DOMESTIC_DNS) }
- private val dnsHosts by lazy { findPreference(AppConfig.PREF_DNS_HOSTS) }
- private val delayTestUrl by lazy { findPreference(AppConfig.PREF_DELAY_TEST_URL) }
- private val mode by lazy { findPreference(AppConfig.PREF_MODE) }
-
- override fun onCreatePreferences(bundle: Bundle?, s: String?) {
- addPreferencesFromResource(R.xml.pref_settings)
-
- perAppProxy?.setOnPreferenceClickListener {
- startActivity(Intent(activity, PerAppProxyActivity::class.java))
- perAppProxy?.isChecked = true
- false
- }
- localDns?.setOnPreferenceChangeListener { _, any ->
- updateLocalDns(any as Boolean)
- true
- }
- localDnsPort?.setOnPreferenceChangeListener { _, any ->
- val nval = any as String
- localDnsPort?.summary =
- if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
- true
- }
- vpnDns?.setOnPreferenceChangeListener { _, any ->
- vpnDns?.summary = any as String
- true
- }
-
- mux?.setOnPreferenceChangeListener { _, newValue ->
- updateMux(newValue as Boolean)
- true
- }
- muxConcurrency?.setOnPreferenceChangeListener { _, newValue ->
- updateMuxConcurrency(newValue as String)
- true
- }
- muxXudpConcurrency?.setOnPreferenceChangeListener { _, newValue ->
- updateMuxXudpConcurrency(newValue as String)
- true
- }
-
- fragment?.setOnPreferenceChangeListener { _, newValue ->
- updateFragment(newValue as Boolean)
- true
- }
- fragmentPackets?.setOnPreferenceChangeListener { _, newValue ->
- updateFragmentPackets(newValue as String)
- true
- }
- fragmentLength?.setOnPreferenceChangeListener { _, newValue ->
- updateFragmentLength(newValue as String)
- true
- }
- fragmentInterval?.setOnPreferenceChangeListener { _, newValue ->
- updateFragmentInterval(newValue as String)
- true
- }
-
- autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
- val value = newValue as Boolean
- autoUpdateCheck?.isChecked = value
- autoUpdateInterval?.isEnabled = value
- autoUpdateInterval?.text?.toLongEx()?.let {
- if (newValue) configureUpdateTask(it) else cancelUpdateTask()
- }
- true
- }
- autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
- var nval = any as String
-
- // It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
- nval =
- if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
- autoUpdateInterval?.summary = nval
- configureUpdateTask(nval.toLongEx())
- true
- }
-
- socksPort?.setOnPreferenceChangeListener { _, any ->
- val nval = any as String
- socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
- true
- }
-
- remoteDns?.setOnPreferenceChangeListener { _, any ->
- val nval = any as String
- remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
- true
- }
- domesticDns?.setOnPreferenceChangeListener { _, any ->
- val nval = any as String
- domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
- true
- }
- dnsHosts?.setOnPreferenceChangeListener { _, any ->
- val nval = any as String
- dnsHosts?.summary = nval
- true
- }
- delayTestUrl?.setOnPreferenceChangeListener { _, any ->
- val nval = any as String
- delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval
- true
- }
- mode?.setOnPreferenceChangeListener { _, newValue ->
- updateMode(newValue.toString())
- true
- }
- mode?.dialogLayoutResource = R.layout.preference_with_help_link
- //loglevel.summary = "LogLevel"
-
- }
-
- override fun onStart() {
- super.onStart()
- updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
- localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
- fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
- appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false)
- localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
- vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
-
- updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false))
- mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
- muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")
- muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
-
- updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false))
- fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)
- fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
- fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
- fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
-
- autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
- autoUpdateInterval?.summary =
- MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
- autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
-
- socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
- remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
- domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
- dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
- delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
-
- initSharedPreference()
- }
-
- private fun initSharedPreference() {
- listOf(
- localDnsPort,
- vpnDns,
- muxConcurrency,
- muxXudpConcurrency,
- fragmentLength,
- fragmentInterval,
- autoUpdateInterval,
- socksPort,
- remoteDns,
- domesticDns,
- delayTestUrl
- ).forEach { key ->
- key?.text = key?.summary.toString()
- }
-
- listOf(
- AppConfig.PREF_SNIFFING_ENABLED,
- ).forEach { key ->
- findPreference(key)?.isChecked =
- MmkvManager.decodeSettingsBool(key, true)
- }
-
- listOf(
- AppConfig.PREF_ROUTE_ONLY_ENABLED,
- AppConfig.PREF_IS_BOOTED,
- AppConfig.PREF_BYPASS_APPS,
- AppConfig.PREF_SPEED_ENABLED,
- AppConfig.PREF_CONFIRM_REMOVE,
- AppConfig.PREF_START_SCAN_IMMEDIATE,
- AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
- AppConfig.PREF_PREFER_IPV6,
- AppConfig.PREF_PROXY_SHARING,
- AppConfig.PREF_ALLOW_INSECURE
- ).forEach { key ->
- findPreference(key)?.isChecked =
- MmkvManager.decodeSettingsBool(key, false)
- }
-
- listOf(
- AppConfig.PREF_VPN_BYPASS_LAN,
- AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
- AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
- AppConfig.PREF_MUX_XUDP_QUIC,
- AppConfig.PREF_FRAGMENT_PACKETS,
- AppConfig.PREF_LANGUAGE,
- AppConfig.PREF_UI_MODE_NIGHT,
- AppConfig.PREF_LOGLEVEL,
- AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
- AppConfig.PREF_MODE
- ).forEach { key ->
- if (MmkvManager.decodeSettingsString(key) != null) {
- findPreference(key)?.value = MmkvManager.decodeSettingsString(key)
- }
- }
- }
-
- private fun updateMode(mode: String?) {
- val vpn = mode == VPN
- perAppProxy?.isEnabled = vpn
- perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
- localDns?.isEnabled = vpn
- fakeDns?.isEnabled = vpn
- appendHttpProxy?.isEnabled = vpn
- localDnsPort?.isEnabled = vpn
- vpnDns?.isEnabled = vpn
- vpnBypassLan?.isEnabled = vpn
- vpnInterfaceAddress?.isEnabled = vpn
- if (vpn) {
- updateLocalDns(
- MmkvManager.decodeSettingsBool(
- AppConfig.PREF_LOCAL_DNS_ENABLED,
- false
- )
- )
- }
- }
-
- private fun updateLocalDns(enabled: Boolean) {
- fakeDns?.isEnabled = enabled
- localDnsPort?.isEnabled = enabled
- vpnDns?.isEnabled = !enabled
- }
-
- private fun configureUpdateTask(interval: Long) {
- val rw = RemoteWorkManager.getInstance(AngApplication.application)
- rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
- rw.enqueueUniquePeriodicWork(
- AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME,
- ExistingPeriodicWorkPolicy.UPDATE,
- PeriodicWorkRequest.Builder(
- SubscriptionUpdater.UpdateTask::class.java,
- interval,
- TimeUnit.MINUTES
- )
- .apply {
- setInitialDelay(interval, TimeUnit.MINUTES)
- }
- .build()
- )
- }
-
- private fun cancelUpdateTask() {
- val rw = RemoteWorkManager.getInstance(AngApplication.application)
- rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
- }
-
- private fun updateMux(enabled: Boolean) {
- muxConcurrency?.isEnabled = enabled
- muxXudpConcurrency?.isEnabled = enabled
- muxXudpQuic?.isEnabled = enabled
- if (enabled) {
- updateMuxConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
- updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
- }
- }
-
- private fun updateMuxConcurrency(value: String?) {
- val concurrency = value?.toIntOrNull() ?: 8
- muxConcurrency?.summary = concurrency.toString()
- }
-
-
- private fun updateMuxXudpConcurrency(value: String?) {
- if (value == null) {
- muxXudpQuic?.isEnabled = true
- } else {
- val concurrency = value.toIntOrNull() ?: 8
- muxXudpConcurrency?.summary = concurrency.toString()
- muxXudpQuic?.isEnabled = concurrency >= 0
- }
- }
-
- private fun updateFragment(enabled: Boolean) {
- fragmentPackets?.isEnabled = enabled
- fragmentLength?.isEnabled = enabled
- fragmentInterval?.isEnabled = enabled
- if (enabled) {
- updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
- updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
- updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
- }
- }
-
- private fun updateFragmentPackets(value: String?) {
- fragmentPackets?.summary = value.toString()
- }
-
- private fun updateFragmentLength(value: String?) {
- fragmentLength?.summary = value.toString()
- }
-
- private fun updateFragmentInterval(value: String?) {
- fragmentInterval?.summary = value.toString()
- }
- }
-
- fun onModeHelpClicked(view: View) {
- Utils.openUri(this, AppConfig.APP_WIKI_MODE)
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt
deleted file mode 100644
index f85382f1..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt
+++ /dev/null
@@ -1,165 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.os.Bundle
-import android.text.TextUtils
-import android.view.Menu
-import android.view.MenuItem
-import androidx.appcompat.app.AlertDialog
-import androidx.lifecycle.lifecycleScope
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivitySubEditBinding
-import com.v2ray.ang.dto.SubscriptionItem
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class SubEditActivity : BaseActivity() {
- private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
-
- private var del_config: MenuItem? = null
- private var save_config: MenuItem? = null
-
- private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- title = getString(R.string.title_sub_setting)
-
- val subItem = MmkvManager.decodeSubscription(editSubId)
- if (subItem != null) {
- bindingServer(subItem)
- } else {
- clearServer()
- }
- }
-
- /**
- * binding selected server config
- */
- private fun bindingServer(subItem: SubscriptionItem): Boolean {
- binding.etRemarks.text = Utils.getEditable(subItem.remarks)
- binding.etUrl.text = Utils.getEditable(subItem.url)
- binding.etFilter.text = Utils.getEditable(subItem.filter)
- binding.chkEnable.isChecked = subItem.enabled
- binding.autoUpdateCheck.isChecked = subItem.autoUpdate
- binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
- binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
- binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
- return true
- }
-
- /**
- * clear or init server config
- */
- private fun clearServer(): Boolean {
- binding.etRemarks.text = null
- binding.etUrl.text = null
- binding.etFilter.text = null
- binding.chkEnable.isChecked = true
- binding.etPreProfile.text = null
- binding.etNextProfile.text = null
- return true
- }
-
- /**
- * save server config
- */
- private fun saveServer(): Boolean {
- val subItem = MmkvManager.decodeSubscription(editSubId) ?: SubscriptionItem()
-
- subItem.remarks = binding.etRemarks.text.toString()
- subItem.url = binding.etUrl.text.toString()
- subItem.filter = binding.etFilter.text.toString()
- subItem.enabled = binding.chkEnable.isChecked
- subItem.autoUpdate = binding.autoUpdateCheck.isChecked
- subItem.prevProfile = binding.etPreProfile.text.toString()
- subItem.nextProfile = binding.etNextProfile.text.toString()
- subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
-
- if (TextUtils.isEmpty(subItem.remarks)) {
- toast(R.string.sub_setting_remarks)
- return false
- }
- if (subItem.url.isNotEmpty()) {
- if (!Utils.isValidUrl(subItem.url)) {
- toast(R.string.toast_invalid_url)
- return false
- }
-
- if (!Utils.isValidSubUrl(subItem.url)) {
- toast(R.string.toast_insecure_url_protocol)
- if (!subItem.allowInsecureUrl) {
- return false
- }
- }
- }
-
- MmkvManager.encodeSubscription(editSubId, subItem)
- toastSuccess(R.string.toast_success)
- finish()
- return true
- }
-
- /**
- * save server config
- */
- private fun deleteServer(): Boolean {
- if (editSubId.isNotEmpty()) {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
- AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- lifecycleScope.launch(Dispatchers.IO) {
- MmkvManager.removeSubscription(editSubId)
- launch(Dispatchers.Main) {
- finish()
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- // do nothing
- }
- .show()
- } else {
- lifecycleScope.launch(Dispatchers.IO) {
- MmkvManager.removeSubscription(editSubId)
- launch(Dispatchers.Main) {
- finish()
- }
- }
- }
- }
- return true
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.action_server, menu)
- del_config = menu.findItem(R.id.del_config)
- save_config = menu.findItem(R.id.save_config)
-
- if (editSubId.isEmpty()) {
- del_config?.isVisible = false
- }
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.del_config -> {
- deleteServer()
- true
- }
-
- R.id.save_config -> {
- saveServer()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt
deleted file mode 100644
index ef27fcff..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivitySubSettingBinding
-import com.v2ray.ang.dto.SubscriptionItem
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.AngConfigManager
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-class SubSettingActivity : BaseActivity() {
- private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
-
- var subscriptions: List> = listOf()
- private val adapter by lazy { SubSettingRecyclerAdapter(this) }
- private var mItemTouchHelper: ItemTouchHelper? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- title = getString(R.string.title_sub_setting)
-
- binding.recyclerView.setHasFixedSize(true)
- binding.recyclerView.layoutManager = LinearLayoutManager(this)
- addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
- binding.recyclerView.adapter = adapter
-
- mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
- mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
- }
-
- override fun onResume() {
- super.onResume()
- refreshData()
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.action_sub_setting, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.add_config -> {
- startActivity(Intent(this, SubEditActivity::class.java))
- true
- }
-
- R.id.sub_update -> {
- binding.pbWaiting.show()
-
- lifecycleScope.launch(Dispatchers.IO) {
- val count = AngConfigManager.updateConfigViaSubAll()
- delay(500L)
- launch(Dispatchers.Main) {
- if (count > 0) {
- toastSuccess(R.string.toast_success)
- } else {
- toastError(R.string.toast_failure)
- }
- binding.pbWaiting.hide()
- }
- }
-
- true
- }
-
- else -> super.onOptionsItemSelected(item)
-
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun refreshData() {
- subscriptions = MmkvManager.decodeSubscriptions()
- adapter.notifyDataSetChanged()
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt
deleted file mode 100644
index cc2d5404..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.content.Intent
-import android.graphics.Color
-import android.text.TextUtils
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.app.AlertDialog
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.RecyclerView
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ItemQrcodeBinding
-import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.helper.ItemTouchHelperAdapter
-import com.v2ray.ang.helper.ItemTouchHelperViewHolder
-import com.v2ray.ang.util.QRCodeDecoder
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter {
-
- private var mActivity: SubSettingActivity = activity
-
- private val share_method: Array by lazy {
- mActivity.resources.getStringArray(R.array.share_sub_method)
- }
-
- override fun getItemCount() = mActivity.subscriptions.size
-
- override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
- val subId = mActivity.subscriptions[position].first
- val subItem = mActivity.subscriptions[position].second
- holder.itemSubSettingBinding.tvName.text = subItem.remarks
- holder.itemSubSettingBinding.tvUrl.text = subItem.url
- holder.itemSubSettingBinding.chkEnable.isChecked = subItem.enabled
- holder.itemView.setBackgroundColor(Color.TRANSPARENT)
-
- holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
- mActivity.startActivity(
- Intent(mActivity, SubEditActivity::class.java)
- .putExtra("subId", subId)
- )
- }
-
- holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
- removeSubscription(subId, position)
- }
-
- holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
- if (!it.isPressed) return@setOnCheckedChangeListener
- subItem.enabled = isChecked
- MmkvManager.encodeSubscription(subId, subItem)
-
- }
-
- if (TextUtils.isEmpty(subItem.url)) {
- holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE
- holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
- holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
- } else {
- holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE
- holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
- holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
- holder.itemSubSettingBinding.layoutShare.setOnClickListener {
- AlertDialog.Builder(mActivity)
- .setItems(share_method.asList().toTypedArray()) { _, i ->
- try {
- when (i) {
- 0 -> {
- val ivBinding =
- ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
- ivBinding.ivQcode.setImageBitmap(
- QRCodeDecoder.createQRCode(
- subItem.url
-
- )
- )
- AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
- }
-
- 1 -> {
- Utils.setClipboard(mActivity, subItem.url)
- }
-
- else -> mActivity.toast("else")
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Share subscription failed", e)
- }
- }.show()
- }
- }
- }
-
- private fun removeSubscription(subId: String, position: Int) {
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
- AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- removeSubscriptionSub(subId, position)
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do noting
- }
- .show()
- } else {
- removeSubscriptionSub(subId, position)
- }
- }
-
- private fun removeSubscriptionSub(subId: String, position: Int) {
- mActivity.lifecycleScope.launch(Dispatchers.IO) {
- MmkvManager.removeSubscription(subId)
- launch(Dispatchers.Main) {
- notifyItemRemoved(position)
- notifyItemRangeChanged(position, mActivity.subscriptions.size)
- mActivity.refreshData()
- }
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
- return MainViewHolder(
- ItemRecyclerSubSettingBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- )
- }
-
- class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) :
- BaseViewHolder(itemSubSettingBinding.root), ItemTouchHelperViewHolder
-
- open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
- fun onItemSelected() {
- itemView.setBackgroundColor(Color.LTGRAY)
- }
-
- fun onItemClear() {
- itemView.setBackgroundColor(0)
- }
- }
-
- override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
- SettingsManager.swapSubscriptions(fromPosition, toPosition)
- notifyItemMoved(fromPosition, toPosition)
- return true
- }
-
- override fun onItemMoveCompleted() {
- mActivity.refreshData()
- }
-
- override fun onItemDismiss(position: Int) {
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt
deleted file mode 100644
index 364f442a..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.util.Log
-import androidx.lifecycle.lifecycleScope
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityLogcatBinding
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.handler.AngConfigManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.net.URLDecoder
-
-class UrlSchemeActivity : BaseActivity() {
- private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- try {
- intent.apply {
- if (action == Intent.ACTION_SEND) {
- if ("text/plain" == type) {
- intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
- parseUri(it, null)
- }
- }
- } else if (action == Intent.ACTION_VIEW) {
- when (data?.host) {
- "install-config" -> {
- val uri: Uri? = intent.data
- val shareUrl = uri?.getQueryParameter("url").orEmpty()
- parseUri(shareUrl, uri?.fragment)
- }
-
- "install-sub" -> {
- val uri: Uri? = intent.data
- val shareUrl = uri?.getQueryParameter("url").orEmpty()
- parseUri(shareUrl, uri?.fragment)
- }
-
- else -> {
- toastError(R.string.toast_failure)
- }
- }
- }
- }
-
- startActivity(Intent(this, MainActivity::class.java))
- finish()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Error processing URL scheme", e)
- }
- }
-
- private fun parseUri(uriString: String?, fragment: String?) {
- if (uriString.isNullOrEmpty()) {
- return
- }
- Log.i(AppConfig.TAG, uriString)
-
- var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
- val uri = Uri.parse(decodedUrl)
- if (uri != null) {
- if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
- decodedUrl += "#${fragment}"
- }
- Log.i(AppConfig.TAG, decodedUrl)
- lifecycleScope.launch(Dispatchers.IO) {
- val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
- withContext(Dispatchers.Main) {
- if (count + countSub > 0) {
- toast(R.string.import_subscription_success)
- } else {
- toast(R.string.import_subscription_failure)
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt
deleted file mode 100644
index efc0a39c..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt
+++ /dev/null
@@ -1,376 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.app.AlertDialog
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.provider.OpenableColumns
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View.GONE
-import android.view.View.VISIBLE
-import android.view.ViewGroup
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityUserAssetBinding
-import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
-import com.v2ray.ang.dto.AssetUrlItem
-import com.v2ray.ang.extension.concatUrl
-import com.v2ray.ang.extension.toTrafficString
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.util.HttpUtil
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.io.FileOutputStream
-import java.net.HttpURLConnection
-import java.text.DateFormat
-import java.util.Date
-
-class UserAssetActivity : BaseActivity() {
- private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
-
- val extDir by lazy { File(Utils.userAssetPath(this)) }
- val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
-
- private val requestStoragePermissionLauncher = registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- if (isGranted) {
- val intent = Intent(Intent.ACTION_GET_CONTENT)
- intent.type = "*/*"
- intent.addCategory(Intent.CATEGORY_OPENABLE)
-
- try {
- chooseFile.launch(
- Intent.createChooser(
- intent,
- getString(R.string.title_file_chooser)
- )
- )
- } catch (ex: android.content.ActivityNotFoundException) {
- toast(R.string.toast_require_file_manager)
- }
- } else {
- toast(R.string.toast_permission_denied)
- }
- }
-
- private val requestCameraPermissionLauncher = registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- if (isGranted) {
- scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
- } else {
- toast(R.string.toast_permission_denied)
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- title = getString(R.string.title_user_asset_setting)
-
- binding.recyclerView.setHasFixedSize(true)
- binding.recyclerView.layoutManager = LinearLayoutManager(this)
- addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
- binding.recyclerView.adapter = UserAssetAdapter()
-
- binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
- binding.layoutGeoFilesSources.setOnClickListener {
- setGeoFilesSources()
- }
- }
-
- override fun onResume() {
- super.onResume()
- refreshData()
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_asset, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- // Use when to streamline the option selection
- override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
- R.id.add_file -> showFileChooser().let { true }
- R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
- R.id.add_qrcode -> importAssetFromQRcode().let { true }
- R.id.download_file -> downloadGeoFiles().let { true }
- else -> super.onOptionsItemSelected(item)
- }
-
- private fun getGeoFilesSources(): String {
- return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first()
- }
-
- private fun setGeoFilesSources() {
- AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i ->
- try {
- val value = AppConfig.GEO_FILES_SOURCES[i]
- MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
- binding.tvGeoFilesSourcesSummary.text = value
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to set geo files sources", e)
- }
- }.show()
- }
-
- private fun showFileChooser() {
- val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- Manifest.permission.READ_MEDIA_IMAGES
- } else {
- Manifest.permission.READ_EXTERNAL_STORAGE
- }
- requestStoragePermissionLauncher.launch(permission)
- }
-
- private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- val uri = result.data?.data
- if (result.resultCode == RESULT_OK && uri != null) {
- val assetId = Utils.getUuid()
- runCatching {
- val assetItem = AssetUrlItem(
- getCursorName(uri) ?: uri.toString(),
- "file"
- )
-
- val assetList = MmkvManager.decodeAssetUrls()
- if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
- toast(R.string.msg_remark_is_duplicate)
- } else {
- MmkvManager.encodeAsset(assetId, assetItem)
- copyFile(uri)
- }
- }.onFailure {
- toastError(R.string.toast_asset_copy_failed)
- MmkvManager.removeAssetUrl(assetId)
- }
- }
- }
-
- private fun copyFile(uri: Uri): String {
- val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
- contentResolver.openInputStream(uri).use { inputStream ->
- targetFile.outputStream().use { fileOut ->
- inputStream?.copyTo(fileOut)
- toastSuccess(R.string.toast_success)
- refreshData()
- }
- }
- return targetFile.path
- }
-
- private fun getCursorName(uri: Uri): String? = try {
- contentResolver.query(uri, null, null, null, null)?.let { cursor ->
- cursor.run {
- if (moveToFirst()) getString(getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
- else null
- }.also { cursor.close() }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get cursor name", e)
- null
- }
-
- private fun importAssetFromQRcode(): Boolean {
- requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
- return true
- }
-
- private val scanQRCodeForAssetURL = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- importAsset(it.data?.getStringExtra("SCAN_RESULT"))
- }
- }
-
- private fun importAsset(url: String?): Boolean {
- try {
- if (!Utils.isValidUrl(url)) {
- toast(R.string.toast_invalid_url)
- return false
- }
- // Send URL to UserAssetUrlActivity for Processing
- startActivity(
- Intent(this, UserAssetUrlActivity::class.java)
- .putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
- )
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
- return false
- }
- return true
- }
-
- private fun downloadGeoFiles() {
- binding.pbWaiting.show()
- toast(R.string.msg_downloading_content)
-
- val httpPort = SettingsManager.getHttpPort()
- var assets = MmkvManager.decodeAssetUrls()
- assets = addBuiltInGeoItems(assets)
-
- var resultCount = 0
- lifecycleScope.launch(Dispatchers.IO) {
- assets.forEach {
- try {
- var result = downloadGeo(it.second, 15000, httpPort)
- if (!result) {
- result = downloadGeo(it.second, 15000, 0)
- }
- if (result)
- resultCount++
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
- }
- }
- withContext(Dispatchers.Main) {
- if (resultCount > 0) {
- toast(getString(R.string.title_update_config_count, resultCount))
- refreshData()
- } else {
- toast(getString(R.string.toast_failure))
- }
- binding.pbWaiting.hide()
- }
- }
- }
-
- private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
- val targetTemp = File(extDir, item.remarks + "_temp")
- val target = File(extDir, item.remarks)
- Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
-
- val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
- try {
- val inputStream = conn.inputStream
- val responseCode = conn.responseCode
- if (responseCode == HttpURLConnection.HTTP_OK) {
- FileOutputStream(targetTemp).use { output ->
- inputStream.copyTo(output)
- }
-
- targetTemp.renameTo(target)
- }
- return true
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
- return false
- } finally {
- conn.disconnect()
- }
- }
-
- private fun addBuiltInGeoItems(assets: List>): List> {
- val list = mutableListOf>()
- builtInGeoFiles
- .filter { geoFile -> assets.none { it.second.remarks == geoFile } }
- .forEach {
- list.add(
- Utils.getUuid() to AssetUrlItem(
- it,
- String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
- locked = true
- )
- )
- }
-
- return list + assets
- }
-
- fun initAssets() {
- lifecycleScope.launch(Dispatchers.Default) {
- SettingsManager.initAssets(this@UserAssetActivity, assets)
- withContext(Dispatchers.Main) {
- refreshData()
- }
- }
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun refreshData() {
- binding.recyclerView.adapter?.notifyDataSetChanged()
- }
-
- inner class UserAssetAdapter : RecyclerView.Adapter() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
- return UserAssetViewHolder(
- ItemRecyclerUserAssetBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
- )
- }
-
- @SuppressLint("SetTextI18n")
- override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
- var assets = MmkvManager.decodeAssetUrls()
- assets = addBuiltInGeoItems(assets)
- val item = assets.getOrNull(position) ?: return
-// file with name == item.second.remarks
- val file = extDir.listFiles()?.find { it.name == item.second.remarks }
-
- holder.itemUserAssetBinding.assetName.text = item.second.remarks
-
- if (file != null) {
- val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
- holder.itemUserAssetBinding.assetProperties.text =
- "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
- } else {
- holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
- }
-
- if (item.second.locked == true) {
- holder.itemUserAssetBinding.layoutEdit.visibility = GONE
- //holder.itemUserAssetBinding.layoutRemove.visibility = GONE
- } else {
- holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
- //holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
- }
-
- holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
- val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
- intent.putExtra("assetId", item.first)
- startActivity(intent)
- }
- holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
- AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- file?.delete()
- MmkvManager.removeAssetUrl(item.first)
- initAssets()
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- //do noting
- }
- .show()
- }
- }
-
- override fun getItemCount(): Int {
- var assets = MmkvManager.decodeAssetUrls()
- assets = addBuiltInGeoItems(assets)
- return assets.size
- }
- }
-
- class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
- RecyclerView.ViewHolder(itemUserAssetBinding.root)
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt
deleted file mode 100644
index 3d390967..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-package com.v2ray.ang.ui
-
-import android.os.Bundle
-import android.text.TextUtils
-import android.util.Log
-import android.view.Menu
-import android.view.MenuItem
-import androidx.appcompat.app.AlertDialog
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
-import com.v2ray.ang.dto.AssetUrlItem
-import com.v2ray.ang.extension.toast
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.util.Utils
-import java.io.File
-
-class UserAssetUrlActivity : BaseActivity() {
- // Receive QRcode URL from UserAssetActivity
- companion object {
- const val ASSET_URL_QRCODE = "ASSET_URL_QRCODE"
- }
-
- private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
-
- private var del_config: MenuItem? = null
- private var save_config: MenuItem? = null
-
- private val extDir by lazy { File(Utils.userAssetPath(this)) }
- private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- title = getString(R.string.title_user_asset_add_url)
-
- val assetItem = MmkvManager.decodeAsset(editAssetId)
- val assetUrlQrcode = intent.getStringExtra(ASSET_URL_QRCODE)
- val assetNameQrcode = File(assetUrlQrcode.toString()).name
- when {
- assetItem != null -> bindingAsset(assetItem)
- assetUrlQrcode != null -> {
- binding.etRemarks.setText(assetNameQrcode)
- binding.etUrl.setText(assetUrlQrcode)
- }
-
- else -> clearAsset()
- }
- }
-
- /**
- * bingding seleced asset config
- */
- private fun bindingAsset(assetItem: AssetUrlItem): Boolean {
- binding.etRemarks.text = Utils.getEditable(assetItem.remarks)
- binding.etUrl.text = Utils.getEditable(assetItem.url)
- return true
- }
-
- /**
- * clear or init asset config
- */
- private fun clearAsset(): Boolean {
- binding.etRemarks.text = null
- binding.etUrl.text = null
- return true
- }
-
- /**
- * save asset config
- */
- private fun saveServer(): Boolean {
- var assetItem = MmkvManager.decodeAsset(editAssetId)
- var assetId = editAssetId
- if (assetItem != null) {
- // remove file associated with the asset
- val file = extDir.resolve(assetItem.remarks)
- if (file.exists()) {
- try {
- file.delete()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
- }
- }
- } else {
- assetId = Utils.getUuid()
- assetItem = AssetUrlItem()
- }
-
- assetItem.remarks = binding.etRemarks.text.toString()
- assetItem.url = binding.etUrl.text.toString()
-
- // check remarks unique
- val assetList = MmkvManager.decodeAssetUrls()
- if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
- toast(R.string.msg_remark_is_duplicate)
- return false
- }
-
-
- if (TextUtils.isEmpty(assetItem.remarks)) {
- toast(R.string.sub_setting_remarks)
- return false
- }
- if (TextUtils.isEmpty(assetItem.url)) {
- toast(R.string.title_url)
- return false
- }
-
- MmkvManager.encodeAsset(assetId, assetItem)
- toastSuccess(R.string.toast_success)
- finish()
- return true
- }
-
- /**
- * save server config
- */
- private fun deleteServer(): Boolean {
- if (editAssetId.isNotEmpty()) {
- AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- MmkvManager.removeAssetUrl(editAssetId)
- finish()
- }
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- // do nothing
- }
- .show()
- }
- return true
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.action_server, menu)
- del_config = menu.findItem(R.id.del_config)
- save_config = menu.findItem(R.id.save_config)
-
- if (editAssetId.isEmpty()) {
- del_config?.isVisible = false
- }
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
- R.id.del_config -> {
- deleteServer()
- true
- }
-
- R.id.save_config -> {
- saveServer()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt
deleted file mode 100644
index 577698ea..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.v2ray.ang.util
-
-import android.content.Context
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import com.v2ray.ang.dto.AppInfo
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-object AppManagerUtil {
- /**
- * Load the list of network applications.
- *
- * @param context The context to use.
- * @return A list of AppInfo objects representing the network applications.
- */
- suspend fun loadNetworkAppList(context: Context): ArrayList =
- withContext(Dispatchers.IO) {
- val packageManager = context.packageManager
- val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
- val apps = ArrayList()
-
- for (pkg in packages) {
- val applicationInfo = pkg.applicationInfo ?: continue
-
- val appName = applicationInfo.loadLabel(packageManager).toString()
- val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
- val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0
-
- val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
- apps.add(appInfo)
- }
-
- return@withContext apps
- }
-
- fun getLastUpdateTime(context: Context): Long =
- context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
-
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
deleted file mode 100644
index 7172728e..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
+++ /dev/null
@@ -1,223 +0,0 @@
-package com.v2ray.ang.util
-
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.LOOPBACK
-import com.v2ray.ang.BuildConfig
-import com.v2ray.ang.util.Utils.encode
-import com.v2ray.ang.util.Utils.urlDecode
-import java.io.IOException
-import java.net.HttpURLConnection
-import java.net.IDN
-import java.net.Inet6Address
-import java.net.InetAddress
-import java.net.InetSocketAddress
-import java.net.Proxy
-import java.net.URL
-
-object HttpUtil {
-
- /**
- * Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format.
- *
- * For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path".
- *
- * @param str The URL string to convert (can contain non-ASCII characters in the domain).
- * @return The URL string with the domain part converted to ASCII-compatible (Punycode) format.
- */
- fun toIdnUrl(str: String): String {
- val url = URL(str)
- val host = url.host
- val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
- if (host != asciiHost) {
- return str.replace(host, asciiHost)
- } else {
- return str
- }
- }
-
- /**
- * Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format.
- * If the input is an IP address or already an ASCII domain, returns the original string.
- *
- * @param domain The domain string to convert (can include non-ASCII internationalized characters).
- * @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII.
- */
- fun toIdnDomain(domain: String): String {
- // Return as is if it's a pure IP address (IPv4 or IPv6)
- if (Utils.isPureIpAddress(domain)) {
- return domain
- }
-
- // Return as is if already ASCII (English domain or already punycode)
- if (domain.all { it.code < 128 }) {
- return domain
- }
-
- // Otherwise, convert to ASCII using IDN
- return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED)
- }
-
- /**
- * Resolves a hostname to an IP address, returns original input if it's already an IP
- *
- * @param host The hostname or IP address to resolve
- * @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false
- * @return The resolved IP address or the original input (if it's already an IP or resolution fails)
- */
- fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List? {
- try {
- // If it's already an IP address, return it as a list
- if (Utils.isPureIpAddress(host)) {
- return null
- }
-
- // Get all IP addresses
- val addresses = InetAddress.getAllByName(host)
- if (addresses.isEmpty()) {
- return null
- }
-
- // Sort addresses based on preference
- val sortedAddresses = if (ipv6Preferred) {
- addresses.sortedWith(compareByDescending { it is Inet6Address })
- } else {
- addresses.sortedWith(compareBy { it is Inet6Address })
- }
-
- val ipList = sortedAddresses.mapNotNull { it.hostAddress }
-
- Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
-
- return ipList
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to resolve host to IP", e)
- return null
- }
- }
-
-
- /**
- * Retrieves the content of a URL as a string.
- *
- * @param url The URL to fetch content from.
- * @param timeout The timeout value in milliseconds.
- * @param httpPort The HTTP port to use.
- * @return The content of the URL as a string.
- */
- fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? {
- val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null
- try {
- return conn.inputStream.bufferedReader().readText()
- } catch (_: Exception) {
- } finally {
- conn.disconnect()
- }
- return null
- }
-
- /**
- * Retrieves the content of a URL as a string with a custom User-Agent header.
- *
- * @param url The URL to fetch content from.
- * @param timeout The timeout value in milliseconds.
- * @param httpPort The HTTP port to use.
- * @return The content of the URL as a string.
- * @throws IOException If an I/O error occurs.
- */
- @Throws(IOException::class)
- fun getUrlContentWithUserAgent(url: String?, timeout: Int = 15000, httpPort: Int = 0): String {
- var currentUrl = url
- var redirects = 0
- val maxRedirects = 3
-
- while (redirects++ < maxRedirects) {
- if (currentUrl == null) continue
- val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue
- conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
- conn.connect()
-
- val responseCode = conn.responseCode
- when (responseCode) {
- in 300..399 -> {
- val location = conn.getHeaderField("Location")
- conn.disconnect()
- if (location.isNullOrEmpty()) {
- throw IOException("Redirect location not found")
- }
- currentUrl = location
- continue
- }
-
- else -> try {
- return conn.inputStream.use { it.bufferedReader().readText() }
- } finally {
- conn.disconnect()
- }
- }
- }
- throw IOException("Too many redirects")
- }
-
- /**
- * Creates an HttpURLConnection object connected through a proxy.
- *
- * @param urlStr The target URL address.
- * @param port The port of the proxy server.
- * @param connectTimeout The connection timeout in milliseconds (default is 15000 ms).
- * @param readTimeout The read timeout in milliseconds (default is 15000 ms).
- * @param needStream Whether the connection needs to support streaming.
- * @return Returns a configured HttpURLConnection object, or null if it fails.
- */
- fun createProxyConnection(
- urlStr: String,
- port: Int,
- connectTimeout: Int = 15000,
- readTimeout: Int = 15000,
- needStream: Boolean = false
- ): HttpURLConnection? {
-
- var conn: HttpURLConnection? = null
- try {
- val url = URL(urlStr)
- // Create a connection
- conn = if (port == 0) {
- url.openConnection()
- } else {
- url.openConnection(
- Proxy(
- Proxy.Type.HTTP,
- InetSocketAddress(LOOPBACK, port)
- )
- )
- } as HttpURLConnection
-
- // Set connection and read timeouts
- conn.connectTimeout = connectTimeout
- conn.readTimeout = readTimeout
- if (!needStream) {
- // Set request headers
- conn.setRequestProperty("Connection", "close")
- // Disable automatic redirects
- conn.instanceFollowRedirects = false
- // Disable caching
- conn.useCaches = false
- }
-
- //Add Basic Authorization
- url.userInfo?.let {
- conn.setRequestProperty(
- "Authorization",
- "Basic ${encode(urlDecode(it))}"
- )
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to create proxy connection", e)
- // If an exception occurs, close the connection and return null
- conn?.disconnect()
- return null
- }
- return conn
- }
-}
-
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt
deleted file mode 100644
index 80a40fbf..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.v2ray.ang.util
-
-import android.util.Log
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import com.google.gson.JsonObject
-import com.google.gson.JsonParser
-import com.google.gson.JsonPrimitive
-import com.google.gson.JsonSerializationContext
-import com.google.gson.JsonSerializer
-import com.google.gson.reflect.TypeToken
-import com.v2ray.ang.AppConfig
-import java.lang.reflect.Type
-
-object JsonUtil {
- private var gson = Gson()
-
- /**
- * Converts an object to its JSON representation.
- *
- * @param src The object to convert.
- * @return The JSON representation of the object.
- */
- fun toJson(src: Any?): String {
- return gson.toJson(src)
- }
-
- /**
- * Parses a JSON string into an object of the specified class.
- *
- * @param src The JSON string to parse.
- * @param cls The class of the object to parse into.
- * @return The parsed object.
- */
- fun fromJson(src: String, cls: Class): T {
- return gson.fromJson(src, cls)
- }
-
- /**
- * Converts an object to its pretty-printed JSON representation.
- *
- * @param src The object to convert.
- * @return The pretty-printed JSON representation of the object, or null if the object is null.
- */
- fun toJsonPretty(src: Any?): String? {
- if (src == null)
- return null
- val gsonPre = GsonBuilder()
- .setPrettyPrinting()
- .disableHtmlEscaping()
- .registerTypeAdapter( // custom serializer is needed here since JSON by default parse number as Double, core will fail to start
- object : TypeToken() {}.type,
- JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
- JsonPrimitive(
- src?.toInt()
- )
- }
- )
- .create()
- return gsonPre.toJson(src)
- }
-
- /**
- * Parses a JSON string into a JsonObject.
- *
- * @param src The JSON string to parse.
- * @return The parsed JsonObject, or null if parsing fails.
- */
- fun parseString(src: String?): JsonObject? {
- if (src == null)
- return null
- try {
- return JsonParser.parseString(src).getAsJsonObject()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to parse JSON string", e)
- return null
- }
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt
deleted file mode 100644
index c84443c7..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.v2ray.ang.util
-
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.service.V2RayTestService
-import java.io.Serializable
-
-object MessageUtil {
-
-
- /**
- * Sends a message to the service.
- *
- * @param ctx The context.
- * @param what The message identifier.
- * @param content The message content.
- */
- fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
- sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
- }
-
- /**
- * Sends a message to the UI.
- *
- * @param ctx The context.
- * @param what The message identifier.
- * @param content The message content.
- */
- fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
- sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
- }
-
- /**
- * Sends a message to the test service.
- *
- * @param ctx The context.
- * @param what The message identifier.
- * @param content The message content.
- */
- fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
- try {
- val intent = Intent()
- intent.component = ComponentName(ctx, V2RayTestService::class.java)
- intent.putExtra("key", what)
- intent.putExtra("content", content)
- ctx.startService(intent)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to send message to test service", e)
- }
- }
-
- /**
- * Sends a message with the specified action.
- *
- * @param ctx The context.
- * @param action The action string.
- * @param what The message identifier.
- * @param content The message content.
- */
- private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
- try {
- val intent = Intent()
- intent.action = action
- intent.`package` = AppConfig.ANG_PACKAGE
- intent.putExtra("key", what)
- intent.putExtra("content", content)
- ctx.sendBroadcast(intent)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to send message with action: $action", e)
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt
deleted file mode 100644
index a769368f..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.v2ray.ang.util
-
-import android.content.Context
-import android.content.ContextWrapper
-import android.content.res.Configuration
-import android.content.res.Resources
-import android.os.Build
-import android.os.LocaleList
-import androidx.annotation.RequiresApi
-import java.util.Locale
-
-open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
- companion object {
- /**
- * Wraps the context with a new locale.
- *
- * @param context The original context.
- * @param newLocale The new locale to set.
- * @return A ContextWrapper with the new locale.
- */
- @RequiresApi(Build.VERSION_CODES.N)
- fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
- var mContext = context
- val res: Resources = mContext.resources
- val configuration: Configuration = res.configuration
- mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- configuration.setLocale(newLocale)
- val localeList = LocaleList(newLocale)
- LocaleList.setDefault(localeList)
- configuration.setLocales(localeList)
- mContext.createConfigurationContext(configuration)
- } else {
- configuration.setLocale(newLocale)
- mContext.createConfigurationContext(configuration)
- }
- return ContextWrapper(mContext)
- }
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt
deleted file mode 100644
index 2b9f71aa..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.v2ray.ang.util
-
-import android.content.Context
-import android.os.SystemClock
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.EConfigType
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.fmt.Hysteria2Fmt
-import com.v2ray.ang.handler.SpeedtestManager
-import com.v2ray.ang.service.ProcessService
-import java.io.File
-
-object PluginUtil {
- private const val HYSTERIA2 = "libhysteria2.so"
-
- private val procService: ProcessService by lazy {
- ProcessService()
- }
-
- /**
- * Run the plugin based on the provided configuration.
- *
- * @param context The context to use.
- * @param config The profile configuration.
- * @param socksPort The port information.
- */
- fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
- Log.i(AppConfig.TAG, "Starting plugin execution")
-
- if (config == null) {
- Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
- return
- }
-
- try {
- if (config.configType == EConfigType.HYSTERIA2) {
- if (socksPort == null) {
- Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null")
- return
- }
- Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
- val configFile = genConfigHy2(context, config, socksPort) ?: return
- val cmd = genCmdHy2(context, configFile)
-
- procService.runProcess(context, cmd)
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Error running plugin", e)
- }
- }
-
- /**
- * Stop the running plugin.
- */
- fun stopPlugin() {
- stopHy2()
- }
-
- /**
- * Perform a real ping using Hysteria2.
- *
- * @param context The context to use.
- * @param config The profile configuration.
- * @return The ping delay in milliseconds, or -1 if it fails.
- */
- fun realPingHy2(context: Context, config: ProfileItem?): Long {
- Log.i(AppConfig.TAG, "realPingHy2")
- val retFailure = -1L
-
- if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
- val socksPort = Utils.findFreePort(listOf(0))
- val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure
- val cmd = genCmdHy2(context, configFile)
-
- val proc = ProcessService()
- proc.runProcess(context, cmd)
- Thread.sleep(1000L)
- val delay = SpeedtestManager.testConnection(context, socksPort)
- proc.stopProcess()
-
- return delay.first
- }
- return retFailure
- }
-
- /**
- * Generate the configuration file for Hysteria2.
- *
- * @param context The context to use.
- * @param config The profile configuration.
- * @param socksPort The port information.
- * @return The generated configuration file.
- */
- private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
- Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
-
- val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
-
- val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
- Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}")
-
- configFile.parentFile?.mkdirs()
- configFile.writeText(JsonUtil.toJson(hy2Config))
- Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config))
-
- return configFile
- }
-
- /**
- * Generate the command to run Hysteria2.
- *
- * @param context The context to use.
- * @param configFile The configuration file.
- * @return The command to run Hysteria2.
- */
- private fun genCmdHy2(context: Context, configFile: File): MutableList {
- return mutableListOf(
- File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
- "--disable-update-check",
- "--config",
- configFile.absolutePath,
- "--log-level",
- "warn",
- "client"
- )
- }
-
- /**
- * Stop the Hysteria2 process.
- */
- private fun stopHy2() {
- try {
- Log.i(AppConfig.TAG, "$HYSTERIA2 destroy")
- procService?.stopProcess()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e)
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java
new file mode 100644
index 00000000..1a16ac3e
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java
@@ -0,0 +1,116 @@
+package com.v2ray.ang.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.RGBLuminanceSource;
+import com.google.zxing.Result;
+import com.google.zxing.common.GlobalHistogramBinarizer;
+import com.google.zxing.common.HybridBinarizer;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 描述:解析二维码图片
+ */
+public class QRCodeDecoder {
+ public static final Map HINTS = new EnumMap<>(DecodeHintType.class);
+
+ static {
+ List allFormats = new ArrayList<>();
+ allFormats.add(BarcodeFormat.AZTEC);
+ allFormats.add(BarcodeFormat.CODABAR);
+ allFormats.add(BarcodeFormat.CODE_39);
+ allFormats.add(BarcodeFormat.CODE_93);
+ allFormats.add(BarcodeFormat.CODE_128);
+ allFormats.add(BarcodeFormat.DATA_MATRIX);
+ allFormats.add(BarcodeFormat.EAN_8);
+ allFormats.add(BarcodeFormat.EAN_13);
+ allFormats.add(BarcodeFormat.ITF);
+ allFormats.add(BarcodeFormat.MAXICODE);
+ allFormats.add(BarcodeFormat.PDF_417);
+ allFormats.add(BarcodeFormat.QR_CODE);
+ allFormats.add(BarcodeFormat.RSS_14);
+ allFormats.add(BarcodeFormat.RSS_EXPANDED);
+ allFormats.add(BarcodeFormat.UPC_A);
+ allFormats.add(BarcodeFormat.UPC_E);
+ allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION);
+ HINTS.put(DecodeHintType.TRY_HARDER, BarcodeFormat.QR_CODE);
+ HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats);
+ HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8");
+ }
+
+ private QRCodeDecoder() {
+ }
+
+ /**
+ * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
+ *
+ * @param picturePath 要解析的二维码图片本地路径
+ * @return 返回二维码图片里的内容 或 null
+ */
+ public static String syncDecodeQRCode(String picturePath) {
+ return syncDecodeQRCode(getDecodeAbleBitmap(picturePath));
+ }
+
+ /**
+ * 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
+ *
+ * @param bitmap 要解析的二维码图片
+ * @return 返回二维码图片里的内容 或 null
+ */
+ public static String syncDecodeQRCode(Bitmap bitmap) {
+ Result result = null;
+ RGBLuminanceSource source = null;
+ try {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int[] pixels = new int[width * height];
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+ source = new RGBLuminanceSource(width, height, pixels);
+ result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS);
+ return result.getText();
+ } catch (Exception e) {
+ e.printStackTrace();
+ if (source != null) {
+ try {
+ result = new MultiFormatReader().decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), HINTS);
+ return result.getText();
+ } catch (Throwable e2) {
+ e2.printStackTrace();
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
+ *
+ * @param picturePath 本地图片文件路径
+ * @return
+ */
+ private static Bitmap getDecodeAbleBitmap(String picturePath) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(picturePath, options);
+ int sampleSize = options.outHeight / 400;
+ if (sampleSize <= 0) {
+ sampleSize = 1;
+ }
+ options.inSampleSize = sampleSize;
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeFile(picturePath, options);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt
deleted file mode 100644
index 446739b6..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-package com.v2ray.ang.util
-
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import com.google.zxing.BarcodeFormat
-import com.google.zxing.BinaryBitmap
-import com.google.zxing.DecodeHintType
-import com.google.zxing.EncodeHintType
-import com.google.zxing.NotFoundException
-import com.google.zxing.RGBLuminanceSource
-import com.google.zxing.common.GlobalHistogramBinarizer
-import com.google.zxing.qrcode.QRCodeReader
-import com.google.zxing.qrcode.QRCodeWriter
-import java.util.EnumMap
-
-/**
- * QR code decoder utility.
- */
-object QRCodeDecoder {
- val HINTS: MutableMap = EnumMap(DecodeHintType::class.java)
-
- /**
- * Creates a QR code bitmap from the given text.
- *
- * @param text The text to encode in the QR code.
- * @param size The size of the QR code bitmap.
- * @return The generated QR code bitmap, or null if an error occurs.
- */
- fun createQRCode(text: String, size: Int = 800): Bitmap? {
- return runCatching {
- val hints = mapOf(EncodeHintType.CHARACTER_SET to Charsets.UTF_8)
- val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints)
- val pixels = IntArray(size * size) { i ->
- if (bitMatrix.get(i % size, i / size)) 0xff000000.toInt() else 0xffffffff.toInt()
- }
- Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
- setPixels(pixels, 0, size, 0, 0, size, size)
- }
- }.getOrNull()
- }
-
- /**
- * Decodes a QR code from a local image file. This method is time-consuming and should be called in a background thread.
- *
- * @param picturePath The local path of the image file to decode.
- * @return The content of the QR code, or null if decoding fails.
- */
- fun syncDecodeQRCode(picturePath: String): String? {
- return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
- }
-
- /**
- * Decodes a QR code from a bitmap. This method is time-consuming and should be called in a background thread.
- *
- * @param bitmap The bitmap to decode.
- * @return The content of the QR code, or null if decoding fails.
- */
- fun syncDecodeQRCode(bitmap: Bitmap?): String? {
- return bitmap?.let {
- runCatching {
- val pixels = IntArray(it.width * it.height).also { array ->
- it.getPixels(array, 0, it.width, 0, 0, it.width, it.height)
- }
- val source = RGBLuminanceSource(it.width, it.height, pixels)
- val qrReader = QRCodeReader()
-
- try {
- qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), mapOf(DecodeHintType.TRY_HARDER to true)).text
- } catch (e: NotFoundException) {
- qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())), mapOf(DecodeHintType.TRY_HARDER to true)).text
- }
- }.getOrNull()
- }
- }
-
- /**
- * Converts a local image file to a bitmap that can be decoded as a QR code. The image is compressed to avoid being too large.
- *
- * @param picturePath The local path of the image file.
- * @return The decoded bitmap, or null if an error occurs.
- */
- private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
- return try {
- val options = BitmapFactory.Options()
- options.inJustDecodeBounds = true
- BitmapFactory.decodeFile(picturePath, options)
- var sampleSize = options.outHeight / 400
- if (sampleSize <= 0) {
- sampleSize = 1
- }
- options.inSampleSize = sampleSize
- options.inJustDecodeBounds = false
- BitmapFactory.decodeFile(picturePath, options)
- } catch (e: Exception) {
- null
- }
- }
-
- init {
- val allFormats: List = arrayListOf(
- BarcodeFormat.AZTEC,
- BarcodeFormat.CODABAR,
- BarcodeFormat.CODE_39,
- BarcodeFormat.CODE_93,
- BarcodeFormat.CODE_128,
- BarcodeFormat.DATA_MATRIX,
- BarcodeFormat.EAN_8,
- BarcodeFormat.EAN_13,
- BarcodeFormat.ITF,
- BarcodeFormat.MAXICODE,
- BarcodeFormat.PDF_417,
- BarcodeFormat.QR_CODE,
- BarcodeFormat.RSS_14,
- BarcodeFormat.RSS_EXPANDED,
- BarcodeFormat.UPC_A,
- BarcodeFormat.UPC_E,
- BarcodeFormat.UPC_EAN_EXTENSION
- )
- HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
- HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
- HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt
deleted file mode 100644
index 148ce4ec..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt
+++ /dev/null
@@ -1,570 +0,0 @@
-package com.v2ray.ang.util
-
-import android.content.ClipData
-import android.content.ClipboardManager
-import android.content.Context
-import android.content.Intent
-import android.content.res.Configuration.UI_MODE_NIGHT_MASK
-import android.content.res.Configuration.UI_MODE_NIGHT_NO
-import android.os.Build
-import android.os.LocaleList
-import android.provider.Settings
-import android.text.Editable
-import android.util.Base64
-import android.util.Log
-import android.util.Patterns
-import android.webkit.URLUtil
-import androidx.core.content.ContextCompat
-import androidx.core.net.toUri
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.LOOPBACK
-import com.v2ray.ang.BuildConfig
-import java.io.IOException
-import java.net.InetAddress
-import java.net.ServerSocket
-import java.net.URI
-import java.net.URLDecoder
-import java.net.URLEncoder
-import java.util.Locale
-import java.util.UUID
-
-object Utils {
-
- private val IPV4_REGEX =
- Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
- private val IPV6_REGEX = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
-
- /**
- * Convert string to editable for Kotlin.
- *
- * @param text The string to convert.
- * @return An Editable instance containing the text.
- */
- fun getEditable(text: String?): Editable {
- return Editable.Factory.getInstance().newEditable(text.orEmpty())
- }
-
- /**
- * Find the position of a value in an array.
- *
- * @param array The array to search.
- * @param value The value to find.
- * @return The index of the value in the array, or -1 if not found.
- */
- fun arrayFind(array: Array, value: String): Int {
- return array.indexOf(value)
- }
-
- /**
- * Parse a string to an integer with a default value.
- *
- * @param str The string to parse.
- * @param default The default value if parsing fails.
- * @return The parsed integer, or the default value if parsing fails.
- */
- fun parseInt(str: String?, default: Int = 0): Int {
- return str?.toIntOrNull() ?: default
- }
-
- /**
- * Get text from the clipboard.
- *
- * @param context The context to use.
- * @return The text from the clipboard, or an empty string if an error occurs.
- */
- fun getClipboard(context: Context): String {
- return try {
- val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- cmb.primaryClip?.getItemAt(0)?.text.toString()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
- ""
- }
- }
-
- /**
- * Set text to the clipboard.
- *
- * @param context The context to use.
- * @param content The text to set to the clipboard.
- */
- fun setClipboard(context: Context, content: String) {
- try {
- val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- val clipData = ClipData.newPlainText(null, content)
- cmb.setPrimaryClip(clipData)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to set clipboard content", e)
- }
- }
-
- /**
- * Decode a base64 encoded string.
- *
- * @param text The base64 encoded string.
- * @return The decoded string, or an empty string if decoding fails.
- */
- fun decode(text: String?): String {
- return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
- }
-
- /**
- * Try to decode a base64 encoded string.
- *
- * @param text The base64 encoded string.
- * @return The decoded string, or null if decoding fails.
- */
- fun tryDecodeBase64(text: String?): String? {
- if (text.isNullOrEmpty()) return null
-
- try {
- return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to decode standard base64", e)
- }
- try {
- return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
- }
- return null
- }
-
- /**
- * Encode a string to base64.
- *
- * @param text The string to encode.
- * @return The base64 encoded string, or an empty string if encoding fails.
- */
- fun encode(text: String): String {
- return try {
- Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to encode text to base64", e)
- ""
- }
- }
-
- /**
- * Check if a string is a valid IP address.
- *
- * @param value The string to check.
- * @return True if the string is a valid IP address, false otherwise.
- */
- fun isIpAddress(value: String?): Boolean {
- if (value.isNullOrEmpty()) return false
-
- try {
- var addr = value.trim()
- if (addr.isEmpty()) return false
-
- //CIDR
- if (addr.contains("/")) {
- val arr = addr.split("/")
- if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) {
- addr = arr[0]
- }
- }
-
- // Handle IPv4-mapped IPv6 addresses
- if (addr.startsWith("::ffff:") && '.' in addr) {
- addr = addr.drop(7)
- } else if (addr.startsWith("[::ffff:") && '.' in addr) {
- addr = addr.drop(8).replace("]", "")
- }
-
- val octets = addr.split('.')
- if (octets.size == 4) {
- if (octets[3].contains(":")) {
- addr = addr.substring(0, addr.indexOf(":"))
- }
- return isIpv4Address(addr)
- }
-
- return isIpv6Address(addr)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to validate IP address", e)
- return false
- }
- }
-
- /**
- * Check if a string is a pure IP address (IPv4 or IPv6).
- *
- * @param value The string to check.
- * @return True if the string is a pure IP address, false otherwise.
- */
- fun isPureIpAddress(value: String): Boolean {
- return isIpv4Address(value) || isIpv6Address(value)
- }
-
- /**
- * Check if a string is a valid domain name.
- *
- * A valid domain name must not be an IP address and must be a valid URL format.
- *
- * @param input The string to check.
- * @return True if the string is a valid domain name, false otherwise.
- */
- fun isDomainName(input: String?): Boolean {
- if (input.isNullOrEmpty()) return false
-
- // Must not be an IP address and must be a valid URL format
- return !isPureIpAddress(input) && isValidUrl(input)
- }
-
- /**
- * Check if a string is a valid IPv4 address.
- *
- * @param value The string to check.
- * @return True if the string is a valid IPv4 address, false otherwise.
- */
- private fun isIpv4Address(value: String): Boolean {
- return IPV4_REGEX.matches(value)
- }
-
- /**
- * Check if a string is a valid IPv6 address.
- *
- * @param value The string to check.
- * @return True if the string is a valid IPv6 address, false otherwise.
- */
- private fun isIpv6Address(value: String): Boolean {
- var addr = value
- if (addr.startsWith("[") && addr.endsWith("]")) {
- addr = addr.drop(1).dropLast(1)
- }
- return IPV6_REGEX.matches(addr)
- }
-
- /**
- * Check if a string is a CoreDNS address.
- *
- * @param s The string to check.
- * @return True if the string is a CoreDNS address, false otherwise.
- */
- fun isCoreDNSAddress(s: String): Boolean {
- return s.startsWith("https") ||
- s.startsWith("tcp") ||
- s.startsWith("quic") ||
- s == "localhost"
- }
-
- /**
- * Check if a string is a valid URL.
- *
- * @param value The string to check.
- * @return True if the string is a valid URL, false otherwise.
- */
- fun isValidUrl(value: String?): Boolean {
- if (value.isNullOrEmpty()) return false
-
- return try {
- Patterns.WEB_URL.matcher(value).matches() ||
- Patterns.DOMAIN_NAME.matcher(value).matches() ||
- URLUtil.isValidUrl(value)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to validate URL", e)
- false
- }
- }
-
- /**
- * Open a URI in a browser.
- *
- * @param context The context to use.
- * @param uriString The URI string to open.
- */
- fun openUri(context: Context, uriString: String) {
- try {
- val uri = uriString.toUri()
- context.startActivity(Intent(Intent.ACTION_VIEW, uri))
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to open URI", e)
- }
- }
-
- /**
- * Generate a UUID.
- *
- * @return A UUID string without dashes.
- */
- fun getUuid(): String {
- return try {
- UUID.randomUUID().toString().replace("-", "")
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to generate UUID", e)
- ""
- }
- }
-
- /**
- * Decode a URL-encoded string.
- *
- * @param url The URL-encoded string.
- * @return The decoded string, or the original string if decoding fails.
- */
- fun urlDecode(url: String): String {
- return try {
- URLDecoder.decode(url, Charsets.UTF_8.toString())
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to decode URL", e)
- url
- }
- }
-
- /**
- * Encode a string to URL-encoded format.
- *
- * @param url The string to encode.
- * @return The URL-encoded string, or the original string if encoding fails.
- */
- fun urlEncode(url: String): String {
- return try {
- URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to encode URL", e)
- url
- }
- }
-
- /**
- * Read text from an asset file.
- *
- * @param context The context to use.
- * @param fileName The name of the asset file.
- * @return The content of the asset file as a string.
- */
- fun readTextFromAssets(context: Context?, fileName: String): String {
- if (context == null) return ""
-
- return try {
- context.assets.open(fileName).use { inputStream ->
- inputStream.bufferedReader().use { reader ->
- reader.readText()
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
- ""
- }
- }
-
- /**
- * Get the path to the user asset directory.
- *
- * @param context The context to use.
- * @return The path to the user asset directory.
- */
- fun userAssetPath(context: Context?): String {
- if (context == null) return ""
-
- return try {
- context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
- ?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get user asset path", e)
- ""
- }
- }
-
- /**
- * Get the path to the backup directory.
- *
- * @param context The context to use.
- * @return The path to the backup directory.
- */
- fun backupPath(context: Context?): String {
- if (context == null) return ""
-
- return try {
- context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
- ?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get backup path", e)
- ""
- }
- }
-
- /**
- * Get the device ID for XUDP base key.
- *
- * @return The device ID for XUDP base key.
- */
- fun getDeviceIdForXUDPBaseKey(): String {
- return try {
- val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
- Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to generate device ID", e)
- ""
- }
- }
-
- /**
- * Get the dark mode status.
- *
- * @param context The context to use.
- * @return True if dark mode is enabled, false otherwise.
- */
- fun getDarkModeStatus(context: Context): Boolean {
- return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
- }
-
- /**
- * Get the IPv6 address in a formatted string.
- *
- * @param address The IPv6 address.
- * @return The formatted IPv6 address, or the original address if not valid.
- */
- fun getIpv6Address(address: String?): String {
- if (address.isNullOrEmpty()) return ""
-
- return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
- "[$address]"
- } else {
- address
- }
- }
-
- /**
- * Get the system locale.
- *
- * @return The system locale.
- */
- fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- LocaleList.getDefault()[0]
- } else {
- Locale.getDefault()
- }
-
- /**
- * Fix illegal characters in a URL.
- *
- * @param str The URL string.
- * @return The URL string with illegal characters replaced.
- */
- fun fixIllegalUrl(str: String): String {
- return str.replace(" ", "%20")
- .replace("|", "%7C")
- }
-
- /**
- * Find a free port from a list of ports.
- *
- * @param ports The list of ports to check.
- * @return The first free port found.
- * @throws IOException If no free port is found.
- */
- fun findFreePort(ports: List): Int {
- for (port in ports) {
- try {
- return ServerSocket(port).use { it.localPort }
- } catch (ex: IOException) {
- continue // try next port
- }
- }
-
- // if the program gets here, no port in the range was found
- throw IOException("no free port found")
- }
-
- /**
- * Check if a string is a valid subscription URL.
- *
- * @param value The string to check.
- * @return True if the string is a valid subscription URL, false otherwise.
- */
- fun isValidSubUrl(value: String?): Boolean {
- if (value.isNullOrEmpty()) return false
-
- try {
- if (URLUtil.isHttpsUrl(value)) return true
- if (URLUtil.isHttpUrl(value)) {
- if (value.contains(LOOPBACK)) return true
-
- //Check private ip address
- val uri = URI(fixIllegalUrl(value))
- if (isIpAddress(uri.host)) {
- AppConfig.PRIVATE_IP_LIST.forEach {
- if (isIpInCidr(uri.host, it)) return true
- }
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
- }
- return false
- }
-
- /**
- * Get the receiver flags based on the Android version.
- *
- * @return The receiver flags.
- */
- fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- ContextCompat.RECEIVER_EXPORTED
- } else {
- ContextCompat.RECEIVER_NOT_EXPORTED
- }
-
- /**
- * Check if the package is Xray.
- *
- * @return True if the package is Xray, false otherwise.
- */
- fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang")
-
- /**
- * Check if it is the Google Play version.
- *
- * @return True if the package is Google Play, false otherwise.
- */
- fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore"
-
- /**
- * Converts an InetAddress to its long representation
- *
- * @param ip The InetAddress to convert
- * @return The long representation of the IP address
- */
- private fun inetAddressToLong(ip: InetAddress): Long {
- val bytes = ip.address
- var result: Long = 0
- for (i in bytes.indices) {
- result = result shl 8 or (bytes[i].toInt() and 0xff).toLong()
- }
- return result
- }
-
- /**
- * Check if an IP address is within a CIDR range
- *
- * @param ip The IP address to check
- * @param cidr The CIDR notation range (e.g., "192.168.1.0/24")
- * @return True if the IP is within the CIDR range, false otherwise
- */
- fun isIpInCidr(ip: String, cidr: String): Boolean {
- try {
- if (!isIpAddress(ip)) return false
-
- // Parse CIDR (e.g., "192.168.1.0/24")
- val (cidrIp, prefixLen) = cidr.split("/")
- val prefixLength = prefixLen.toInt()
-
- // Convert IP and CIDR's IP portion to Long
- val ipLong = inetAddressToLong(InetAddress.getByName(ip))
- val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp))
-
- // Calculate subnet mask (e.g., /24 → 0xFFFFFF00)
- val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength))
-
- // Check if they're in the same subnet
- return (ipLong and mask) == (cidrIpLong and mask)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
- return false
- }
- }
-}
-
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt
deleted file mode 100644
index 9d9dce62..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.v2ray.ang.util
-
-import android.util.Log
-import com.v2ray.ang.AppConfig
-import java.io.BufferedOutputStream
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
-import java.io.IOException
-import java.io.InputStream
-import java.util.zip.ZipEntry
-import java.util.zip.ZipFile
-import java.util.zip.ZipOutputStream
-
-object ZipUtil {
- private const val BUFFER_SIZE = 4096
-
- /**
- * Zip the contents of a folder.
- *
- * @param folderPath The path to the folder to zip.
- * @param outputZipFilePath The path to the output zip file.
- * @return True if the operation is successful, false otherwise.
- * @throws IOException If an I/O error occurs.
- */
- @Throws(IOException::class)
- fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
- val buffer = ByteArray(BUFFER_SIZE)
-
- try {
- if (folderPath.isEmpty() || outputZipFilePath.isEmpty()) {
- return false
- }
-
- val filesToCompress = ArrayList()
- val directory = File(folderPath)
- if (directory.isDirectory) {
- directory.listFiles()?.forEach {
- if (it.isFile) {
- filesToCompress.add(it.absolutePath)
- }
- }
- }
- if (filesToCompress.isEmpty()) {
- return false
- }
-
- val zos = ZipOutputStream(FileOutputStream(outputZipFilePath))
-
- filesToCompress.forEach { file ->
- val ze = ZipEntry(File(file).name)
- zos.putNextEntry(ze)
- val inputStream = FileInputStream(file)
- while (true) {
- val len = inputStream.read(buffer)
- if (len <= 0) break
- zos.write(buffer, 0, len)
- }
-
- inputStream.close()
- }
-
- zos.closeEntry()
- zos.close()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to zip folder", e)
- return false
- }
- return true
- }
-
- /**
- * Unzip the contents of a zip file to a folder.
- *
- * @param zipFile The zip file to unzip.
- * @param destDirectory The destination directory.
- * @return True if the operation is successful, false otherwise.
- * @throws IOException If an I/O error occurs.
- */
- @Throws(IOException::class)
- fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
- File(destDirectory).run {
- if (!exists()) {
- mkdirs()
- }
- }
- try {
- ZipFile(zipFile).use { zip ->
- zip.entries().asSequence().forEach { entry ->
- zip.getInputStream(entry).use { input ->
- val filePath = destDirectory + File.separator + entry.name
- if (!entry.isDirectory) {
- extractFile(input, filePath)
- } else {
- val dir = File(filePath)
- dir.mkdir()
- }
- }
- }
- }
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to unzip file", e)
- return false
- }
- return true
- }
-
- /**
- * Extract a file from an input stream.
- *
- * @param inputStream The input stream to read from.
- * @param destFilePath The destination file path.
- * @throws IOException If an I/O error occurs.
- */
- @Throws(IOException::class)
- private fun extractFile(inputStream: InputStream, destFilePath: String) {
- val bos = BufferedOutputStream(FileOutputStream(destFilePath))
- val bytesIn = ByteArray(BUFFER_SIZE)
- var read: Int
- while (inputStream.read(bytesIn).also { read = it } != -1) {
- bos.write(bytesIn, 0, read)
- }
- bos.close()
- }
-}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt
deleted file mode 100644
index ec5cb7ee..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt
+++ /dev/null
@@ -1,447 +0,0 @@
-package com.v2ray.ang.viewmodel
-
-import android.app.Application
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.res.AssetManager
-import android.util.Log
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.viewModelScope
-import com.v2ray.ang.AngApplication
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.R
-import com.v2ray.ang.dto.ProfileItem
-import com.v2ray.ang.dto.ServersCache
-import com.v2ray.ang.extension.serializable
-import com.v2ray.ang.extension.toastError
-import com.v2ray.ang.extension.toastSuccess
-import com.v2ray.ang.handler.AngConfigManager
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-import com.v2ray.ang.handler.SpeedtestManager
-import com.v2ray.ang.util.MessageUtil
-import com.v2ray.ang.util.Utils
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancelChildren
-import kotlinx.coroutines.launch
-import java.util.Collections
-
-class MainViewModel(application: Application) : AndroidViewModel(application) {
- private var serverList = MmkvManager.decodeServerList()
- var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
-
- //var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
- var keywordFilter = ""
- val serversCache = mutableListOf()
- val isRunning by lazy { MutableLiveData() }
- val updateListAction by lazy { MutableLiveData() }
- val updateTestResultAction by lazy { MutableLiveData() }
- private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
-
- /**
- * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
- * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
- */
- fun startListenBroadcast() {
- isRunning.value = false
- val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
- ContextCompat.registerReceiver(getApplication(), mMsgReceiver, mFilter, Utils.receiverFlags())
- MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
- }
-
- /**
- * Called when the ViewModel is cleared.
- */
- override fun onCleared() {
- getApplication().unregisterReceiver(mMsgReceiver)
- tcpingTestScope.coroutineContext[Job]?.cancelChildren()
- SpeedtestManager.closeAllTcpSockets()
- Log.i(AppConfig.TAG, "Main ViewModel is cleared")
- super.onCleared()
- }
-
- /**
- * Reloads the server list.
- */
- fun reloadServerList() {
- serverList = MmkvManager.decodeServerList()
- updateCache()
- updateListAction.value = -1
- }
-
- /**
- * Removes a server by its GUID.
- * @param guid The GUID of the server to remove.
- */
- fun removeServer(guid: String) {
- serverList.remove(guid)
- MmkvManager.removeServer(guid)
- val index = getPosition(guid)
- if (index >= 0) {
- serversCache.removeAt(index)
- }
- }
-
-// /**
-// * Appends a custom configuration server.
-// * @param server The server configuration to append.
-// * @return True if the server was successfully appended, false otherwise.
-// */
-// fun appendCustomConfigServer(server: String): Boolean {
-// if (server.contains("inbounds")
-// && server.contains("outbounds")
-// && server.contains("routing")
-// ) {
-// try {
-// val config = CustomFmt.parse(server) ?: return false
-// config.subscriptionId = subscriptionId
-// val key = MmkvManager.encodeServerConfig("", config)
-// MmkvManager.encodeServerRaw(key, server)
-// serverList.add(0, key)
-//// val profile = ProfileLiteItem(
-//// configType = config.configType,
-//// subscriptionId = config.subscriptionId,
-//// remarks = config.remarks,
-//// server = config.getProxyOutbound()?.getServerAddress(),
-//// serverPort = config.getProxyOutbound()?.getServerPort(),
-//// )
-// serversCache.add(0, ServersCache(key, config))
-// return true
-// } catch (e: Exception) {
-// e.printStackTrace()
-// }
-// }
-// return false
-// }
-
- /**
- * Swaps the positions of two servers.
- * @param fromPosition The initial position of the server.
- * @param toPosition The target position of the server.
- */
- fun swapServer(fromPosition: Int, toPosition: Int) {
- if (subscriptionId.isEmpty()) {
- Collections.swap(serverList, fromPosition, toPosition)
- } else {
- val fromPosition2 = serverList.indexOf(serversCache[fromPosition].guid)
- val toPosition2 = serverList.indexOf(serversCache[toPosition].guid)
- Collections.swap(serverList, fromPosition2, toPosition2)
- }
- Collections.swap(serversCache, fromPosition, toPosition)
- MmkvManager.encodeServerList(serverList)
- }
-
- /**
- * Updates the cache of servers.
- */
- @Synchronized
- fun updateCache() {
- serversCache.clear()
- for (guid in serverList) {
- var profile = MmkvManager.decodeServerConfig(guid) ?: continue
-// var profile = MmkvManager.decodeProfileConfig(guid)
-// if (profile == null) {
-// val config = MmkvManager.decodeServerConfig(guid) ?: continue
-// profile = ProfileLiteItem(
-// configType = config.configType,
-// subscriptionId = config.subscriptionId,
-// remarks = config.remarks,
-// server = config.getProxyOutbound()?.getServerAddress(),
-// serverPort = config.getProxyOutbound()?.getServerPort(),
-// )
-// MmkvManager.encodeServerConfig(guid, config)
-// }
-
- if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
- continue
- }
-
- if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) {
- serversCache.add(ServersCache(guid, profile))
- }
- }
- }
-
- /**
- * Updates the configuration via subscription for all servers.
- * @return The number of updated configurations.
- */
- fun updateConfigViaSubAll(): Int {
- if (subscriptionId.isEmpty()) {
- return AngConfigManager.updateConfigViaSubAll()
- } else {
- val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
- return AngConfigManager.updateConfigViaSub(Pair(subscriptionId, subItem))
- }
- }
-
- /**
- * Exports all servers.
- * @return The number of exported servers.
- */
- fun exportAllServer(): Int {
- val serverListCopy =
- if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
- serverList
- } else {
- serversCache.map { it.guid }.toList()
- }
-
- val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
- getApplication(),
- serverListCopy
- )
- return ret
- }
-
- /**
- * Tests the TCP ping for all servers.
- */
- fun testAllTcping() {
- tcpingTestScope.coroutineContext[Job]?.cancelChildren()
- SpeedtestManager.closeAllTcpSockets()
- MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
-
- val serversCopy = serversCache.toList()
- for (item in serversCopy) {
- item.profile.let { outbound ->
- val serverAddress = outbound.server
- val serverPort = outbound.serverPort
- if (serverAddress != null && serverPort != null) {
- tcpingTestScope.launch {
- val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
- launch(Dispatchers.Main) {
- MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
- updateListAction.value = getPosition(item.guid)
- }
- }
- }
- }
- }
- }
-
- /**
- * Tests the real ping for all servers.
- */
- fun testAllRealPing() {
- MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
- MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
- updateListAction.value = -1
-
- val serversCopy = serversCache.toList()
- viewModelScope.launch(Dispatchers.Default) {
- for (item in serversCopy) {
- MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
- }
- }
- }
-
- /**
- * Tests the real ping for the current server.
- */
- fun testCurrentServerRealPing() {
- MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
- }
-
- /**
- * Changes the subscription ID.
- * @param id The new subscription ID.
- */
- fun subscriptionIdChanged(id: String) {
- if (subscriptionId != id) {
- subscriptionId = id
- MmkvManager.encodeSettings(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
- reloadServerList()
- }
- }
-
- /**
- * Gets the subscriptions.
- * @param context The context.
- * @return A pair of lists containing the subscription IDs and remarks.
- */
- fun getSubscriptions(context: Context): Pair?, MutableList?> {
- val subscriptions = MmkvManager.decodeSubscriptions()
- if (subscriptionId.isNotEmpty()
- && !subscriptions.map { it.first }.contains(subscriptionId)
- ) {
- subscriptionIdChanged("")
- }
- if (subscriptions.isEmpty()) {
- return null to null
- }
- val listId = subscriptions.map { it.first }.toMutableList()
- listId.add(0, "")
- val listRemarks = subscriptions.map { it.second.remarks }.toMutableList()
- listRemarks.add(0, context.getString(R.string.filter_config_all))
-
- return listId to listRemarks
- }
-
- /**
- * Gets the position of a server by its GUID.
- * @param guid The GUID of the server.
- * @return The position of the server.
- */
- fun getPosition(guid: String): Int {
- serversCache.forEachIndexed { index, it ->
- if (it.guid == guid)
- return index
- }
- return -1
- }
-
- /**
- * Removes duplicate servers.
- * @return The number of removed servers.
- */
- fun removeDuplicateServer(): Int {
- val serversCacheCopy = mutableListOf>()
- for (it in serversCache) {
- val config = MmkvManager.decodeServerConfig(it.guid) ?: continue
- serversCacheCopy.add(Pair(it.guid, config))
- }
-
- val deleteServer = mutableListOf()
- serversCacheCopy.forEachIndexed { index, it ->
- val outbound = it.second
- serversCacheCopy.forEachIndexed { index2, it2 ->
- if (index2 > index) {
- val outbound2 = it2.second
- if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) {
- deleteServer.add(it2.first)
- }
- }
- }
- }
- for (it in deleteServer) {
- MmkvManager.removeServer(it)
- }
-
- return deleteServer.count()
- }
-
- /**
- * Removes all servers.
- * @return The number of removed servers.
- */
- fun removeAllServer(): Int {
- val count =
- if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
- MmkvManager.removeAllServer()
- } else {
- val serversCopy = serversCache.toList()
- for (item in serversCopy) {
- MmkvManager.removeServer(item.guid)
- }
- serversCache.toList().count()
- }
- return count
- }
-
- /**
- * Removes invalid servers.
- * @return The number of removed servers.
- */
- fun removeInvalidServer(): Int {
- var count = 0
- if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
- count += MmkvManager.removeInvalidServer("")
- } else {
- val serversCopy = serversCache.toList()
- for (item in serversCopy) {
- count += MmkvManager.removeInvalidServer(item.guid)
- }
- }
- return count
- }
-
- /**
- * Sorts servers by their test results.
- */
- fun sortByTestResults() {
- data class ServerDelay(var guid: String, var testDelayMillis: Long)
-
- val serverDelays = mutableListOf()
- val serverList = MmkvManager.decodeServerList()
- serverList.forEach { key ->
- val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
- serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
- }
- serverDelays.sortBy { it.testDelayMillis }
-
- serverDelays.forEach {
- serverList.remove(it.guid)
- serverList.add(it.guid)
- }
-
- MmkvManager.encodeServerList(serverList)
- }
-
- /**
- * Initializes assets.
- * @param assets The asset manager.
- */
- fun initAssets(assets: AssetManager) {
- viewModelScope.launch(Dispatchers.Default) {
- SettingsManager.initAssets(getApplication(), assets)
- }
- }
-
- /**
- * Filters the configuration by a keyword.
- * @param keyword The keyword to filter by.
- */
- fun filterConfig(keyword: String) {
- if (keyword == keywordFilter) {
- return
- }
- keywordFilter = keyword
- MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
- reloadServerList()
- }
-
- private val mMsgReceiver = object : BroadcastReceiver() {
- override fun onReceive(ctx: Context?, intent: Intent?) {
- when (intent?.getIntExtra("key", 0)) {
- AppConfig.MSG_STATE_RUNNING -> {
- isRunning.value = true
- }
-
- AppConfig.MSG_STATE_NOT_RUNNING -> {
- isRunning.value = false
- }
-
- AppConfig.MSG_STATE_START_SUCCESS -> {
- getApplication().toastSuccess(R.string.toast_services_success)
- isRunning.value = true
- }
-
- AppConfig.MSG_STATE_START_FAILURE -> {
- getApplication().toastError(R.string.toast_services_failure)
- isRunning.value = false
- }
-
- AppConfig.MSG_STATE_STOP_SUCCESS -> {
- isRunning.value = false
- }
-
- AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
- updateTestResultAction.value = intent.getStringExtra("content")
- }
-
- AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
- val resultPair = intent.serializable>("content") ?: return
- MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
- updateListAction.value = getPosition(resultPair.first)
- }
- }
- }
- }
-}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt
deleted file mode 100644
index 7ac5d60f..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-package com.v2ray.ang.viewmodel
-
-import android.app.Application
-import android.content.SharedPreferences
-import android.util.Log
-import androidx.lifecycle.AndroidViewModel
-import androidx.preference.PreferenceManager
-import com.v2ray.ang.AppConfig
-import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.handler.SettingsManager
-
-class SettingsViewModel(application: Application) : AndroidViewModel(application),
- SharedPreferences.OnSharedPreferenceChangeListener {
-
- /**
- * Starts listening for preference changes.
- */
- fun startListenPreferenceChange() {
- PreferenceManager.getDefaultSharedPreferences(getApplication())
- .registerOnSharedPreferenceChangeListener(this)
- }
-
- /**
- * Called when the ViewModel is cleared.
- */
- override fun onCleared() {
- PreferenceManager.getDefaultSharedPreferences(getApplication())
- .unregisterOnSharedPreferenceChangeListener(this)
- Log.i(AppConfig.TAG, "Settings ViewModel is cleared")
- super.onCleared()
- }
-
- /**
- * Called when a shared preference is changed.
- * @param sharedPreferences The shared preferences.
- * @param key The key of the changed preference.
- */
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
- Log.i(AppConfig.TAG, "Observe settings changed: $key")
- when (key) {
- AppConfig.PREF_MODE,
- AppConfig.PREF_VPN_DNS,
- AppConfig.PREF_VPN_BYPASS_LAN,
- AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
- AppConfig.PREF_REMOTE_DNS,
- AppConfig.PREF_DOMESTIC_DNS,
- AppConfig.PREF_DNS_HOSTS,
- AppConfig.PREF_DELAY_TEST_URL,
- AppConfig.PREF_LOCAL_DNS_PORT,
- AppConfig.PREF_SOCKS_PORT,
- AppConfig.PREF_LOGLEVEL,
- AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
- AppConfig.PREF_LANGUAGE,
- AppConfig.PREF_UI_MODE_NIGHT,
- AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
- AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,
- AppConfig.PREF_FRAGMENT_PACKETS,
- AppConfig.PREF_FRAGMENT_LENGTH,
- AppConfig.PREF_FRAGMENT_INTERVAL,
- AppConfig.PREF_MUX_XUDP_QUIC,
- -> {
- MmkvManager.encodeSettings(key, sharedPreferences.getString(key, ""))
- }
-
- AppConfig.PREF_ROUTE_ONLY_ENABLED,
- AppConfig.PREF_IS_BOOTED,
- AppConfig.PREF_SPEED_ENABLED,
- AppConfig.PREF_PROXY_SHARING,
- AppConfig.PREF_LOCAL_DNS_ENABLED,
- AppConfig.PREF_FAKE_DNS_ENABLED,
- AppConfig.PREF_APPEND_HTTP_PROXY,
- AppConfig.PREF_ALLOW_INSECURE,
- AppConfig.PREF_PREFER_IPV6,
- AppConfig.PREF_PER_APP_PROXY,
- AppConfig.PREF_BYPASS_APPS,
- AppConfig.PREF_CONFIRM_REMOVE,
- AppConfig.PREF_START_SCAN_IMMEDIATE,
- AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
- AppConfig.SUBSCRIPTION_AUTO_UPDATE,
- AppConfig.PREF_FRAGMENT_ENABLED,
- AppConfig.PREF_MUX_ENABLED,
- -> {
- MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, false))
- }
-
- AppConfig.PREF_SNIFFING_ENABLED -> {
- MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, true))
- }
-
- AppConfig.PREF_MUX_CONCURRENCY,
- AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
- MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8"))
- }
- }
- if (key == AppConfig.PREF_UI_MODE_NIGHT) {
- SettingsManager.setNightMode()
- }
- }
-}
diff --git a/V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so
new file mode 100755
index 00000000..0bbc6891
Binary files /dev/null and b/V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so differ
diff --git a/V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so
new file mode 100755
index 00000000..4f588c79
Binary files /dev/null and b/V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so differ
diff --git a/V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so
new file mode 100755
index 00000000..9ce3e627
Binary files /dev/null and b/V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so differ
diff --git a/V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so
new file mode 100755
index 00000000..58199692
Binary files /dev/null and b/V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so differ
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
new file mode 100644
index 00000000..6ca82ded
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
@@ -0,0 +1,29 @@
+package com.v2ray.ang
+
+import androidx.multidex.MultiDexApplication
+import androidx.preference.PreferenceManager
+import com.tencent.mmkv.MMKV
+
+class AngApplication : MultiDexApplication() {
+ companion object {
+ const val PREF_LAST_VERSION = "pref_last_version"
+ }
+
+ var curIndex = -1 //Current proxy that is opened. (Used to implement restart feature)
+ var firstRun = false
+ private set
+
+ override fun onCreate() {
+ super.onCreate()
+
+// LeakCanary.install(this)
+
+ val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
+ if (firstRun)
+ defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
+
+ //Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
+ MMKV.initialize(this)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
new file mode 100644
index 00000000..5f37e276
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
@@ -0,0 +1,82 @@
+package com.v2ray.ang
+
+/**
+ *
+ * App Config Const
+ */
+object AppConfig {
+ const val ANG_PACKAGE = "com.v2ray.ang"
+
+ // legacy
+ const val ANG_CONFIG = "ang_config"
+ const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
+ const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
+
+ // Preferences mapped to MMKV
+ const val PREF_MODE = "pref_mode"
+ const val PREF_SPEED_ENABLED = "pref_speed_enabled"
+ const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
+ const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
+ const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
+ const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
+ const val PREF_VPN_DNS = "pref_vpn_dns"
+ const val PREF_REMOTE_DNS = "pref_remote_dns"
+ const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
+ const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
+ const val PREF_FORWARD_IPV6 = "pref_forward_ipv6"
+ const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
+ const val PREF_ROUTING_MODE = "pref_routing_mode"
+ const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
+ const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
+ const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
+ const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
+ const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
+ const val PREF_BYPASS_APPS = "pref_bypass_apps"
+ // const val PREF_BYPASS_MAINLAND = "pref_bypass_mainland"
+ // const val PREF_START_ON_BOOT = "pref_start_on_boot"
+ // const val PREF_MUX_ENAimport libv2ray.Libv2rayBLED = "pref_mux_enabled"
+ // const val PREF_SOCKS_PORT = "pref_socks_port"
+// const val PREF_HTTP_PORT = "pref_http_port"
+ // const val PREF_DONATE = "pref_donate"
+ // const val PREF_LICENSES = "pref_licenses"
+// const val PREF_FEEDBACK = "pref_feedback"
+// const val PREF_TG_GROUP = "pref_tg_group"
+ // const val PREF_AUTO_RESTART = "pref_auto_restart"
+
+ const val HTTP_PROTOCOL: String = "http://"
+ const val HTTPS_PROTOCOL: String = "https://"
+
+ const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
+ const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
+ const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
+
+ const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
+ const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
+ const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
+ const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
+ const val TASKER_DEFAULT_GUID = "Default"
+
+ const val TAG_AGENT = "proxy"
+ const val TAG_DIRECT = "direct"
+ const val TAG_BLOCKED = "block"
+
+ const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
+ const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
+ const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
+ const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode"
+ const val promotionUrl = "https://1.2345345.xyz/ads.html"
+
+ const val DNS_AGENT = "1.1.1.1"
+ const val DNS_DIRECT = "223.5.5.5"
+
+ const val MSG_REGISTER_CLIENT = 1
+ const val MSG_STATE_RUNNING = 11
+ const val MSG_STATE_NOT_RUNNING = 12
+ const val MSG_UNREGISTER_CLIENT = 2
+ const val MSG_STATE_START = 3
+ const val MSG_STATE_START_SUCCESS = 31
+ const val MSG_STATE_START_FAILURE = 32
+ const val MSG_STATE_STOP = 4
+ const val MSG_STATE_STOP_SUCCESS = 41
+ const val MSG_STATE_RESTART = 5
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt
new file mode 100644
index 00000000..c51d78b6
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt
@@ -0,0 +1,28 @@
+package com.v2ray.ang.dto
+
+data class AngConfig(
+ var index: Int,
+ var vmess: ArrayList,
+ var subItem: ArrayList
+) {
+ data class VmessBean(var guid: String = "123456",
+ var address: String = "v2ray.cool",
+ var port: Int = 10086,
+ var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
+ var alterId: Int = 64,
+ var security: String = "aes-128-cfb",
+ var network: String = "tcp",
+ var remarks: String = "def",
+ var headerType: String = "",
+ var requestHost: String = "",
+ var path: String = "",
+ var streamSecurity: String = "",
+ var configType: Int = 1,
+ var configVersion: Int = 1,
+ var testResult: String = "",
+ var subid: String = "")
+
+ data class SubItemBean(var id: String = "",
+ var remarks: String = "",
+ var url: String = "")
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt
new file mode 100644
index 00000000..f99655a8
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt
@@ -0,0 +1,9 @@
+package com.v2ray.ang.dto
+
+import android.graphics.drawable.Drawable
+
+data class AppInfo(val appName: String,
+ val packageName: String,
+ val appIcon: Drawable,
+ val isSystemApp: Boolean,
+ var isSelected: Int)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt
new file mode 100644
index 00000000..3f12160f
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt
@@ -0,0 +1,14 @@
+package com.v2ray.ang.dto
+
+enum class EConfigType(val value: Int, val protocolScheme: String) {
+ VMESS(1, "vmess://"),
+ CUSTOM(2, ""),
+ SHADOWSOCKS(3, "ss://"),
+ SOCKS(4, "socks://"),
+ VLESS(5, "vless://"),
+ TROJAN(6, "trojan://");
+
+ companion object {
+ fun fromInt(value: Int) = values().firstOrNull { it.value == value }
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt
similarity index 100%
rename from V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt
rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt
new file mode 100644
index 00000000..db89c946
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt
@@ -0,0 +1,69 @@
+package com.v2ray.ang.dto
+
+import com.v2ray.ang.AppConfig.TAG_AGENT
+import com.v2ray.ang.AppConfig.TAG_BLOCKED
+import com.v2ray.ang.AppConfig.TAG_DIRECT
+import com.v2ray.ang.util.Utils
+
+data class ServerConfig(
+ val configVersion: Int = 3,
+ val configType: EConfigType,
+ var subscriptionId: String = "",
+ val addedTime: Long = System.currentTimeMillis(),
+ var remarks: String = "",
+ val outboundBean: V2rayConfig.OutboundBean? = null,
+ var fullConfig: V2rayConfig? = null
+) {
+ companion object {
+ fun create(configType: EConfigType): ServerConfig {
+ when(configType) {
+ EConfigType.VMESS, EConfigType.VLESS ->
+ return ServerConfig(
+ configType = configType,
+ outboundBean = V2rayConfig.OutboundBean(
+ protocol = configType.name.lowercase(),
+ settings = V2rayConfig.OutboundBean.OutSettingsBean(
+ vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
+ users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
+ streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
+ EConfigType.CUSTOM ->
+ return ServerConfig(configType = configType)
+ EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
+ return ServerConfig(
+ configType = configType,
+ outboundBean = V2rayConfig.OutboundBean(
+ protocol = configType.name.lowercase(),
+ settings = V2rayConfig.OutboundBean.OutSettingsBean(
+ servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
+ streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
+ }
+ }
+ }
+
+ fun getProxyOutbound(): V2rayConfig.OutboundBean? {
+ if (configType != EConfigType.CUSTOM) {
+ return outboundBean
+ }
+ return fullConfig?.getProxyOutbound()
+ }
+
+ fun getAllOutboundTags(): MutableList {
+ if (configType != EConfigType.CUSTOM) {
+ return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED)
+ }
+ fullConfig?.let { config ->
+ return config.outbounds.map { it.tag }.toMutableList()
+ }
+ return mutableListOf()
+ }
+
+ fun getV2rayPointDomainAndPort(): String {
+ val address = getProxyOutbound()?.getServerAddress().orEmpty()
+ val port = getProxyOutbound()?.getServerPort()
+ return if (Utils.isIpv6Address(address)) {
+ String.format("[%s]:%s", address, port)
+ } else {
+ String.format("%s:%s", address, port)
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt
new file mode 100644
index 00000000..b2195148
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt
@@ -0,0 +1,8 @@
+package com.v2ray.ang.dto
+
+data class SubscriptionItem(
+ var remarks: String = "",
+ var url: String = "",
+ var enabled: Boolean = true,
+ val addedTime: Long = System.currentTimeMillis()) {
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
new file mode 100644
index 00000000..e3bc1e22
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
@@ -0,0 +1,446 @@
+package com.v2ray.ang.dto
+
+import android.text.TextUtils
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import com.google.gson.annotations.SerializedName
+import com.google.gson.reflect.TypeToken
+import java.lang.reflect.Type
+
+data class V2rayConfig(
+ var stats: Any? = null,
+ val log: LogBean,
+ var policy: PolicyBean?,
+ val inbounds: ArrayList,
+ var outbounds: ArrayList,
+ var dns: DnsBean,
+ val routing: RoutingBean,
+ val api: Any? = null,
+ val transport: Any? = null,
+ val reverse: Any? = null,
+ var fakedns: Any? = null,
+ val browserForwarder: Any? = null) {
+ companion object {
+ const val DEFAULT_PORT = 443
+ const val DEFAULT_SECURITY = "auto"
+ const val DEFAULT_LEVEL = 8
+ const val DEFAULT_NETWORK = "tcp"
+ const val DEFAULT_FLOW = "xtls-rprx-splice"
+
+ const val TLS = "tls"
+ const val XTLS = "xtls"
+ const val HTTP = "http"
+ }
+
+ data class LogBean(val access: String,
+ val error: String,
+ var loglevel: String?,
+ val dnsLog: Boolean? = null)
+
+ data class InboundBean(
+ var tag: String,
+ var port: Int,
+ var protocol: String,
+ var listen: String? = null,
+ val settings: Any? = null,
+ val sniffing: SniffingBean?,
+ val streamSettings: Any? = null,
+ val allocate: Any? = null) {
+
+ data class InSettingsBean(val auth: String? = null,
+ val udp: Boolean? = null,
+ val userLevel: Int? = null,
+ val address: String? = null,
+ val port: Int? = null,
+ val network: String? = null)
+
+ data class SniffingBean(var enabled: Boolean,
+ val destOverride: ArrayList,
+ val metadataOnly: Boolean? = null)
+ }
+
+ data class OutboundBean(val tag: String = "proxy",
+ var protocol: String,
+ var settings: OutSettingsBean? = null,
+ var streamSettings: StreamSettingsBean? = null,
+ val proxySettings: Any? = null,
+ val sendThrough: String? = null,
+ val mux: MuxBean? = MuxBean(false)) {
+
+ data class OutSettingsBean(var vnext: List? = null,
+ var servers: List? = null,
+ /*Blackhole*/
+ var response: Response? = null,
+ /*DNS*/
+ val network: String? = null,
+ val address: String? = null,
+ val port: Int? = null,
+ /*Freedom*/
+ var domainStrategy: String? = null,
+ val redirect: String? = null,
+ val userLevel: Int? = null,
+ /*Loopback*/
+ val inboundTag: String? = null) {
+
+ data class VnextBean(var address: String = "",
+ var port: Int = DEFAULT_PORT,
+ var users: List) {
+
+ data class UsersBean(var id: String = "",
+ var security: String = DEFAULT_SECURITY,
+ var level: Int = DEFAULT_LEVEL,
+ var encryption: String = "",
+ var flow: String = "")
+ }
+
+ data class ServersBean(var address: String = "",
+ var method: String = "chacha20-poly1305",
+ var ota: Boolean = false,
+ var password: String = "",
+ var port: Int = DEFAULT_PORT,
+ var level: Int = DEFAULT_LEVEL,
+ val email: String? = null,
+ val flow: String? = null,
+ val ivCheck: Boolean? = null,
+ var users: List? = null) {
+
+
+ data class SocksUsersBean(var user: String = "",
+ var pass: String = "",
+ var level: Int = DEFAULT_LEVEL)
+ }
+
+ data class Response(var type: String)
+ }
+
+ data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
+ var security: String = "",
+ var tcpSettings: TcpSettingsBean? = null,
+ var kcpSettings: KcpSettingsBean? = null,
+ var wsSettings: WsSettingsBean? = null,
+ var httpSettings: HttpSettingsBean? = null,
+ var tlsSettings: TlsSettingsBean? = null,
+ var quicSettings: QuicSettingBean? = null,
+ var xtlsSettings: TlsSettingsBean? = null,
+ var grpcSettings: GrpcSettingsBean? = null,
+ val dsSettings: Any? = null,
+ val sockopt: Any? = null
+ ) {
+
+ data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
+ val acceptProxyProtocol: Boolean? = null) {
+ data class HeaderBean(var type: String = "none",
+ var request: RequestBean? = null,
+ var response: Any? = null) {
+ data class RequestBean(var path: List = ArrayList(),
+ var headers: HeadersBean = HeadersBean(),
+ val version: String? = null,
+ val method: String? = null) {
+ data class HeadersBean(var Host: List = ArrayList(),
+ @SerializedName("User-Agent")
+ val userAgent: List? = null,
+ @SerializedName("Accept-Encoding")
+ val acceptEncoding: List? = null,
+ val Connection: List? = null,
+ val Pragma: String? = null)
+ }
+ }
+ }
+
+ data class KcpSettingsBean(var mtu: Int = 1350,
+ var tti: Int = 50,
+ var uplinkCapacity: Int = 12,
+ var downlinkCapacity: Int = 100,
+ var congestion: Boolean = false,
+ var readBufferSize: Int = 1,
+ var writeBufferSize: Int = 1,
+ var header: HeaderBean = HeaderBean(),
+ var seed: String? = null) {
+ data class HeaderBean(var type: String = "none")
+ }
+
+ data class WsSettingsBean(var path: String = "",
+ var headers: HeadersBean = HeadersBean(),
+ val maxEarlyData: Int? = null,
+ val useBrowserForwarding: Boolean? = null,
+ val acceptProxyProtocol: Boolean? = null) {
+ data class HeadersBean(var Host: String = "")
+ }
+
+ data class HttpSettingsBean(var host: List = ArrayList(),
+ var path: String = "")
+
+ data class TlsSettingsBean(var allowInsecure: Boolean = false,
+ var serverName: String = "",
+ val alpn: List? = null,
+ val minVersion: String? = null,
+ val maxVersion: String? = null,
+ val preferServerCipherSuites: Boolean? = null,
+ val cipherSuites: String? = null,
+ val fingerprint: String? = null,
+ val certificates: List? = null,
+ val disableSystemRoot: Boolean? = null,
+ val enableSessionResumption: Boolean? = null)
+
+ data class QuicSettingBean(var security: String = "none",
+ var key: String = "",
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none")
+ }
+
+ data class GrpcSettingsBean(var serviceName: String = "",
+ var multiMode: Boolean? = null)
+
+ fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
+ quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
+ var sni = ""
+ network = transport
+ when (network) {
+ "tcp" -> {
+ val tcpSetting = TcpSettingsBean()
+ if (headerType == HTTP) {
+ tcpSetting.header.type = HTTP
+ if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
+ val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
+ requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ tcpSetting.header.request = requestObj
+ sni = requestObj.headers.Host.getOrNull(0) ?: sni
+ }
+ } else {
+ tcpSetting.header.type = "none"
+ sni = host ?: ""
+ }
+ tcpSettings = tcpSetting
+ }
+ "kcp" -> {
+ val kcpsetting = KcpSettingsBean()
+ kcpsetting.header.type = headerType ?: "none"
+ if (seed.isNullOrEmpty()) {
+ kcpsetting.seed = null
+ } else {
+ kcpsetting.seed = seed
+ }
+ kcpSettings = kcpsetting
+ }
+ "ws" -> {
+ val wssetting = WsSettingsBean()
+ wssetting.headers.Host = host ?: ""
+ sni = wssetting.headers.Host
+ wssetting.path = path ?: "/"
+ wsSettings = wssetting
+ }
+ "h2", "http" -> {
+ network = "h2"
+ val h2Setting = HttpSettingsBean()
+ h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ sni = h2Setting.host.getOrNull(0) ?: sni
+ h2Setting.path = path ?: "/"
+ httpSettings = h2Setting
+ }
+ "quic" -> {
+ val quicsetting = QuicSettingBean()
+ quicsetting.security = quicSecurity ?: "none"
+ quicsetting.key = key ?: ""
+ quicsetting.header.type = headerType ?: "none"
+ quicSettings = quicsetting
+ }
+ "grpc" -> {
+ val grpcSetting = GrpcSettingsBean()
+ grpcSetting.multiMode = mode == "multi"
+ grpcSetting.serviceName = serviceName ?: ""
+ sni = host ?: ""
+ grpcSettings = grpcSetting
+ }
+ }
+ return sni
+ }
+
+ fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String) {
+ security = streamSecurity
+ val tlsSetting = TlsSettingsBean(
+ allowInsecure = allowInsecure,
+ serverName = sni
+ )
+ if (security == TLS) {
+ tlsSettings = tlsSetting
+ } else if (security == XTLS) {
+ xtlsSettings = tlsSetting
+ }
+ }
+ }
+
+ data class MuxBean(var enabled: Boolean, var concurrency: Int = 8)
+
+ fun getServerAddress(): String? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)) {
+ return settings?.vnext?.get(0)?.address
+ } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
+ || protocol.equals(EConfigType.SOCKS.name, true)
+ || protocol.equals(EConfigType.TROJAN.name, true)) {
+ return settings?.servers?.get(0)?.address
+ }
+ return null
+ }
+
+ fun getServerPort(): Int? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)) {
+ return settings?.vnext?.get(0)?.port
+ } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
+ || protocol.equals(EConfigType.SOCKS.name, true)
+ || protocol.equals(EConfigType.TROJAN.name, true)) {
+ return settings?.servers?.get(0)?.port
+ }
+ return null
+ }
+
+ fun getPassword(): String? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)) {
+ return settings?.vnext?.get(0)?.users?.get(0)?.id
+ } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
+ || protocol.equals(EConfigType.TROJAN.name, true)) {
+ return settings?.servers?.get(0)?.password
+ } else if (protocol.equals(EConfigType.SOCKS.name, true)) {
+ return settings?.servers?.get(0)?.users?.get(0)?.pass
+ }
+ return null
+ }
+
+ fun getSecurityEncryption(): String? {
+ return when {
+ protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
+ protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
+ protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
+ else -> null
+ }
+ }
+
+ fun getTransportSettingDetails(): List? {
+ if (protocol.equals(EConfigType.VMESS.name, true)
+ || protocol.equals(EConfigType.VLESS.name, true)) {
+ val transport = streamSettings?.network ?: return null
+ return when (transport) {
+ "tcp" -> {
+ val tcpSetting = streamSettings?.tcpSettings ?: return null
+ listOf(tcpSetting.header.type,
+ tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(),
+ tcpSetting.header.request?.path?.joinToString().orEmpty())
+ }
+ "kcp" -> {
+ val kcpSetting = streamSettings?.kcpSettings ?: return null
+ listOf(kcpSetting.header.type,
+ "",
+ kcpSetting.seed.orEmpty())
+ }
+ "ws" -> {
+ val wsSetting = streamSettings?.wsSettings ?: return null
+ listOf("",
+ wsSetting.headers.Host,
+ wsSetting.path)
+ }
+ "h2" -> {
+ val h2Setting = streamSettings?.httpSettings ?: return null
+ listOf("",
+ h2Setting.host.joinToString(),
+ h2Setting.path)
+ }
+ "quic" -> {
+ val quicSetting = streamSettings?.quicSettings ?: return null
+ listOf(quicSetting.header.type,
+ quicSetting.security,
+ quicSetting.key)
+ }
+ "grpc" -> {
+ val grpcSetting = streamSettings?.grpcSettings ?: return null
+ listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
+ "",
+ grpcSetting.serviceName)
+ }
+ else -> null
+ }
+ }
+ return null
+ }
+ }
+
+ data class DnsBean(var servers: ArrayList? = null,
+ var hosts: Map? = null,
+ val clientIp: String? = null,
+ val disableCache: Boolean? = null,
+ val queryStrategy: String? = null,
+ val tag: String? = null
+ ) {
+ data class ServersBean(var address: String = "",
+ var port: Int? = null,
+ var domains: List? = null,
+ var expectIPs: List? = null,
+ val clientIp: String? = null)
+ }
+
+ data class RoutingBean(var domainStrategy: String,
+ val domainMatcher: String? = null,
+ var rules: ArrayList,
+ val balancers: List? = null) {
+
+ data class RulesBean(var type: String = "",
+ var ip: ArrayList? = null,
+ var domain: ArrayList? = null,
+ var outboundTag: String = "",
+ var balancerTag: String? = null,
+ var port: String? = null,
+ val sourcePort: String? = null,
+ val network: String? = null,
+ val source: List? = null,
+ val user: List? = null,
+ var inboundTag: List? = null,
+ val protocol: List? = null,
+ val attrs: String? = null,
+ val domainMatcher: String? = null
+ )
+ }
+
+ data class PolicyBean(var levels: Map,
+ var system: Any? = null) {
+ data class LevelBean(
+ var handshake: Int? = null,
+ var connIdle: Int? = null,
+ var uplinkOnly: Int? = null,
+ var downlinkOnly: Int? = null,
+ val statsUserUplink: Boolean? = null,
+ val statsUserDownlink: Boolean? = null,
+ var bufferSize: Int? = null)
+ }
+
+ data class FakednsBean(var ipPool: String = "198.18.0.0/15",
+ var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
+
+ fun getProxyOutbound(): OutboundBean? {
+ outbounds.forEach { outbound ->
+ if (outbound.protocol.equals(EConfigType.VMESS.name, true) ||
+ outbound.protocol.equals(EConfigType.VLESS.name, true) ||
+ outbound.protocol.equals(EConfigType.SHADOWSOCKS.name, true) ||
+ outbound.protocol.equals(EConfigType.SOCKS.name, true) ||
+ outbound.protocol.equals(EConfigType.TROJAN.name, true)) {
+ return outbound
+ }
+ }
+ return null
+ }
+
+ fun toPrettyPrinting(): String {
+ return GsonBuilder()
+ .setPrettyPrinting()
+ .disableHtmlEscaping()
+ .registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
+ object : TypeToken() {}.type,
+ JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
+ )
+ .create()
+ .toJson(this)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt
new file mode 100644
index 00000000..a114336c
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt
@@ -0,0 +1,14 @@
+package com.v2ray.ang.dto
+
+data class VmessQRCode(var v: String = "",
+ var ps: String = "",
+ var add: String = "",
+ var port: String = "",
+ var id: String = "",
+ var aid: String = "0",
+ var net: String = "",
+ var type: String = "",
+ var host: String = "",
+ var path: String = "",
+ var tls: String = "",
+ var sni: String = "")
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt
new file mode 100644
index 00000000..4d317e67
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt
@@ -0,0 +1,76 @@
+package com.v2ray.ang.extension
+
+import android.content.Context
+import android.os.Build
+import android.widget.Toast
+import com.v2ray.ang.AngApplication
+import me.drakeet.support.toast.ToastCompat
+import org.json.JSONObject
+import java.net.URLConnection
+
+/**
+ * Some extensions
+ */
+
+val Context.v2RayApplication: AngApplication
+ get() = applicationContext as AngApplication
+
+inline fun Context.toast(message: Int): Toast = ToastCompat
+ .makeText(this, message, Toast.LENGTH_SHORT)
+ .apply {
+ show()
+ }
+
+inline fun Context.toast(message: CharSequence): Toast = ToastCompat
+ .makeText(this, message, Toast.LENGTH_SHORT)
+ .apply {
+ show()
+ }
+
+fun JSONObject.putOpt(pair: Pair) = putOpt(pair.first, pair.second)
+fun JSONObject.putOpt(pairs: Map) = pairs.forEach { putOpt(it.key to it.value) }
+
+const val threshold = 1000
+const val divisor = 1024F
+
+fun Long.toSpeedString() = toTrafficString() + "/s"
+
+fun Long.toTrafficString(): String {
+ if (this == 0L)
+ return "\t\t\t0\t B"
+
+ if (this < threshold)
+ return "${this.toFloat().toShortString()}\t B"
+
+ val kib = this / divisor
+ if (kib < threshold)
+ return "${kib.toShortString()}\t KB"
+
+ val mib = kib / divisor
+ if (mib < threshold)
+ return "${mib.toShortString()}\t MB"
+
+ val gib = mib / divisor
+ if (gib < threshold)
+ return "${gib.toShortString()}\t GB"
+
+ val tib = gib / divisor
+ if (tib < threshold)
+ return "${tib.toShortString()}\t TB"
+
+ val pib = tib / divisor
+ if (pib < threshold)
+ return "${pib.toShortString()}\t PB"
+
+ return "∞"
+}
+
+private fun Float.toShortString(): String {
+ val s = "%.2f".format(this)
+ if (s.length <= 4)
+ return s
+ return s.substring(0, 4).removeSuffix(".")
+}
+
+val URLConnection.responseLength: Long
+ get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt
similarity index 50%
rename from V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt
rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt
index bdb7bf9a..08a3184a 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt
@@ -4,38 +4,38 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.text.TextUtils
+import com.google.zxing.WriterException
+import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.service.V2RayServiceManager
+import com.v2ray.ang.util.MmkvManager
+
+import com.v2ray.ang.util.Utils
class TaskerReceiver : BroadcastReceiver() {
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
- /**
- * This method is called when the BroadcastReceiver is receiving an Intent broadcast.
- * It retrieves the bundle from the intent and checks the switch and guid values.
- * Depending on the switch value, it starts or stops the V2Ray service.
- *
- * @param context The Context in which the receiver is running.
- * @param intent The Intent being received.
- */
override fun onReceive(context: Context, intent: Intent?) {
+
try {
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
- val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
+ val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
- if (switch == null || TextUtils.isEmpty(guid)) {
+ if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
return
} else if (switch) {
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
- V2RayServiceManager.startVServiceFromToggle(context)
+ Utils.startVServiceFromToggle(context)
} else {
- V2RayServiceManager.startVService(context, guid)
+ mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
+ V2RayServiceManager.startV2Ray(context)
}
} else {
- V2RayServiceManager.stopVService(context)
+ Utils.stopVService(context)
}
- } catch (e: Exception) {
- android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
+ } catch (e: WriterException) {
+ e.printStackTrace()
}
}
}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt
new file mode 100644
index 00000000..20259d4e
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt
@@ -0,0 +1,67 @@
+package com.v2ray.ang.receiver
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.widget.RemoteViews
+import com.v2ray.ang.R
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.service.V2RayServiceManager
+import com.v2ray.ang.util.Utils
+
+class WidgetProvider : AppWidgetProvider() {
+ /**
+ * 每次窗口小部件被更新都调用一次该方法
+ */
+ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+ updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning)
+ }
+
+ private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
+ val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
+ val intent = Intent(context, WidgetProvider::class.java)
+ intent.action = AppConfig.BROADCAST_ACTION_WIDGET_CLICK
+ val pendingIntent = PendingIntent.getBroadcast(context, R.id.layout_switch, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
+ if (isRunning) {
+ remoteViews.setInt(R.id.layout_switch, "setBackgroundResource", R.drawable.ic_rounded_corner_theme)
+ } else {
+ remoteViews.setInt(R.id.layout_switch, "setBackgroundResource", R.drawable.ic_rounded_corner_grey)
+ }
+
+ for (appWidgetId in appWidgetIds) {
+ appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
+ }
+ }
+
+ /**
+ * 接收窗口小部件发送的广播
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
+ if (V2RayServiceManager.v2rayPoint.isRunning) {
+ Utils.stopVService(context)
+ } else {
+ Utils.startVServiceFromToggle(context)
+ }
+ } else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
+ AppWidgetManager.getInstance(context)?.let { manager ->
+ when (intent.getIntExtra("key", 0)) {
+ AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
+ updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
+ true)
+ }
+ AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
+ updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
+ false)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt
similarity index 58%
rename from V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt
rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt
index 7aecf634..b7e07e91 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt
@@ -1,5 +1,6 @@
package com.v2ray.ang.service
+import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -8,80 +9,52 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
-import android.util.Log
-import androidx.annotation.RequiresApi
-import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference
-@RequiresApi(Build.VERSION_CODES.N)
+@TargetApi(Build.VERSION_CODES.N)
class QSTileService : TileService() {
- /**
- * Sets the state of the tile.
- * @param state The state to set.
- */
fun setState(state: Int) {
- qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
if (state == Tile.STATE_INACTIVE) {
qsTile?.state = Tile.STATE_INACTIVE
qsTile?.label = getString(R.string.app_name)
+ qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v_idle)
} else if (state == Tile.STATE_ACTIVE) {
qsTile?.state = Tile.STATE_ACTIVE
- qsTile?.label = V2RayServiceManager.getRunningServerName()
+ qsTile?.label = V2RayServiceManager.currentConfig?.remarks
+ qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v)
}
qsTile?.updateTile()
}
- /**
- * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
- * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
- */
override fun onStartListening() {
super.onStartListening()
-
- if (V2RayServiceManager.isRunning()) {
- setState(Tile.STATE_ACTIVE)
- } else {
- setState(Tile.STATE_INACTIVE)
- }
+ setState(Tile.STATE_INACTIVE)
mMsgReceive = ReceiveMessageHandler(this)
- val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
- ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
+ registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
}
- /**
- * Called when the tile stops listening.
- */
override fun onStopListening() {
super.onStopListening()
- try {
- applicationContext.unregisterReceiver(mMsgReceive)
- mMsgReceive = null
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to unregister receiver", e)
- }
-
+ unregisterReceiver(mMsgReceive)
+ mMsgReceive = null
}
- /**
- * Called when the tile is clicked.
- */
override fun onClick() {
super.onClick()
when (qsTile.state) {
Tile.STATE_INACTIVE -> {
- V2RayServiceManager.startVServiceFromToggle(this)
+ Utils.startVServiceFromToggle(this)
}
-
Tile.STATE_ACTIVE -> {
- V2RayServiceManager.stopVService(this)
+ Utils.stopVService(this)
}
}
}
@@ -89,26 +62,22 @@ class QSTileService : TileService() {
private var mMsgReceive: BroadcastReceiver? = null
private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() {
- var mReference: SoftReference = SoftReference(context)
+ internal var mReference: SoftReference = SoftReference(context)
override fun onReceive(ctx: Context?, intent: Intent?) {
val context = mReference.get()
when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_STATE_RUNNING -> {
context?.setState(Tile.STATE_ACTIVE)
}
-
AppConfig.MSG_STATE_NOT_RUNNING -> {
context?.setState(Tile.STATE_INACTIVE)
}
-
AppConfig.MSG_STATE_START_SUCCESS -> {
context?.setState(Tile.STATE_ACTIVE)
}
-
AppConfig.MSG_STATE_START_FAILURE -> {
context?.setState(Tile.STATE_INACTIVE)
}
-
AppConfig.MSG_STATE_STOP_SUCCESS -> {
context?.setState(Tile.STATE_INACTIVE)
}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt
new file mode 100644
index 00000000..2998342a
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt
@@ -0,0 +1,14 @@
+package com.v2ray.ang.service
+
+import android.app.Service
+
+interface ServiceControl {
+ fun getService(): Service
+
+ fun startService(parameters: String)
+
+ fun stopService()
+
+ fun vpnProtect(socket: Int): Boolean
+
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt
new file mode 100644
index 00000000..4e4db1fb
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt
@@ -0,0 +1,43 @@
+package com.v2ray.ang.service
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import java.lang.ref.SoftReference
+
+class V2RayProxyOnlyService : Service(), ServiceControl {
+ override fun onCreate() {
+ super.onCreate()
+ V2RayServiceManager.serviceControl = SoftReference(this)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ V2RayServiceManager.startV2rayPoint()
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ V2RayServiceManager.stopV2rayPoint()
+ }
+
+ override fun getService(): Service {
+ return this
+ }
+
+ override fun startService(parameters: String) {
+ // do nothing
+ }
+
+ override fun stopService() {
+ stopSelf()
+ }
+
+ override fun vpnProtect(socket: Int): Boolean {
+ return true
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt
new file mode 100644
index 00000000..cf44bfa2
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt
@@ -0,0 +1,377 @@
+package com.v2ray.ang.service
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.Color
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import android.util.Log
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.AppConfig.TAG_DIRECT
+import com.v2ray.ang.R
+import com.v2ray.ang.dto.ServerConfig
+import com.v2ray.ang.extension.toSpeedString
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.ui.MainActivity
+import com.v2ray.ang.util.MessageUtil
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import com.v2ray.ang.util.V2rayConfigUtil
+import go.Seq
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import libv2ray.Libv2ray
+import libv2ray.V2RayPoint
+import libv2ray.V2RayVPNServiceSupportsSet
+import rx.Observable
+import rx.Subscription
+import java.lang.ref.SoftReference
+import kotlin.math.min
+
+object V2RayServiceManager {
+ private const val NOTIFICATION_ID = 1
+ private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
+ private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
+ private const val NOTIFICATION_ICON_THRESHOLD = 3000
+
+ val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback())
+ private val mMsgReceive = ReceiveMessageHandler()
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ var serviceControl: SoftReference? = null
+ set(value) {
+ field = value
+ val context = value?.get()?.getService()?.applicationContext
+ context?.let {
+ v2rayPoint.packageName = Utils.packagePath(context)
+ v2rayPoint.packageCodePath = context.applicationInfo.nativeLibraryDir + "/"
+ Seq.setContext(context)
+ }
+ }
+ var currentConfig: ServerConfig? = null
+
+ private var lastQueryTime = 0L
+ private var mBuilder: NotificationCompat.Builder? = null
+ private var mSubscription: Subscription? = null
+ private var mNotificationManager: NotificationManager? = null
+
+ fun startV2Ray(context: Context) {
+ if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
+ context.toast(R.string.toast_warning_pref_proxysharing_short)
+ }else{
+ context.toast(R.string.toast_services_start)
+ }
+ val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
+ Intent(context.applicationContext, V2RayVpnService::class.java)
+ } else {
+ Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
+ }
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+
+ private class V2RayCallback : V2RayVPNServiceSupportsSet {
+ override fun shutdown(): Long {
+ val serviceControl = serviceControl?.get() ?: return -1
+ // called by go
+ // shutdown the whole vpn service
+ return try {
+ serviceControl.stopService()
+ 0
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ -1
+ }
+ }
+
+ override fun prepare(): Long {
+ return 0
+ }
+
+ override fun protect(l: Long): Long {
+ val serviceControl = serviceControl?.get() ?: return 0
+ return if (serviceControl.vpnProtect(l.toInt())) 0 else 1
+ }
+
+ override fun onEmitStatus(l: Long, s: String?): Long {
+ //Logger.d(s)
+ return 0
+ }
+
+ override fun setup(s: String): Long {
+ val serviceControl = serviceControl?.get() ?: return -1
+ //Logger.d(s)
+ return try {
+ serviceControl.startService(s)
+ lastQueryTime = System.currentTimeMillis()
+ startSpeedNotification()
+ 0
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ -1
+ }
+ }
+
+ }
+
+ fun startV2rayPoint() {
+ val service = serviceControl?.get()?.getService() ?: return
+ val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
+ val config = MmkvManager.decodeServerConfig(guid) ?: return
+ if (!v2rayPoint.isRunning) {
+ val result = V2rayConfigUtil.getV2rayConfig(service, guid)
+ if (!result.status)
+ return
+
+ try {
+ val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
+ mFilter.addAction(Intent.ACTION_SCREEN_ON)
+ mFilter.addAction(Intent.ACTION_SCREEN_OFF)
+ mFilter.addAction(Intent.ACTION_USER_PRESENT)
+ service.registerReceiver(mMsgReceive, mFilter)
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+
+ v2rayPoint.configureFileContent = result.content
+ v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
+ currentConfig = config
+ v2rayPoint.enableLocalDNS = settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) ?: false
+ v2rayPoint.forwardIpv6 = settingsStorage?.decodeBool(AppConfig.PREF_FORWARD_IPV6) ?: false
+ v2rayPoint.proxyOnly = settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" != "VPN"
+
+ try {
+ v2rayPoint.runLoop()
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+
+ if (v2rayPoint.isRunning) {
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
+ showNotification()
+ } else {
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
+ cancelNotification()
+ }
+ }
+ }
+
+ fun stopV2rayPoint() {
+ val service = serviceControl?.get()?.getService() ?: return
+
+ if (v2rayPoint.isRunning) {
+ GlobalScope.launch(Dispatchers.Default) {
+ try {
+ v2rayPoint.stopLoop()
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+ }
+ }
+
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
+ cancelNotification()
+
+ try {
+ service.unregisterReceiver(mMsgReceive)
+ } catch (e: Exception) {
+ Log.d(ANG_PACKAGE, e.toString())
+ }
+ }
+
+ private class ReceiveMessageHandler : BroadcastReceiver() {
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ val serviceControl = serviceControl?.get() ?: return
+ when (intent?.getIntExtra("key", 0)) {
+ AppConfig.MSG_REGISTER_CLIENT -> {
+ //Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
+ if (v2rayPoint.isRunning) {
+ MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
+ } else {
+ MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
+ }
+ }
+ AppConfig.MSG_UNREGISTER_CLIENT -> {
+ // nothing to do
+ }
+ AppConfig.MSG_STATE_START -> {
+ // nothing to do
+ }
+ AppConfig.MSG_STATE_STOP -> {
+ serviceControl.stopService()
+ }
+ AppConfig.MSG_STATE_RESTART -> {
+ startV2rayPoint()
+ }
+ }
+
+ when (intent?.action) {
+ Intent.ACTION_SCREEN_OFF -> {
+ Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
+ stopSpeedNotification()
+ }
+ Intent.ACTION_SCREEN_ON -> {
+ Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
+ startSpeedNotification()
+ }
+ }
+ }
+ }
+
+ private fun showNotification() {
+ val service = serviceControl?.get()?.getService() ?: return
+ val startMainIntent = Intent(service, MainActivity::class.java)
+ val contentPendingIntent = PendingIntent.getActivity(service,
+ NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
+ stopV2RayIntent.`package` = ANG_PACKAGE
+ stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
+
+ val stopV2RayPendingIntent = PendingIntent.getBroadcast(service,
+ NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val channelId =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel()
+ } else {
+ // If earlier version channel ID is not used
+ // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
+ ""
+ }
+
+ mBuilder = NotificationCompat.Builder(service, channelId)
+ .setSmallIcon(R.drawable.ic_v)
+ .setContentTitle(currentConfig?.remarks)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setOngoing(true)
+ .setShowWhen(false)
+ .setOnlyAlertOnce(true)
+ .setContentIntent(contentPendingIntent)
+ .addAction(R.drawable.ic_close_grey_800_24dp,
+ service.getString(R.string.notification_action_stop_v2ray),
+ stopV2RayPendingIntent)
+ //.build()
+
+ //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
+
+ service.startForeground(NOTIFICATION_ID, mBuilder?.build())
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createNotificationChannel(): String {
+ val channelId = "RAY_NG_M_CH_ID"
+ val channelName = "V2rayNG Background Service"
+ val chan = NotificationChannel(channelId,
+ channelName, NotificationManager.IMPORTANCE_HIGH)
+ chan.lightColor = Color.DKGRAY
+ chan.importance = NotificationManager.IMPORTANCE_NONE
+ chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
+ getNotificationManager()?.createNotificationChannel(chan)
+ return channelId
+ }
+
+ fun cancelNotification() {
+ val service = serviceControl?.get()?.getService() ?: return
+ service.stopForeground(true)
+ mBuilder = null
+ mSubscription?.unsubscribe()
+ mSubscription = null
+ }
+
+ private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
+ if (mBuilder != null) {
+ if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
+ mBuilder?.setSmallIcon(R.drawable.ic_v)
+ } else if (proxyTraffic > directTraffic) {
+ mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
+ } else {
+ mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
+ }
+ mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
+ mBuilder?.setContentText(contentText) // Emui4.1 need content text even if style is set as BigTextStyle
+ getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
+ }
+ }
+
+ private fun getNotificationManager(): NotificationManager? {
+ if (mNotificationManager == null) {
+ val service = serviceControl?.get()?.getService() ?: return null
+ mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ }
+ return mNotificationManager
+ }
+
+ fun startSpeedNotification() {
+ if (mSubscription == null &&
+ v2rayPoint.isRunning &&
+ settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) {
+ var lastZeroSpeed = false
+ val outboundTags = currentConfig?.getAllOutboundTags()
+ outboundTags?.remove(TAG_DIRECT)
+
+ mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
+ .subscribe {
+ val queryTime = System.currentTimeMillis()
+ val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
+ var proxyTotal = 0L
+ val text = StringBuilder()
+ outboundTags?.forEach {
+ val up = v2rayPoint.queryStats(it, "uplink")
+ val down = v2rayPoint.queryStats(it, "downlink")
+ if (up + down > 0) {
+ appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
+ proxyTotal += up + down
+ }
+ }
+ val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
+ val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
+ val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L)
+ if (!zeroSpeed || !lastZeroSpeed) {
+ if (proxyTotal == 0L) {
+ appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
+ }
+ appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
+ directDownlink / sinceLastQueryInSeconds)
+ updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
+ }
+ lastZeroSpeed = zeroSpeed
+ lastQueryTime = queryTime
+ }
+ }
+ }
+
+ private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
+ var n = name ?: "no tag"
+ n = n.substring(0, min(n.length, 6))
+ text.append(n)
+ for (i in n.length..6 step 2) {
+ text.append("\t")
+ }
+ text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
+ }
+
+ fun stopSpeedNotification() {
+ if (mSubscription != null) {
+ mSubscription?.unsubscribe() //stop queryStats
+ mSubscription = null
+ updateNotification(currentConfig?.remarks, 0, 0)
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt
new file mode 100644
index 00000000..586c5c00
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt
@@ -0,0 +1,259 @@
+package com.v2ray.ang.service
+
+import android.app.*
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.*
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import android.os.StrictMode
+import androidx.annotation.RequiresApi
+import android.util.Log
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import java.io.File
+import java.lang.ref.SoftReference
+
+class V2RayVpnService : VpnService(), ServiceControl {
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+
+ private lateinit var mInterface: ParcelFileDescriptor
+
+ /**
+ * Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
+ *
+ * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
+ * satisfies default network capabilities but only THE default network. Unfortunately we need to have
+ * android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
+ *
+ * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
+ */
+ @delegate:RequiresApi(Build.VERSION_CODES.P)
+ private val defaultNetworkRequest by lazy {
+ NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .build()
+ }
+
+ private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
+
+ @delegate:RequiresApi(Build.VERSION_CODES.P)
+ private val defaultNetworkCallback by lazy {
+ object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ setUnderlyingNetworks(arrayOf(network))
+ }
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ // it's a good idea to refresh capabilities
+ setUnderlyingNetworks(arrayOf(network))
+ }
+ override fun onLost(network: Network) {
+ setUnderlyingNetworks(null)
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
+ StrictMode.setThreadPolicy(policy)
+ V2RayServiceManager.serviceControl = SoftReference(this)
+ }
+
+ override fun onRevoke() {
+ stopV2Ray()
+ }
+
+ override fun onLowMemory() {
+ stopV2Ray()
+ super.onLowMemory()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ stopV2Ray()
+ }
+
+ private fun setup(parameters: String) {
+
+ val prepare = prepare(this)
+ if (prepare != null) {
+ return
+ }
+
+ // If the old interface has exactly the same parameters, use it!
+ // Configure a builder while parsing the parameters.
+ val builder = Builder()
+ val enableLocalDns = settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) ?: false
+ val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: "0"
+
+ parameters.split(" ")
+ .map { it.split(",") }
+ .forEach {
+ when (it[0][0]) {
+ 'm' -> builder.setMtu(java.lang.Short.parseShort(it[1]).toInt())
+ 's' -> builder.addSearchDomain(it[1])
+ 'a' -> builder.addAddress(it[1], Integer.parseInt(it[2]))
+ 'r' -> {
+ if (routingMode == "1" || routingMode == "3") {
+ if (it[1] == "::") { //not very elegant, should move Vpn setting in Kotlin, simplify go code
+ builder.addRoute("2000::", 3)
+ } else {
+ resources.getStringArray(R.array.bypass_private_ip_address).forEach { cidr ->
+ val addr = cidr.split('/')
+ builder.addRoute(addr[0], addr[1].toInt())
+ }
+ }
+ } else {
+ builder.addRoute(it[1], Integer.parseInt(it[2]))
+ }
+ }
+ 'd' -> builder.addDnsServer(it[1])
+ }
+ }
+
+ if(!enableLocalDns) {
+ Utils.getVpnDnsServers()
+ .forEach {
+ if (Utils.isPureIpAddress(it)) {
+ builder.addDnsServer(it)
+ }
+ }
+ }
+
+ builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
+ settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
+ val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
+ val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
+ apps?.forEach {
+ try {
+ if (bypassApps)
+ builder.addDisallowedApplication(it)
+ else
+ builder.addAllowedApplication(it)
+ } catch (e: PackageManager.NameNotFoundException) {
+ //Logger.d(e)
+ }
+ }
+ }
+
+ // Close the old interface since the parameters have been changed.
+ try {
+ mInterface.close()
+ } catch (ignored: Exception) {
+ // ignored
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ try {
+ connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ builder.setMetered(false)
+ }
+
+ // Create a new interface using the builder and save the parameters.
+ try {
+ mInterface = builder.establish()!!
+ } catch (e: Exception) {
+ // non-nullable lateinit var
+ e.printStackTrace()
+ stopV2Ray()
+ }
+
+ sendFd()
+ }
+
+ private fun sendFd() {
+ val fd = mInterface.fileDescriptor
+ val path = File(Utils.packagePath(applicationContext), "sock_path").absolutePath
+
+ GlobalScope.launch(Dispatchers.IO) {
+ var tries = 0
+ while (true) try {
+ Thread.sleep(1000L shl tries)
+ Log.d(packageName, "sendFd tries: $tries")
+ LocalSocket().use { localSocket ->
+ localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
+ localSocket.setFileDescriptorsForSend(arrayOf(fd))
+ localSocket.outputStream.write(42)
+ }
+ break
+ } catch (e: Exception) {
+ Log.d(packageName, e.toString())
+ if (tries > 5) break
+ tries += 1
+ }
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ V2RayServiceManager.startV2rayPoint()
+ return START_STICKY
+ //return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun stopV2Ray(isForced: Boolean = true) {
+// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
+// val emptyInfo = VpnNetworkInfo()
+// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
+// saveVpnNetworkInfo(configName, info)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ try {
+ connectivity.unregisterNetworkCallback(defaultNetworkCallback)
+ } catch (ignored: Exception) {
+ // ignored
+ }
+ }
+
+ V2RayServiceManager.stopV2rayPoint()
+
+ if (isForced) {
+ //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
+ //It's strage but true.
+ //This can be verified by putting stopself() behind and call stopLoop and startLoop
+ //in a row for several times. You will find that later created v2ray core report port in use
+ //which means the first v2ray core somehow failed to stop and release the port.
+ stopSelf()
+
+ try {
+ mInterface.close()
+ } catch (ignored: Exception) {
+ // ignored
+ }
+
+ }
+ }
+
+ override fun getService(): Service {
+ return this
+ }
+
+ override fun startService(parameters: String) {
+ setup(parameters)
+ }
+
+ override fun stopService() {
+ stopV2Ray(true)
+ }
+
+ override fun vpnProtect(socket: Int): Boolean {
+ return protect(socket)
+ }
+
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt
new file mode 100644
index 00000000..3f11b649
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt
@@ -0,0 +1,14 @@
+package com.v2ray.ang.ui
+
+import androidx.appcompat.app.AppCompatActivity
+import android.view.MenuItem
+
+abstract class BaseActivity : AppCompatActivity() {
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt
similarity index 90%
rename from V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt
rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt
index deeb43c3..d9d12992 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt
@@ -5,7 +5,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List) :
- FragmentStateAdapter(fragmentActivity) {
+ FragmentStateAdapter(fragmentActivity) {
override fun createFragment(position: Int): Fragment {
return mFragments[position]
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt
new file mode 100644
index 00000000..146884fa
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt
@@ -0,0 +1,90 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.text.method.ScrollingMovementMethod
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityLogcatBinding
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import java.io.IOException
+import java.util.LinkedHashSet
+
+class LogcatActivity : BaseActivity() {
+ private lateinit var binding: ActivityLogcatBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityLogcatBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ title = getString(R.string.title_logcat)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ logcat(false)
+ }
+
+ private fun logcat(shouldFlushLog: Boolean) {
+
+ try {
+ binding.pbWaiting.visibility = View.VISIBLE
+
+ GlobalScope.launch(Dispatchers.Default) {
+ if (shouldFlushLog) {
+ val lst = LinkedHashSet()
+ lst.add("logcat")
+ lst.add("-c")
+ val process = Runtime.getRuntime().exec(lst.toTypedArray())
+ process.waitFor()
+ }
+ val lst = LinkedHashSet()
+ lst.add("logcat")
+ lst.add("-d")
+ lst.add("-v")
+ lst.add("time")
+ lst.add("-s")
+ lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
+ val process = Runtime.getRuntime().exec(lst.toTypedArray())
+// val bufferedReader = BufferedReader(
+// InputStreamReader(process.inputStream))
+// val allText = bufferedReader.use(BufferedReader::readText)
+ val allText = process.inputStream.bufferedReader().use { it.readText() }
+ launch(Dispatchers.Main) {
+ binding.tvLogcat.text = allText
+ binding.tvLogcat.movementMethod = ScrollingMovementMethod()
+ binding.pbWaiting.visibility = View.GONE
+ Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
+ }
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_logcat, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.copy_all -> {
+ Utils.setClipboard(this, binding.tvLogcat.text.toString())
+ toast(R.string.toast_success)
+ true
+ }
+ R.id.delete -> {
+ logcat(true)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt
new file mode 100644
index 00000000..09a6dff9
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt
@@ -0,0 +1,560 @@
+package com.v2ray.ang.ui
+
+import android.Manifest
+import android.content.Intent
+import android.net.Uri
+import android.net.VpnService
+import android.os.Bundle
+import com.google.android.material.navigation.NavigationView
+import androidx.core.view.GravityCompat
+import androidx.appcompat.app.ActionBarDrawerToggle
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.ItemTouchHelper
+import android.text.TextUtils
+import android.util.Log
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import com.tbruyelle.rxpermissions.RxPermissions
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.BuildConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityMainBinding
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
+import com.v2ray.ang.service.V2RayServiceManager
+import com.v2ray.ang.util.AngConfigManager
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import com.v2ray.ang.viewmodel.MainViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import libv2ray.Libv2ray
+import me.drakeet.support.toast.ToastCompat
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import java.util.concurrent.TimeUnit
+
+class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
+ private lateinit var binding: ActivityMainBinding
+
+ private val adapter by lazy { MainRecyclerAdapter(this) }
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
+ private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ startV2Ray()
+ }
+ }
+ private var mItemTouchHelper: ItemTouchHelper? = null
+ val mainViewModel: MainViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+ title = getString(R.string.title_server)
+ setSupportActionBar(binding.toolbar)
+
+ binding.fab.setOnClickListener {
+ if (mainViewModel.isRunning.value == true) {
+ Utils.stopVService(this)
+ } else if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
+ val intent = VpnService.prepare(this)
+ if (intent == null) {
+ startV2Ray()
+ } else {
+ requestVpnPermission.launch(intent)
+ }
+ } else {
+ startV2Ray()
+ }
+ }
+ binding.layoutTest.setOnClickListener {
+ if (mainViewModel.isRunning.value == true) {
+ binding.tvTestState.text = getString(R.string.connection_test_testing)
+ mainViewModel.testCurrentServerRealPing()
+ } else {
+// tv_test_state.text = getString(R.string.connection_test_fail)
+ }
+ }
+
+ binding.recyclerView.setHasFixedSize(true)
+ binding.recyclerView.layoutManager = LinearLayoutManager(this)
+ binding.recyclerView.adapter = adapter
+
+ val callback = SimpleItemTouchHelperCallback(adapter)
+ mItemTouchHelper = ItemTouchHelper(callback)
+ mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
+
+
+ val toggle = ActionBarDrawerToggle(
+ this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
+ binding.drawerLayout.addDrawerListener(toggle)
+ toggle.syncState()
+ binding.navView.setNavigationItemSelectedListener(this)
+ binding.version.text = "v${BuildConfig.VERSION_NAME} (${Libv2ray.checkVersionX()})"
+
+ setupViewModelObserver()
+ migrateLegacy()
+ }
+
+ private fun setupViewModelObserver() {
+ mainViewModel.updateListAction.observe(this) {
+ val index = it ?: return@observe
+ if (index >= 0) {
+ adapter.notifyItemChanged(index)
+ } else {
+ adapter.notifyDataSetChanged()
+ }
+ }
+ mainViewModel.updateTestResultAction.observe(this) { binding.tvTestState.text = it }
+ mainViewModel.isRunning.observe(this) {
+ val isRunning = it ?: return@observe
+ adapter.isRunning = isRunning
+ if (isRunning) {
+ binding.fab.setImageResource(R.drawable.ic_v)
+ binding.tvTestState.text = getString(R.string.connection_connected)
+ binding.layoutTest.isFocusable = true
+ } else {
+ binding.fab.setImageResource(R.drawable.ic_v_idle)
+ binding.tvTestState.text = getString(R.string.connection_not_connected)
+ binding.layoutTest.isFocusable = false
+ }
+ hideCircle()
+ }
+ mainViewModel.startListenBroadcast()
+ }
+
+ private fun migrateLegacy() {
+ GlobalScope.launch(Dispatchers.IO) {
+ val result = AngConfigManager.migrateLegacyConfig(this@MainActivity)
+ if (result != null) {
+ launch(Dispatchers.Main) {
+ if (result) {
+ toast(getString(R.string.migration_success))
+ mainViewModel.reloadServerList()
+ } else {
+ toast(getString(R.string.migration_fail))
+ }
+ }
+ }
+ }
+ }
+
+ fun startV2Ray() {
+ if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
+ return
+ }
+ showCircle()
+// toast(R.string.toast_services_start)
+ V2RayServiceManager.startV2Ray(this)
+ hideCircle()
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ mainViewModel.reloadServerList()
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.import_qrcode -> {
+ importQRcode(true)
+ true
+ }
+ R.id.import_clipboard -> {
+ importClipboard()
+ true
+ }
+ R.id.import_manually_vmess -> {
+ startActivity(Intent().putExtra("createConfigType", EConfigType.VMESS.value).
+ setClass(this, ServerActivity::class.java))
+ true
+ }
+ R.id.import_manually_ss -> {
+ startActivity(Intent().putExtra("createConfigType", EConfigType.SHADOWSOCKS.value).
+ setClass(this, ServerActivity::class.java))
+ true
+ }
+ R.id.import_manually_socks -> {
+ startActivity(Intent().putExtra("createConfigType", EConfigType.SOCKS.value).
+ setClass(this, ServerActivity::class.java))
+ true
+ }
+ R.id.import_config_custom_clipboard -> {
+ importConfigCustomClipboard()
+ true
+ }
+ R.id.import_config_custom_local -> {
+ importConfigCustomLocal()
+ true
+ }
+ R.id.import_config_custom_url -> {
+ importConfigCustomUrlClipboard()
+ true
+ }
+ R.id.import_config_custom_url_scan -> {
+ importQRcode(false)
+ true
+ }
+
+// R.id.sub_setting -> {
+// startActivity()
+// true
+// }
+
+ R.id.sub_update -> {
+ importConfigViaSub()
+ true
+ }
+
+ R.id.export_all -> {
+ if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) {
+ toast(R.string.toast_success)
+ } else {
+ toast(R.string.toast_failure)
+ }
+ true
+ }
+
+ R.id.ping_all -> {
+ mainViewModel.testAllTcping()
+ true
+ }
+
+// R.id.settings -> {
+// startActivity("isRunning" to isRunning)
+// true
+// }
+// R.id.logcat -> {
+// startActivity()
+// true
+// }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+
+ /**
+ * import config from qrcode
+ */
+ fun importQRcode(forConfig: Boolean): Boolean {
+// try {
+// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
+// .addCategory(Intent.CATEGORY_DEFAULT)
+// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
+// } catch (e: Exception) {
+ RxPermissions(this)
+ .request(Manifest.permission.CAMERA)
+ .subscribe {
+ if (it)
+ if (forConfig)
+ scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
+ else
+ scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
+ else
+ toast(R.string.toast_permission_denied)
+ }
+// }
+ 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
+ */
+ fun importClipboard()
+ : Boolean {
+ try {
+ val clipboard = Utils.getClipboard(this)
+ importBatchConfig(clipboard)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ fun importBatchConfig(server: String?, subid: String = "") {
+ var count = AngConfigManager.importBatchConfig(server, subid)
+ if (count <= 0) {
+ count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid)
+ }
+ if (count > 0) {
+ toast(R.string.toast_success)
+ mainViewModel.reloadServerList()
+ } else {
+ toast(R.string.toast_failure)
+ }
+ }
+
+ fun importConfigCustomClipboard()
+ : Boolean {
+ try {
+ val configText = Utils.getClipboard(this)
+ if (TextUtils.isEmpty(configText)) {
+ toast(R.string.toast_none_data_clipboard)
+ return false
+ }
+ importCustomizeConfig(configText)
+ return true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ }
+
+ /**
+ * import config from local config file
+ */
+ fun importConfigCustomLocal(): Boolean {
+ try {
+ showFileChooser()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ fun importConfigCustomUrlClipboard()
+ : Boolean {
+ try {
+ val url = Utils.getClipboard(this)
+ if (TextUtils.isEmpty(url)) {
+ toast(R.string.toast_none_data_clipboard)
+ return false
+ }
+ return importConfigCustomUrl(url)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ }
+
+ /**
+ * import config from url
+ */
+ fun importConfigCustomUrl(url: String?): Boolean {
+ try {
+ if (!Utils.isValidUrl(url)) {
+ toast(R.string.toast_invalid_url)
+ return false
+ }
+ GlobalScope.launch(Dispatchers.IO) {
+ val configText = try {
+ Utils.getUrlContentWithCustomUserAgent(url)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ""
+ }
+ launch(Dispatchers.Main) {
+ importCustomizeConfig(configText)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ /**
+ * import config from sub
+ */
+ fun importConfigViaSub()
+ : Boolean {
+ try {
+ toast(R.string.title_sub_update)
+ MmkvManager.decodeSubscriptions().forEach {
+ if (TextUtils.isEmpty(it.first)
+ || TextUtils.isEmpty(it.second.remarks)
+ || TextUtils.isEmpty(it.second.url)
+ ) {
+ return@forEach
+ }
+ val url = it.second.url
+ if (!Utils.isValidUrl(url)) {
+ return@forEach
+ }
+ Log.d(ANG_PACKAGE, url)
+ GlobalScope.launch(Dispatchers.IO) {
+ val configText = try {
+ Utils.getUrlContentWithCustomUserAgent(url)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ launch(Dispatchers.Main) {
+ toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
+ }
+ return@launch
+ }
+ launch(Dispatchers.Main) {
+ importBatchConfig(Utils.decode(configText), it.first)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return true
+ }
+
+ /**
+ * show file chooser
+ */
+ private fun showFileChooser() {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.type = "*/*"
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+
+ try {
+ chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
+ } catch (ex: android.content.ActivityNotFoundException) {
+ toast(R.string.toast_require_file_manager)
+ }
+ }
+
+ private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val uri = it.data?.data
+ if (it.resultCode == RESULT_OK && uri != null) {
+ readContentFromUri(uri)
+ }
+ }
+
+ /**
+ * read content from uri
+ */
+ private fun readContentFromUri(uri: Uri) {
+ RxPermissions(this)
+ .request(Manifest.permission.READ_EXTERNAL_STORAGE)
+ .subscribe {
+ if (it) {
+ try {
+ contentResolver.openInputStream(uri).use { input ->
+ importCustomizeConfig(input?.bufferedReader()?.readText())
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else
+ toast(R.string.toast_permission_denied)
+ }
+ }
+
+ /**
+ * import customize config
+ */
+ fun importCustomizeConfig(server: String?) {
+ try {
+ if (server == null || TextUtils.isEmpty(server)) {
+ toast(R.string.toast_none_data)
+ return
+ }
+ mainViewModel.appendCustomConfigServer(server)
+ toast(R.string.toast_success)
+ adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
+ } catch (e: Exception) {
+ ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
+ e.printStackTrace()
+ return
+ }
+ }
+
+// val mConnection = object : ServiceConnection {
+// override fun onServiceDisconnected(name: ComponentName?) {
+// }
+//
+// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "")
+// }
+// }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ moveTaskToBack(false)
+ return true
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ fun showCircle() {
+ binding.fabProgressCircle.show()
+ }
+
+ fun hideCircle() {
+ try {
+ Observable.timer(300, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ if (binding.fabProgressCircle.isShown) {
+ binding.fabProgressCircle.hide()
+ }
+ }
+ } catch (e: Exception) {
+ }
+ }
+
+ override fun onBackPressed() {
+ if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START)
+ } else {
+ super.onBackPressed()
+ }
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ // Handle navigation view item clicks here.
+ when (item.itemId) {
+ //R.id.server_profile -> activityClass = MainActivity::class.java
+ R.id.sub_setting -> {
+ startActivity(Intent(this, SubSettingActivity::class.java))
+ }
+ R.id.settings -> {
+ startActivity(Intent(this, SettingsActivity::class.java)
+ .putExtra("isRunning", mainViewModel.isRunning.value == true))
+ }
+ R.id.feedback -> {
+ Utils.openUri(this, AppConfig.v2rayNGIssues)
+ }
+ R.id.promotion -> {
+ Utils.openUri(this, AppConfig.promotionUrl)
+ }
+ R.id.donate -> {
+// startActivity()
+ }
+ R.id.logcat -> {
+ startActivity(Intent(this, LogcatActivity::class.java))
+ }
+ }
+ binding.drawerLayout.closeDrawer(GravityCompat.START)
+ return true
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt
new file mode 100644
index 00000000..92e5c681
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt
@@ -0,0 +1,231 @@
+package com.v2ray.ang.ui
+
+import android.content.Intent
+import android.graphics.Color
+import androidx.core.content.ContextCompat
+import androidx.appcompat.app.AlertDialog
+import androidx.recyclerview.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ItemQrcodeBinding
+import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
+import com.v2ray.ang.databinding.ItemRecyclerMainBinding
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.dto.SubscriptionItem
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.helper.ItemTouchHelperAdapter
+import com.v2ray.ang.helper.ItemTouchHelperViewHolder
+import com.v2ray.ang.service.V2RayServiceManager
+import com.v2ray.ang.util.AngConfigManager
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import java.util.concurrent.TimeUnit
+
+class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter()
+ , ItemTouchHelperAdapter {
+ companion object {
+ private const val VIEW_TYPE_ITEM = 1
+ private const val VIEW_TYPE_FOOTER = 2
+ }
+
+ private var mActivity: MainActivity = activity
+ private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
+ private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
+ private val share_method: Array by lazy {
+ mActivity.resources.getStringArray(R.array.share_method)
+ }
+ var isRunning = false
+
+ override fun getItemCount() = mActivity.mainViewModel.serverList.size + 1
+
+ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+ if (holder is MainViewHolder) {
+ val guid = mActivity.mainViewModel.serverList.getOrNull(position) ?: return
+ val config = mActivity.mainViewModel.serversCache.getOrElse(guid) { MmkvManager.decodeServerConfig(guid) } ?: return
+ val outbound = config.getProxyOutbound()
+ val aff = MmkvManager.decodeServerAffiliationInfo(guid)
+
+ holder.itemMainBinding.tvName.text = config.remarks
+ holder.itemMainBinding.btnRadio.isChecked = guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
+ holder.itemView.setBackgroundColor(Color.TRANSPARENT)
+ holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: ""
+ if (aff?.testDelayMillis?:0L < 0L) {
+ holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, android.R.color.holo_red_dark))
+ } else {
+ holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
+ }
+ holder.itemMainBinding.tvSubscription.text = ""
+ val json = subStorage?.decodeString(config.subscriptionId)
+ if (!json.isNullOrBlank()) {
+ val sub = Gson().fromJson(json, SubscriptionItem::class.java)
+ holder.itemMainBinding.tvSubscription.text = sub.remarks
+ }
+
+ var shareOptions = share_method.asList()
+ when (config.configType) {
+ EConfigType.CUSTOM -> {
+ holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
+ shareOptions = shareOptions.takeLast(1)
+ }
+ EConfigType.VLESS -> {
+ holder.itemMainBinding.tvType.text = config.configType.name
+ }
+ else -> {
+ holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
+ }
+ }
+ holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
+
+ holder.itemMainBinding.layoutShare.setOnClickListener {
+ AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
+ try {
+ when (i) {
+ 0 -> {
+ if (config.configType == EConfigType.CUSTOM) {
+ shareFullContent(guid)
+ } else {
+ val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
+ ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
+ AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
+ }
+ }
+ 1 -> {
+ if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
+ mActivity.toast(R.string.toast_success)
+ } else {
+ mActivity.toast(R.string.toast_failure)
+ }
+ }
+ 2 -> shareFullContent(guid)
+ else -> mActivity.toast("else")
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }.show()
+ }
+
+ holder.itemMainBinding.layoutEdit.setOnClickListener {
+ val intent = Intent().putExtra("guid", guid)
+ .putExtra("isRunning", isRunning)
+ if (config.configType == EConfigType.CUSTOM) {
+ mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
+ } else {
+ mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
+ }
+ }
+ holder.itemMainBinding.layoutRemove.setOnClickListener {
+ if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
+ mActivity.mainViewModel.removeServer(guid)
+ notifyItemRemoved(position)
+ notifyItemRangeChanged(position, mActivity.mainViewModel.serverList.size)
+ }
+ }
+
+ holder.itemMainBinding.infoContainer.setOnClickListener {
+ val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
+ if (guid != selected) {
+ mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
+ notifyItemChanged(mActivity.mainViewModel.serverList.indexOf(selected))
+ notifyItemChanged(mActivity.mainViewModel.serverList.indexOf(guid))
+ if (isRunning) {
+ mActivity.showCircle()
+ Utils.stopVService(mActivity)
+ Observable.timer(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ V2RayServiceManager.startV2Ray(mActivity)
+ mActivity.hideCircle()
+ }
+ }
+ }
+ }
+ }
+ if (holder is FooterViewHolder) {
+ //if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
+ if (true) {
+ holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
+ } else {
+ holder.itemFooterBinding.layoutEdit.setOnClickListener {
+ Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
+ }
+ }
+ }
+ }
+
+ private fun shareFullContent(guid: String) {
+ if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
+ mActivity.toast(R.string.toast_success)
+ } else {
+ mActivity.toast(R.string.toast_failure)
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
+ return when (viewType) {
+ VIEW_TYPE_ITEM ->
+ MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ else ->
+ FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == mActivity.mainViewModel.serverList.size) {
+ VIEW_TYPE_FOOTER
+ } else {
+ VIEW_TYPE_ITEM
+ }
+ }
+
+ open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ fun onItemSelected() {
+ itemView.setBackgroundColor(Color.LTGRAY)
+ }
+
+ fun onItemClear() {
+ itemView.setBackgroundColor(0)
+ }
+ }
+
+ class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
+ BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
+
+ class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
+ BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
+
+ override fun onItemDismiss(position: Int) {
+ val guid = mActivity.mainViewModel.serverList.getOrNull(position) ?: return
+ if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
+// mActivity.alert(R.string.del_config_comfirm) {
+// positiveButton(android.R.string.ok) {
+ mActivity.mainViewModel.removeServer(guid)
+ notifyItemRemoved(position)
+// }
+// show()
+// }
+ }
+ }
+
+ override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
+ mActivity.mainViewModel.swapServer(fromPosition, toPosition)
+ notifyItemMoved(fromPosition, toPosition)
+ // position is changed, since position is used by click callbacks, need to update range
+ if (toPosition > fromPosition)
+ notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1)
+ else
+ notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1)
+ return true
+ }
+
+ override fun onItemMoveCompleted() {
+ // do nothing
+ }
+}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt
new file mode 100644
index 00000000..003e06dd
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt
@@ -0,0 +1,286 @@
+package com.v2ray.ang.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.content.Context
+import android.os.Bundle
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import android.text.TextUtils
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import com.v2ray.ang.R
+import com.v2ray.ang.util.AppManagerUtil
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import java.text.Collator
+import java.util.*
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.AppConfig.ANG_PACKAGE
+import com.v2ray.ang.databinding.ActivityBypassListBinding
+import com.v2ray.ang.dto.AppInfo
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.extension.v2RayApplication
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import java.net.URL
+
+class PerAppProxyActivity : BaseActivity() {
+ private lateinit var binding: ActivityBypassListBinding
+
+ private var adapter: PerAppProxyAdapter? = null
+ private var appsAll: List? = null
+ private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityBypassListBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
+ binding.recyclerView.addItemDecoration(dividerItemDecoration)
+
+ val blacklist = defaultSharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, null)
+
+ AppManagerUtil.rxLoadNetworkAppList(this)
+ .subscribeOn(Schedulers.io())
+ .map {
+ if (blacklist != null) {
+ it.forEach { one ->
+ if ((blacklist.contains(one.packageName))) {
+ one.isSelected = 1
+ } else {
+ one.isSelected = 0
+ }
+ }
+ val comparator = Comparator { p1, p2 ->
+ when {
+ p1.isSelected > p2.isSelected -> -1
+ p1.isSelected == p2.isSelected -> 0
+ else -> 1
+ }
+ }
+ it.sortedWith(comparator)
+ } else {
+ val comparator = object : Comparator