()
-
- 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/AssetsUtil.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java
new file mode 100644
index 00000000..38f17d9c
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java
@@ -0,0 +1,121 @@
+package com.v2ray.ang.util;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+
+public class AssetsUtil {
+ public static boolean copyAssetFolder(AssetManager assetManager,
+ String fromAssetPath, String toPath) {
+ try {
+ String[] files = assetManager.list(fromAssetPath);
+ new File(toPath).mkdirs();
+ boolean res = true;
+ for (String file : files)
+ if (file.contains("."))
+ res &= copyAsset(assetManager,
+ fromAssetPath + "/" + file,
+ toPath + "/" + file);
+ else
+ res &= copyAssetFolder(assetManager,
+ fromAssetPath + "/" + file,
+ toPath + "/" + file);
+ return res;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public static boolean copyAsset(AssetManager assetManager,
+ String fromAssetPath, String toPath) {
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ in = assetManager.open(fromAssetPath);
+ new File(toPath).createNewFile();
+ out = new FileOutputStream(toPath);
+ copyFile(in, out);
+ in.close();
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ }
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static String readTextFromAssets(AssetManager assetManager, String fileName) {
+ try {
+ InputStreamReader inputReader = new InputStreamReader(assetManager.open(fileName));
+ BufferedReader bufReader = new BufferedReader(inputReader);
+ String line;
+ String Result = "";
+ while ((line = bufReader.readLine()) != null)
+ Result += line;
+ return Result;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ public static String getAssetPath(Context context, String assetPath) {
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ context.deleteFile(assetPath);
+
+ in = context.getAssets().open(assetPath);
+ out = context.openFileOutput(assetPath, MODE_PRIVATE);
+ copyFile(in, out);
+ in.close();
+
+ String path = context.getFilesDir().toString();
+ return path + "/" + assetPath;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "";
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ }
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private static void copyFile(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java
new file mode 100644
index 00000000..eb69a062
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java
@@ -0,0 +1,570 @@
+// Portions copyright 2002, Google, Inc.
+//
+// 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.util;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * http://iharder.net/xmlizable
+ * periodically to check for updates or to contribute improvements.
+ *
+ *
+ * @author Robert Harder
+ * @author rharder@usa.net
+ * @version 1.3
+ */
+
+/**
+ * Base64 converter class. This code is not a complete MIME encoder;
+ * it simply converts binary data to base64 data and back.
+ *
+ * Note {@link CharBase64} is a GWT-compatible implementation of this
+ * class.
+ */
+public class Base64 {
+ /** Specify encoding (value is {@code true}). */
+ public final static boolean ENCODE = true;
+
+ /** Specify decoding (value is {@code false}). */
+ public final static boolean DECODE = false;
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte) '\n';
+
+ /**
+ * The 64 valid Base64 values.
+ */
+ private final static byte[] ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '+', (byte) '/'};
+
+ /**
+ * The 64 valid web safe Base64 values.
+ */
+ private final static byte[] WEBSAFE_ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '-', (byte) '_'};
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ **/
+ private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9, -9, -9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ /** The web safe decodabet */
+ private final static byte[] WEBSAFE_DECODABET =
+ {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+ 62, // Dash '-' sign at decimal 45
+ -9, -9, // Decimal 46-47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, // Decimal 91-94
+ 63, // Underscore '_' at decimal 95
+ -9, // Decimal 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ // Indicates white space in encoding
+ private final static byte WHITE_SPACE_ENC = -5;
+ // Indicates equals sign in encoding
+ private final static byte EQUALS_SIGN_ENC = -1;
+
+ /** Defeats instantiation. */
+ private Base64() {
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to three bytes of the array source
+ * and writes the resulting four Base64 bytes to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param alphabet is the encoding alphabet
+ * @return the destination array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset,
+ int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index alphabet
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff =
+ (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+ return destination;
+ case 2:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ case 1:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Equivalent to calling
+ * {@code encodeBytes(source, 0, source.length)}
+ *
+ * @param source The data to convert
+ * @since 1.4
+ */
+ public static String encode(byte[] source) {
+ return encode(source, 0, source.length, ALPHABET, true);
+ }
+
+ /**
+ * Encodes a byte array into web safe Base64 notation.
+ *
+ * @param source The data to convert
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ */
+ public static String encodeWebSafe(byte[] source, boolean doPadding) {
+ return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet the encoding alphabet
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ * @since 1.4
+ */
+ public static String encode(byte[] source, int off, int len, byte[] alphabet,
+ boolean doPadding) {
+ byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+ int outLen = outBuff.length;
+
+ // If doPadding is false, set length to truncate '='
+ // padding characters
+ while (doPadding == false && outLen > 0) {
+ if (outBuff[outLen - 1] != '=') {
+ break;
+ }
+ outLen -= 1;
+ }
+
+ return new String(outBuff, 0, outLen);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source the data to convert
+ * @param off offset in array where conversion should begin
+ * @param len length of data to convert
+ * @param alphabet is the encoding alphabet
+ * @param maxLineLength maximum length of one line.
+ * @return the BASE64-encoded byte array
+ */
+ public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
+ int maxLineLength) {
+ int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+ int len43 = lenDiv3 * 4;
+ byte[] outBuff = new byte[len43 // Main 4:3
+ + (len43 / maxLineLength)]; // New lines
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d += 3, e += 4) {
+
+ // The following block of code is the same as
+ // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+ // but inlined for faster encoding (~20% improvement)
+ int inBuff =
+ ((source[d + off] << 24) >>> 8)
+ | ((source[d + 1 + off] << 24) >>> 16)
+ | ((source[d + 2 + off] << 24) >>> 24);
+ outBuff[e] = alphabet[(inBuff >>> 18)];
+ outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // end for: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ // Add a last newline
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ }
+ e += 4;
+ }
+
+ assert (e == outBuff.length);
+ return outBuff;
+ }
+
+
+ /* ******** D E C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Decodes four bytes from array source
+ * and writes the resulting bytes (up to three of them)
+ * to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ *
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3(byte[] source, int srcOffset,
+ byte[] destination, int destOffset, byte[] decodabet) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Example: DkL=
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ } else {
+ // Example: DkLE
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
+ | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+ return 3;
+ }
+ } // end decodeToBytes
+
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decode(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes data from web safe Base64 notation.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decodeWebSafe(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source The Base64 encoded data
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source) throws Base64DecoderException {
+ return decode(source, 0, source.length);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded data.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source)
+ throws Base64DecoderException {
+ return decodeWebSafe(source, 0, source.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, DECODABET);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded byte array.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @return decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, WEBSAFE_DECODABET);
+ }
+
+ /**
+ * Decodes Base64 content using the supplied decodabet and returns
+ * the decoded byte array.
+ *
+ * @param source the Base64 encoded data
+ * @param off the offset of where to begin decoding
+ * @param len the length of characters to decode
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return decoded data
+ */
+ public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
+ throws Base64DecoderException {
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for (i = 0; i < len; i++) {
+ sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
+ sbiDecode = decodabet[sbiCrop];
+
+ if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ // An equals sign (for padding) must not occur at position 0 or 1
+ // and must be the last byte[s] in the encoded value
+ if (sbiCrop == EQUALS_SIGN) {
+ int bytesLeft = len - i;
+ byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+ if (b4Posn == 0 || b4Posn == 1) {
+ throw new Base64DecoderException(
+ "invalid padding byte '=' at byte offset " + i);
+ } else if ((b4Posn == 3 && bytesLeft > 2)
+ || (b4Posn == 4 && bytesLeft > 1)) {
+ throw new Base64DecoderException(
+ "padding byte '=' falsely signals end of encoded value "
+ + "at offset " + i);
+ } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+ throw new Base64DecoderException(
+ "encoded value has invalid trailing byte");
+ }
+ break;
+ }
+
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn == 4) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ b4Posn = 0;
+ }
+ }
+ } else {
+ throw new Base64DecoderException("Bad Base64 input character at " + i
+ + ": " + source[i + off] + "(decimal)");
+ }
+ }
+
+ // Because web safe encoding allows non padding base64 encodes, we
+ // need to pad the rest of the b4 buffer with equal signs when
+ // b4Posn != 0. There can be at most 2 equal signs at the end of
+ // four characters, so the b4 buffer must have two or three
+ // characters. This also catches the case where the input is
+ // padded with EQUALS_SIGN
+ if (b4Posn != 0) {
+ if (b4Posn == 1) {
+ throw new Base64DecoderException("single trailing character at offset "
+ + (len - 1));
+ }
+ b4[b4Posn++] = EQUALS_SIGN;
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ }
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java
new file mode 100644
index 00000000..b113e43f
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java
@@ -0,0 +1,32 @@
+// Copyright 2002, Google, Inc.
+//
+// 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.util;
+
+/**
+ * Exception thrown when encountering an invalid Base64 input character.
+ *
+ * @author nelson
+ */
+public class Base64DecoderException extends Exception {
+ public Base64DecoderException() {
+ super();
+ }
+
+ public Base64DecoderException(String s) {
+ super(s);
+ }
+
+ private static final long serialVersionUID = 1L;
+}
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/IabException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java
new file mode 100644
index 00000000..e6320808
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java
@@ -0,0 +1,43 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * 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.util;
+
+/**
+ * Exception thrown when something went wrong with in-app billing.
+ * An IabException has an associated IabResult (an error).
+ * To get the IAB result that caused this exception to be thrown,
+ * call {@link #getResult()}.
+ */
+public class IabException extends Exception {
+ IabResult mResult;
+
+ public IabException(IabResult r) {
+ this(r, null);
+ }
+ public IabException(int response, String message) {
+ this(new IabResult(response, message));
+ }
+ public IabException(IabResult r, Exception cause) {
+ super(r.getMessage(), cause);
+ mResult = r;
+ }
+ public IabException(int response, String message, Exception cause) {
+ this(new IabResult(response, message), cause);
+ }
+
+ /** Returns the IAB result (error) that this exception signals. */
+ public IabResult getResult() { return mResult; }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java
new file mode 100644
index 00000000..911d20da
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java
@@ -0,0 +1,979 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * 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.util;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vending.billing.IInAppBillingService;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides convenience methods for in-app billing. You can create one instance of this
+ * class for your application and use it to process in-app billing operations.
+ * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
+ * many common in-app billing operations, as well as automatic signature
+ * verification.
+ *
+ * After instantiating, you must perform setup in order to start using the object.
+ * To perform setup, call the {@link #startSetup} method and provide a listener;
+ * that listener will be notified when setup is complete, after which (and not before)
+ * you may call other methods.
+ *
+ * After setup is complete, you will typically want to request an inventory of owned
+ * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
+ * and related methods.
+ *
+ * When you are done with this object, don't forget to call {@link #dispose}
+ * to ensure proper cleanup. This object holds a binding to the in-app billing
+ * service, which will leak unless you dispose of it correctly. If you created
+ * the object on an Activity's onCreate method, then the recommended
+ * place to dispose of it is the Activity's onDestroy method.
+ *
+ * A note about threading: When using this object from a background thread, you may
+ * call the blocking versions of methods; when using from a UI thread, call
+ * only the asynchronous versions and handle the results via callbacks.
+ * Also, notice that you can only call one asynchronous operation at a time;
+ * attempting to start a second asynchronous operation while the first one
+ * has not yet completed will result in an exception being thrown.
+ *
+ * @author Bruno Oliveira (Google)
+ */
+public class IabHelper {
+ // Is debug logging enabled?
+ boolean mDebugLog = false;
+ String mDebugTag = "IabHelper";
+
+ // Is setup done?
+ boolean mSetupDone = false;
+
+ // Has this object been disposed of? (If so, we should ignore callbacks, etc)
+ boolean mDisposed = false;
+
+ // Are subscriptions supported?
+ boolean mSubscriptionsSupported = false;
+
+ // Is an asynchronous operation in progress?
+ // (only one at a time can be in progress)
+ boolean mAsyncInProgress = false;
+
+ // (for logging/debugging)
+ // if mAsyncInProgress == true, what asynchronous operation is in progress?
+ String mAsyncOperation = "";
+
+ // Context we were passed during initialization
+ Context mContext;
+
+ // Connection to the service
+ IInAppBillingService mService;
+ ServiceConnection mServiceConn;
+
+ // The request code used to launch purchase flow
+ int mRequestCode;
+
+ // The item type of the current purchase flow
+ String mPurchasingItemType;
+
+ // Public key for verifying signature, in base64 encoding
+ String mSignatureBase64 = null;
+
+ // Billing response codes
+ public static final int BILLING_RESPONSE_RESULT_OK = 0;
+ public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
+ public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
+ public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
+ public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
+
+ // IAB Helper error codes
+ public static final int IABHELPER_ERROR_BASE = -1000;
+ public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
+ public static final int IABHELPER_BAD_RESPONSE = -1002;
+ public static final int IABHELPER_VERIFICATION_FAILED = -1003;
+ public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
+ public static final int IABHELPER_USER_CANCELLED = -1005;
+ public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
+ public static final int IABHELPER_MISSING_TOKEN = -1007;
+ public static final int IABHELPER_UNKNOWN_ERROR = -1008;
+ public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
+ public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
+
+ // Keys for the responses from InAppBillingService
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
+ public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
+ public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
+ public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
+ public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
+ public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+
+ // Item types
+ public static final String ITEM_TYPE_INAPP = "inapp";
+ public static final String ITEM_TYPE_SUBS = "subs";
+
+ // some fields on the getSkuDetails response bundle
+ public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
+ public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
+
+ /**
+ * Creates an instance. After creation, it will not yet be ready to use. You must perform
+ * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
+ * block and is safe to call from a UI thread.
+ *
+ * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
+ * @param base64PublicKey Your application's public key, encoded in base64.
+ * This is used for verification of purchase signatures. You can find your app's base64-encoded
+ * public key in your application's page on Google Play Developer Console. Note that this
+ * is NOT your "developer public key".
+ */
+ public IabHelper(Context ctx, String base64PublicKey) {
+ mContext = ctx.getApplicationContext();
+ mSignatureBase64 = base64PublicKey;
+ logDebug("IAB helper created.");
+ }
+
+ /**
+ * Enables or disable debug logging through LogCat.
+ */
+ public void enableDebugLogging(boolean enable, String tag) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ mDebugTag = tag;
+ }
+
+ public void enableDebugLogging(boolean enable) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ }
+
+ /**
+ * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
+ * when the setup process is complete.
+ */
+ public interface OnIabSetupFinishedListener {
+ /**
+ * Called to notify that setup is complete.
+ *
+ * @param result The result of the setup process.
+ */
+ void onIabSetupFinished(IabResult result);
+ }
+
+ /**
+ * Starts the setup process. This will start up the setup process asynchronously.
+ * You will be notified through the listener when the setup process is complete.
+ * This method is safe to call from a UI thread.
+ *
+ * @param listener The listener to notify when the setup process is complete.
+ */
+ public void startSetup(final OnIabSetupFinishedListener listener) {
+ // If already set up, can't do it again.
+ checkNotDisposed();
+ if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
+
+ // Connection to IAB service
+ logDebug("Starting in-app billing setup.");
+ mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ logDebug("Billing service disconnected.");
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (mDisposed) return;
+ logDebug("Billing service connected.");
+ mService = IInAppBillingService.Stub.asInterface(service);
+ String packageName = mContext.getPackageName();
+ try {
+ logDebug("Checking for in-app billing 3 support.");
+
+ // check for in-app billing v3 support
+ int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ if (listener != null) listener.onIabSetupFinished(new IabResult(response,
+ "Error checking for billing v3 support."));
+
+ // if in-app purchases aren't supported, neither are subscriptions.
+ mSubscriptionsSupported = false;
+ return;
+ }
+ logDebug("In-app billing version 3 supported for " + packageName);
+
+ // check for v3 subscriptions support
+ response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Subscriptions AVAILABLE.");
+ mSubscriptionsSupported = true;
+ } else {
+ logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
+ }
+
+ mSetupDone = true;
+ } catch (RemoteException e) {
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
+ "RemoteException while setting up in-app billing."));
+ }
+ e.printStackTrace();
+ return;
+ }
+
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
+ }
+ }
+ };
+
+// Intent serviceIntent = new Intent("ir.cafebazaar.pardakht.InAppBillingService.BIND");
+// serviceIntent.setPackage("com.farsitel.bazaar");
+ Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
+ serviceIntent.setPackage("com.android.vending");
+ if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
+ // service available to handle that Intent
+ mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ } else {
+ // no service available to handle that Intent
+ if (listener != null) {
+ listener.onIabSetupFinished(
+ new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
+ "Billing service unavailable on device."));
+ }
+ }
+ }
+
+ /**
+ * Dispose of object, releasing resources. It's very important to call this
+ * method when you are done with this object. It will release any resources
+ * used by it such as service connections. Naturally, once the object is
+ * disposed of, it can't be used again.
+ */
+ public void dispose() {
+ logDebug("Disposing.");
+ mSetupDone = false;
+ if (mServiceConn != null) {
+ logDebug("Unbinding from service.");
+ if (mContext != null) mContext.unbindService(mServiceConn);
+ }
+ mDisposed = true;
+ mContext = null;
+ mServiceConn = null;
+ mService = null;
+ mPurchaseListener = null;
+ }
+
+ private void checkNotDisposed() {
+ if (mDisposed)
+ throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
+ }
+
+ /**
+ * Returns whether subscriptions are supported.
+ */
+ public boolean subscriptionsSupported() {
+ checkNotDisposed();
+ return mSubscriptionsSupported;
+ }
+
+
+ /**
+ * Callback that notifies when a purchase is finished.
+ */
+ public interface OnIabPurchaseFinishedListener {
+ /**
+ * Called to notify that an in-app purchase finished. If the purchase was successful,
+ * then the sku parameter specifies which item was purchased. If the purchase failed,
+ * the sku and extraData parameters may or may not be null, depending on how far the purchase
+ * process went.
+ *
+ * @param result The result of the purchase.
+ * @param info The purchase information (null if purchase failed)
+ */
+ void onIabPurchaseFinished(IabResult result, Purchase info);
+ }
+
+ // The listener registered on launchPurchaseFlow, which we have to call back when
+ // the purchase finishes
+ OnIabPurchaseFinishedListener mPurchaseListener;
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
+ launchPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener) {
+ launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
+ }
+
+ /**
+ * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
+ * which will involve bringing up the Google Play screen. The calling activity will be paused while
+ * the user interacts with Google Play, and the result will be delivered via the activity's
+ * {@link android.app.Activity#onActivityResult} method, at which point you must call
+ * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param act The calling activity.
+ * @param sku The sku of the item to purchase.
+ * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
+ * @param requestCode A request code (to differentiate from other responses --
+ * as in {@link android.app.Activity#startActivityForResult}).
+ * @param listener The listener to notify when the purchase process finishes
+ * @param extraData Extra data (developer payload), which will be returned with the purchase data
+ * when the purchase completes. This extra data will be permanently bound to that purchase
+ * and will always be returned when the purchase is queried.
+ */
+ public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData) {
+ checkNotDisposed();
+ checkSetupDone("launchPurchaseFlow");
+ flagStartAsync("launchPurchaseFlow");
+ IabResult result;
+
+ if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
+ IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
+ "Subscriptions are not available.");
+ flagEndAsync();
+ if (listener != null) listener.onIabPurchaseFinished(r, null);
+ return;
+ }
+
+ try {
+ logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
+ Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
+ int response = getResponseCodeFromBundle(buyIntentBundle);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logError("Unable to buy item, Error response: " + getResponseDesc(response));
+ flagEndAsync();
+ result = new IabResult(response, "Unable to buy item");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ return;
+ }
+
+ PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
+ logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
+ mRequestCode = requestCode;
+ mPurchaseListener = listener;
+ mPurchasingItemType = itemType;
+ act.startIntentSenderForResult(pendingIntent.getIntentSender(),
+ requestCode, new Intent(),
+ Integer.valueOf(0), Integer.valueOf(0),
+ Integer.valueOf(0));
+ } catch (SendIntentException e) {
+ logError("SendIntentException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ } catch (RemoteException e) {
+ logError("RemoteException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ }
+
+ /**
+ * Handles an activity result that's part of the purchase flow in in-app billing. If you
+ * are calling {@link #launchPurchaseFlow}, then you must call this method from your
+ * Activity's {@link android.app.Activity@onActivityResult} method. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param requestCode The requestCode as you received it.
+ * @param resultCode The resultCode as you received it.
+ * @param data The data (Intent) as you received it.
+ * @return Returns true if the result was related to a purchase flow and was handled;
+ * false if the result was not related to a purchase, in which case you should
+ * handle it normally.
+ */
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ IabResult result;
+ if (requestCode != mRequestCode) return false;
+
+ checkNotDisposed();
+ checkSetupDone("handleActivityResult");
+
+ // end of async purchase operation that started on launchPurchaseFlow
+ flagEndAsync();
+
+ if (data == null) {
+ logError("Null data in IAB activity result.");
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ int responseCode = getResponseCodeFromIntent(data);
+ String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
+ String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
+
+ if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successful resultcode from purchase activity.");
+ logDebug("Purchase data: " + purchaseData);
+ logDebug("Data signature: " + dataSignature);
+ logDebug("Extras: " + data.getExtras());
+ logDebug("Expected item type: " + mPurchasingItemType);
+
+ if (purchaseData == null || dataSignature == null) {
+ logError("BUG: either purchaseData or dataSignature is null.");
+ logDebug("Extras: " + data.getExtras().toString());
+ result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ Purchase purchase = null;
+ try {
+ purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
+ String sku = purchase.getSku();
+
+ // Verify signature
+ if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
+ logError("Purchase signature verification FAILED for sku " + sku);
+ result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, purchase);
+ return true;
+ }
+ logDebug("Purchase signature successfully verified.");
+ } catch (JSONException e) {
+ logError("Failed to parse purchase data.");
+ e.printStackTrace();
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
+ if (mPurchaseListener != null)
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ if (mPurchaseListener != null) {
+ mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
+ }
+ } else if (resultCode == Activity.RESULT_OK) {
+ // result code was OK, but in-app billing response was not OK.
+ logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
+ if (mPurchaseListener != null) {
+ result = new IabResult(responseCode, "Problem purchashing item.");
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ } else {
+ logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ + ". Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ return true;
+ }
+
+ public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException {
+ return queryInventory(querySkuDetails, moreSkus, null);
+ }
+
+ /**
+ * Queries the inventory. This will query all owned items from the server, as well as
+ * information on additional skus, if specified. This method may block or take long to execute.
+ * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
+ *
+ * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
+ * as purchase information.
+ * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @throws IabException if a problem occurs while refreshing the inventory.
+ */
+ public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
+ List moreSubsSkus) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ try {
+ Inventory inv = new Inventory();
+ int r = queryPurchases(inv, ITEM_TYPE_INAPP);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned items).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of items).");
+ }
+ }
+
+ // if subscriptions are supported, then also query for subscriptions
+ if (mSubscriptionsSupported) {
+ r = queryPurchases(inv, ITEM_TYPE_SUBS);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
+ }
+ }
+ }
+
+ return inv;
+ } catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
+ } catch (JSONException e) {
+ throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
+ }
+ }
+
+ /**
+ * Listener that notifies when an inventory query operation completes.
+ */
+ public interface QueryInventoryFinishedListener {
+ /**
+ * Called to notify that an inventory query operation completed.
+ *
+ * @param result The result of the operation.
+ * @param inv The inventory.
+ */
+ void onQueryInventoryFinished(IabResult result, Inventory inv);
+ }
+
+
+ /**
+ * Asynchronous wrapper for inventory query. This will perform an inventory
+ * query as described in {@link #queryInventory}, but will do so asynchronously
+ * and call back the specified listener upon completion. This method is safe to
+ * call from a UI thread.
+ *
+ * @param querySkuDetails as in {@link #queryInventory}
+ * @param moreSkus as in {@link #queryInventory}
+ * @param listener The listener to notify when the refresh operation completes.
+ */
+ public void queryInventoryAsync(final boolean querySkuDetails,
+ final List moreSkus,
+ final QueryInventoryFinishedListener listener) {
+ final Handler handler = new Handler();
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ flagStartAsync("refresh inventory");
+ (new Thread(new Runnable() {
+ public void run() {
+ IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
+ Inventory inv = null;
+ try {
+ inv = queryInventory(querySkuDetails, moreSkus);
+ } catch (IabException ex) {
+ result = ex.getResult();
+ }
+
+ flagEndAsync();
+
+ final IabResult result_f = result;
+ final Inventory inv_f = inv;
+ if (!mDisposed && listener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ listener.onQueryInventoryFinished(result_f, inv_f);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(true, null, listener);
+ }
+
+ public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
+ queryInventoryAsync(querySkuDetails, null, listener);
+ }
+
+
+ /**
+ * Consumes a given in-app product. Consuming can only be done on an item
+ * that's owned, and as a result of consumption, the user will no longer own it.
+ * This method may block or take long to return. Do not call from the UI thread.
+ * For that, see {@link #consumeAsync}.
+ *
+ * @param itemInfo The PurchaseInfo that represents the item to consume.
+ * @throws IabException if there is a problem during consumption.
+ */
+ void consume(Purchase itemInfo) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+
+ if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
+ throw new IabException(IABHELPER_INVALID_CONSUMPTION,
+ "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
+ }
+
+ try {
+ String token = itemInfo.getToken();
+ String sku = itemInfo.getSku();
+ if (token == null || token.equals("")) {
+ logError("Can't consume " + sku + ". No token.");
+ throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ + sku + " " + itemInfo);
+ }
+
+ logDebug("Consuming sku: " + sku + ", token: " + token);
+ int response = mService.consumePurchase(3, mContext.getPackageName(), token);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successfully consumed sku: " + sku);
+ } else {
+ logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
+ throw new IabException(response, "Error consuming sku " + sku);
+ }
+ } catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
+ }
+ }
+
+ /**
+ * Callback that notifies when a consumption operation finishes.
+ */
+ public interface OnConsumeFinishedListener {
+ /**
+ * Called to notify that a consumption has finished.
+ *
+ * @param purchase The purchase that was (or was to be) consumed.
+ * @param result The result of the consumption operation.
+ */
+ void onConsumeFinished(Purchase purchase, IabResult result);
+ }
+
+ /**
+ * Callback that notifies when a multi-item consumption operation finishes.
+ */
+ public interface OnConsumeMultiFinishedListener {
+ /**
+ * Called to notify that a consumption of multiple items has finished.
+ *
+ * @param purchases The purchases that were (or were to be) consumed.
+ * @param results The results of each consumption operation, corresponding to each
+ * sku.
+ */
+ void onConsumeMultiFinished(List purchases, List results);
+ }
+
+ /**
+ * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
+ * performs the consumption in the background and notifies completion through
+ * the provided listener. This method is safe to call from a UI thread.
+ *
+ * @param purchase The purchase to be consumed.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ List purchases = new ArrayList();
+ purchases.add(purchase);
+ consumeAsyncInternal(purchases, listener, null);
+ }
+
+ /**
+ * Same as {@link consumeAsync}, but for multiple items at once.
+ *
+ * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ consumeAsyncInternal(purchases, null, listener);
+ }
+
+ /**
+ * Returns a human-readable description for the given response code.
+ *
+ * @param code The response code
+ * @return A human-readable string explaining the result code.
+ * It also includes the result code numerically.
+ */
+ public static String getResponseDesc(int code) {
+ String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
+ "3:Billing Unavailable/4:Item unavailable/" +
+ "5:Developer Error/6:Error/7:Item Already Owned/" +
+ "8:Item not owned").split("/");
+ String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
+ "-1002:Bad response received/" +
+ "-1003:Purchase signature verification failed/" +
+ "-1004:Send intent failed/" +
+ "-1005:User cancelled/" +
+ "-1006:Unknown purchase response/" +
+ "-1007:Missing token/" +
+ "-1008:Unknown error/" +
+ "-1009:Subscriptions not available/" +
+ "-1010:Invalid consumption attempt").split("/");
+
+ if (code <= IABHELPER_ERROR_BASE) {
+ int index = IABHELPER_ERROR_BASE - code;
+ if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
+ else return String.valueOf(code) + ":Unknown IAB Helper Error";
+ } else if (code < 0 || code >= iab_msgs.length)
+ return String.valueOf(code) + ":Unknown";
+ else
+ return iab_msgs[code];
+ }
+
+
+ // Checks that setup was done; if not, throws an exception.
+ void checkSetupDone(String operation) {
+ if (!mSetupDone) {
+ logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
+ throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromBundle(Bundle b) {
+ Object o = b.get(RESPONSE_CODE);
+ if (o == null) {
+ logDebug("Bundle with null response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ } else if (o instanceof Integer) return ((Integer) o).intValue();
+ else if (o instanceof Long) return (int) ((Long) o).longValue();
+ else {
+ logError("Unexpected type for bundle response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromIntent(Intent i) {
+ Object o = i.getExtras().get(RESPONSE_CODE);
+ if (o == null) {
+ logError("Intent with no response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ } else if (o instanceof Integer) return ((Integer) o).intValue();
+ else if (o instanceof Long) return (int) ((Long) o).longValue();
+ else {
+ logError("Unexpected type for intent response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
+ }
+ }
+
+ void flagStartAsync(String operation) {
+ if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
+ operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
+ mAsyncOperation = operation;
+ mAsyncInProgress = true;
+ logDebug("Starting async operation: " + operation);
+ }
+
+ void flagEndAsync() {
+ logDebug("Ending async operation: " + mAsyncOperation);
+ mAsyncOperation = "";
+ mAsyncInProgress = false;
+ }
+
+
+ int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
+ // Query purchases
+ logDebug("Querying owned items, item type: " + itemType);
+ logDebug("Package name: " + mContext.getPackageName());
+ boolean verificationFailed = false;
+ String continueToken = null;
+
+ do {
+ logDebug("Calling getPurchases with continuation token: " + continueToken);
+ Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
+ itemType, continueToken);
+
+ int response = getResponseCodeFromBundle(ownedItems);
+ logDebug("Owned items response: " + String.valueOf(response));
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getPurchases() failed: " + getResponseDesc(response));
+ return response;
+ }
+ if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
+ logError("Bundle returned from getPurchases() doesn't contain required fields.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+
+ ArrayList ownedSkus = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_ITEM_LIST);
+ ArrayList purchaseDataList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_PURCHASE_DATA_LIST);
+ ArrayList signatureList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_SIGNATURE_LIST);
+
+ for (int i = 0; i < purchaseDataList.size(); ++i) {
+ String purchaseData = purchaseDataList.get(i);
+ String signature = signatureList.get(i);
+ String sku = ownedSkus.get(i);
+ if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
+ logDebug("Sku is owned: " + sku);
+ Purchase purchase = new Purchase(itemType, purchaseData, signature);
+
+ if (TextUtils.isEmpty(purchase.getToken())) {
+ logWarn("BUG: empty/null token!");
+ logDebug("Purchase data: " + purchaseData);
+ }
+
+ // Record ownership and token
+ inv.addPurchase(purchase);
+ } else {
+ logWarn("Purchase signature verification **FAILED**. Not adding item.");
+ logDebug(" Purchase data: " + purchaseData);
+ logDebug(" Signature: " + signature);
+ verificationFailed = true;
+ }
+ }
+
+ continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
+ logDebug("Continuation token: " + continueToken);
+ } while (!TextUtils.isEmpty(continueToken));
+
+ return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
+ }
+
+ int querySkuDetails(String itemType, Inventory inv, List moreSkus)
+ throws RemoteException, JSONException {
+ logDebug("Querying SKU details.");
+ ArrayList skuList = new ArrayList();
+ skuList.addAll(inv.getAllOwnedSkus(itemType));
+ if (moreSkus != null) {
+ for (String sku : moreSkus) {
+ if (!skuList.contains(sku)) {
+ skuList.add(sku);
+ }
+ }
+ }
+
+ if (skuList.size() == 0) {
+ logDebug("queryPrices: nothing to do because there are no SKUs.");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
+ Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
+ itemType, querySkus);
+
+ if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
+ int response = getResponseCodeFromBundle(skuDetails);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getSkuDetails() failed: " + getResponseDesc(response));
+ return response;
+ } else {
+ logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+ }
+
+ ArrayList responseList = skuDetails.getStringArrayList(
+ RESPONSE_GET_SKU_DETAILS_LIST);
+
+ for (String thisResponse : responseList) {
+ SkuDetails d = new SkuDetails(itemType, thisResponse);
+ logDebug("Got sku details: " + d);
+ inv.addSkuDetails(d);
+ }
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+
+ void consumeAsyncInternal(final List purchases,
+ final OnConsumeFinishedListener singleListener,
+ final OnConsumeMultiFinishedListener multiListener) {
+ final Handler handler = new Handler();
+ flagStartAsync("consume");
+ (new Thread(new Runnable() {
+ public void run() {
+ final List results = new ArrayList();
+ for (Purchase purchase : purchases) {
+ try {
+ consume(purchase);
+ results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
+ } catch (IabException ex) {
+ results.add(ex.getResult());
+ }
+ }
+
+ flagEndAsync();
+ if (!mDisposed && singleListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ singleListener.onConsumeFinished(purchases.get(0), results.get(0));
+ }
+ });
+ }
+ if (!mDisposed && multiListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ multiListener.onConsumeMultiFinished(purchases, results);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ void logDebug(String msg) {
+ if (mDebugLog) Log.d(mDebugTag, msg);
+ }
+
+ void logError(String msg) {
+ Log.e(mDebugTag, "In-app billing error: " + msg);
+ }
+
+ void logWarn(String msg) {
+ Log.w(mDebugTag, "In-app billing warning: " + msg);
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java
new file mode 100644
index 00000000..0fbe5b58
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java
@@ -0,0 +1,45 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * 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.util;
+
+/**
+ * Represents the result of an in-app billing operation.
+ * A result is composed of a response code (an integer) and possibly a
+ * message (String). You can get those by calling
+ * {@link #getResponse} and {@link #getMessage()}, respectively. You
+ * can also inquire whether a result is a success or a failure by
+ * calling {@link #isSuccess()} and {@link #isFailure()}.
+ */
+public class IabResult {
+ int mResponse;
+ String mMessage;
+
+ public IabResult(int response, String message) {
+ mResponse = response;
+ if (message == null || message.trim().length() == 0) {
+ mMessage = IabHelper.getResponseDesc(response);
+ }
+ else {
+ mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
+ }
+ }
+ public int getResponse() { return mResponse; }
+ public String getMessage() { return mMessage; }
+ public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
+ public boolean isFailure() { return !isSuccess(); }
+ public String toString() { return "IabResult: " + getMessage(); }
+}
+
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java
new file mode 100644
index 00000000..ae13e74f
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java
@@ -0,0 +1,91 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * 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.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a block of information about in-app items.
+ * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
+ */
+public class Inventory {
+ Map mSkuMap = new HashMap();
+ Map mPurchaseMap = new HashMap();
+
+ Inventory() { }
+
+ /** Returns the listing details for an in-app product. */
+ public SkuDetails getSkuDetails(String sku) {
+ return mSkuMap.get(sku);
+ }
+
+ /** Returns purchase information for a given product, or null if there is no purchase. */
+ public Purchase getPurchase(String sku) {
+ return mPurchaseMap.get(sku);
+ }
+
+ /** Returns whether or not there exists a purchase of the given product. */
+ public boolean hasPurchase(String sku) {
+ return mPurchaseMap.containsKey(sku);
+ }
+
+ /** Return whether or not details about the given product are available. */
+ public boolean hasDetails(String sku) {
+ return mSkuMap.containsKey(sku);
+ }
+
+ /**
+ * Erase a purchase (locally) from the inventory, given its product ID. This just
+ * modifies the Inventory object locally and has no effect on the server! This is
+ * useful when you have an existing Inventory object which you know to be up to date,
+ * and you have just consumed an item successfully, which means that erasing its
+ * purchase data from the Inventory you already have is quicker than querying for
+ * a new Inventory.
+ */
+ public void erasePurchase(String sku) {
+ if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
+ }
+
+ /** Returns a list of all owned product IDs. */
+ List getAllOwnedSkus() {
+ return new ArrayList(mPurchaseMap.keySet());
+ }
+
+ /** Returns a list of all owned product IDs of a given type */
+ List getAllOwnedSkus(String itemType) {
+ List result = new ArrayList();
+ for (Purchase p : mPurchaseMap.values()) {
+ if (p.getItemType().equals(itemType)) result.add(p.getSku());
+ }
+ return result;
+ }
+
+ /** Returns a list of all purchases. */
+ List getAllPurchases() {
+ return new ArrayList(mPurchaseMap.values());
+ }
+
+ void addSkuDetails(SkuDetails d) {
+ mSkuMap.put(d.getSku(), d);
+ }
+
+ void addPurchase(Purchase p) {
+ mPurchaseMap.put(p.getSku(), p);
+ }
+}
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/LogRecorder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java
new file mode 100644
index 00000000..ad4d54ae
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java
@@ -0,0 +1,540 @@
+package com.v2ray.ang.util;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Reference to http://blog.csdn.net/way_ping_li/article/details/8487866
+ * and improved some features...
+ */
+public class LogRecorder {
+
+ public static final int LOG_LEVEL_NO_SET = 0;
+
+ public static final int LOG_BUFFER_MAIN = 1;
+ public static final int LOG_BUFFER_SYSTEM = 1 << 1;
+ public static final int LOG_BUFFER_RADIO = 1 << 2;
+ public static final int LOG_BUFFER_EVENTS = 1 << 3;
+ public static final int LOG_BUFFER_KERNEL = 1 << 4; // not be supported by now
+
+ public static final int LOG_BUFFER_DEFAULT = LOG_BUFFER_MAIN | LOG_BUFFER_SYSTEM;
+
+ public static final int INVALID_PID = -1;
+
+ public String mFileSuffix;
+ public String mFolderPath;
+ public int mFileSizeLimitation;
+ public int mLevel;
+ public List mFilterTags = new ArrayList<>();
+ public int mPID = INVALID_PID;
+
+ public boolean mUseLogcatFileOut = false;
+
+ private LogDumper mLogDumper = null;
+
+ public static final int EVENT_RESTART_LOG = 1001;
+
+ private RestartHandler mHandler;
+
+ private static class RestartHandler extends Handler {
+ final LogRecorder logRecorder;
+ public RestartHandler(LogRecorder logRecorder) {
+ this.logRecorder = logRecorder;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_RESTART_LOG) {
+ logRecorder.stop();
+ logRecorder.start();
+ }
+ }
+ }
+
+ public LogRecorder() {
+ mHandler = new RestartHandler(this);
+ }
+
+ public void start() {
+ // make sure the out folder exist
+ // TODO support multi-phase path
+ File file = new File(mFolderPath);
+ if (!file.exists()) {
+ file.mkdirs();
+ }
+
+ String cmdStr = collectLogcatCommand();
+
+ if (mLogDumper != null) {
+ mLogDumper.stopDumping();
+ mLogDumper = null;
+ }
+
+ mLogDumper = new LogDumper(mFolderPath, mFileSuffix, mFileSizeLimitation, cmdStr, mHandler);
+ mLogDumper.start();
+ }
+
+ public void stop() {
+ // TODO maybe should clean the log buffer first?
+ if (mLogDumper != null) {
+ mLogDumper.stopDumping();
+ mLogDumper = null;
+ }
+ }
+
+ private String collectLogcatCommand() {
+ StringBuilder stringBuilder = new StringBuilder();
+ final String SPACE = " ";
+ stringBuilder.append("logcat");
+
+ // TODO select ring buffer, -b
+
+ // TODO set out format
+ stringBuilder.append(SPACE);
+ stringBuilder.append("-v time");
+
+ // append tag filters
+ String levelStr = getLevelStr();
+
+ if (!mFilterTags.isEmpty()) {
+ stringBuilder.append(SPACE);
+ stringBuilder.append("-s");
+ for (int i = 0; i < mFilterTags.size(); i++) {
+ String tag = mFilterTags.get(i) + ":" + levelStr;
+ stringBuilder.append(SPACE);
+ stringBuilder.append(tag);
+ }
+ } else {
+ if (!TextUtils.isEmpty(levelStr)) {
+ stringBuilder.append(SPACE);
+ stringBuilder.append("*:" + levelStr);
+ }
+ }
+
+ // logcat -f , but the rotated count default is 4?
+ // can`t be sure to use that feature
+ if (mPID != INVALID_PID) {
+ mUseLogcatFileOut = false;
+ String pidStr = adjustPIDStr();
+ if (!TextUtils.isEmpty(pidStr)) {
+ stringBuilder.append(SPACE);
+ stringBuilder.append("|");
+ stringBuilder.append(SPACE);
+ stringBuilder.append("grep (" + pidStr + ")");
+ }
+ }
+
+ return stringBuilder.toString();
+ }
+
+ private String getLevelStr() {
+ switch (mLevel) {
+ case 2:
+ return "V";
+ case 3:
+ return "D";
+ case 4:
+ return "I";
+ case 5:
+ return "W";
+ case 6:
+ return "E";
+ case 7:
+ return "F";
+ }
+
+ return "V";
+ }
+
+ /**
+ * Android`s user app pid is bigger than 1000.
+ *
+ * @return
+ */
+ private String adjustPIDStr() {
+ if (mPID == INVALID_PID) {
+ return null;
+ }
+
+ String pidStr = String.valueOf(mPID);
+ int length = pidStr.length();
+ if (length < 4) {
+ pidStr = " 0" + pidStr;
+ }
+
+ if (length == 4) {
+ pidStr = " " + pidStr;
+ }
+
+ return pidStr;
+ }
+
+
+ private class LogDumper extends Thread {
+ final String logPath;
+ final String logFileSuffix;
+ final int logFileLimitation;
+ final String logCmd;
+
+ final RestartHandler restartHandler;
+
+ private Process logcatProc;
+ private BufferedReader mReader = null;
+ private FileOutputStream out = null;
+
+ private boolean mRunning = true;
+ final private Object mRunningLock = new Object();
+
+ private long currentFileSize;
+
+ public LogDumper(String folderPath, String suffix,
+ int fileSizeLimitation, String command,
+ RestartHandler handler) {
+ logPath = folderPath;
+ logFileSuffix = suffix;
+ logFileLimitation = fileSizeLimitation;
+ logCmd = command;
+ restartHandler = handler;
+
+ String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
+ .format(new Date(System.currentTimeMillis()));
+ String fileName = (TextUtils.isEmpty(logFileSuffix)) ? date : (logFileSuffix + "-"+ date);
+ try {
+ out = new FileOutputStream(new File(logPath, fileName + ".log"));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void stopDumping() {
+ synchronized (mRunningLock) {
+ mRunning = false;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ logcatProc = Runtime.getRuntime().exec(logCmd);
+ mReader = new BufferedReader(new InputStreamReader(
+ logcatProc.getInputStream()), 1024);
+ String line = null;
+ while (mRunning && (line = mReader.readLine()) != null) {
+ if (!mRunning) {
+ break;
+ }
+ if (line.length() == 0) {
+ continue;
+ }
+ if (out != null && !line.isEmpty()) {
+ byte[] data = (line + "\n").getBytes();
+ out.write(data);
+ if (logFileLimitation != 0) {
+ currentFileSize += data.length;
+ if (currentFileSize > logFileLimitation*1024) {
+ restartHandler.sendEmptyMessage(EVENT_RESTART_LOG);
+ break;
+ }
+ }
+ }
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (logcatProc != null) {
+ logcatProc.destroy();
+ logcatProc = null;
+ }
+ if (mReader != null) {
+ try {
+ mReader.close();
+ mReader = null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ out = null;
+ }
+ }
+ }
+ }
+
+ public static class Builder {
+
+ /**
+ * context object
+ */
+ private Context mContext;
+
+ /**
+ * the folder name that we save log files to,
+ * just folder name, not the whole path,
+ * if set this, will save log files to /sdcard/$mLogFolderName folder,
+ * use /sdcard/$ApplicationName as default.
+ */
+ private String mLogFolderName;
+
+ /**
+ * the whole folder path that we save log files to,
+ * this setting`s priority is bigger than folder name.
+ */
+ private String mLogFolderPath;
+
+ /**
+ * the log file suffix,
+ * if this is sot, it will be appended to log file name automatically
+ */
+ private String mLogFileNameSuffix = "";
+
+ /**
+ * single log file size limitation,
+ * in k-bytes, ex. set to 16, is 16KB limitation.
+ */
+ private int mLogFileSizeLimitation = 0;
+
+ /**
+ * log level, see android.util.Log, 2 - 7,
+ * if not be set, will use verbose as default
+ */
+ private int mLogLevel = LogRecorder.LOG_LEVEL_NO_SET;
+
+ /**
+ * can set several filter tags
+ * logcat -s ActivityManager:V SystemUI:V
+ */
+ private List mLogFilterTags = new ArrayList<>();
+
+ /**
+ * filter through pid, by setting this with your APP PID,
+ * the log recorder will just record the APP`s own log,
+ * use one call: android.os.Process.myPid().
+ */
+ private int mPID = LogRecorder.INVALID_PID;
+
+ /**
+ * which log buffer to catch...
+ *
+ * Request alternate ring buffer, 'main', 'system', 'radio'
+ * or 'events'. Multiple -b parameters are allowed and the
+ * results are interleaved.
+ *
+ * The default is -b main -b system.
+ */
+ private int mLogBuffersSelected = LogRecorder.LOG_BUFFER_DEFAULT;
+
+ /**
+ * log output format, don`t support config yet, use $time format as default.
+ *
+ * Log messages contain a number of metadata fields, in addition to the tag and priority.
+ * You can modify the output format for messages so that they display a specific metadata
+ * field. To do so, you use the -v option and specify one of the supported output formats
+ * listed below.
+ *
+ * brief — Display priority/tag and PID of the process issuing the message.
+ * process — Display PID only.
+ * tag — Display the priority/tag only.
+ * thread - Display the priority, tag, and the PID(process ID) and TID(thread ID)
+ * of the thread issuing the message.
+ * raw — Display the raw log message, with no other metadata fields.
+ * time — Display the date, invocation time, priority/tag, and PID of
+ * the process issuing the message.
+ * threadtime — Display the date, invocation time, priority, tag, and the PID(process ID)
+ * and TID(thread ID) of the thread issuing the message.
+ * long — Display all metadata fields and separate messages with blank lines.
+ */
+ private int mLogOutFormat;
+
+ /**
+ * set log out folder name
+ *
+ * @param logFolderName folder name
+ * @return The same Builder.
+ */
+ public Builder setLogFolderName(String logFolderName) {
+ this.mLogFolderName = logFolderName;
+ return this;
+ }
+
+ /**
+ * set log out folder path
+ *
+ * @param logFolderPath out folder absolute path
+ * @return the same Builder
+ */
+ public Builder setLogFolderPath(String logFolderPath) {
+ this.mLogFolderPath = logFolderPath;
+ return this;
+ }
+
+ /**
+ * set log file name suffix
+ *
+ * @param logFileNameSuffix auto appened suffix
+ * @return the same Builder
+ */
+ public Builder setLogFileNameSuffix(String logFileNameSuffix) {
+ this.mLogFileNameSuffix = logFileNameSuffix;
+ return this;
+ }
+
+ /**
+ * set the file size limitation
+ *
+ * @param fileSizeLimitation file size limitation in KB
+ * @return the same Builder
+ */
+ public Builder setLogFileSizeLimitation(int fileSizeLimitation) {
+ this.mLogFileSizeLimitation = fileSizeLimitation;
+ return this;
+ }
+
+ /**
+ * set the log level
+ *
+ * @param logLevel log level, 2-7
+ * @return the same Builder
+ */
+ public Builder setLogLevel(int logLevel) {
+ this.mLogLevel = logLevel;
+ return this;
+ }
+
+ /**
+ * add log filterspec tag name, can add multiple ones,
+ * they use the same log level set by setLogLevel()
+ *
+ * @param tag tag name
+ * @return the same Builder
+ */
+ public Builder addLogFilterTag(String tag) {
+ mLogFilterTags.add(tag);
+ return this;
+ }
+
+ /**
+ * which process`s log
+ *
+ * @param mPID process id
+ * @return the same Builder
+ */
+ public Builder setPID(int mPID) {
+ this.mPID = mPID;
+ return this;
+ }
+
+ /**
+ * -b radio, -b main, -b system, -b events
+ * -b main -b system as default
+ *
+ * @param logBuffersSelected one of
+ * LOG_BUFFER_MAIN = 1 << 0;
+ * LOG_BUFFER_SYSTEM = 1 << 1;
+ * LOG_BUFFER_RADIO = 1 << 2;
+ * LOG_BUFFER_EVENTS = 1 << 3;
+ * LOG_BUFFER_KERNEL = 1 << 4;
+ * @return the same Builder
+ */
+ public Builder setLogBufferSelected(int logBuffersSelected) {
+ this.mLogBuffersSelected = logBuffersSelected;
+ return this;
+ }
+
+ /**
+ * sets log out format, -v parameter
+ *
+ * @param logOutFormat out format, like -v time
+ * @return the same Builder
+ */
+ public Builder setLogOutFormat(int logOutFormat) {
+ this.mLogOutFormat = mLogOutFormat;
+ return this;
+ }
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * call this only if mLogFolderName and mLogFolderPath not
+ * be set both.
+ *
+ * @return
+ */
+ private void applyAppNameAsOutfolderName() {
+ try {
+ String appName = mContext.getPackageName();
+ String versionName = mContext.getPackageManager().getPackageInfo(
+ appName, 0).versionName;
+ int versionCode = mContext.getPackageManager()
+ .getPackageInfo(appName, 0).versionCode;
+ mLogFolderName = appName + "-" + versionName + "-" + versionCode;
+ mLogFolderPath = applyOutfolderPath();
+ } catch (Exception e) {
+ }
+ }
+
+ private String applyOutfolderPath() {
+ String outPath = "";
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ outPath = Environment.getExternalStorageDirectory()
+ .getAbsolutePath() + File.separator + mLogFolderName;
+ }
+
+ return outPath;
+ }
+
+ /**
+ * Combine all of the options that have been set and return
+ * a new {@link LogRecorder} object.
+ */
+ public LogRecorder build() {
+ LogRecorder logRecorder = new LogRecorder();
+
+ // no folder name & folder path be set
+ if (TextUtils.isEmpty(mLogFolderName)
+ && TextUtils.isEmpty(mLogFolderPath)) {
+ applyAppNameAsOutfolderName();
+ }
+
+ // make sure out path be set
+ if (TextUtils.isEmpty(mLogFolderPath)) {
+ mLogFolderPath = applyOutfolderPath();
+ }
+
+ logRecorder.mFolderPath = mLogFolderPath;
+ logRecorder.mFileSuffix = mLogFileNameSuffix;
+ logRecorder.mFileSizeLimitation = mLogFileSizeLimitation;
+ logRecorder.mLevel = mLogLevel;
+ if (!mLogFilterTags.isEmpty()) {
+ for (int i = 0; i < mLogFilterTags.size(); i++) {
+ logRecorder.mFilterTags.add(mLogFilterTags.get(i));
+ }
+ }
+ logRecorder.mPID = mPID;
+
+ return logRecorder;
+ }
+ }
+
+}
\ 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/Purchase.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java
new file mode 100644
index 00000000..d5e59153
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java
@@ -0,0 +1,63 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * 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.util;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app billing purchase.
+ */
+public class Purchase {
+ String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
+ String mOrderId;
+ String mPackageName;
+ String mSku;
+ long mPurchaseTime;
+ int mPurchaseState;
+ String mDeveloperPayload;
+ String mToken;
+ String mOriginalJson;
+ String mSignature;
+
+ public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
+ mItemType = itemType;
+ mOriginalJson = jsonPurchaseInfo;
+ JSONObject o = new JSONObject(mOriginalJson);
+ mOrderId = o.optString("orderId");
+ mPackageName = o.optString("packageName");
+ mSku = o.optString("productId");
+ mPurchaseTime = o.optLong("purchaseTime");
+ mPurchaseState = o.optInt("purchaseState");
+ mDeveloperPayload = o.optString("developerPayload");
+ mToken = o.optString("token", o.optString("purchaseToken"));
+ mSignature = signature;
+ }
+
+ public String getItemType() { return mItemType; }
+ public String getOrderId() { return mOrderId; }
+ public String getPackageName() { return mPackageName; }
+ public String getSku() { return mSku; }
+ public long getPurchaseTime() { return mPurchaseTime; }
+ public int getPurchaseState() { return mPurchaseState; }
+ public String getDeveloperPayload() { return mDeveloperPayload; }
+ public String getToken() { return mToken; }
+ public String getOriginalJson() { return mOriginalJson; }
+ public String getSignature() { return mSignature; }
+
+ @Override
+ public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
+}
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/Security.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java
new file mode 100644
index 00000000..50f02e3c
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java
@@ -0,0 +1,119 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * 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.util;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * Security-related methods. For a secure implementation, all of this code
+ * should be implemented on a server that communicates with the
+ * application on the device. For the sake of simplicity and clarity of this
+ * example, this code is included here and is executed on the device. If you
+ * must verify the purchases on the phone, you should obfuscate this code to
+ * make it harder for an attacker to replace the code with stubs that treat all
+ * purchases as verified.
+ */
+public class Security {
+ private static final String TAG = "IABUtil/Security";
+
+ private static final String KEY_FACTORY_ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+ /**
+ * Verifies that the data was signed with the given signature, and returns
+ * the verified purchase. The data is in JSON format and signed
+ * with a private key. The data also contains the {@link PurchaseState}
+ * and product ID of the purchase.
+ * @param base64PublicKey the base64-encoded public key to use for verifying.
+ * @param signedData the signed JSON string (signed, not encrypted)
+ * @param signature the signature for the data, signed with the private key
+ */
+ public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
+ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
+ TextUtils.isEmpty(signature)) {
+ Log.e(TAG, "Purchase verification failed: missing data.");
+ return false;
+ }
+
+ PublicKey key = Security.generatePublicKey(base64PublicKey);
+ return Security.verify(key, signedData, signature);
+ }
+
+ /**
+ * Generates a PublicKey instance from a string containing the
+ * Base64-encoded public key.
+ *
+ * @param encodedPublicKey Base64-encoded public key
+ * @throws IllegalArgumentException if encodedPublicKey is invalid
+ */
+ public static PublicKey generatePublicKey(String encodedPublicKey) {
+ try {
+ byte[] decodedKey = Base64.decode(encodedPublicKey);
+ KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+ return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ Log.e(TAG, "Invalid key specification.");
+ throw new IllegalArgumentException(e);
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Verifies that the signature from the server matches the computed
+ * signature on the data. Returns true if the data is correctly signed.
+ *
+ * @param publicKey public key associated with the developer account
+ * @param signedData signed data from server
+ * @param signature server signature
+ * @return true if the data and signature match
+ */
+ public static boolean verify(PublicKey publicKey, String signedData, String signature) {
+ Signature sig;
+ try {
+ sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+ sig.initVerify(publicKey);
+ sig.update(signedData.getBytes());
+ if (!sig.verify(Base64.decode(signature))) {
+ Log.e(TAG, "Signature verification failed.");
+ return false;
+ }
+ return true;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "NoSuchAlgorithmException.");
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Invalid key specification.");
+ } catch (SignatureException e) {
+ Log.e(TAG, "Signature exception.");
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ }
+ return false;
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java
new file mode 100644
index 00000000..b15cd472
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java
@@ -0,0 +1,58 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * 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.util;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app product's listing details.
+ */
+public class SkuDetails {
+ String mItemType;
+ String mSku;
+ String mType;
+ String mPrice;
+ String mTitle;
+ String mDescription;
+ String mJson;
+
+ public SkuDetails(String jsonSkuDetails) throws JSONException {
+ this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
+ }
+
+ public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
+ mItemType = itemType;
+ mJson = jsonSkuDetails;
+ JSONObject o = new JSONObject(mJson);
+ mSku = o.optString("productId");
+ mType = o.optString("type");
+ mPrice = o.optString("price");
+ mTitle = o.optString("title");
+ mDescription = o.optString("description");
+ }
+
+ public String getSku() { return mSku; }
+ public String getType() { return mType; }
+ public String getPrice() { return mPrice; }
+ public String getTitle() { return mTitle; }
+ public String getDescription() { return mDescription; }
+
+ @Override
+ public String toString() {
+ return "SkuDetails:" + mJson;
+ }
+}
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/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
new file mode 100644
index 00000000..e54812cd
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
@@ -0,0 +1,31 @@
+package com.v2ray.ang
+
+import android.app.Application
+//import com.squareup.leakcanary.LeakCanary
+import com.v2ray.ang.util.AngConfigManager
+import me.dozen.dpreference.DPreference
+import org.jetbrains.anko.defaultSharedPreferences
+
+class AngApplication : Application() {
+ companion object {
+ const val PREF_LAST_VERSION = "pref_last_version"
+ }
+
+ var firstRun = false
+ private set
+
+ val defaultDPreference by lazy { DPreference(this, packageName + "_preferences") }
+
+ override fun onCreate() {
+ super.onCreate()
+
+// LeakCanary.install(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)
+ AngConfigManager.inject(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..9f2278db
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
@@ -0,0 +1,61 @@
+package com.v2ray.ang
+
+/**
+ *
+ * App Config Const
+ */
+object AppConfig {
+ const val ANG_PACKAGE = "com.v2ray.ang"
+ const val ANG_CONFIG = "ang_config"
+ const val PREF_CURR_CONFIG = "pref_v2ray_config"
+ const val PREF_CURR_CONFIG_GUID = "pref_v2ray_config_guid"
+ const val PREF_CURR_CONFIG_NAME = "pref_v2ray_config_name"
+ const val PREF_CURR_CONFIG_DOMAIN = "pref_v2ray_config_domain"
+ const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
+ const val VMESS_PROTOCOL: String = "vmess://"
+ const val SS_PROTOCOL: String = "ss://"
+ const val SOCKS_PROTOCOL: String = "socks://"
+ 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 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 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 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
+
+ object EConfigType {
+ val Vmess = 1
+ val Custom = 2
+ val Shadowsocks = 3
+ val Socks = 4
+ }
+
+}
\ No newline at end of file
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/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
new file mode 100644
index 00000000..ec1a87ec
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt
@@ -0,0 +1,142 @@
+package com.v2ray.ang.dto
+
+data class V2rayConfig(
+ val stats: Any?=null,
+ val log: LogBean,
+ val policy: PolicyBean,
+ val inbounds: ArrayList,
+ var outbounds: ArrayList,
+ var dns: DnsBean,
+ val routing: RoutingBean) {
+
+ data class LogBean(val access: String,
+ val error: String,
+ val loglevel: String)
+
+ data class InboundBean(
+ var tag: String,
+ var port: Int,
+ var protocol: String,
+ var listen: String?=null,
+ val settings: InSettingsBean,
+ val sniffing: SniffingBean?) {
+
+ 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: List)
+ }
+
+ data class OutboundBean(val tag: String,
+ var protocol: String,
+ var settings: OutSettingsBean?,
+ var streamSettings: StreamSettingsBean?,
+ var mux: MuxBean?) {
+
+ data class OutSettingsBean(var vnext: List?,
+ var servers: List?,
+ var response: Response) {
+
+ data class VnextBean(var address: String,
+ var port: Int,
+ var users: List) {
+
+ data class UsersBean(var id: String,
+ var alterId: Int,
+ var security: String,
+ var level: Int)
+ }
+
+ data class ServersBean(var address: String,
+ var method: String,
+ var ota: Boolean,
+ var password: String,
+ var port: Int,
+ var level: Int)
+
+ data class Response(var type: String)
+ }
+
+ data class StreamSettingsBean(var network: String,
+ var security: String,
+ var tcpSettings: TcpsettingsBean?,
+ var kcpsettings: KcpsettingsBean?,
+ var wssettings: WssettingsBean?,
+ var httpsettings: HttpsettingsBean?,
+ var tlssettings: TlssettingsBean?,
+ var quicsettings: QuicsettingBean?
+ ) {
+
+ data class TcpsettingsBean(var connectionReuse: Boolean = true,
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none",
+ var request: Any? = null,
+ var response: Any? = null)
+ }
+
+ data class KcpsettingsBean(var mtu: Int = 1350,
+ var tti: Int = 20,
+ var uplinkCapacity: Int = 12,
+ var downlinkCapacity: Int = 100,
+ var congestion: Boolean = false,
+ var readBufferSize: Int = 1,
+ var writeBufferSize: Int = 1,
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none")
+ }
+
+ data class WssettingsBean(var connectionReuse: Boolean = true,
+ var path: String = "",
+ var headers: HeadersBean = HeadersBean()) {
+ data class HeadersBean(var Host: String = "")
+ }
+
+ data class HttpsettingsBean(var host: List = ArrayList(), var path: String = "")
+
+ data class TlssettingsBean(var allowInsecure: Boolean = true,
+ var serverName: String = "")
+
+ data class QuicsettingBean(var security: String = "none",
+ var key: String = "",
+ var header: HeaderBean = HeaderBean()) {
+ data class HeaderBean(var type: String = "none")
+ }
+ }
+
+ data class MuxBean(var enabled: Boolean)
+ }
+
+ //data class DnsBean(var servers: List)
+ data class DnsBean(var servers: List?=null,
+ var hosts: Map?=null
+ ) {
+ data class ServersBean(var address: String = "",
+ var port: Int = 0,
+ var domains: List?)
+ }
+
+ data class RoutingBean(var domainStrategy: String,
+ var rules: ArrayList) {
+
+ data class RulesBean(var type: String = "",
+ var ip: ArrayList