diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NativePlugin.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NativePlugin.kt new file mode 100644 index 000000000..55b7a9902 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NativePlugin.kt @@ -0,0 +1,33 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.content.pm.ResolveInfo +import android.os.Build +import androidx.annotation.RequiresApi + +@RequiresApi(Build.VERSION_CODES.KITKAT) class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) { + init { + check(resolveInfo.providerInfo != null) + } + + override val componentInfo get() = resolveInfo.providerInfo!! +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt new file mode 100644 index 000000000..b57b26799 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt @@ -0,0 +1,103 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.os.Bundle +import android.os.ParcelFileDescriptor + +/** + * Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required + * to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this + * class, implement the abstract methods, and add it to your manifest like this: + * + *
<manifest>
+ *    ...
+ *    <application>
+ *        ...
+ *        <provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
+ *                     android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
+ *            <intent-filter>
+ *                <category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
+ *            </intent-filter>
+ *        </provider>
+ *        ...
+ *    </application>
+ *</manifest>
+ */ +abstract class NativePluginProvider : ContentProvider() { + override fun getType(uri: Uri): String? = "application/x-elf" + + override fun onCreate(): Boolean = true + + /** + * Provide all files needed for native plugin. + * + * @param provider A helper object to use to add files. + */ + protected abstract fun populateFiles(provider: PathProvider) + + override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, + sortOrder: String?): Cursor? { + check(selection == null && selectionArgs == null && sortOrder == null) + val result = MatrixCursor(projection) + populateFiles(PathProvider(uri, result)) + return result + } + + /** + * Returns executable entry absolute path. + * This is used for fast mode initialization where ss-local launches your native binary at the path given directly. + * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml: + * - android:installLocation="internalOnly" for + * - android:extractNativeLibs="true" for + * + * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the + * default behavior. + * + * @return Absolute path for executable entry. + */ + open fun getExecutable(): String = throw UnsupportedOperationException() + + abstract fun openFile(uri: Uri): ParcelFileDescriptor + override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor { + check(mode == "r") + return openFile(uri) + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) { + PluginContract.METHOD_GET_EXECUTABLE -> Bundle().apply { putString(PluginContract.EXTRA_ENTRY, getExecutable()) } + else -> super.call(method, arg, extras) + } + + // Methods that should not be used + override fun insert(uri: Uri, values: ContentValues?): Uri? = throw UnsupportedOperationException() + + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = + throw UnsupportedOperationException() + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = + throw UnsupportedOperationException() +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NoPlugin.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NoPlugin.kt new file mode 100644 index 000000000..11f8091af --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/NoPlugin.kt @@ -0,0 +1,9 @@ +package com.github.shadowsocks.plugin + +import org.telegram.messenger.LocaleController +import org.telegram.messenger.R + +object NoPlugin : Plugin() { + override val id: String get() = "" + override val label: CharSequence get() = LocaleController.getString("Disable", R.string.Disable) +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PathProvider.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PathProvider.kt new file mode 100644 index 000000000..4b18bb8eb --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PathProvider.kt @@ -0,0 +1,56 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.database.MatrixCursor +import android.net.Uri +import android.os.Build +import androidx.annotation.RequiresApi +import java.io.File + +/** + * Helper class to provide relative paths of files to copy. + */ +class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) { + private val basePath = baseUri.path?.trim('/') ?: "" + + @RequiresApi(Build.VERSION_CODES.KITKAT) fun addPath(path: String, mode: Int = 0b110100100): PathProvider { + val trimmed = path.trim('/') + if (trimmed.startsWith(basePath)) cursor.newRow() + .add(PluginContract.COLUMN_PATH, trimmed) + .add(PluginContract.COLUMN_MODE, mode) + return this + } + @RequiresApi(Build.VERSION_CODES.KITKAT) fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider { + var sub = to + file.name + if (basePath.startsWith(sub)) if (file.isDirectory) { + sub += '/' + file.listFiles()!!.forEach { addTo(it, sub, mode) } + } else addPath(sub, mode) + return this + } + fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider { + if (basePath.startsWith(at)) { + if (file.isDirectory) file.listFiles()!!.forEach { addTo(it, at, mode) } else addPath(at, mode) + } + return this + } +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/Plugin.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/Plugin.kt new file mode 100644 index 000000000..c805a6229 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/Plugin.kt @@ -0,0 +1,41 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.graphics.drawable.Drawable + +abstract class Plugin { + abstract val id: String + open val idAliases get() = emptyArray() + abstract val label: CharSequence + open val icon: Drawable? get() = null + open val defaultConfig: String? get() = null + open val packageName: String get() = "" + open val trusted: Boolean get() = true + open val directBootAware: Boolean get() = true + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return id == (other as Plugin).id + } + override fun hashCode() = id.hashCode() +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginConfiguration.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginConfiguration.kt new file mode 100644 index 000000000..843502359 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginConfiguration.kt @@ -0,0 +1,73 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.os.Build +import androidx.annotation.RequiresApi +import com.github.shadowsocks.utils.Commandline +import org.telegram.messenger.FileLog +import org.telegram.messenger.LocaleController +import org.telegram.messenger.R +import java.util.* +import kotlin.collections.HashMap + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +class PluginConfiguration(val pluginsOptions: MutableMap, var selected: String) { + private constructor(plugins: List) : this( + HashMap(plugins.filter { it.id.isNotEmpty() }.associateBy { it.id }), + if (plugins.isEmpty()) "" else plugins[0].id) + + constructor(plugin: String) : this(plugin.split('\n').map { line -> + if (line.startsWith("kcptun ")) { + val opt = PluginOptions() + opt.id = "kcptun" + try { + val iterator = Commandline.translateCommandline(line).drop(1).iterator() + while (iterator.hasNext()) { + val option = iterator.next() + when { + option == "--nocomp" -> opt["nocomp"] = null + option.startsWith("--") -> opt[option.substring(2)] = iterator.next() + else -> throw IllegalArgumentException("Unknown kcptun parameter: $option") + } + } + } catch (exc: Exception) { + FileLog.e(exc) + } + opt + } else PluginOptions(line) + }) + + val selectedName get() = selected.takeIf { it.isNotBlank() } ?: LocaleController.getString("Disable", R.string.Disable) + + @JvmOverloads + fun getOptions( + id: String = selected, + defaultConfig: () -> String? = { PluginManager.fetchPlugins().lookup[id]?.defaultConfig } + ) = if (id.isEmpty()) PluginOptions() else pluginsOptions[id] ?: PluginOptions(id, defaultConfig()) + + override fun toString(): String { + val result = LinkedList() + for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt) + if (!pluginsOptions.contains(selected)) result.addFirst(getOptions()) + return result.joinToString("\n") { it.toString(false) } + } +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginContract.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginContract.kt new file mode 100644 index 000000000..07745d0a3 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginContract.kt @@ -0,0 +1,142 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +/** + * The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc. + * + * This class is written in Java to keep Java interoperability. + */ +object PluginContract { + /** + * ContentProvider Action: Used for NativePluginProvider. + * + * Constant Value: "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" + */ + const val ACTION_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" + + /** + * Activity Action: Used for ConfigurationActivity. + * + * Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE" + */ + const val ACTION_CONFIGURE = "com.github.shadowsocks.plugin.ACTION_CONFIGURE" + /** + * Activity Action: Used for HelpActivity or HelpCallback. + * + * Constant Value: "com.github.shadowsocks.plugin.ACTION_HELP" + */ + const val ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP" + + /** + * The lookup key for a string that provides the plugin entry binary. + * + * Example: "/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so" + * + * Constant Value: "com.github.shadowsocks.plugin.EXTRA_ENTRY" + */ + const val EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY" + /** + * The lookup key for a string that provides the options as a string. + * + * Example: "obfs=http;obfs-host=www.baidu.com" + * + * Constant Value: "com.github.shadowsocks.plugin.EXTRA_OPTIONS" + */ + const val EXTRA_OPTIONS = "com.github.shadowsocks.plugin.EXTRA_OPTIONS" + /** + * The lookup key for a CharSequence that provides user relevant help message. + * + * Example: "obfs=|tls> Enable obfuscating: HTTP or TLS (Experimental). + * obfs-host= Hostname for obfuscating (Experimental)." + * + * Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE" + */ + const val EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE" + + /** + * The metadata key to retrieve plugin id. Required for plugins. + * + * Constant Value: "com.github.shadowsocks.plugin.id" + */ + const val METADATA_KEY_ID = "com.github.shadowsocks.plugin.id" + /** + * The metadata key to retrieve plugin id aliases. + * Can be a string (representing one alias) or a resource to a string or string array. + * + * Constant Value: "com.github.shadowsocks.plugin.id.aliases" + */ + const val METADATA_KEY_ID_ALIASES = "com.github.shadowsocks.plugin.id.aliases" + /** + * The metadata key to retrieve default configuration. Default value is empty. + * + * Constant Value: "com.github.shadowsocks.plugin.default_config" + */ + const val METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config" + /** + * The metadata key to retrieve executable path to your native binary. + * This path should be relative to your application's nativeLibraryDir. + * + * If this is set, the host app will prefer this value and (probably) not launch your app at all (aka faster mode). + * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml: + * - android:installLocation="internalOnly" for + * - android:extractNativeLibs="true" for + * + * Do not use this if you plan to do some setup work before giving away your binary path, + * or your native binary is not at a fixed location relative to your application's nativeLibraryDir. + * + * Since plugin lib: 1.3.0 + * + * Constant Value: "com.github.shadowsocks.plugin.executable_path" + */ + const val METADATA_KEY_EXECUTABLE_PATH = "com.github.shadowsocks.plugin.executable_path" + + const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable" + + /** ConfigurationActivity result: fallback to manual edit mode. */ + const val RESULT_FALLBACK = 1 + + /** + * Relative to the file to be copied. This column is required. + * + * Example: "kcptun", "doc/help.txt" + * + * Type: String + */ + const val COLUMN_PATH = "path" + /** + * File mode bits. Default value is 644 in octal. + * + * Example: 0b110100100 (for 755 in octal) + * + * Type: Int or String (deprecated) + */ + const val COLUMN_MODE = "mode" + + /** + * The scheme for general plugin actions. + */ + const val SCHEME = "plugin" + /** + * The authority for general plugin actions. + */ + const val AUTHORITY = "com.github.shadowsocks" +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginList.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginList.kt new file mode 100644 index 000000000..1f318d884 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginList.kt @@ -0,0 +1,52 @@ +/******************************************************************************* + * * + * Copyright (C) 2020 by Max Lv * + * Copyright (C) 2020 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RequiresApi +import org.telegram.messenger.ApplicationLoader +import org.telegram.messenger.LocaleController +import org.telegram.messenger.R + +@RequiresApi(Build.VERSION_CODES.KITKAT) class PluginList : ArrayList() { + init { + add(NoPlugin) + addAll(ApplicationLoader.applicationContext.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA).map { NativePlugin(it) }) + } + + val lookup = mutableMapOf().apply { + for (plugin in this@PluginList) { + fun check(old: Plugin?) = check(old == null || old === plugin) { LocaleController.formatString("SSPluginConflictingName",R.string.SSPluginConflictingName,plugin.id) } + check(put(plugin.id, plugin)) + for (alias in plugin.idAliases) check(put(alias, plugin)) + } + } + val lookupNames get() = lookup.values.map { + if (it.label.isNotBlank()) { + "${it.label} (${it.id})" + } else { + it.id + } + }.map { it.takeIf { it.isNotBlank() } ?: LocaleController.getString("Disable", R.string.Disable) }.toTypedArray() +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt new file mode 100644 index 000000000..dcff2e58d --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt @@ -0,0 +1,183 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Intent +import android.content.pm.ComponentInfo +import android.content.pm.PackageManager +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.system.Os +import androidx.annotation.RequiresApi +import com.github.shadowsocks.utils.listenForPackageChanges +import org.telegram.messenger.ApplicationLoader +import org.telegram.messenger.FileLog +import java.io.File +import java.io.FileNotFoundException + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +object PluginManager { + + val app get() = ApplicationLoader.applicationContext + + fun bundleOf(pair: Pair) = Bundle().apply { putString(pair.first, pair.second) } + + class PluginNotFoundException(plugin: String) : FileNotFoundException(plugin) { + } + + private var receiver: BroadcastReceiver? = null + private var cachedPlugins: PluginList? = null + + @JvmStatic + fun fetchPlugins() = synchronized(this) { + if (receiver == null) receiver = ApplicationLoader.applicationContext.listenForPackageChanges { + synchronized(this) { + receiver = null + cachedPlugins = null + } + } + if (cachedPlugins == null) cachedPlugins = PluginList() + cachedPlugins!! + } + + private fun buildUri(id: String) = Uri.Builder() + .scheme(PluginContract.SCHEME) + .authority(PluginContract.AUTHORITY) + .path("/$id") + .build() + + @JvmStatic + fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id)) + + // the following parts are meant to be used by :bg + @Throws(Throwable::class) + fun init(configuration: PluginConfiguration): Pair? { + if (configuration.selected.isEmpty()) return null + var throwable: Throwable? = null + + try { + val result = initNative(configuration) + if (result != null) return result + } catch (t: Throwable) { + if (throwable == null) throwable = t else FileLog.e(t) + } + + // add other plugin types here + + throw throwable ?: PluginNotFoundException(configuration.selected) + } + + private fun initNative(configuration: PluginConfiguration): Pair? { + var flags = PackageManager.GET_META_DATA + if (Build.VERSION.SDK_INT >= 24) { + flags = flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE + } + val providers = app.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(configuration.selected)), flags) + if (providers.isEmpty()) return null + val provider = providers.single().providerInfo + val options = configuration.getOptions { provider.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) } + var failure: Throwable? = null + try { + initNativeFaster(provider)?.also { return it to options } + } catch (t: Throwable) { + FileLog.w("Initializing native plugin faster mode failed") + failure = t + } + + val uri = Uri.Builder().apply { + scheme(ContentResolver.SCHEME_CONTENT) + authority(provider.authority) + }.build() + try { + return initNativeFast(app.contentResolver, options, uri)?.let { it to options } + } catch (t: Throwable) { + FileLog.w("Initializing native plugin fast mode failed") + failure?.also { t.addSuppressed(it) } + failure = t + } + + try { + return initNativeSlow(app.contentResolver, options, uri)?.let { it to options } + } catch (t: Throwable) { + failure?.also { t.addSuppressed(it) } + throw t + } + } + + private fun initNativeFaster(provider: ProviderInfo): String? { + return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)?.let { relativePath -> + File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply { + check(canExecute()) + }.absolutePath + } + } + + private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String? { + return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, + bundleOf(PluginContract.EXTRA_OPTIONS to options.id))?.getString(PluginContract.EXTRA_ENTRY)?.also { + check(File(it).canExecute()) + } + } + + @SuppressLint("Recycle") + private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? { + var initialized = false + fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found") + val pluginDir = File(app.noBackupFilesDir, "plugin") + (cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null) + ?: return null).use { cursor -> + if (!cursor.moveToFirst()) entryNotFound() + pluginDir.deleteRecursively() + if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory") + val pluginDirPath = pluginDir.absolutePath + '/' + do { + val path = cursor.getString(0) + val file = File(pluginDir, path) + check(file.absolutePath.startsWith(pluginDirPath)) + cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream -> + file.outputStream().use { outStream -> inStream.copyTo(outStream) } + } + Os.chmod(file.absolutePath, when (cursor.getType(1)) { + Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1) + Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8) + else -> throw IllegalArgumentException("File mode should be of type int") + }) + if (path == options.id) initialized = true + } while (cursor.moveToNext()) + } + if (!initialized) entryNotFound() + return File(pluginDir, options.id).absolutePath + } + + fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) { + is String -> value + is Int -> app.packageManager.getResourcesForApplication(applicationInfo).getString(value) + null -> null + else -> error("meta-data $key has invalid type ${value.javaClass}") + } +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginOptions.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginOptions.kt new file mode 100644 index 000000000..990db1965 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/PluginOptions.kt @@ -0,0 +1,111 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.os.Build +import androidx.annotation.RequiresApi +import java.util.* + +/** + * Helper class for processing plugin options. + * + * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java + */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +class PluginOptions : HashMap { + var id = "" + + constructor() : super() + constructor(initialCapacity: Int) : super(initialCapacity) + constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor) + + private constructor(options: String?, parseId: Boolean) : this() { + @Suppress("NAME_SHADOWING") + var parseId = parseId + if (options.isNullOrEmpty()) return + check(options.all { !it.isISOControl() }) { "No control characters allowed." } + val tokenizer = StringTokenizer("$options;", "\\=;", true) + val current = StringBuilder() + var key: String? = null + while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) { + "\\" -> current.append(tokenizer.nextToken()) + "=" -> if (key == null) { + key = current.toString() + current.setLength(0) + } else current.append(nextToken) + ";" -> { + if (key != null) { + put(key, current.toString()) + key = null + } else if (current.isNotEmpty()) + if (parseId) id = current.toString() else put(current.toString(), null) + current.setLength(0) + parseId = false + } + else -> current.append(nextToken) + } + } + + constructor(options: String?) : this(options, true) + constructor(id: String, options: String?) : this(options, false) { + this.id = id + } + + /** + * Put but if value is null or default, the entry is deleted. + * + * @return Old value before put. + */ + fun putWithDefault(key: String, value: String?, default: String? = null) = + if (value == null || value == default) remove(key) else put(key, value) + + private fun append(result: StringBuilder, str: String) = str.indices.map { str[it] }.forEach { + when (it) { + '\\', '=', ';' -> { + result.append('\\') // intentionally no break + result.append(it) + } + else -> result.append(it) + } + } + + fun toString(trimId: Boolean): String { + val result = StringBuilder() + if (!trimId) if (id.isEmpty()) return "" else append(result, id) + for ((key, value) in entries) { + if (result.isNotEmpty()) result.append(';') + append(result, key) + if (value != null) { + result.append('=') + append(result, value) + } + } + return result.toString() + } + + override fun toString(): String = toString(true) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id + } + override fun hashCode(): Int = Objects.hash(super.hashCode(), id) +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/ResolvedPlugin.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/ResolvedPlugin.kt new file mode 100644 index 000000000..cc88fc1e5 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/plugin/ResolvedPlugin.kt @@ -0,0 +1,55 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.plugin + +import android.content.pm.ComponentInfo +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.os.Build +import androidx.annotation.RequiresApi +import com.github.shadowsocks.plugin.PluginManager.app +import com.github.shadowsocks.plugin.PluginManager.loadString + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() { + protected abstract val componentInfo: ComponentInfo + + override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! } + override val idAliases: Array by lazy { + when (val value = componentInfo.metaData.get(PluginContract.METADATA_KEY_ID_ALIASES)) { + is String -> arrayOf(value) + is Int -> app.packageManager.getResourcesForApplication(componentInfo.applicationInfo).run { + when (getResourceTypeName(value)) { + "string" -> arrayOf(getString(value)) + else -> getStringArray(value) + } + } + null -> emptyArray() + else -> error("unknown type for plugin meta-data idAliases") + } + } + override val label: CharSequence get() = resolveInfo.loadLabel(app.packageManager) + override val icon: Drawable get() = resolveInfo.loadIcon(app.packageManager) + override val defaultConfig by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) } + override val packageName: String get() = componentInfo.packageName + override val trusted = true + override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/utils/Commandline.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/utils/Commandline.kt new file mode 100644 index 000000000..71a02c793 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/utils/Commandline.kt @@ -0,0 +1,159 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.utils + +import java.util.* + +/** + * Commandline objects help handling command lines specifying processes to + * execute. + * + * The class can be used to define a command line as nested elements or as a + * helper to define a command line by an application. + * + * + * ` + *

+ *   

+ *     

+ *     

+ *     

+ *   


+ *


+` * + * + * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java + * + * Adds support for escape character '\'. + */ +object Commandline { + + /** + * Quote the parts of the given array in way that makes them + * usable as command line arguments. + * @param args the list of arguments to quote. + * @return empty string for null or no command, else every argument split + * by spaces and quoted by quoting rules. + */ + fun toString(args: Iterable?): String { + // empty path return empty string + args ?: return "" + // path containing one or more elements + val result = StringBuilder() + for (arg in args) { + if (result.isNotEmpty()) result.append(' ') + arg.indices.map { arg[it] }.forEach { + when (it) { + ' ', '\\', '"', '\'' -> { + result.append('\\') // intentionally no break + result.append(it) + } + else -> result.append(it) + } + } + } + return result.toString() + } + + /** + * Quote the parts of the given array in way that makes them + * usable as command line arguments. + * @param args the list of arguments to quote. + * @return empty string for null or no command, else every argument split + * by spaces and quoted by quoting rules. + */ + fun toString(args: Array) = toString(args.asIterable()) // thanks to Java, arrays aren't iterable + + /** + * Crack a command line. + * @param toProcess the command line to process. + * @return the command line broken into strings. + * An empty or null toProcess parameter results in a zero sized array. + */ + fun translateCommandline(toProcess: String?): Array { + if (toProcess == null || toProcess.isEmpty()) { + //no command? no string + return arrayOf() + } + // parse with a simple finite state machine + + val normal = 0 + val inQuote = 1 + val inDoubleQuote = 2 + var state = normal + val tok = StringTokenizer(toProcess, "\\\"\' ", true) + val result = ArrayList() + val current = StringBuilder() + var lastTokenHasBeenQuoted = false + var lastTokenIsSlash = false + + while (tok.hasMoreTokens()) { + val nextTok = tok.nextToken() + when (state) { + inQuote -> if ("\'" == nextTok) { + lastTokenHasBeenQuoted = true + state = normal + } else current.append(nextTok) + inDoubleQuote -> when (nextTok) { + "\"" -> if (lastTokenIsSlash) { + current.append(nextTok) + lastTokenIsSlash = false + } else { + lastTokenHasBeenQuoted = true + state = normal + } + "\\" -> lastTokenIsSlash = if (lastTokenIsSlash) { + current.append(nextTok) + false + } else true + else -> { + if (lastTokenIsSlash) { + current.append("\\") // unescaped + lastTokenIsSlash = false + } + current.append(nextTok) + } + } + else -> { + when { + lastTokenIsSlash -> { + current.append(nextTok) + lastTokenIsSlash = false + } + "\\" == nextTok -> lastTokenIsSlash = true + "\'" == nextTok -> state = inQuote + "\"" == nextTok -> state = inDoubleQuote + " " == nextTok -> if (lastTokenHasBeenQuoted || current.isNotEmpty()) { + result.add(current.toString()) + current.setLength(0) + } + else -> current.append(nextTok) + } + lastTokenHasBeenQuoted = false + } + } + } + if (lastTokenHasBeenQuoted || current.isNotEmpty()) result.add(current.toString()) + require(state != inQuote && state != inDoubleQuote) { "unbalanced quotes in $toProcess" } + require(!lastTokenIsSlash) { "escape character following nothing in $toProcess" } + return result.toTypedArray() + } +} diff --git a/TMessagesProj/src/main/java/com/github/shadowsocks/utils/Utils.kt b/TMessagesProj/src/main/java/com/github/shadowsocks/utils/Utils.kt new file mode 100644 index 000000000..a95950988 --- /dev/null +++ b/TMessagesProj/src/main/java/com/github/shadowsocks/utils/Utils.kt @@ -0,0 +1,136 @@ +/******************************************************************************* + * * + * Copyright (C) 2018 by Max Lv * + * Copyright (C) 2018 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.utils + +import android.annotation.SuppressLint +import android.content.* +import android.content.pm.PackageInfo +import android.content.res.Resources +import android.graphics.BitmapFactory +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import android.system.Os +import android.system.OsConstants +import android.util.TypedValue +import androidx.annotation.AttrRes +import androidx.annotation.RequiresApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import org.telegram.messenger.FileLog +import java.io.FileDescriptor +import java.net.HttpURLConnection +import java.net.InetAddress +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +fun Iterable.forEachTry(action: (T) -> Unit) { + var result: Exception? = null + for (element in this) try { + action(element) + } catch (e: Exception) { + if (result == null) result = e else result.addSuppressed(e) + } + if (result != null) { + FileLog.e(result) + throw result + } +} + +val Throwable.readableMessage get() = localizedMessage ?: javaClass.name + +/** + * https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466 + */ +private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$") +val FileDescriptor.int get() = getInt.invoke(this) as Int + +private val parseNumericAddress by lazy @SuppressLint("SoonBlockedPrivateApi") { + InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply { + isAccessible = true + } +} + +/** + * A slightly more performant variant of parseNumericAddress. + * + * Bug in Android 9.0 and lower: https://issuetracker.google.com/issues/123456213 + */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this) + ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { + if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(null, this) as InetAddress + } + +suspend fun HttpURLConnection.useCancellable(block: suspend HttpURLConnection.() -> T): T { + return suspendCancellableCoroutine { cont -> + cont.invokeOnCancellation { + if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() } + } + GlobalScope.launch(Dispatchers.IO) { + try { + cont.resume(block()) + } catch (e: Throwable) { + cont.resumeWithException(e) + } + } + } +} + +fun parsePort(str: String?, default: Int, min: Int = 1025): Int { + val value = str?.toIntOrNull() ?: default + return if (value < min || value > 65535) default else value +} + +fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) = callback(context, intent) +} + +fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + callback() + if (onetime) context.unregisterReceiver(this) + } +}.apply { + registerReceiver(this, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }) +} + +fun ContentResolver.openBitmap(uri: Uri) = + if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri)) + else BitmapFactory.decodeStream(openInputStream(uri)) + +val PackageInfo.signaturesCompat + get() = + if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures + +/** + * Based on: https://stackoverflow.com/a/26348729/2245107 + */ +fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int { + val typedValue = TypedValue() + if (!resolveAttribute(resId, typedValue, true)) throw Resources.NotFoundException() + return typedValue.resourceId +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/com/v2ray/ang/V2RayConfig.kt b/TMessagesProj/src/main/java/com/v2ray/ang/V2RayConfig.kt new file mode 100644 index 000000000..b60f30258 --- /dev/null +++ b/TMessagesProj/src/main/java/com/v2ray/ang/V2RayConfig.kt @@ -0,0 +1,68 @@ +package com.v2ray.ang + +/** + * + * App Config Const + */ +object V2RayConfig { + 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 VMESS1_PROTOCOL = "vmess1://" + const val SS_PROTOCOL: String = "ss://" + const val SSR_PROTOCOL: String = "ssr://" + const val RB_PROTOCOL: String = "rb://" + + 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 { + + @JvmField + val Vmess = 1 + + @JvmField + val Shadowsocks = 3 + + } + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/com/v2ray/ang/dto/AngConfig.kt b/TMessagesProj/src/main/java/com/v2ray/ang/dto/AngConfig.kt new file mode 100644 index 000000000..5af545880 --- /dev/null +++ b/TMessagesProj/src/main/java/com/v2ray/ang/dto/AngConfig.kt @@ -0,0 +1,82 @@ +package com.v2ray.ang.dto + +import cn.hutool.core.codec.Base64 +import com.google.gson.Gson +import com.v2ray.ang.V2RayConfig +import com.v2ray.ang.V2RayConfig.SS_PROTOCOL +import com.v2ray.ang.V2RayConfig.VMESS_PROTOCOL + +data class AngConfig( + var index: Int, + var vmess: ArrayList, + var subItem: ArrayList +) { + data class VmessBean(var guid: String = "123456", + var address: String = "", + var port: Int = 443, + var id: String = "", + var alterId: Int = 64, + var security: String = "auto", + var network: String = "tcp", + var remarks: String = "", + var headerType: String = "none", + var requestHost: String = "", + var path: String = "", + var streamSecurity: String = "", + var configType: Int = 1, + var configVersion: Int = 2, + var testResult: String = "") { + + override fun equals(other: Any?): Boolean { + return super.equals(other) || (other is VmessBean && + address == other.address && + port == other.port && + id == other.id && + network == other.network && + headerType == other.headerType && + requestHost == other.requestHost && + path == other.path) + } + + override fun toString(): String { + + if (configType == V2RayConfig.EConfigType.Vmess) { + + val vmessQRCode = VmessQRCode() + + vmessQRCode.v = configVersion.toString() + vmessQRCode.ps = remarks + vmessQRCode.add = address + vmessQRCode.port = port.toString() + vmessQRCode.id = id + vmessQRCode.aid = alterId.toString() + vmessQRCode.net = network + vmessQRCode.type = headerType + vmessQRCode.host = requestHost + vmessQRCode.path = path + vmessQRCode.tls = streamSecurity + + return VMESS_PROTOCOL + cn.hutool.core.codec.Base64.encode(Gson().toJson(vmessQRCode)) + + } else if (configType == V2RayConfig.EConfigType.Shadowsocks) { + + val remark = "#" + Base64.encodeUrlSafe(remarks) + + val url = String.format("%s:%s@%s:%s", security, id, address, port) + + return SS_PROTOCOL + Base64.encode(url.toByteArray(charset("UTF-8"))) + remark + + } else { + + error("invalid vmess bean type") + + } + + } + + } + + data class SubItemBean(var id: String = "", + var remarks: String = "", + var url: String = "") +} diff --git a/TMessagesProj/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt b/TMessagesProj/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt new file mode 100644 index 000000000..c3712f5d0 --- /dev/null +++ b/TMessagesProj/src/main/java/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? = null, + var domain: ArrayList? = null, + var outboundTag: String = "", + var port: String? = null, + var inboundTag: ArrayList? = null) + } + + data class PolicyBean(var levels: Map, + var system: Any?=null) { + data class LevelBean( + var handshake: Int? = null, + var connIdle: Int? = null, + var uplinkOnly: Int? = null, + var downlinkOnly: Int? = null) + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt b/TMessagesProj/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt new file mode 100644 index 000000000..30618017b --- /dev/null +++ b/TMessagesProj/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt @@ -0,0 +1,13 @@ +package com.v2ray.ang.dto + +data class VmessQRCode(var v: String = "", + var ps: String = "", + var add: String = "", + var port: String = "", + var id: String = "", + var aid: String = "", + var net: String = "", + var type: String = "", + var host: String = "", + var path: String = "", + var tls: String = "") \ No newline at end of file diff --git a/TMessagesProj/src/main/java/com/v2ray/ang/util/Utils.kt b/TMessagesProj/src/main/java/com/v2ray/ang/util/Utils.kt new file mode 100644 index 000000000..21a5a6535 --- /dev/null +++ b/TMessagesProj/src/main/java/com/v2ray/ang/util/Utils.kt @@ -0,0 +1,172 @@ +package com.v2ray.ang.util + +import android.text.TextUtils +import cn.hutool.core.codec.Base64 +import org.telegram.messenger.ApplicationLoader +import java.io.IOException +import java.net.Socket +import java.net.URLDecoder +import java.net.URLEncoder +import java.net.UnknownHostException + + +object Utils { + + + /** + * parseInt + */ + fun parseInt(str: String): Int { + try { + return Integer.parseInt(str) + } catch (e: Exception) { + e.printStackTrace() + return 0 + } + } + + /** + * is ip address + */ + fun isIpAddress(value: String): Boolean { + try { + var addr = value + if (addr.isEmpty() || addr.isBlank()) { + return false + } + //CIDR + if (addr.indexOf("/") > 0) { + val arr = addr.split("/") + if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) { + addr = arr[0] + } + } + + // "::ffff:192.168.173.22" + // "[::ffff:192.168.173.22]:80" + if (addr.startsWith("::ffff:") && '.' in addr) { + addr = addr.drop(7) + } else if (addr.startsWith("[::ffff:") && '.' in addr) { + addr = addr.drop(8).replace("]", "") + } + + // addr = addr.toLowerCase() + var octets = addr.split('.').toTypedArray() + if (octets.size == 4) { + if (octets[3].indexOf(":") > 0) { + addr = addr.substring(0, addr.indexOf(":")) + } + return isIpv4Address(addr) + } + + // Ipv6addr [2001:abc::123]:8080 + return isIpv6Address(addr) + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + fun isPureIpAddress(value: String): Boolean { + return (isIpv4Address(value) || isIpv6Address(value)) + } + + @JvmStatic + fun isIpv4Address(value: String): Boolean { + val regV4 = 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])$") + return regV4.matches(value) + } + + @JvmStatic + fun isIpv6Address(value: String): Boolean { + var addr = value + if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) { + addr = addr.drop(1) + addr = addr.dropLast(addr.count() - addr.lastIndexOf("]")) + } + val regV6 = 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}$") + return regV6.matches(addr) + } + + fun urlDecode(url: String): String { + try { + return URLDecoder.decode(url, "UTF-8") + } catch (e: Exception) { + e.printStackTrace() + return url + } + } + + fun urlEncode(url: String): String { + try { + return URLEncoder.encode(url, "UTF-8") + } catch (e: Exception) { + e.printStackTrace() + return url + } + } + + /** + * readTextFromAssets + */ + fun readTextFromAssets(fileName: String): String { + val content = ApplicationLoader.applicationContext.assets.open(fileName).bufferedReader().use { + it.readText() + } + return content + } + + /** + * ping + */ + fun ping(url: String): String { + try { + val command = "/system/bin/ping -c 3 $url" + val process = Runtime.getRuntime().exec(command) + val allText = process.inputStream.bufferedReader().use { it.readText() } + if (!TextUtils.isEmpty(allText)) { + val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19) + val temps = tempInfo.split("/".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray() + if (temps.count() > 0 && temps[0].length < 10) { + return temps[0].toFloat().toInt().toString() + "ms" + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "-1ms" + } + + /** + * tcping + */ + fun tcping(url: String, port: Int): String { + var time = -1L + for (k in 0 until 2) { + val one = socketConnectTime(url, port) + if (one != -1L) + if (time == -1L || one < time) { + time = one + } + } + return time.toString() + "ms" + } + + fun socketConnectTime(url: String, port: Int): Long { + try { + val start = System.currentTimeMillis() + val socket = Socket(url, port) + val time = System.currentTimeMillis() - start + socket.close() + return time + } catch (e: UnknownHostException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + } + return -1 + } +} + diff --git a/TMessagesProj/src/main/java/com/v2ray/ang/util/V2rayConfigUtil.kt b/TMessagesProj/src/main/java/com/v2ray/ang/util/V2rayConfigUtil.kt new file mode 100644 index 000000000..f36d10abf --- /dev/null +++ b/TMessagesProj/src/main/java/com/v2ray/ang/util/V2rayConfigUtil.kt @@ -0,0 +1,371 @@ +package com.v2ray.ang.util + +import android.text.TextUtils +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.v2ray.ang.V2RayConfig +import com.v2ray.ang.dto.AngConfig.VmessBean +import com.v2ray.ang.dto.V2rayConfig +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +object V2rayConfigUtil { + private val requestObj: JsonObject by lazy { + Gson().fromJson("""{"version":"1.1","method":"GET","path":["/"],"headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""", JsonObject::class.java) + } + +// private val responseObj: JSONObject by lazy { +// JSONObject("""{"version":"1.1","status":"200","reason":"OK","headers":{"Content-Type":["application/octet-stream","video/mpeg"],"Transfer-Encoding":["chunked"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""") +// } + + data class Result(var status: Boolean, var content: String) + + @JvmStatic + var currDomain: String = "" + + /** + * 生成v2ray的客户端配置文件 + */ + @JvmStatic + fun getV2rayConfig(vmess: VmessBean, port: Int): Result { + val result = Result(false, "") + try { + //取得默认配置 + val assets = Utils.readTextFromAssets("v2ray_config.json") + if (TextUtils.isEmpty(assets)) { + return result + } + + //转成Json + val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result +// if (v2rayConfig == null) { +// return result +// } + + inbounds(vmess, v2rayConfig, port) + + outbounds(vmess, v2rayConfig) + + routing(vmess, v2rayConfig) + + val finalConfig = GsonBuilder().setPrettyPrinting().create().toJson(v2rayConfig) + + result.status = true + result.content = finalConfig + return result + + } catch (e: Exception) { + e.printStackTrace() + return result + } + } + + /** + * + */ + private fun inbounds(vmess: VmessBean, v2rayConfig: V2rayConfig, port: Int): Boolean { + try { + v2rayConfig.inbounds.forEach { curInbound -> + curInbound.listen = "127.0.0.1" + } + v2rayConfig.inbounds[0].port = port +// val socksPort = Utils.parseInt(app.defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808")) +// val lanconnPort = Utils.parseInt(app.defaultDPreference.getPrefString(SettingsActivity.PREF_HTTP_PORT, "")) + +// if (socksPort > 0) { +// v2rayConfig.inbounds[0].port = socksPort +// } +// if (lanconnPort > 0) { +// val httpCopy = v2rayConfig.inbounds[0].copy() +// httpCopy.port = lanconnPort +// httpCopy.protocol = "http" +// v2rayConfig.inbounds.add(httpCopy) +// } + v2rayConfig.inbounds[0].sniffing?.enabled = false + + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * vmess协议服务器配置 + */ + private fun outbounds(vmess: VmessBean, v2rayConfig: V2rayConfig): Boolean { + try { + val outbound = v2rayConfig.outbounds[0] + + when (vmess.configType) { + V2RayConfig.EConfigType.Vmess -> { + outbound.settings?.servers = null + + val vnext = v2rayConfig.outbounds[0].settings?.vnext?.get(0) + vnext?.address = vmess.address + vnext?.port = vmess.port + val user = vnext?.users?.get(0) + user?.id = vmess.id + user?.alterId = vmess.alterId + user?.security = vmess.security + user?.level = 8 + + //Mux + val muxEnabled = false//app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_MUX_ENABLED, false) + outbound.mux?.enabled = muxEnabled + + //远程服务器底层传输配置 + outbound.streamSettings = boundStreamSettings(vmess) + + outbound.protocol = "vmess" + } + V2RayConfig.EConfigType.Shadowsocks -> { + outbound.settings?.vnext = null + + val server = outbound.settings?.servers?.get(0) + server?.address = vmess.address + server?.method = vmess.security + server?.ota = false + server?.password = vmess.id + server?.port = vmess.port + server?.level = 8 + + //Mux + outbound.mux?.enabled = false + + outbound.protocol = "shadowsocks" + } + else -> { + } + } + + var serverDomain: String + if (Utils.isIpv6Address(vmess.address)) { + serverDomain = String.format("[%s]:%s", vmess.address, vmess.port) + } else { + serverDomain = String.format("%s:%s", vmess.address, vmess.port) + } + currDomain = serverDomain + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * 远程服务器底层传输配置 + */ + private fun boundStreamSettings(vmess: VmessBean): V2rayConfig.OutboundBean.StreamSettingsBean { + val streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean("", "", null, null, null, null, null, null) + try { + //远程服务器底层传输配置 + streamSettings.network = vmess.network + streamSettings.security = vmess.streamSecurity + + //streamSettings + when (streamSettings.network) { + "kcp" -> { + val kcpsettings = V2rayConfig.OutboundBean.StreamSettingsBean.KcpsettingsBean() + kcpsettings.mtu = 1350 + kcpsettings.tti = 50 + kcpsettings.uplinkCapacity = 12 + kcpsettings.downlinkCapacity = 100 + kcpsettings.congestion = false + kcpsettings.readBufferSize = 1 + kcpsettings.writeBufferSize = 1 + kcpsettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.KcpsettingsBean.HeaderBean() + kcpsettings.header.type = vmess.headerType + streamSettings.kcpsettings = kcpsettings + } + "ws" -> { + val wssettings = V2rayConfig.OutboundBean.StreamSettingsBean.WssettingsBean() + wssettings.connectionReuse = true + val host = vmess.requestHost.trim() + val path = vmess.path.trim() + + if (!TextUtils.isEmpty(host)) { + wssettings.headers = V2rayConfig.OutboundBean.StreamSettingsBean.WssettingsBean.HeadersBean() + wssettings.headers.Host = host + } + if (!TextUtils.isEmpty(path)) { + wssettings.path = path + } + streamSettings.wssettings = wssettings + + val tlssettings = V2rayConfig.OutboundBean.StreamSettingsBean.TlssettingsBean() + tlssettings.allowInsecure = true + if (!TextUtils.isEmpty(host)) { + tlssettings.serverName = host + } + streamSettings.tlssettings = tlssettings + } + "h2" -> { + val httpsettings = V2rayConfig.OutboundBean.StreamSettingsBean.HttpsettingsBean() + val host = vmess.requestHost.trim() + val path = vmess.path.trim() + + if (!TextUtils.isEmpty(host)) { + httpsettings.host = host.split(",").map { it.trim() } + } + httpsettings.path = path + streamSettings.httpsettings = httpsettings + + val tlssettings = V2rayConfig.OutboundBean.StreamSettingsBean.TlssettingsBean() + tlssettings.allowInsecure = true + streamSettings.tlssettings = tlssettings + } + "quic" -> { + val quicsettings = V2rayConfig.OutboundBean.StreamSettingsBean.QuicsettingBean() + val host = vmess.requestHost.trim() + val path = vmess.path.trim() + + quicsettings.security = host + quicsettings.key = path + + quicsettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.QuicsettingBean.HeaderBean() + quicsettings.header.type = vmess.headerType + + streamSettings.quicsettings = quicsettings + } + else -> { + //tcp带http伪装 + if (vmess.headerType == "http") { + val tcpSettings = V2rayConfig.OutboundBean.StreamSettingsBean.TcpsettingsBean() + tcpSettings.connectionReuse = true + tcpSettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.TcpsettingsBean.HeaderBean() + tcpSettings.header.type = vmess.headerType + +// if (requestObj.has("headers") +// || requestObj.optJSONObject("headers").has("Pragma")) { +// val arrHost = ArrayList() +// vmess.requestHost +// .split(",") +// .forEach { +// arrHost.add(it) +// } +// requestObj.optJSONObject("headers") +// .put("Host", arrHost) +// +// } + if (!TextUtils.isEmpty(vmess.requestHost)) { + val arrHost = ArrayList() + vmess.requestHost + .split(",") + .forEach { + arrHost.add("\"$it\"") + } + requestObj.getAsJsonObject("headers") + .add("Host", Gson().fromJson(arrHost.toString(), JsonArray::class.java)) + } + if (!TextUtils.isEmpty(vmess.path)) { + val arrPath = ArrayList() + vmess.path + .split(",") + .forEach { + arrPath.add("\"$it\"") + } + requestObj.add("path", Gson().fromJson(arrPath.toString(), JsonArray::class.java)) + } + tcpSettings.header.request = requestObj + //tcpSettings.header.response = responseObj + streamSettings.tcpSettings = tcpSettings + } + } + } + } catch (e: Exception) { + e.printStackTrace() + return streamSettings + } + return streamSettings + } + + /** + * routing + */ + private fun routing(vmess: VmessBean, v2rayConfig: V2rayConfig) { + + v2rayConfig.routing.domainStrategy = "IPIfNonMatch" + + } + + /** + * is valid config + */ + fun isValidConfig(conf: String): Boolean { + try { + val jObj = JSONObject(conf) + var hasBound = false + //hasBound = (jObj.has("outbounds") and jObj.has("inbounds")) or (jObj.has("outbound") and jObj.has("inbound")) + hasBound = (jObj.has("outbounds")) or (jObj.has("outbound")) + return hasBound + } catch (e: JSONException) { + return false + } + } + + private fun parseDomainName(jsonConfig: String): String { + try { + val jObj = JSONObject(jsonConfig) + var domainName: String + if (jObj.has("outbound")) { + domainName = parseDomainName(jObj.optJSONObject("outbound")) + if (!TextUtils.isEmpty(domainName)) { + return domainName + } + } + if (jObj.has("outbounds")) { + for (i in 0..(jObj.optJSONArray("outbounds").length() - 1)) { + domainName = parseDomainName(jObj.optJSONArray("outbounds").getJSONObject(i)) + if (!TextUtils.isEmpty(domainName)) { + return domainName + } + } + } + if (jObj.has("outboundDetour")) { + for (i in 0..(jObj.optJSONArray("outboundDetour").length() - 1)) { + domainName = parseDomainName(jObj.optJSONArray("outboundDetour").getJSONObject(i)) + if (!TextUtils.isEmpty(domainName)) { + return domainName + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + + private fun parseDomainName(outbound: JSONObject): String { + try { + if (outbound.has("settings")) { + var vnext: JSONArray? + if (outbound.optJSONObject("settings").has("vnext")) { + // vmess + vnext = outbound.optJSONObject("settings").optJSONArray("vnext") + } else if (outbound.optJSONObject("settings").has("servers")) { + // shadowsocks or socks + vnext = outbound.optJSONObject("settings").optJSONArray("servers") + } else { + return "" + } + for (i in 0..(vnext.length() - 1)) { + val item = vnext.getJSONObject(i) + val address = item.getString("address") + val port = item.getString("port") + if (Utils.isIpv6Address(address)) { + return String.format("[%s]:%s", address, port) + } else { + return String.format("%s:%s", address, port) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java index 89cfc8a70..7928cb1b4 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java @@ -19,7 +19,6 @@ import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -48,10 +47,6 @@ import android.provider.CallLog; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.provider.Settings; - -import androidx.core.content.FileProvider; -import androidx.viewpager.widget.ViewPager; - import android.telephony.TelephonyManager; import android.text.Selection; import android.text.Spannable; @@ -67,8 +62,8 @@ import android.util.DisplayMetrics; import android.util.StateSet; import android.util.TypedValue; import android.view.Display; -import android.view.MotionEvent; import android.view.Gravity; +import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewGroup; @@ -89,10 +84,14 @@ import android.widget.ListView; import android.widget.ScrollView; import android.widget.TextView; +import androidx.core.content.FileProvider; +import androidx.viewpager.widget.ViewPager; + import com.android.internal.telephony.ITelephony; import org.telegram.PhoneFormat.PhoneFormat; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.RequestTimeDelegate; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.AlertDialog; @@ -135,10 +134,25 @@ import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import cn.hutool.core.util.StrUtil; +import kotlin.Unit; +import tw.nekomimi.nekogram.BottomBuilder; import tw.nekomimi.nekogram.NekoConfig; +import tw.nekomimi.nekogram.NekoXConfig; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.EnvUtil; +import tw.nekomimi.nekogram.utils.FileUtil; +import tw.nekomimi.nekogram.utils.ProxyUtil; +import tw.nekomimi.nekogram.utils.UIUtil; + +import static com.v2ray.ang.V2RayConfig.SSR_PROTOCOL; +import static com.v2ray.ang.V2RayConfig.SS_PROTOCOL; +import static com.v2ray.ang.V2RayConfig.VMESS1_PROTOCOL; +import static com.v2ray.ang.V2RayConfig.VMESS_PROTOCOL; public class AndroidUtilities { @@ -285,8 +299,36 @@ public class AndroidUtilities { return true; }; + public static boolean addProxyLinks(Spannable text) { + if (text == null) return false; + final URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); + for (int i = old.length - 1; i >= 0; i--) { + String url = old[i].getURL(); + if (url.startsWith("vmess") || url.startsWith("ss")) { + text.removeSpan(old[i]); + } + } + final ArrayList links = new ArrayList<>(); + gatherLinks(links, text, LinkifyPort.PROXY_PATTERN, new String[]{VMESS_PROTOCOL, VMESS1_PROTOCOL, SS_PROTOCOL, SSR_PROTOCOL/*, RB_PROTOCOL*/}, sUrlMatchFilter); + pruneOverlaps(links); + if (links.size() == 0) { + return false; + } + for (int a = 0, N = links.size(); a < N; a++) { + LinkSpec link = links.get(a); + URLSpan[] oldSpans = text.getSpans(link.start, link.end, URLSpan.class); + if (oldSpans != null && oldSpans.length > 0) { + for (int b = 0; b < oldSpans.length; b++) { + text.removeSpan(oldSpans[b]); + } + } + text.setSpan(new URLSpan(link.url), link.start, link.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return true; + } + public static boolean addLinks(Spannable text, int mask) { - if (text != null && containsUnsupportedCharacters(text.toString()) || mask == 0) { + if (text == null || containsUnsupportedCharacters(text.toString()) || mask == 0) { return false; } final URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class); @@ -308,8 +350,8 @@ public class AndroidUtilities { LinkSpec link = links.get(a); URLSpan[] oldSpans = text.getSpans(link.start, link.end, URLSpan.class); if (oldSpans != null && oldSpans.length > 0) { - for (int b = 0; b < oldSpans.length; b++) { - text.removeSpan(oldSpans[b]); + for (URLSpan oldSpan : oldSpans) { + text.removeSpan(oldSpan); } } text.setSpan(new URLSpan(link.url), link.start, link.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -381,7 +423,7 @@ public class AndroidUtilities { color = 1; } else if (name.contains(".pdf") || name.contains(".ppt") || name.contains(".key")) { color = 2; - } else if (name.contains(".zip") || name.contains(".rar") || name.contains(".ai") || name.contains(".mp3") || name.contains(".mov") || name.contains(".avi")) { + } else if (name.contains(".zip") || name.contains(".rar") || name.contains(".ai") || name.contains(".mp3") || name.contains(".mov") || name.contains(".avi")) { color = 3; } if (color == -1) { @@ -594,12 +636,6 @@ public class AndroidUtilities { if (pathString.matches(Pattern.quote(new File(ApplicationLoader.applicationContext.getCacheDir(), "voip_logs").getAbsolutePath()) + "/\\d+\\.log")) { return false; } - if (NekoConfig.saveCacheToPrivateDirectory && pathString.startsWith(new File(ApplicationLoader.applicationContext.getCacheDir(), "sdcard").getAbsolutePath())) { - return false; - } - if (NekoConfig.saveCacheToPrivateDirectory && pathString.startsWith(new File(ApplicationLoader.applicationContext.getFilesDir(), "Telegram").getAbsolutePath())) { - return false; - } int tries = 0; while (true) { if (pathString != null && pathString.length() > 4096) { @@ -1245,15 +1281,14 @@ public class AndroidUtilities { } } if (!TextUtils.isEmpty(locale2)) { - return new String[]{locale.replace('_', '-'), locale2}; + return new String[]{locale.replace('_', '-'), locale2, "en"}; } else { - return new String[]{locale.replace('_', '-')}; + return new String[]{locale.replace('_', '-'), "en"}; } } else { - return new String[]{locale.replace('_', '-')}; + return new String[]{locale.replace('_', '-'), "en"}; } } catch (Exception ignore) { - } return new String[]{"en"}; } @@ -1276,33 +1311,13 @@ public class AndroidUtilities { public static File getCacheDir() { String state = null; try { - state = Environment.getExternalStorageState(); - } catch (Exception e) { + File file = new File(EnvUtil.getTelegramPath(), "caches"); + FileUtil.initDir(file); + return file; + } catch (Throwable e) { FileLog.e(e); } - if (!NekoConfig.saveCacheToPrivateDirectory && (state == null || state.startsWith(Environment.MEDIA_MOUNTED))) { - try { - File file = ApplicationLoader.applicationContext.getExternalCacheDir(); - if (file != null) { - return file; - } - } catch (Exception e) { - FileLog.e(e); - } - } - try { - File file = ApplicationLoader.applicationContext.getCacheDir(); - if (file != null) { - if(NekoConfig.saveCacheToPrivateDirectory) { - file = new File(file, "sdcard"); - file.mkdirs(); - } - return file; - } - } catch (Exception e) { - FileLog.e(e); - } - return new File(""); + return new File(ApplicationLoader.getDataDirFixed(), "cache/media/caches"); } public static int dp(float value) { @@ -1481,10 +1496,8 @@ public class AndroidUtilities { } public static boolean isTablet() { - if (NekoConfig.forceTablet) { - return true; - } else if (isTablet == null) { - isTablet = ApplicationLoader.applicationContext.getResources().getBoolean(R.bool.isTablet); + if (isTablet == null) { + isTablet = NekoConfig.forceTablet || ApplicationLoader.applicationContext.getResources().getBoolean(R.bool.isTablet); } return isTablet; } @@ -1626,8 +1639,7 @@ public class AndroidUtilities { Rect insets = (Rect) mStableInsetsField.get(mAttachInfo); return insets.bottom; } - } catch (Exception e) { - FileLog.e(e); + } catch (Exception ignored) { } return 0; } @@ -1847,6 +1859,7 @@ public class AndroidUtilities { } return false; } + } public static boolean needShowPasscode() { @@ -1938,11 +1951,33 @@ public class AndroidUtilities { }*/ public static void startAppCenter(Activity context) { - + /*try { + if (BuildVars.DEBUG_VERSION) { + Distribute.setEnabledForDebuggableBuild(true); + AppCenter.start(context.getApplication(), BuildVars.DEBUG_VERSION ? BuildVars.APPCENTER_HASH_DEBUG : BuildVars.APPCENTER_HASH, Distribute.class, Crashes.class); + } else { + AppCenter.start(context.getApplication(), BuildVars.DEBUG_VERSION ? BuildVars.APPCENTER_HASH_DEBUG : BuildVars.APPCENTER_HASH, Crashes.class); + } + AppCenter.setUserId("uid=" + UserConfig.getInstance(UserConfig.selectedAccount).clientUserId); + } catch (Throwable e) { + FileLog.e(e); + }*/ } private static long lastUpdateCheckTime; + public static void checkForUpdates() { + /*try { + if (BuildVars.DEBUG_VERSION) { + if (SystemClock.elapsedRealtime() - lastUpdateCheckTime < 60 * 60 * 1000) { + return; + } + lastUpdateCheckTime = SystemClock.elapsedRealtime(); + Distribute.checkForUpdate(); + } + } catch (Throwable e) { + FileLog.e(e); + }*/ } public static void addToClipboard(CharSequence str) { @@ -1983,7 +2018,7 @@ public class AndroidUtilities { } File storageDir = null; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { - storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Telegram"); + storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "NekoX"); if (!storageDir.mkdirs()) { if (!storageDir.exists()) { if (BuildVars.LOGS_ENABLED) { @@ -2294,8 +2329,7 @@ public class AndroidUtilities { return String.format(Locale.US, "%d:%02d:%02d / %02d:%02d", ph, pm, ps, m, s); } else if (ph == 0) { return String.format(Locale.US, "%02d:%02d / %d:%02d:%02d", pm, ps, h, m, s); - } - else { + } else { return String.format(Locale.US, "%d:%02d:%02d / %d:%02d:%02d", ph, pm, ps, h, m, s); } } @@ -2440,7 +2474,7 @@ public class AndroidUtilities { parentFragment.presentFragment(new ThemePreviewActivity(themeInfo)); } else { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); builder.setMessage(LocaleController.getString("IncorrectTheme", R.string.IncorrectTheme)); builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); parentFragment.showDialog(builder.create()); @@ -2463,18 +2497,18 @@ public class AndroidUtilities { } } if (Build.VERSION.SDK_INT >= 24) { - intent.setDataAndType(FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", f), realMimeType != null ? realMimeType : "text/plain"); + intent.setDataAndType(FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", f), realMimeType != null ? realMimeType : "*/*"); } else { - intent.setDataAndType(Uri.fromFile(f), realMimeType != null ? realMimeType : "text/plain"); + intent.setDataAndType(Uri.fromFile(f), realMimeType != null ? realMimeType : "*/*"); } if (realMimeType != null) { try { activity.startActivityForResult(intent, 500); } catch (Exception e) { if (Build.VERSION.SDK_INT >= 24) { - intent.setDataAndType(FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", f), "text/plain"); + intent.setDataAndType(FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", f), "*/*"); } else { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); + intent.setDataAndType(Uri.fromFile(f), "*/*"); } activity.startActivityForResult(intent, 500); } @@ -2486,7 +2520,7 @@ public class AndroidUtilities { return; } AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); builder.setMessage(LocaleController.formatString("NoHandleAppInstalled", R.string.NoHandleAppInstalled, message.getDocument().mime_type)); if (parentFragment != null) { @@ -2527,17 +2561,17 @@ public class AndroidUtilities { } } if (Build.VERSION.SDK_INT >= 26 && realMimeType != null && realMimeType.equals("application/vnd.android.package-archive") && !ApplicationLoader.applicationContext.getPackageManager().canRequestPackageInstalls()) { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder.setMessage(LocaleController.getString("ApkRestricted", R.string.ApkRestricted)); - builder.setPositiveButton(LocaleController.getString("PermissionOpenSettings", R.string.PermissionOpenSettings), (dialogInterface, i) -> { + BottomBuilder builder = new BottomBuilder(activity); + builder.addTitle(LocaleController.getString("ApkRestricted", R.string.ApkRestricted)); + builder.addItem(LocaleController.getString("PermissionOpenSettings", R.string.PermissionOpenSettings), R.drawable.baseline_settings_24,(i) -> { try { activity.startActivity(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + activity.getPackageName()))); } catch (Exception e) { FileLog.e(e); } + return Unit.INSTANCE; }); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + builder.addCancelItem(); builder.show(); return true; } @@ -2677,62 +2711,22 @@ public class AndroidUtilities { if (intent == null) { return false; } - try { - if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { - return false; - } - Uri data = intent.getData(); - if (data != null) { - String user = null; - String password = null; - String port = null; - String address = null; - String secret = null; - String scheme = data.getScheme(); - if (scheme != null) { - if ((scheme.equals("http") || scheme.equals("https"))) { - String host = data.getHost().toLowerCase(); - if (host.equals("telegram.me") || host.equals("t.me") || host.equals("telegram.dog")) { - String path = data.getPath(); - if (path != null) { - if (path.startsWith("/socks") || path.startsWith("/proxy")) { - address = data.getQueryParameter("server"); - port = data.getQueryParameter("port"); - user = data.getQueryParameter("user"); - password = data.getQueryParameter("pass"); - secret = data.getQueryParameter("secret"); - } - } - } - } else if (scheme.equals("tg")) { - String url = data.toString(); - if (url.startsWith("tg:proxy") || url.startsWith("tg://proxy") || url.startsWith("tg:socks") || url.startsWith("tg://socks")) { - url = url.replace("tg:proxy", "tg://telegram.org").replace("tg://proxy", "tg://telegram.org").replace("tg://socks", "tg://telegram.org").replace("tg:socks", "tg://telegram.org"); - data = Uri.parse(url); - address = data.getQueryParameter("server"); - port = data.getQueryParameter("port"); - user = data.getQueryParameter("user"); - password = data.getQueryParameter("pass"); - secret = data.getQueryParameter("secret"); - } - } - } - if (!TextUtils.isEmpty(address) && !TextUtils.isEmpty(port)) { - if (user == null) { - user = ""; - } - if (password == null) { - password = ""; - } - if (secret == null) { - secret = ""; - } - showProxyAlert(activity, address, port, user, password, secret); - return true; - } - } - } catch (Exception ignore) { - + if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { + return false; + } + Uri data = intent.getData(); + if (data == null) return false; + String link = data.toString(); + if (link.startsWith("tg://proxy") || + link.startsWith("tg://socks") || + link.startsWith("https://t.me/proxy") || + link.startsWith("https://t.me/socks") || + link.startsWith(VMESS_PROTOCOL) || + link.startsWith(VMESS1_PROTOCOL) || + link.startsWith(SS_PROTOCOL) || + link.startsWith(SSR_PROTOCOL) /*|| + data.startsWith(RB_PROTOCOL)*/) { + return ProxyUtil.importProxy(activity, link); } return false; } @@ -2752,7 +2746,7 @@ public class AndroidUtilities { return true; } - public static void showProxyAlert(Activity activity, final String address, final String port, final String user, final String password, final String secret) { + public static void showProxyAlert(Context activity, final String address, final String port, final String user, final String password, final String secret, final String remarks) { BottomSheet.Builder builder = new BottomSheet.Builder(activity); final Runnable dismissRunnable = builder.getDismissRunnable(); @@ -2791,7 +2785,7 @@ public class AndroidUtilities { } else if (a == 4) { text = password; detail = LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword); - } else if (a == 5) { + } else { text = LocaleController.getString("Checking", R.string.Checking); detail = LocaleController.getString("Checking", R.string.Checking); } @@ -2825,44 +2819,538 @@ public class AndroidUtilities { pickerBottomLayout.cancelButton.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); pickerBottomLayout.cancelButton.setText(LocaleController.getString("Cancel", R.string.Cancel).toUpperCase()); pickerBottomLayout.cancelButton.setOnClickListener(view -> dismissRunnable.run()); + + pickerBottomLayout.doneButtonTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.doneButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + + pickerBottomLayout.doneButtonBadgeTextView.setVisibility(View.GONE); + pickerBottomLayout.middleButtonTextView.setText(LocaleController.getString("Save", R.string.Save).toUpperCase()); + pickerBottomLayout.middleButton.setVisibility(View.VISIBLE); + pickerBottomLayout.middleButton.setOnClickListener((it) -> { + + int p = Utilities.parseInt(port); + + SharedConfig.ProxyInfo info; + + if (TextUtils.isEmpty(secret)) { + + info = new SharedConfig.ProxyInfo(address, p, user, password, ""); + + } else { + + info = new SharedConfig.ProxyInfo(address, p, "", "", secret); + + } + + info.setRemarks(remarks); + + SharedConfig.addProxy(info); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + + }); + + pickerBottomLayout.doneButtonTextView.setText(LocaleController.getString("ConnectingConnectProxy", R.string.ConnectingConnectProxy).toUpperCase()); + pickerBottomLayout.doneButton.setOnClickListener(v -> { + int p = Utilities.parseInt(port); + + SharedConfig.ProxyInfo info; + + if (TextUtils.isEmpty(secret)) { + + info = new SharedConfig.ProxyInfo(address, p, user, password, ""); + + } else { + + info = new SharedConfig.ProxyInfo(address, p, "", "", secret); + + } + + info.setRemarks(remarks); + + SharedConfig.setCurrentProxy(SharedConfig.addProxy(info)); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + }); + builder.show(); + } + + public static void showVmessAlert(Context activity, final SharedConfig.VmessProxy info) { + BottomSheet.Builder builder = new BottomSheet.Builder(activity); + final Runnable dismissRunnable = builder.getDismissRunnable(); + + builder.setApplyTopPadding(false); + builder.setApplyBottomPadding(false); + LinearLayout linearLayout = new LinearLayout(activity); + builder.setCustomView(linearLayout); + linearLayout.setOrientation(LinearLayout.VERTICAL); + for (int a = 0; a < 8; a++) { + String text = null; + String detail = null; + if (a == 0) { + text = info.bean.getAddress(); + detail = LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress); + } else if (a == 1) { + text = "" + info.bean.getPort(); + detail = LocaleController.getString("UseProxyPort", R.string.UseProxyPort); + } else if (a == 2) { + text = info.bean.getId(); + detail = LocaleController.getString("VmessUserId", R.string.VmessUserId); + } else if (a == 3) { + text = info.bean.getSecurity(); + if ("none".equals(text)) continue; + detail = LocaleController.getString("VmessSecurity", R.string.VmessSecurity); + } else if (a == 4) { + text = info.bean.getNetwork() + (StrUtil.isBlank(info.bean.getStreamSecurity()) ? "" : ", tls"); + detail = LocaleController.getString("VmessNetwork", R.string.VmessNetwork); + } else if (a == 5) { + text = info.bean.getHeaderType(); + if ("none".equals(text)) continue; + detail = LocaleController.getString("VmessHeadType", R.string.VmessHeadType); + } else if (a == 6) { + text = info.bean.getRequestHost(); + detail = LocaleController.getString("VmessRequestHost", R.string.VmessRequestHost); + } else { + text = LocaleController.getString("Checking", R.string.Checking); + detail = LocaleController.getString("Checking", R.string.Checking); + } + if (TextUtils.isEmpty(text)) { + continue; + } + TextDetailSettingsCell cell = new TextDetailSettingsCell(activity); + cell.setTextAndValue(text, detail, true); + cell.getTextView().setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + cell.getValueTextView().setTextColor(Theme.getColor(Theme.key_dialogTextGray3)); + linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + AtomicInteger count = new AtomicInteger(); + if (a == 7) { + + RequestTimeDelegate callback = new RequestTimeDelegate() { + @Override + public void run(long time) { + int c = count.getAndIncrement(); + String colorKey; + if (time != -1) { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Available", R.string.Available), LocaleController.formatString("Ping", R.string.Ping, time), true); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else if (c < 2) { + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", t -> AndroidUtilities.runOnUIThread(() -> run(t), 500)); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Unavailable", R.string.Unavailable), LocaleController.getString("Unavailable", R.string.Unavailable), true); + colorKey = Theme.key_windowBackgroundWhiteRedText4; + } + cell.getValueTextView().setTextColor(Theme.getColor(colorKey)); + } + + }; + + + UIUtil.runOnIoDispatcher(() -> { + + try { + info.start(); + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", time -> AndroidUtilities.runOnUIThread(() -> callback.run(time))); + } catch (Exception e) { + FileLog.e(e); + AlertUtil.showToast(e); + } + + }); + + } + } + + PickerBottomLayout pickerBottomLayout = new PickerBottomLayout(activity, false); + pickerBottomLayout.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground)); + linearLayout.addView(pickerBottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); + pickerBottomLayout.cancelButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + pickerBottomLayout.cancelButton.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.cancelButton.setText(LocaleController.getString("Cancel", R.string.Cancel).toUpperCase()); + pickerBottomLayout.cancelButton.setOnClickListener(view -> { + info.stop(); + dismissRunnable.run(); + }); pickerBottomLayout.doneButtonTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); pickerBottomLayout.doneButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); pickerBottomLayout.doneButtonBadgeTextView.setVisibility(View.GONE); + pickerBottomLayout.middleButtonTextView.setText(LocaleController.getString("Save", R.string.Save).toUpperCase()); + pickerBottomLayout.middleButton.setVisibility(View.VISIBLE); + pickerBottomLayout.middleButton.setOnClickListener((it) -> { + SharedConfig.addProxy(info); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + + }); pickerBottomLayout.doneButtonTextView.setText(LocaleController.getString("ConnectingConnectProxy", R.string.ConnectingConnectProxy).toUpperCase()); pickerBottomLayout.doneButton.setOnClickListener(v -> { - SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); - editor.putBoolean("proxy_enabled", true); - editor.putString("proxy_ip", address); - int p = Utilities.parseInt(port); - editor.putInt("proxy_port", p); - SharedConfig.ProxyInfo info; - if (TextUtils.isEmpty(secret)) { - editor.remove("proxy_secret"); - if (TextUtils.isEmpty(password)) { - editor.remove("proxy_pass"); - } else { - editor.putString("proxy_pass", password); - } - if (TextUtils.isEmpty(user)) { - editor.remove("proxy_user"); - } else { - editor.putString("proxy_user", user); - } - info = new SharedConfig.ProxyInfo(address, p, user, password, ""); - } else { - editor.remove("proxy_pass"); - editor.remove("proxy_user"); - editor.putString("proxy_secret", secret); - info = new SharedConfig.ProxyInfo(address, p, "", "", secret); - } - editor.commit(); + SharedConfig.setCurrentProxy(SharedConfig.addProxy(info)); - SharedConfig.currentProxy = SharedConfig.addProxy(info); - - ConnectionsManager.setProxySettings(true, address, p, user, password, secret); NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + dismissRunnable.run(); + + }); + builder.show(); + } + + public static void showShadowsocksAlert(Context activity, final SharedConfig.ShadowsocksProxy info) { + try { + BottomSheet.Builder builder = new BottomSheet.Builder(activity); + final Runnable dismissRunnable = builder.getDismissRunnable(); + + builder.setApplyTopPadding(false); + builder.setApplyBottomPadding(false); + LinearLayout linearLayout = new LinearLayout(activity); + builder.setCustomView(linearLayout); + linearLayout.setOrientation(LinearLayout.VERTICAL); + for (int a = 0; a < 5; a++) { + String text = null; + String detail = null; + if (a == 0) { + text = info.bean.getHost(); + detail = LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress); + } else if (a == 1) { + text = "" + info.bean.getRemotePort(); + detail = LocaleController.getString("UseProxyPort", R.string.UseProxyPort); + } else if (a == 2) { + text = info.bean.getPassword(); + detail = LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword); + } else if (a == 3) { + text = info.bean.getMethod(); + detail = LocaleController.getString("SSMethod", R.string.SSMethod); + } else { + text = LocaleController.getString("Checking", R.string.Checking); + detail = LocaleController.getString("Checking", R.string.Checking); + } + if (TextUtils.isEmpty(text)) { + continue; + } + TextDetailSettingsCell cell = new TextDetailSettingsCell(activity); + cell.setTextAndValue(text, detail, true); + cell.getTextView().setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + cell.getValueTextView().setTextColor(Theme.getColor(Theme.key_dialogTextGray3)); + linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + AtomicInteger count = new AtomicInteger(); + if (a == 4) { + + RequestTimeDelegate callback = new RequestTimeDelegate() { + @Override + public void run(long time) { + int c = count.getAndIncrement(); + String colorKey; + if (time != -1) { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Available", R.string.Available), LocaleController.formatString("Ping", R.string.Ping, time), true); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else if (c < 2) { + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", t -> AndroidUtilities.runOnUIThread(() -> run(t), 500)); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Unavailable", R.string.Unavailable), LocaleController.getString("Unavailable", R.string.Unavailable), true); + colorKey = Theme.key_windowBackgroundWhiteRedText4; + } + cell.getValueTextView().setTextColor(Theme.getColor(colorKey)); + } + + }; + + UIUtil.runOnIoDispatcher(() -> { + + try { + info.start(); + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", time -> AndroidUtilities.runOnUIThread(() -> callback.run(time))); + } catch (Exception e) { + FileLog.e(e); + AlertUtil.showToast(e); + } + + }); + + } + } + + PickerBottomLayout pickerBottomLayout = new PickerBottomLayout(activity, false); + pickerBottomLayout.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground)); + linearLayout.addView(pickerBottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); + pickerBottomLayout.cancelButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + pickerBottomLayout.cancelButton.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.cancelButton.setText(LocaleController.getString("Cancel", R.string.Cancel).toUpperCase()); + pickerBottomLayout.cancelButton.setOnClickListener(view -> { + info.stop(); + dismissRunnable.run(); + }); + pickerBottomLayout.doneButtonTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.doneButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + pickerBottomLayout.doneButtonBadgeTextView.setVisibility(View.GONE); + pickerBottomLayout.middleButtonTextView.setText(LocaleController.getString("Save", R.string.Save).toUpperCase()); + pickerBottomLayout.middleButton.setVisibility(View.VISIBLE); + pickerBottomLayout.middleButton.setOnClickListener((it) -> { + SharedConfig.addProxy(info); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + + }); + pickerBottomLayout.doneButtonTextView.setText(LocaleController.getString("ConnectingConnectProxy", R.string.ConnectingConnectProxy).toUpperCase()); + pickerBottomLayout.doneButton.setOnClickListener(v -> { + + SharedConfig.setCurrentProxy(SharedConfig.addProxy(info)); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + + }); + builder.show(); + } catch (Exception e) { + FileLog.e(e); + AlertUtil.showToast(e); + } + } + + public static void showShadowsocksRAlert(Context activity, final SharedConfig.ShadowsocksRProxy info) { + BottomSheet.Builder builder = new BottomSheet.Builder(activity); + final Runnable dismissRunnable = builder.getDismissRunnable(); + + builder.setApplyTopPadding(false); + builder.setApplyBottomPadding(false); + LinearLayout linearLayout = new LinearLayout(activity); + builder.setCustomView(linearLayout); + linearLayout.setOrientation(LinearLayout.VERTICAL); + for (int a = 0; a < 7; a++) { + String text = null; + String detail = null; + if (a == 0) { + text = info.bean.getHost(); + detail = LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress); + } else if (a == 1) { + text = "" + info.bean.getRemotePort(); + detail = LocaleController.getString("UseProxyPort", R.string.UseProxyPort); + } else if (a == 2) { + text = info.bean.getPassword(); + detail = LocaleController.getString("SSPassword", R.string.SSPassword); + } else if (a == 3) { + text = info.bean.getMethod(); + detail = LocaleController.getString("SSMethod", R.string.SSMethod); + } else if (a == 4) { + text = info.bean.getProtocol(); + if (!StrUtil.isBlank(info.bean.getProtocol_param())) { + text += ", " + info.bean.getProtocol_param(); + } + detail = LocaleController.getString("SSRProtocol", R.string.SSRProtocol); + } else if (a == 5) { + text = info.bean.getObfs(); + if (!StrUtil.isBlank(info.bean.getObfs_param())) { + text += ", " + info.bean.getObfs_param(); + } + detail = LocaleController.getString("SSRObfs", R.string.SSRObfs); + } else { + text = LocaleController.getString("Checking", R.string.Checking); + detail = LocaleController.getString("Checking", R.string.Checking); + } + if (TextUtils.isEmpty(text)) { + continue; + } + TextDetailSettingsCell cell = new TextDetailSettingsCell(activity); + cell.setTextAndValue(text, detail, true); + cell.getTextView().setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + cell.getValueTextView().setTextColor(Theme.getColor(Theme.key_dialogTextGray3)); + linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + AtomicInteger count = new AtomicInteger(); + if (a == 6) { + + RequestTimeDelegate callback = new RequestTimeDelegate() { + @Override + public void run(long time) { + int c = count.getAndIncrement(); + String colorKey; + if (time != -1) { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Available", R.string.Available), LocaleController.formatString("Ping", R.string.Ping, time), true); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else if (c < 2) { + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", t -> AndroidUtilities.runOnUIThread(() -> run(t), 500)); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Unavailable", R.string.Unavailable), LocaleController.getString("Unavailable", R.string.Unavailable), true); + colorKey = Theme.key_windowBackgroundWhiteRedText4; + } + cell.getValueTextView().setTextColor(Theme.getColor(colorKey)); + } + + }; + + UIUtil.runOnIoDispatcher(() -> { + + try { + info.start(); + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", time -> AndroidUtilities.runOnUIThread(() -> callback.run(time))); + } catch (Exception e) { + FileLog.e(e); + AlertUtil.showToast(e); + } + + }); + + } + } + + PickerBottomLayout pickerBottomLayout = new PickerBottomLayout(activity, false); + pickerBottomLayout.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground)); + linearLayout.addView(pickerBottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); + pickerBottomLayout.cancelButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + pickerBottomLayout.cancelButton.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.cancelButton.setText(LocaleController.getString("Cancel", R.string.Cancel).toUpperCase()); + pickerBottomLayout.cancelButton.setOnClickListener(view -> { + info.stop(); + dismissRunnable.run(); + }); + pickerBottomLayout.doneButtonTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.doneButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + pickerBottomLayout.doneButtonBadgeTextView.setVisibility(View.GONE); + pickerBottomLayout.middleButtonTextView.setText(LocaleController.getString("Save", R.string.Save).toUpperCase()); + pickerBottomLayout.middleButton.setVisibility(View.VISIBLE); + pickerBottomLayout.middleButton.setOnClickListener((it) -> { + SharedConfig.addProxy(info); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + + }); + pickerBottomLayout.doneButtonTextView.setText(LocaleController.getString("ConnectingConnectProxy", R.string.ConnectingConnectProxy).toUpperCase()); + pickerBottomLayout.doneButton.setOnClickListener(v -> { + + SharedConfig.setCurrentProxy(SharedConfig.addProxy(info)); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + + }); + builder.show(); + } + + public static void showRelayBatonAlert(Context activity, final SharedConfig.RelayBatonProxy info) { + BottomSheet.Builder builder = new BottomSheet.Builder(activity); + final Runnable dismissRunnable = builder.getDismissRunnable(); + + builder.setApplyTopPadding(false); + builder.setApplyBottomPadding(false); + LinearLayout linearLayout = new LinearLayout(activity); + builder.setCustomView(linearLayout); + linearLayout.setOrientation(LinearLayout.VERTICAL); + for (int a = 0; a < 5; a++) { + String text = null; + String detail = null; + if (a == 0) { + text = info.bean.getServer(); + detail = LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress); + } else if (a == 1) { + text = "" + info.bean.getUsername(); + detail = LocaleController.getString("UseProxyPort", R.string.UseProxyUsername); + } else if (a == 2) { + text = info.bean.getPassword(); + detail = LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword); + } else if (a == 3) { + text = info.bean.getEsni() ? "Y" : "N"; + detail = LocaleController.getString("ESNI", R.string.ESNI); + } else { + text = LocaleController.getString("Checking", R.string.Checking); + detail = LocaleController.getString("Checking", R.string.Checking); + } + if (TextUtils.isEmpty(text)) { + continue; + } + TextDetailSettingsCell cell = new TextDetailSettingsCell(activity); + cell.setTextAndValue(text, detail, true); + cell.getTextView().setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); + cell.getValueTextView().setTextColor(Theme.getColor(Theme.key_dialogTextGray3)); + linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + AtomicInteger count = new AtomicInteger(); + if (a == 4) { + RequestTimeDelegate callback = new RequestTimeDelegate() { + @Override + public void run(long time) { + int c = count.getAndIncrement(); + String colorKey; + if (time != -1) { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Available", R.string.Available), LocaleController.formatString("Ping", R.string.Ping, time), true); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else if (c < 3) { + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", t -> AndroidUtilities.runOnUIThread(() -> run(t), 500)); + colorKey = Theme.key_windowBackgroundWhiteGreenText; + } else { + info.stop(); + cell.setTextAndValue(LocaleController.getString("Unavailable", R.string.Unavailable), LocaleController.getString("Unavailable", R.string.Unavailable), true); + colorKey = Theme.key_windowBackgroundWhiteRedText4; + } + cell.getValueTextView().setTextColor(Theme.getColor(colorKey)); + } + + }; + + UIUtil.runOnIoDispatcher(() -> { + + try { + info.start(); + ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", time -> AndroidUtilities.runOnUIThread(() -> callback.run(time))); + } catch (Exception e) { + FileLog.e(e); + AlertUtil.showToast(e); + } + + }); + + } + } + + PickerBottomLayout pickerBottomLayout = new PickerBottomLayout(activity, false); + pickerBottomLayout.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground)); + linearLayout.addView(pickerBottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); + pickerBottomLayout.cancelButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + pickerBottomLayout.cancelButton.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.cancelButton.setText(LocaleController.getString("Cancel", R.string.Cancel).toUpperCase()); + pickerBottomLayout.cancelButton.setOnClickListener(view -> { + info.stop(); + dismissRunnable.run(); + }); + pickerBottomLayout.doneButtonTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2)); + pickerBottomLayout.doneButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0); + pickerBottomLayout.doneButtonBadgeTextView.setVisibility(View.GONE); + pickerBottomLayout.middleButtonTextView.setText(LocaleController.getString("Save", R.string.Save).toUpperCase()); + pickerBottomLayout.middleButton.setVisibility(View.VISIBLE); + pickerBottomLayout.middleButton.setOnClickListener((it) -> { + SharedConfig.addProxy(info); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + + }); + pickerBottomLayout.doneButtonTextView.setText(LocaleController.getString("ConnectingConnectProxy", R.string.ConnectingConnectProxy).toUpperCase()); + pickerBottomLayout.doneButton.setOnClickListener(v -> { + + SharedConfig.setCurrentProxy(SharedConfig.addProxy(info)); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + dismissRunnable.run(); + }); builder.show(); } @@ -2877,13 +3365,13 @@ public class AndroidUtilities { return null; } - public static void fixGoogleMapsBug() { //https://issuetracker.google.com/issues/154855417#comment301 + public static void fixGoogleMapsBug() {/* //https://issuetracker.google.com/issues/154855417#comment301 SharedPreferences googleBug = ApplicationLoader.applicationContext.getSharedPreferences("google_bug_154855417", Context.MODE_PRIVATE); if (!googleBug.contains("fixed")) { File corruptedZoomTables = new File(ApplicationLoader.getFilesDirFixed(), "ZoomTables.data"); corruptedZoomTables.delete(); googleBug.edit().putBoolean("fixed", true).apply(); - } + }*/ } public static CharSequence concat(CharSequence... text) { @@ -3031,7 +3519,7 @@ public class AndroidUtilities { hsb[1] = Math.min(1.0f, hsb[1] + 0.05f); if (hsb[2] > 0.5f) { hsb[2] = Math.max(0.0f, hsb[2] * 0.90f); - } else{ + } else { hsb[2] = Math.max(0.0f, hsb[2] * 0.90f); } return HSBtoRGB(hsb[0], hsb[1], hsb[2]) | 0xff000000; @@ -3155,7 +3643,7 @@ public class AndroidUtilities { if (parentFragment == null || parentFragment.getParentActivity() == null) { return; } - if (set) { + if (set && !NekoXConfig.disableFlagSecure) { try { parentFragment.getParentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); flagSecureFragment = new WeakReference<>(parentFragment); @@ -3204,7 +3692,7 @@ public class AndroidUtilities { return ""; } - private static char[] characters = new char[] {' ', '!', '"', '#', '%', '&', '\'', '(', ')', '*', ',', '-', '.', '/', ':', ';', '?', '@', '[', '\\', ']', '_', '{', '}', '¡', '§', '«', '¶', '·', '»', '¿', ';', '·', '՚', '՛', '՜', '՝', '՞', '՟', '։', '֊', '־', '׀', '׃', '׆', '׳', '״', '؉', '؊', '،', '؍', '؛', '؞', '؟', '٪', '٫', '٬', '٭', '۔', '܀', '܁', '܂', '܃', '܄', '܅', '܆', '܇', '܈', '܉', '܊', '܋', '܌', '܍', '߷', '߸', '߹', '࠰', '࠱', '࠲', '࠳', '࠴', '࠵', '࠶', '࠷', '࠸', '࠹', '࠺', '࠻', '࠼', '࠽', '࠾', '࡞', '।', '॥', '॰', '৽', '੶', '૰', '౷', '಄', '෴', '๏', '๚', '๛', '༄', '༅', '༆', '༇', '༈', '༉', '༊', '་', '༌', '།', '༎', '༏', '༐', '༑', '༒', '༔', '༺', '༻', '༼', '༽', '྅', '࿐', '࿑', '࿒', '࿓', '࿔', '࿙', '࿚', '၊', '။', '၌', '၍', '၎', '၏', '჻', '፠', '፡', '።', '፣', '፤', '፥', '፦', '፧', '፨', '᐀', '᙮', '᚛', '᚜', '᛫', '᛬', '᛭', '᜵', '᜶', '។', '៕', '៖', '៘', '៙', '៚', '᠀', '᠁', '᠂', '᠃', '᠄', '᠅', '᠆', '᠇', '᠈', '᠉', '᠊', '᥄', '᥅', '᨞', '᨟', '᪠', '᪡', '᪢', '᪣', '᪤', '᪥', '᪦', '᪨', '᪩', '᪪', '᪫', '᪬', '᪭', '᭚', '᭛', '᭜', '᭝', '᭞', '᭟', '᭠', '᯼', '᯽', '᯾', '᯿', '᰻', '᰼', '᰽', '᰾', '᰿', '᱾', '᱿', '᳀', '᳁', '᳂', '᳃', '᳄', '᳅', '᳆', '᳇', '᳓', '‐', '‑', '‒', '–', '—', '―', '‖', '‗', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '†', '‡', '•', '‣', '․', '‥', '…', '‧', '‰', '‱', '′', '″', '‴', '‵', '‶', '‷', '‸', '‹', '›', '※', '‼', '‽', '‾', '‿', '⁀', '⁁', '⁂', '⁃', '⁅', '⁆', '⁇', '⁈', '⁉', '⁊', '⁋', '⁌', '⁍', '⁎', '⁏', '⁐', '⁑', '⁓', '⁔', '⁕', '⁖', '⁗', '⁘', '⁙', '⁚', '⁛', '⁜', '⁝', '⁞', '⁽', '⁾', '₍', '₎', '⌈', '⌉', '⌊', '⌋', '〈', '〉', '❨', '❩', '❪', '❫', '❬', '❭', '❮', '❯', '❰', '❱', '❲', '❳', '❴', '❵', '⟅', '⟆', '⟦', '⟧', '⟨', '⟩', '⟪', '⟫', '⟬', '⟭', '⟮', '⟯', '⦃', '⦄', '⦅', '⦆', '⦇', '⦈', '⦉', '⦊', '⦋', '⦌', '⦍', '⦎', '⦏', '⦐', '⦑', '⦒', '⦓', '⦔', '⦕', '⦖', '⦗', '⦘', '⧘', '⧙', '⧚', '⧛', '⧼', '⧽', '⳹', '⳺', '⳻', '⳼', '⳾', '⳿', '⵰', '⸀', '⸁', '⸂', '⸃', '⸄', '⸅', '⸆', '⸇', '⸈', '⸉', '⸊', '⸋', '⸌', '⸍', '⸎', '⸏', '⸐', '⸑', '⸒', '⸓', '⸔', '⸕', '⸖', '⸗', '⸘', '⸙', '⸚', '⸛', '⸜', '⸝', '⸞', '⸟', '⸠', '⸡', '⸢', '⸣', '⸤', '⸥', '⸦', '⸧', '⸨', '⸩', '⸪', '⸫', '⸬', '⸭', '⸮', '⸰', '⸱', '⸲', '⸳', '⸴', '⸵', '⸶', '⸷', '⸸', '⸹', '⸺', '⸻', '⸼', '⸽', '⸾', '⸿', '⹀', '⹁', '⹂', '⹃', '⹄', '⹅', '⹆', '⹇', '⹈', '⹉', '⹊', '⹋', '⹌', '⹍', '⹎', '⹏', '、', '。', '〃', '〈', '〉', '《', '》', '「', '」', '『', '』', '【', '】', '〔', '〕', '〖', '〗', '〘', '〙', '〚', '〛', '〜', '〝', '〞', '〟', '〰', '〽', '゠', '・', '꓾', '꓿', '꘍', '꘎', '꘏', '꙳', '꙾', '꛲', '꛳', '꛴', '꛵', '꛶', '꛷', '꡴', '꡵', '꡶', '꡷', '꣎', '꣏', '꣸', '꣹', '꣺', '꣼', '꤮', '꤯', '꥟', '꧁', '꧂', '꧃', '꧄', '꧅', '꧆', '꧇', '꧈', '꧉', '꧊', '꧋', '꧌', '꧍', '꧞', '꧟', '꩜', '꩝', '꩞', '꩟', '꫞', '꫟', '꫰', '꫱', '꯫', '﴾', '﴿', '︐', '︑', '︒', '︓', '︔', '︕', '︖', '︗', '︘', '︙', '︰', '︱', '︲', '︳', '︴', '︵', '︶', '︷', '︸', '︹', '︺', '︻', '︼', '︽', '︾', '︿', '﹀', '﹁', '﹂', '﹃', '﹄', '﹅', '﹆', '﹇', '﹈', '﹉', '﹊', '﹋', '﹌', '﹍', '﹎', '﹏', '﹐', '﹑', '﹒', '﹔', '﹕', '﹖', '﹗', '﹘', '﹙', '﹚', '﹛', '﹜', '﹝', '﹞', '﹟', '﹠', '﹡', '﹣', '﹨', '﹪', '﹫', '!', '"', '#', '%', '&', ''', '(', ')', '*', ',', '-', '.', '/', ':', ';', '?', '@', '[', '\', ']', '_', '{', '}', '⦅', '⦆', '。', '「', '」', '、', '・'}; + private static char[] characters = new char[]{' ', '!', '"', '#', '%', '&', '\'', '(', ')', '*', ',', '-', '.', '/', ':', ';', '?', '@', '[', '\\', ']', '_', '{', '}', '¡', '§', '«', '¶', '·', '»', '¿', ';', '·', '՚', '՛', '՜', '՝', '՞', '՟', '։', '֊', '־', '׀', '׃', '׆', '׳', '״', '؉', '؊', '،', '؍', '؛', '؞', '؟', '٪', '٫', '٬', '٭', '۔', '܀', '܁', '܂', '܃', '܄', '܅', '܆', '܇', '܈', '܉', '܊', '܋', '܌', '܍', '߷', '߸', '߹', '࠰', '࠱', '࠲', '࠳', '࠴', '࠵', '࠶', '࠷', '࠸', '࠹', '࠺', '࠻', '࠼', '࠽', '࠾', '࡞', '।', '॥', '॰', '৽', '੶', '૰', '౷', '಄', '෴', '๏', '๚', '๛', '༄', '༅', '༆', '༇', '༈', '༉', '༊', '་', '༌', '།', '༎', '༏', '༐', '༑', '༒', '༔', '༺', '༻', '༼', '༽', '྅', '࿐', '࿑', '࿒', '࿓', '࿔', '࿙', '࿚', '၊', '။', '၌', '၍', '၎', '၏', '჻', '፠', '፡', '።', '፣', '፤', '፥', '፦', '፧', '፨', '᐀', '᙮', '᚛', '᚜', '᛫', '᛬', '᛭', '᜵', '᜶', '។', '៕', '៖', '៘', '៙', '៚', '᠀', '᠁', '᠂', '᠃', '᠄', '᠅', '᠆', '᠇', '᠈', '᠉', '᠊', '᥄', '᥅', '᨞', '᨟', '᪠', '᪡', '᪢', '᪣', '᪤', '᪥', '᪦', '᪨', '᪩', '᪪', '᪫', '᪬', '᪭', '᭚', '᭛', '᭜', '᭝', '᭞', '᭟', '᭠', '᯼', '᯽', '᯾', '᯿', '᰻', '᰼', '᰽', '᰾', '᰿', '᱾', '᱿', '᳀', '᳁', '᳂', '᳃', '᳄', '᳅', '᳆', '᳇', '᳓', '‐', '‑', '‒', '–', '—', '―', '‖', '‗', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '†', '‡', '•', '‣', '․', '‥', '…', '‧', '‰', '‱', '′', '″', '‴', '‵', '‶', '‷', '‸', '‹', '›', '※', '‼', '‽', '‾', '‿', '⁀', '⁁', '⁂', '⁃', '⁅', '⁆', '⁇', '⁈', '⁉', '⁊', '⁋', '⁌', '⁍', '⁎', '⁏', '⁐', '⁑', '⁓', '⁔', '⁕', '⁖', '⁗', '⁘', '⁙', '⁚', '⁛', '⁜', '⁝', '⁞', '⁽', '⁾', '₍', '₎', '⌈', '⌉', '⌊', '⌋', '〈', '〉', '❨', '❩', '❪', '❫', '❬', '❭', '❮', '❯', '❰', '❱', '❲', '❳', '❴', '❵', '⟅', '⟆', '⟦', '⟧', '⟨', '⟩', '⟪', '⟫', '⟬', '⟭', '⟮', '⟯', '⦃', '⦄', '⦅', '⦆', '⦇', '⦈', '⦉', '⦊', '⦋', '⦌', '⦍', '⦎', '⦏', '⦐', '⦑', '⦒', '⦓', '⦔', '⦕', '⦖', '⦗', '⦘', '⧘', '⧙', '⧚', '⧛', '⧼', '⧽', '⳹', '⳺', '⳻', '⳼', '⳾', '⳿', '⵰', '⸀', '⸁', '⸂', '⸃', '⸄', '⸅', '⸆', '⸇', '⸈', '⸉', '⸊', '⸋', '⸌', '⸍', '⸎', '⸏', '⸐', '⸑', '⸒', '⸓', '⸔', '⸕', '⸖', '⸗', '⸘', '⸙', '⸚', '⸛', '⸜', '⸝', '⸞', '⸟', '⸠', '⸡', '⸢', '⸣', '⸤', '⸥', '⸦', '⸧', '⸨', '⸩', '⸪', '⸫', '⸬', '⸭', '⸮', '⸰', '⸱', '⸲', '⸳', '⸴', '⸵', '⸶', '⸷', '⸸', '⸹', '⸺', '⸻', '⸼', '⸽', '⸾', '⸿', '⹀', '⹁', '⹂', '⹃', '⹄', '⹅', '⹆', '⹇', '⹈', '⹉', '⹊', '⹋', '⹌', '⹍', '⹎', '⹏', '、', '。', '〃', '〈', '〉', '《', '》', '「', '」', '『', '』', '【', '】', '〔', '〕', '〖', '〗', '〘', '〙', '〚', '〛', '〜', '〝', '〞', '〟', '〰', '〽', '゠', '・', '꓾', '꓿', '꘍', '꘎', '꘏', '꙳', '꙾', '꛲', '꛳', '꛴', '꛵', '꛶', '꛷', '꡴', '꡵', '꡶', '꡷', '꣎', '꣏', '꣸', '꣹', '꣺', '꣼', '꤮', '꤯', '꥟', '꧁', '꧂', '꧃', '꧄', '꧅', '꧆', '꧇', '꧈', '꧉', '꧊', '꧋', '꧌', '꧍', '꧞', '꧟', '꩜', '꩝', '꩞', '꩟', '꫞', '꫟', '꫰', '꫱', '꯫', '﴾', '﴿', '︐', '︑', '︒', '︓', '︔', '︕', '︖', '︗', '︘', '︙', '︰', '︱', '︲', '︳', '︴', '︵', '︶', '︷', '︸', '︹', '︺', '︻', '︼', '︽', '︾', '︿', '﹀', '﹁', '﹂', '﹃', '﹄', '﹅', '﹆', '﹇', '﹈', '﹉', '﹊', '﹋', '﹌', '﹍', '﹎', '﹏', '﹐', '﹑', '﹒', '﹔', '﹕', '﹖', '﹗', '﹘', '﹙', '﹚', '﹛', '﹜', '﹝', '﹞', '﹟', '﹠', '﹡', '﹣', '﹨', '﹪', '﹫', '!', '"', '#', '%', '&', ''', '(', ')', '*', ',', '-', '.', '/', ':', ';', '?', '@', '[', '\', ']', '_', '{', '}', '⦅', '⦆', '。', '「', '」', '、', '・'}; //private static String[] longCharacters = new String[] {"𐄀", "𐄁", "𐄂", "𐎟", "𐏐", "𐕯", "𐡗", "𐤟", "𐤿", "𐩐", "𐩑", "𐩒", "𐩓", "𐩔", "𐩕", "𐩖", "𐩗", "𐩘", "𐩿", "𐫰", "𐫱", "𐫲", "𐫳", "𐫴", "𐫵", "𐫶", "𐬹", "𐬺", "𐬻", "𐬼", "𐬽", "𐬾", "𐬿", "𐮙", "𐮚", "𐮛", "𐮜", "𐽕", "𐽖", "𐽗", "𐽘", "𐽙", "𑁇", "𑁈", "𑁉", "𑁊", "𑁋", "𑁌", "𑁍", "𑂻", "𑂼", "𑂾", "𑂿", "𑃀", "𑃁", "𑅀", "𑅁", "𑅂", "𑅃", "𑅴", "𑅵", "𑇅", "𑇆", "𑇇", "𑇈", "𑇍", "𑇛", "𑇝", "𑇞", "𑇟", "𑈸", "𑈹", "𑈺", "𑈻", "𑈼", "𑈽", "𑊩", "𑑋", "𑑌", "𑑍", "𑑎", "𑑏", "𑑛", "𑑝", "𑓆", "𑗁", "𑗂", "𑗃", "𑗄", "𑗅", "𑗆", "𑗇", "𑗈", "𑗉", "𑗊", "𑗋", "𑗌", "𑗍", "𑗎", "𑗏", "𑗐", "𑗑", "𑗒", "𑗓", "𑗔", "𑗕", "𑗖", "𑗗", "𑙁", "𑙂", "𑙃", "𑙠", "𑙡", "𑙢", "𑙣", "𑙤", "𑙥", "𑙦", "𑙧", "𑙨", "𑙩", "𑙪", "𑙫", "𑙬", "𑜼", "𑜽", "𑜾", "𑠻", "𑧢", "𑨿", "𑩀", "𑩁", "𑩂", "𑩃", "𑩄", "𑩅", "𑩆", "𑪚", "𑪛", "𑪜", "𑪞", "𑪟", "𑪠", "𑪡", "𑪢", "𑱁", "𑱂", "𑱃", "𑱄", "𑱅", "𑱰", "𑱱", "𑻷", "𑻸", "𑿿", "𒑰", "𒑱", "𒑲", "𒑳", "𒑴", "𖩮", "𖩯", "𖫵", "𖬷", "𖬸", "𖬹", "𖬺", "𖬻", "𖭄", "𖺗", "𖺘", "𖺙", "𖺚", "𖿢", "𛲟", "𝪇", "𝪈", "𝪉", "𝪊", "𝪋", "𞥞", "𞥟"}; private static HashSet charactersMap; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/LinkifyPort.java b/TMessagesProj/src/main/java/org/telegram/messenger/LinkifyPort.java index 1646b7e68..05cb53903 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/LinkifyPort.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/LinkifyPort.java @@ -156,12 +156,15 @@ public class LinkifyPort { private static final String DOMAIN_NAME_STR = "(" + HOST_NAME + "|" + IP_ADDRESS_STRING + ")"; private static final Pattern DOMAIN_NAME = Pattern.compile(DOMAIN_NAME_STR); private static final String PROTOCOL = "(?i:http|https|ton|tg)://"; + private static final String PROXY_PROTOCOL = "(?i:vmess|vmess1|ss|ssr|rb)://"; private static final String WORD_BOUNDARY = "(?:\\b|$|^)"; private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@"; private static final String PORT_NUMBER = "\\:\\d{1,5}"; private static final String PATH_AND_QUERY = "[/\\?](?:(?:[" + LABEL_CHAR + ";/\\?:@&=#~" + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*"; + private static final String BASE64 = "(?:[A-Za-z0-9+\\/]{4}\\\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)"; + private static final String PATH_AND_QUERY_BASE64 = "[/\\?]?(?:(?:[" + LABEL_CHAR + ";/\\?:@&=#~" + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2})|" + BASE64 + ")*"; private static final String RELAXED_DOMAIN_NAME = "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" + "?)+" + "|" + IP_ADDRESS_STRING + ")"; private static final String WEB_URL_WITHOUT_PROTOCOL = "(" @@ -185,7 +188,20 @@ public class LinkifyPort { + "(?:" + PATH_AND_QUERY + ")?" + WORD_BOUNDARY + ")"; + + private static final String PROXY_URL = "(" + + WORD_BOUNDARY + + "(?:" + + "(?:" + PROXY_PROTOCOL + "(?:" + USER_INFO + ")?" + ")" + + "(?:" + RELAXED_DOMAIN_NAME + ")?" + + "(?:" + PORT_NUMBER + ")?" + + ")" + + "(?:" + PATH_AND_QUERY_BASE64 + ")?" + + WORD_BOUNDARY + + ")"; + public static Pattern WEB_URL = null; + public static Pattern PROXY_PATTERN = Pattern.compile(PROXY_URL); static { try { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java index fc7cd41d7..836b77e18 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java @@ -12,24 +12,49 @@ import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Build; -import android.os.Environment; import android.os.SystemClock; import android.text.TextUtils; import android.util.Base64; import android.util.SparseArray; +import androidx.annotation.Nullable; + +import com.v2ray.ang.V2RayConfig; +import com.v2ray.ang.dto.AngConfig; +import com.v2ray.ang.util.Utils; + +import org.dizitart.no2.objects.filters.ObjectFilters; +import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import org.telegram.tgnet.ConnectionsManager; -import org.telegram.tgnet.SerializedData; import java.io.File; import java.io.RandomAccessFile; -import java.util.ArrayList; +import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; -import tw.nekomimi.nekogram.NekoConfig; +import cn.hutool.core.util.StrUtil; +import okhttp3.HttpUrl; +import tw.nekomimi.nekogram.ProxyManager; +import tw.nekomimi.nekogram.RelayBatonLoader; +import tw.nekomimi.nekogram.ShadowsocksLoader; +import tw.nekomimi.nekogram.ShadowsocksRLoader; +import tw.nekomimi.nekogram.VmessLoader; +import tw.nekomimi.nekogram.sub.SubInfo; +import tw.nekomimi.nekogram.sub.SubManager; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.EnvUtil; +import tw.nekomimi.nekogram.utils.FileUtil; +import tw.nekomimi.nekogram.utils.ThreadUtil; +import tw.nekomimi.nekogram.utils.UIUtil; + +import static com.v2ray.ang.V2RayConfig.SSR_PROTOCOL; +import static com.v2ray.ang.V2RayConfig.SS_PROTOCOL; public class SharedConfig { @@ -99,9 +124,9 @@ public class SharedConfig { public static int repeatMode; public static boolean allowBigEmoji; public static boolean useSystemEmoji; - public static int fontSize = 16; - public static int bubbleRadius = 10; - public static int ivFontSize = 16; + public static int fontSize = 12; + public static int bubbleRadius = 3; + public static int ivFontSize = 12; private static int devicePerformanceClass; public static boolean drawDialogIcons; @@ -114,7 +139,9 @@ public class SharedConfig { loadConfig(); } - public static class ProxyInfo { + public static class ProxyInfo implements Comparable { + + public int group; public String address; public int port; @@ -128,6 +155,30 @@ public class SharedConfig { public boolean available; public long availableCheckTime; + @Override + public int compareTo(ProxyInfo info) { + + if (available && !info.available) { + return -1; + } else if (!available && info.available) { + return 1; + } else if (available && info.available) { + return (int) (ping - info.ping); + } else { + return hashCode() + "".compareTo(info.hashCode() + ""); + } + + } + + public long subId; + + public ProxyInfo() { + address = ""; + password = ""; + username = ""; + secret = ""; + } + public ProxyInfo(String a, int p, String u, String pw, String s) { address = a; port = p; @@ -147,9 +198,801 @@ public class SharedConfig { secret = ""; } } + + public String getAddress() { + + return address + ":" + port; + + } + + public String getType() { + + if (!StrUtil.isBlank(secret)) { + + return "MTProto"; + + } else { + + return "Socks5"; + + } + + } + + public String getTitle() { + + StringBuilder builder = new StringBuilder(); + + builder.append("[ "); + + if (subId != 0L) { + + try { + + builder.append(SubManager.getSubList().find(ObjectFilters.eq("id", subId)).firstOrDefault().displayName()); + + } catch (Exception e) { + + builder.append("Unknown"); + + } + + } else { + + builder.append(getType()); + + } + + builder.append(" ] "); + + if (StrUtil.isBlank(getRemarks())) { + + builder.append(getAddress()); + + } else { + + builder.append(getRemarks()); + + } + + return builder.toString(); + + } + + private String remarks; + + public String getRemarks() { + + return remarks; + + } + + public void setRemarks(String remarks) { + this.remarks = remarks; + if (StrUtil.isBlank(remarks)) { + this.remarks = null; + } + } + + public String toUrl() { + + HttpUrl.Builder builder = HttpUrl.parse(StrUtil.isBlank(secret) ? + "https://t.me/socks" : "https://t.me/proxy").newBuilder() + .addQueryParameter("server", address) + .addQueryParameter("port", port + ""); + + if (!StrUtil.isBlank(secret)) { + + builder.addQueryParameter("secret", secret); + + } else { + + builder.addQueryParameter("user", username) + .addQueryParameter("pass", password); + + } + + if (!StrUtil.isBlank(remarks)) { + + builder.fragment(Utils.INSTANCE.urlEncode(remarks)); + + } + + return builder.toString(); + + } + + public static ProxyInfo fromUrl(String url) { + + Uri lnk = Uri.parse(url); + + if (lnk == null) throw new IllegalArgumentException(url); + + ProxyInfo info = new ProxyInfo(lnk.getQueryParameter("server"), + Utilities.parseInt(lnk.getQueryParameter("port")), + lnk.getQueryParameter("user"), + lnk.getQueryParameter("pass"), + lnk.getQueryParameter("secret")); + + if (StrUtil.isNotBlank(lnk.getFragment())) { + + info.setRemarks(lnk.getFragment()); + + } + + return info; + + } + + public JSONObject toJson() throws JSONException { + + JSONObject object = toJsonInternal(); + + return object; + + } + + public JSONObject toJsonInternal() throws JSONException { + + JSONObject obj = new JSONObject(); + + if (!StrUtil.isBlank(remarks)) { + obj.put("remarks", remarks); + } + + if (group != 0) { + obj.put("group", group); + } + + obj.put("address", address); + obj.put("port", port); + if (StrUtil.isBlank(secret)) { + obj.put("type", "socks5"); + if (!username.isEmpty()) { + obj.put("username", username); + } + if (!password.isEmpty()) { + obj.put("password", password); + } + } else { + obj.put("type", "mtproto"); + obj.put("secret", secret); + } + + return obj; + + } + + public static ProxyInfo fromJson(JSONObject obj) { + + ProxyInfo info; + + switch (obj.optString("type", "null")) { + + case "socks5": { + + info = new ProxyInfo(); + + info.group = obj.optInt("group", 0); + info.address = obj.optString("address", ""); + info.port = obj.optInt("port", 443); + info.username = obj.optString("username", ""); + info.password = obj.optString("password", ""); + + info.remarks = obj.optString("remarks"); + + if (StrUtil.isBlank(info.remarks)) info.remarks = null; + + info.group = obj.optInt("group", 0); + + break; + + } + + case "mtproto": { + + info = new ProxyInfo(); + + info.address = obj.optString("address", ""); + info.port = obj.optInt("port", 443); + info.secret = obj.optString("secret", ""); + + info.remarks = obj.optString("remarks"); + + if (StrUtil.isBlank(info.remarks)) info.remarks = null; + + info.group = obj.optInt("group", 0); + + break; + + } + + case "vmess": { + + info = new VmessProxy(obj.optString("link")); + + break; + + } + + case "shadowsocks": { + + info = new ShadowsocksProxy(obj.optString("link")); + + break; + + } + + case "shadowsocksr": { + + info = new ShadowsocksRProxy(obj.optString("link")); + + break; + + } + + default: { + + throw new IllegalStateException("invalid proxy type " + obj.optString("type", "null")); + + } + + } + + return info; + + } + + @Override + public int hashCode() { + + return (address + port + username + password + secret).hashCode(); + + } + + @Override + public boolean equals(@Nullable Object obj) { + return super.equals(obj) || (obj instanceof ProxyInfo && hashCode() == obj.hashCode()); + } + } + + public abstract static class ExternalSocks5Proxy extends ProxyInfo { + + public ExternalSocks5Proxy() { + + address = "127.0.0.1"; + username = ""; + password = ""; + secret = ""; + + } + + public abstract boolean isStarted(); + + public abstract void start(); + + public abstract void stop(); + + @Override + public abstract String getAddress(); + + @Override + public abstract String toUrl(); + + @Override + public abstract String getRemarks(); + + @Override + public abstract void setRemarks(String remarks); + + @Override + public abstract String getType(); + + @Override + public abstract JSONObject toJsonInternal() throws JSONException; + + } + + public static class VmessProxy extends ExternalSocks5Proxy { + + public AngConfig.VmessBean bean; + public VmessLoader loader; + + public VmessProxy(String vmessLink) { + + this(VmessLoader.parseVmessLink(vmessLink)); + + } + + public VmessProxy(AngConfig.VmessBean bean) { + + this.bean = bean; + + if (BuildVars.isMini) { + + throw new RuntimeException(LocaleController.getString("MiniVersionAlert", R.string.MiniVersionAlert)); + + } + + } + + @Override + public String getAddress() { + return bean.getAddress() + ":" + bean.getPort(); + } + + @Override + public boolean isStarted() { + + return loader != null; + + } + + @Override + public void start() { + + if (loader != null) return; + + VmessLoader loader = new VmessLoader(); + + try { + + loader.initConfig(bean); + + port = loader.start(); + + this.loader = loader; + + if (SharedConfig.proxyEnabled && SharedConfig.currentProxy == this) { + + ConnectionsManager.setProxySettings(true, address, port, username, password, secret); + + } + + } catch (Exception e) { + + FileLog.e(e); + + AlertUtil.showToast(e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); + + } + + } + + @Override + public void stop() { + + if (loader != null) { + + VmessLoader loader = this.loader; + + loader.stop(); + + this.loader = null; + + } + + } + + @Override + public String toUrl() { + return bean.toString(); + } + + @Override + public String getRemarks() { + return bean.getRemarks(); + } + + @Override + public void setRemarks(String remarks) { + bean.setRemarks(remarks); + } + + @Override + public String getType() { + return "Vmess"; + } + + @Override + public JSONObject toJsonInternal() throws JSONException { + + JSONObject obj = new JSONObject(); + obj.put("type", "vmess"); + obj.put("link", toUrl()); + return obj; + + } + + @Override + public int hashCode() { + return (bean.getAddress() + bean.getPort() + bean.getId() + bean.getNetwork() + bean.getPath()).hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + return super.equals(obj) || (obj instanceof VmessProxy && bean.equals(((VmessProxy) obj).bean)); + } + + } + + public static class ShadowsocksProxy extends ExternalSocks5Proxy { + + public ShadowsocksLoader.Bean bean; + public ShadowsocksLoader loader; + + public ShadowsocksProxy(String ssLink) { + + this(ShadowsocksLoader.Bean.Companion.parse(ssLink)); + + } + + public ShadowsocksProxy(ShadowsocksLoader.Bean bean) { + + this.bean = bean; + + if (BuildVars.isMini) { + + throw new RuntimeException(LocaleController.getString("MiniVersionAlert", R.string.MiniVersionAlert)); + + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + + throw new RuntimeException(LocaleController.getString("MinApi21Required", R.string.MinApi21Required)); + + } + + } + + @Override + public String getAddress() { + return bean.getHost() + ":" + bean.getRemotePort(); + } + + @Override + public boolean isStarted() { + + return loader != null; + + } + + @Override + public void start() { + + if (loader != null) return; + + port = ProxyManager.mkPort(); + ShadowsocksLoader loader = new ShadowsocksLoader(); + loader.initConfig(bean, port); + + loader.start(); + + this.loader = loader; + + if (SharedConfig.proxyEnabled && SharedConfig.currentProxy == this) { + + ConnectionsManager.setProxySettings(true, address, port, username, password, secret); + + } + + } + + @Override + public void stop() { + + if (loader != null) { + + FileLog.d(getTitle() + " stopped"); + + ShadowsocksLoader loader = this.loader; + + loader.stop(); + + this.loader = null; + + } + + } + + @Override + public String toUrl() { + return bean.toString(); + } + + + @Override + public String getRemarks() { + return bean.getRemarks(); + } + + @Override + public void setRemarks(String remarks) { + bean.setRemarks(remarks); + } + + @Override + public String getType() { + return "SS"; + } + + @Override + public JSONObject toJsonInternal() throws JSONException { + + JSONObject obj = new JSONObject(); + obj.put("type", "shadowsocks"); + obj.put("link", toUrl()); + return obj; + + } + + @Override + public int hashCode() { + + return (bean.getHost() + bean.getRemotePort() + bean.getMethod()).hashCode(); + + } + + @Override + public boolean equals(@Nullable Object obj) { + return super.equals(obj) || (obj instanceof ShadowsocksProxy && bean.equals(((ShadowsocksProxy) obj).bean)); + } + + } + + public static class ShadowsocksRProxy extends ExternalSocks5Proxy { + + public ShadowsocksRLoader.Bean bean; + public ShadowsocksRLoader loader; + + public ShadowsocksRProxy(String ssLink) { + + this(ShadowsocksRLoader.Bean.Companion.parse(ssLink)); + + } + + public ShadowsocksRProxy(ShadowsocksRLoader.Bean bean) { + + this.bean = bean; + + if (BuildVars.isMini) { + + throw new RuntimeException(LocaleController.getString("MiniVersionAlert", R.string.MiniVersionAlert)); + + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + + throw new RuntimeException(LocaleController.getString("MinApi21Required", R.string.MinApi21Required)); + + } + + } + + @Override + public String getAddress() { + return bean.getHost() + ":" + bean.getRemotePort(); + } + + @Override + public boolean isStarted() { + + return loader != null; + + } + + @Override + public void start() { + + if (loader != null) return; + + port = ProxyManager.mkPort(); + ShadowsocksRLoader loader = new ShadowsocksRLoader(); + loader.initConfig(bean, port); + + loader.start(); + + this.loader = loader; + + if (SharedConfig.proxyEnabled && SharedConfig.currentProxy == this) { + + ConnectionsManager.setProxySettings(true, address, port, username, password, secret); + + } + + } + + @Override + public void stop() { + + if (loader != null) { + + ShadowsocksRLoader loader = this.loader; + + this.loader = null; + + loader.stop(); + + } + + } + + @Override + public String toUrl() { + return bean.toString(); + } + + @Override + public String getRemarks() { + return bean.getRemarks(); + } + + @Override + public void setRemarks(String remarks) { + bean.setRemarks(remarks); + } + + @Override + public String getType() { + return "SSR"; + } + + @Override + public JSONObject toJsonInternal() throws JSONException { + + JSONObject obj = new JSONObject(); + obj.put("type", "shadowsocksr"); + obj.put("link", toUrl()); + return obj; + + } + + @Override + public int hashCode() { + + return (bean.getHost() + bean.getRemotePort() + bean.getMethod() + bean.getProtocol() + bean.getProtocol_param() + bean.getObfs() + bean.getObfs_param()).hashCode(); + + } + + @Override + public boolean equals(@Nullable Object obj) { + return super.equals(obj) || (obj instanceof ShadowsocksRProxy && bean.equals(((ShadowsocksRProxy) obj).bean)); + } + + } + + public static class RelayBatonProxy extends ExternalSocks5Proxy { + + public RelayBatonLoader.Bean bean; + public RelayBatonLoader loader; + + public RelayBatonProxy(String rbLink) { + + this(RelayBatonLoader.Bean.Companion.parse(rbLink)); + + } + + public RelayBatonProxy(RelayBatonLoader.Bean bean) { + + this.bean = bean; + + if (BuildVars.isMini) { + + throw new RuntimeException(LocaleController.getString("MiniVersionAlert", R.string.MiniVersionAlert)); + + } + + } + + @Override + public String getAddress() { + return bean.getServer(); + } + + @Override + public boolean isStarted() { + + return loader != null; + + } + + @Override + public void start() { + + if (loader != null) return; + + port = ProxyManager.mkPort(); + RelayBatonLoader loader = new RelayBatonLoader(); + loader.initConfig(bean, port); + + loader.start(); + + this.loader = loader; + + if (SharedConfig.proxyEnabled && SharedConfig.currentProxy == this) { + + ConnectionsManager.setProxySettings(true, address, port, username, password, secret); + + } + + } + + @Override + public void stop() { + + if (loader != null) { + + RelayBatonLoader loader = this.loader; + + this.loader = null; + + loader.stop(); + + } + + } + + @Override + public String toUrl() { + return bean.toString(); + } + + @Override + public String getRemarks() { + return bean.getRemarks(); + } + + @Override + public void setRemarks(String remarks) { + bean.setRemarks(remarks); + } + + @Override + public String getType() { + return "RB"; + } + + @Override + public JSONObject toJsonInternal() throws JSONException { + + JSONObject obj = new JSONObject(); + obj.put("type", "shadowsocksr"); + obj.put("link", toUrl()); + return obj; + + } + + @Override + public int hashCode() { + + return bean.hashCode(); + + } + + @Override + public boolean equals(@Nullable Object obj) { + return super.equals(obj) || (obj instanceof RelayBatonProxy && bean.equals(((RelayBatonProxy) obj).bean)); + } + + } + + public static LinkedList proxyList = new LinkedList<>(); + + public static LinkedList getProxyList() { + + while (true) { + + try { + + return new LinkedList<>(proxyList); + + } catch (ConcurrentModificationException ignored) { + } + + } + } - public static ArrayList proxyList = new ArrayList<>(); private static boolean proxyListLoaded; public static ProxyInfo currentProxy; @@ -249,8 +1092,8 @@ public class SharedConfig { hasCameraCache = preferences.contains("cameraCache"); roundCamera16to9 = true;//preferences.getBoolean("roundCamera16to9", false); repeatMode = preferences.getInt("repeatMode", 0); - fontSize = preferences.getInt("fons_size", AndroidUtilities.isTablet() ? 18 : 16); - bubbleRadius = preferences.getInt("bubbleRadius", 10); + fontSize = preferences.getInt("fons_size", AndroidUtilities.isTablet() ? 14 : 12); + bubbleRadius = preferences.getInt("bubbleRadius", 3); ivFontSize = preferences.getInt("iv_font_size", fontSize); allowBigEmoji = preferences.getBoolean("allowBigEmoji", true); useSystemEmoji = preferences.getBoolean("useSystemEmoji", false); @@ -745,112 +1588,428 @@ public class SharedConfig { LocaleController.resetImperialSystemType(); } + public static boolean proxyEnabled; + + static { + + loadProxyList(); + + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); + + boolean proxyEnabledValue = preferences.getBoolean("proxy_enabled", false); + + if (proxyEnabledValue && currentProxy == null) proxyEnabledValue = false; + + proxyEnabled = proxyEnabledValue; + + } + + public static void setProxyEnable(boolean enable) { + + proxyEnabled = enable; + + SharedPreferences preferences = MessagesController.getGlobalMainSettings(); + + preferences.edit().putBoolean("proxy_enabled", enable).commit(); + + ProxyInfo info = currentProxy; + + if (info == null) { + + info = new ProxyInfo(); + + } + + ProxyInfo finalInfo = info; + + UIUtil.runOnIoDispatcher(() -> { + + try { + + if (enable && finalInfo instanceof ExternalSocks5Proxy) { + + ((ExternalSocks5Proxy) finalInfo).start(); + + } else if (!enable && finalInfo instanceof ExternalSocks5Proxy) { + + ((ExternalSocks5Proxy) finalInfo).stop(); + + } + + } catch (Exception e) { + + FileLog.e(e); + AlertUtil.showToast(e); + + return; + + } + + ConnectionsManager.setProxySettings(enable, finalInfo.address, finalInfo.port, finalInfo.username, finalInfo.password, finalInfo.secret); + + UIUtil.runOnUIThread(() -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged)); + + }); + + } + + public static void setCurrentProxy(@Nullable ProxyInfo info) { + + currentProxy = info; + + MessagesController.getGlobalMainSettings().edit() + .putInt("current_proxy", info == null ? 0 : info.hashCode()) + .commit(); + + setProxyEnable(info != null); + + } + + public static void reloadProxyList() { + proxyListLoaded = false; + loadProxyList(); + + if (proxyEnabled && currentProxy == null) { + setProxyEnable(false); + } + + } + public static void loadProxyList() { if (proxyListLoaded) { return; } - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - String proxyAddress = preferences.getString("proxy_ip", ""); - String proxyUsername = preferences.getString("proxy_user", ""); - String proxyPassword = preferences.getString("proxy_pass", ""); - String proxySecret = preferences.getString("proxy_secret", ""); - int proxyPort = preferences.getInt("proxy_port", 1080); + + if (!proxyList.isEmpty()) { + for (ProxyInfo proxyInfo : proxyList) { + if (proxyInfo instanceof ExternalSocks5Proxy) { + ((ExternalSocks5Proxy) proxyInfo).stop(); + } + } + ThreadUtil.sleep(500L); + } proxyListLoaded = true; proxyList.clear(); currentProxy = null; - String list = preferences.getString("proxy_list", null); - if (!TextUtils.isEmpty(list)) { - byte[] bytes = Base64.decode(list, Base64.DEFAULT); - SerializedData data = new SerializedData(bytes); - int count = data.readInt32(false); - for (int a = 0; a < count; a++) { - ProxyInfo info = new ProxyInfo( - data.readString(false), - data.readInt32(false), - data.readString(false), - data.readString(false), - data.readString(false)); - proxyList.add(info); - if (currentProxy == null && !TextUtils.isEmpty(proxyAddress)) { - if (proxyAddress.equals(info.address) && proxyPort == info.port && proxyUsername.equals(info.username) && proxyPassword.equals(info.password)) { + + int current = MessagesController.getGlobalMainSettings().getInt("current_proxy", 0); + + for (SubInfo subInfo : SubManager.getSubList().find()) { + + if (!subInfo.enable) continue; + +// if (subInfo.id == 1L) { +// +// try { +// RelayBatonProxy publicProxy = (RelayBatonProxy) parseProxyInfo(RelayBatonLoader.publicServer); +// publicProxy.setRemarks(LocaleController.getString("NekoXProxy",R.string.NekoXProxy)); +// publicProxy.subId = subInfo.id; +// proxyList.add(publicProxy); +// if (publicProxy.hashCode() == current) { +// currentProxy = publicProxy; +// UIUtil.runOnIoDispatcher(publicProxy::start); +// } +// } catch (InvalidProxyException e) { +// e.printStackTrace(); +// } +// +// } + + for (String proxy : subInfo.proxies) { + + try { + + ProxyInfo info = parseProxyInfo(proxy); + + info.subId = subInfo.id; + + if (info.hashCode() == current) { + currentProxy = info; + + if (info instanceof ExternalSocks5Proxy) { + + UIUtil.runOnIoDispatcher(() -> { + + try { + + ((ExternalSocks5Proxy) info).start(); + + } catch (Exception e) { + + FileLog.e(e); + AlertUtil.showToast(e); + + } + + }); + + } + } + + proxyList.add(info); + + } catch (Exception e) { + + FileLog.d("load sub proxy failed: " + e); + } + } - data.cleanup(); + } - if (currentProxy == null && !TextUtils.isEmpty(proxyAddress)) { - ProxyInfo info = currentProxy = new ProxyInfo(proxyAddress, proxyPort, proxyUsername, proxyPassword, proxySecret); - proxyList.add(0, info); + + File proxyListFile = new File(ApplicationLoader.applicationContext.getFilesDir().getParentFile(), "nekox/proxy_list.json"); + + boolean error = false; + + if (proxyListFile.isFile()) { + + try { + + JSONArray proxyArray = new JSONArray(FileUtil.readUtf8String(proxyListFile)); + + for (int a = 0; a < proxyArray.length(); a++) { + + JSONObject proxyObj = proxyArray.getJSONObject(a); + + ProxyInfo info; + + try { + + info = ProxyInfo.fromJson(proxyObj); + + } catch (Exception ex) { + + FileLog.d("load proxy failed: " + ex); + + error = true; + + continue; + + } + + proxyList.add(info); + + if (info.hashCode() == current) { + + currentProxy = info; + + if (info instanceof ExternalSocks5Proxy) { + + if (info instanceof ExternalSocks5Proxy) { + + UIUtil.runOnIoDispatcher(() -> { + + try { + + ((ExternalSocks5Proxy) info).start(); + + } catch (Exception e) { + + FileLog.e(e); + AlertUtil.showToast(e); + + } + + }); + + } + } + + } + + } + + } catch (Exception ex) { + + FileLog.d("invalid proxy list json format" + ex); + + } + } + + if (error) saveProxyList(); + + } + + public static ProxyInfo parseProxyInfo(String url) throws InvalidProxyException { + + if (url.startsWith(V2RayConfig.VMESS_PROTOCOL) || url.startsWith(V2RayConfig.VMESS1_PROTOCOL)) { + + try { + + return new VmessProxy(url); + + } catch (Exception ex) { + + throw new InvalidProxyException(ex); + + } + + } else if (url.startsWith(SS_PROTOCOL)) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + + throw new InvalidProxyException("shadowsocks requires min api 21"); + + } + + try { + + return new ShadowsocksProxy(url); + + } catch (Exception ex) { + + throw new InvalidProxyException(ex); + + } + + } else if (url.startsWith(SSR_PROTOCOL)) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + + throw new InvalidProxyException("shadowsocksR requires min api 21"); + + } + + try { + + return new ShadowsocksRProxy(url); + + } catch (Exception ex) { + + throw new InvalidProxyException(ex); + + } + + }/* else if (url.startsWith(RB_PROTOCOL)) { + + try { + + return new RelayBatonProxy(url); + + } catch (Exception ex) { + + throw new InvalidProxyException(ex); + + } + + } */ + + if (url.startsWith("tg:proxy") || + url.startsWith("tg://proxy") || + url.startsWith("tg:socks") || + url.startsWith("tg://socks") || + url.startsWith("https://t.me/proxy") || + url.startsWith("https://t.me/socks")) { + return ProxyInfo.fromUrl(url); + } + + throw new InvalidProxyException(); + + } + + public static class InvalidProxyException extends Exception { + + public InvalidProxyException() { + } + + public InvalidProxyException(String messsage) { + super(messsage); + } + + public InvalidProxyException(Throwable cause) { + + super(cause); + + } + } public static void saveProxyList() { - SerializedData serializedData = new SerializedData(); - int count = proxyList.size(); - serializedData.writeInt32(count); - for (int a = 0; a < count; a++) { - ProxyInfo info = proxyList.get(a); - serializedData.writeString(info.address != null ? info.address : ""); - serializedData.writeInt32(info.port); - serializedData.writeString(info.username != null ? info.username : ""); - serializedData.writeString(info.password != null ? info.password : ""); - serializedData.writeString(info.secret != null ? info.secret : ""); - } - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().putString("proxy_list", Base64.encodeToString(serializedData.toByteArray(), Base64.NO_WRAP)).commit(); - serializedData.cleanup(); + UIUtil.runOnIoDispatcher(() -> { + + JSONArray proxyArray = new JSONArray(); + + for (ProxyInfo info : getProxyList()) { + try { + JSONObject obj = info.toJsonInternal(); + if (info.subId != 0L) { + continue; + } + proxyArray.put(obj); + } catch (Exception e) { + FileLog.e(e); + } + } + + File proxyListFile = new File(ApplicationLoader.applicationContext.getFilesDir().getParentFile(), "nekox/proxy_list.json"); + + try { + FileUtil.writeUtf8String(proxyArray.toString(), proxyListFile); + } catch (Exception e) { + FileLog.e(e); + } + + }); } public static ProxyInfo addProxy(ProxyInfo proxyInfo) { - loadProxyList(); - int count = proxyList.size(); - for (int a = 0; a < count; a++) { - ProxyInfo info = proxyList.get(a); - if (proxyInfo.address.equals(info.address) && proxyInfo.port == info.port && proxyInfo.username.equals(info.username) && proxyInfo.password.equals(info.password) && proxyInfo.secret.equals(info.secret)) { - return info; + synchronized (sync) { + int count = proxyList.size(); + for (int a = 0; a < count; a++) { + ProxyInfo info = proxyList.get(a); + if (info.equals(proxyInfo)) { + return info; + } } + proxyList.add(proxyInfo); } - proxyList.add(proxyInfo); saveProxyList(); return proxyInfo; } public static void deleteProxy(ProxyInfo proxyInfo) { + if (currentProxy == proxyInfo) { currentProxy = null; - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - boolean enabled = preferences.getBoolean("proxy_enabled", false); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("proxy_ip", ""); - editor.putString("proxy_pass", ""); - editor.putString("proxy_user", ""); - editor.putString("proxy_secret", ""); - editor.putInt("proxy_port", 1080); - editor.putBoolean("proxy_enabled", false); - editor.putBoolean("proxy_enabled_calls", false); - editor.commit(); - if (enabled) { - ConnectionsManager.setProxySettings(false, "", 0, "", "", ""); + if (proxyEnabled) { + setProxyEnable(false); } } proxyList.remove(proxyInfo); + if (proxyInfo.subId != 0) { + SubInfo sub = SubManager.getSubList().find(ObjectFilters.eq("id", proxyInfo.subId)).firstOrDefault(); + sub.proxies.remove(proxyInfo.toUrl()); + SubManager.getSubList().update(sub); + } saveProxyList(); } + public static void deleteAllProxy() { + + setCurrentProxy(null); + + proxyListLoaded = false; + + proxyList.clear(); + + saveProxyList(); + + loadProxyList(); + + } + public static void checkSaveToGalleryFiles() { try { - File telegramPath; - if (NekoConfig.saveCacheToPrivateDirectory) { - telegramPath = new File(ApplicationLoader.applicationContext.getFilesDir(), "Telegram"); - } else { - telegramPath = new File(Environment.getExternalStorageDirectory(), "Telegram"); - } - File imagePath = new File(telegramPath, "Telegram Images"); - imagePath.mkdir(); - File videoPath = new File(telegramPath, "Telegram Video"); - videoPath.mkdir(); + File telegramPath = EnvUtil.getTelegramPath(); + File imagePath = new File(telegramPath, ""); + imagePath.mkdirs(); + File videoPath = new File(telegramPath, "videos"); + videoPath.mkdirs(); if (saveToGallery) { if (imagePath.isDirectory()) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java index f7b6f32cd..0df4f2dfc 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java @@ -348,7 +348,7 @@ public class Browser { } return true; - } else if ("tg".equals(uri.getScheme())) { + } else if ("tg".equals(uri.getScheme()) || "vmess".equals(uri.getScheme()) || "vmesss1".equals(uri.getScheme()) || "ss".equals(uri.getScheme()) || "ssr".equals(uri.getScheme())) { return true; } else if ("telegram.dog".equals(host)) { String path = uri.getPath(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java index 407d57e01..b00a85c39 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java @@ -64,7 +64,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; -import tw.nekomimi.nekogram.NekoConfig; +import tw.nekomimi.nekogram.utils.EnvUtil; public class ChatAttachAlertDocumentLayout extends ChatAttachAlert.AttachAlertLayout { @@ -109,6 +109,12 @@ public class ChatAttachAlertDocumentLayout extends ChatAttachAlert.AttachAlertLa private boolean canSelectOnlyImageFiles; private boolean allowMusic; + public void setAllowPhoto(boolean allowPhoto) { + this.allowPhoto = allowPhoto; + } + + private boolean allowPhoto = true; + private boolean searching; private boolean sortByName; @@ -222,7 +228,7 @@ public class ChatAttachAlertDocumentLayout extends ChatAttachAlert.AttachAlertLa emptyImageView = new ImageView(context); emptyImageView.setImageResource(R.drawable.files_empty); - emptyImageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogEmptyImage), PorterDuff.Mode.MULTIPLY)); + emptyImageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogEmptyImage), PorterDuff.Mode.SRC_IN)); emptyView.addView(emptyImageView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT)); emptyTitleTextView = new TextView(context); @@ -969,12 +975,7 @@ public class ChatAttachAlertDocumentLayout extends ChatAttachAlert.AttachAlertLa ListItem fs; try { - File telegramPath; - if (NekoConfig.saveCacheToPrivateDirectory) { - telegramPath = new File(ApplicationLoader.applicationContext.getFilesDir(), "Telegram"); - } else { - telegramPath = new File(Environment.getExternalStorageDirectory(), "Telegram"); - } + File telegramPath = EnvUtil.getTelegramPath(); if (telegramPath.exists()) { fs = new ListItem(); fs.title = "Telegram"; @@ -987,12 +988,16 @@ public class ChatAttachAlertDocumentLayout extends ChatAttachAlert.AttachAlertLa FileLog.e(e); } - fs = new ListItem(); - fs.title = LocaleController.getString("Gallery", R.string.Gallery); - fs.subtitle = LocaleController.getString("GalleryInfo", R.string.GalleryInfo); - fs.icon = R.drawable.files_gallery; - fs.file = null; - items.add(fs); + if (allowPhoto) { + + fs = new ListItem(); + fs.title = LocaleController.getString("Gallery", R.string.Gallery); + fs.subtitle = LocaleController.getString("GalleryInfo", R.string.GalleryInfo); + fs.icon = R.drawable.files_gallery; + fs.file = null; + items.add(fs); + + } if (allowMusic) { fs = new ListItem(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PickerBottomLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PickerBottomLayout.java index 919be0a34..d9bae9b35 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PickerBottomLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PickerBottomLayout.java @@ -26,6 +26,8 @@ public class PickerBottomLayout extends FrameLayout { public LinearLayout doneButton; public TextView cancelButton; + public LinearLayout middleButton; + public TextView middleButtonTextView; public TextView doneButtonTextView; public TextView doneButtonBadgeTextView; @@ -48,11 +50,31 @@ public class PickerBottomLayout extends FrameLayout { cancelButton.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); addView(cancelButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + LinearLayout rightLayout = new LinearLayout(context); + rightLayout.setOrientation(LinearLayout.HORIZONTAL); + rightLayout.setGravity(Gravity.CENTER_VERTICAL); + addView(rightLayout, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.RIGHT)); + + middleButton = new LinearLayout(context); + middleButton.setOrientation(LinearLayout.HORIZONTAL); + middleButton.setBackgroundDrawable(Theme.createSelectorDrawable(0x0f000000, 0)); + middleButton.setPadding(AndroidUtilities.dp(33), 0, AndroidUtilities.dp(33), 0); + middleButton.setVisibility(GONE); + rightLayout.addView(middleButton); + + middleButtonTextView = new TextView(context); + middleButtonTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + middleButtonTextView.setTextColor(Theme.getColor(Theme.key_picker_enabledButton)); + middleButtonTextView.setGravity(Gravity.CENTER); + middleButtonTextView.setCompoundDrawablePadding(AndroidUtilities.dp(8)); + middleButtonTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + middleButton.addView(middleButtonTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL)); + doneButton = new LinearLayout(context); doneButton.setOrientation(LinearLayout.HORIZONTAL); doneButton.setBackgroundDrawable(Theme.createSelectorDrawable(0x0f000000, 0)); doneButton.setPadding(AndroidUtilities.dp(33), 0, AndroidUtilities.dp(33), 0); - addView(doneButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.RIGHT)); + rightLayout.addView(doneButton); doneButtonBadgeTextView = new TextView(context); doneButtonBadgeTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java index 17137c4f3..20a878cc2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java @@ -45,6 +45,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import okhttp3.OkHttpClient; +import okhttp3.Request; import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONTokener; @@ -59,25 +61,19 @@ import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.R; import org.telegram.messenger.Utilities; import org.telegram.tgnet.TLRPC; +import tw.nekomimi.nekogram.utils.HttpUtil; +import tw.nekomimi.nekogram.utils.ThreadUtil; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLConnection; import java.net.URLDecoder; import java.net.URLEncoder; -import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPInputStream; public class WebPlayerView extends ViewGroup implements VideoPlayer.VideoPlayerDelegate, AudioManager.OnAudioFocusChangeListener { @@ -445,139 +441,48 @@ public class WebPlayerView extends ViewGroup implements VideoPlayer.VideoPlayerD } protected String downloadUrlContent(AsyncTask parentTask, String url, HashMap headers, boolean tryGzip) { - boolean canRetry = true; - InputStream httpConnectionStream = null; - boolean done = false; - StringBuilder result = null; - URLConnection httpConnection = null; - try { - URL downloadUrl = new URL(url); - httpConnection = downloadUrl.openConnection(); - httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)"); - if (tryGzip) { - httpConnection.addRequestProperty("Accept-Encoding", "gzip, deflate"); - } - httpConnection.addRequestProperty("Accept-Language", "en-us,en;q=0.5"); - httpConnection.addRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - httpConnection.addRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); - if (headers != null) { - for (HashMap.Entry entry : headers.entrySet()) { - httpConnection.addRequestProperty(entry.getKey(), entry.getValue()); - } - } - httpConnection.setConnectTimeout(5000); - httpConnection.setReadTimeout(5000); - if (httpConnection instanceof HttpURLConnection) { - HttpURLConnection httpURLConnection = (HttpURLConnection) httpConnection; - httpURLConnection.setInstanceFollowRedirects(true); - int status = httpURLConnection.getResponseCode(); - if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER) { - String newUrl = httpURLConnection.getHeaderField("Location"); - String cookies = httpURLConnection.getHeaderField("Set-Cookie"); - downloadUrl = new URL(newUrl); - httpConnection = downloadUrl.openConnection(); - httpConnection.setRequestProperty("Cookie", cookies); - httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)"); - if (tryGzip) { - httpConnection.addRequestProperty("Accept-Encoding", "gzip, deflate"); - } - httpConnection.addRequestProperty("Accept-Language", "en-us,en;q=0.5"); - httpConnection.addRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - httpConnection.addRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); - if (headers != null) { - for (HashMap.Entry entry : headers.entrySet()) { - httpConnection.addRequestProperty(entry.getKey(), entry.getValue()); - } - } - } - } - httpConnection.connect(); - if (tryGzip) { - try { - httpConnectionStream = new GZIPInputStream(httpConnection.getInputStream()); - } catch (Exception e) { - try { - if (httpConnectionStream != null) { - httpConnectionStream.close(); - } - } catch (Exception ignore) { - } - httpConnection = downloadUrl.openConnection(); - httpConnection.connect(); - httpConnectionStream = httpConnection.getInputStream(); - } - } else { - httpConnectionStream = httpConnection.getInputStream(); + OkHttpClient client = HttpUtil.getOkHttpClientWithCurrProxy().newBuilder() + .followRedirects(true) + .followSslRedirects(true) + .build(); + + Request.Builder request = new Request.Builder().url(url) + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)") + .header("Accept-Language", "en-us,en;q=0.5") + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .header("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); + + if (headers != null) { + + for (Map.Entry header : headers.entrySet()) { + + request.addHeader(header.getKey(), header.getValue()); + } - } catch (Throwable e) { - if (e instanceof SocketTimeoutException) { - if (ApplicationLoader.isNetworkOnline()) { - canRetry = false; - } - } else if (e instanceof UnknownHostException) { - canRetry = false; - } else if (e instanceof SocketException) { - if (e.getMessage() != null && e.getMessage().contains("ECONNRESET")) { - canRetry = false; - } - } else if (e instanceof FileNotFoundException) { - canRetry = false; - } - FileLog.e(e); + } - if (canRetry) { + if (tryGzip) { + + request.addHeader("Accept-Encoding", "gzip, deflate"); + + } + + int count = 0; + + do { try { - if (httpConnection instanceof HttpURLConnection) { - int code = ((HttpURLConnection) httpConnection).getResponseCode(); - if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_ACCEPTED && code != HttpURLConnection.HTTP_NOT_MODIFIED) { - //canRetry = false; - } - } + return client.newCall(request.build()).execute().body().string(); } catch (Exception e) { FileLog.e(e); } + count ++; + ThreadUtil.sleep(1000); + } while (count < 3); - if (httpConnectionStream != null) { - try { - byte[] data = new byte[1024 * 32]; - while (true) { - if (parentTask.isCancelled()) { - break; - } - try { - int read = httpConnectionStream.read(data); - if (read > 0) { - if (result == null) { - result = new StringBuilder(); - } - result.append(new String(data, 0, read, StandardCharsets.UTF_8)); - } else if (read == -1) { - done = true; - break; - } else { - break; - } - } catch (Exception e) { - FileLog.e(e); - break; - } - } - } catch (Throwable e) { - FileLog.e(e); - } - } + return null; - try { - if (httpConnectionStream != null) { - httpConnectionStream.close(); - } - } catch (Throwable e) { - FileLog.e(e); - } - } - return done ? result.toString() : null; } private class YoutubeVideoTask extends AsyncTask { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 66e99cdf0..e04d16108 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -15,12 +15,11 @@ import android.app.Activity; import android.app.ActivityManager; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; -import android.graphics.Canvas; import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; @@ -50,43 +49,50 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.Toast; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.v2ray.ang.V2RayConfig; + import org.telegram.messenger.AccountInstance; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.BuildVars; import org.telegram.messenger.ChatObject; import org.telegram.messenger.ContactsController; -import org.telegram.messenger.LocationController; -import org.telegram.messenger.MediaDataController; import org.telegram.messenger.FileLoader; +import org.telegram.messenger.FileLog; import org.telegram.messenger.ImageLoader; +import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; +import org.telegram.messenger.MediaDataController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; -import org.telegram.messenger.SendMessagesHelper; -import org.telegram.messenger.SharedConfig; -import org.telegram.messenger.UserObject; -import org.telegram.messenger.Utilities; -import org.telegram.messenger.ApplicationLoader; -import org.telegram.messenger.FileLog; -import org.telegram.messenger.LocaleController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; +import org.telegram.messenger.SendMessagesHelper; +import org.telegram.messenger.SharedConfig; +import org.telegram.messenger.UserConfig; +import org.telegram.messenger.UserObject; +import org.telegram.messenger.Utilities; import org.telegram.messenger.browser.Browser; import org.telegram.messenger.camera.CameraController; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; -import org.telegram.messenger.UserConfig; -import org.telegram.ui.ActionBar.AlertDialog; -import org.telegram.ui.Adapters.DrawerLayoutAdapter; import org.telegram.ui.ActionBar.ActionBarLayout; +import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.DrawerLayoutContainer; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Adapters.DrawerLayoutAdapter; +import org.telegram.ui.Cells.DrawerActionCheckCell; import org.telegram.ui.Cells.DrawerAddCell; import org.telegram.ui.Cells.DrawerUserCell; import org.telegram.ui.Cells.LanguageCell; -import org.telegram.ui.Components.AudioPlayerAlert; import org.telegram.ui.Components.AlertsCreator; +import org.telegram.ui.Components.AudioPlayerAlert; import org.telegram.ui.Components.BlockingUpdateView; import org.telegram.ui.Components.ChatActivityEnterView; import org.telegram.ui.Components.Easings; @@ -100,10 +106,9 @@ import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.SharingLocationsAlert; import org.telegram.ui.Components.SideMenultItemAnimator; import org.telegram.ui.Components.StickersAlert; -import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.Switch; import org.telegram.ui.Components.TermsOfServiceView; import org.telegram.ui.Components.ThemeEditorView; -import org.telegram.ui.Components.UpdateAppAlertDialog; import java.io.File; import java.util.ArrayList; @@ -113,11 +118,17 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - +import tw.nekomimi.nekogram.NekoConfig; +import tw.nekomimi.nekogram.NekoXConfig; +import tw.nekomimi.nekogram.NekoXSettingActivity; import tw.nekomimi.nekogram.settings.NekoSettingsActivity; +import tw.nekomimi.nekogram.sub.SubInfo; +import tw.nekomimi.nekogram.sub.SubManager; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.PrivacyUtil; +import tw.nekomimi.nekogram.utils.ProxyUtil; +import tw.nekomimi.nekogram.utils.UIUtil; +import tw.nekomimi.nekogram.utils.UpdateUtil; public class LaunchActivity extends Activity implements ActionBarLayout.ActionBarLayoutDelegate, NotificationCenter.NotificationCenterDelegate, DialogsActivity.DialogsActivityDelegate { @@ -249,7 +260,12 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa Uri uri = intent.getData(); if (uri != null) { String url = uri.toString().toLowerCase(); - isProxy = url.startsWith("tg:proxy") || url.startsWith("tg://proxy") || url.startsWith("tg:socks") || url.startsWith("tg://socks"); + isProxy = url.startsWith("tg:proxy") || url.startsWith("tg://proxy") || url.startsWith("tg:socks") || url.startsWith("tg://socks") || + url.startsWith(V2RayConfig.VMESS_PROTOCOL) || + url.startsWith(V2RayConfig.VMESS1_PROTOCOL) || + url.startsWith(V2RayConfig.SS_PROTOCOL) || + url.startsWith(V2RayConfig.SSR_PROTOCOL) + ; } } } @@ -257,7 +273,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa long crashed_time = preferences.getLong("intro_crashed_time", 0); boolean fromIntro = intent.getBooleanExtra("fromIntro", false); if (fromIntro) { - preferences.edit().putLong("intro_crashed_time", 0).commit(); + preferences.edit().putLong("intro_crashed_time", 0).apply(); } if (!isProxy && Math.abs(crashed_time - System.currentTimeMillis()) >= 60 * 2 * 1000 && intent != null && !fromIntro) { preferences = ApplicationLoader.applicationContext.getSharedPreferences("logininfo2", MODE_PRIVATE); @@ -313,7 +329,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } }); - if (SharedConfig.passcodeHash.length() > 0 && !SharedConfig.allowScreenCapture) { + if (SharedConfig.passcodeHash.length() > 0 && !SharedConfig.allowScreenCapture && !NekoXConfig.disableFlagSecure) { try { getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); } catch (Exception e) { @@ -511,10 +527,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa sideMenu.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); sideMenu.setAllowItemsInteractionDuringAnimation(false); sideMenu.setAdapter(drawerLayoutAdapter = new DrawerLayoutAdapter(this, itemAnimator)); - ItemTouchHelper drawerItemTouchHelper = new ItemTouchHelper(new DrawerItemTouchHelperCallback()); drawerItemTouchHelper.attachToRecyclerView(sideMenu); - drawerLayoutContainer.setDrawerLayout(sideMenu); FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) sideMenu.getLayoutParams(); Point screenSize = AndroidUtilities.getRealScreenSize(); @@ -539,6 +553,52 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa presentFragment(new LoginActivity(freeAccount)); } drawerLayoutContainer.closeDrawer(false); + } else if (view instanceof DrawerActionCheckCell) { + int id = drawerLayoutAdapter.getId(position); + // DrawerLayoutAdapter.CheckItem item = drawerLayoutAdapter.getItem(position); + if (id == 12) { + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("themeconfig", Activity.MODE_PRIVATE); + String dayThemeName = preferences.getString("lastDayTheme", "Blue"); + if (Theme.getTheme(dayThemeName) == null) { + dayThemeName = "Blue"; + } + String nightThemeName = preferences.getString("lastDarkTheme", "Night"); + if (Theme.getTheme(nightThemeName) == null) { + nightThemeName = "Night"; + } + Theme.ThemeInfo themeInfo = Theme.getActiveTheme(); + + ((DrawerActionCheckCell) view).setChecked(!themeInfo.isDark()); + + if (dayThemeName.equals(nightThemeName)) { + if (themeInfo.isDark()) { + dayThemeName = "Blue"; + } else { + nightThemeName = "Night"; + } + } + + if (dayThemeName.equals(themeInfo.getKey())) { + themeInfo = Theme.getTheme(nightThemeName); + } else { + themeInfo = Theme.getTheme(dayThemeName); + } + if (Theme.selectedAutoNightType != Theme.AUTO_NIGHT_TYPE_NONE) { + AlertUtil.showToast(LocaleController.getString("AutoNightModeOff", R.string.AutoNightModeOff)); + Theme.selectedAutoNightType = Theme.AUTO_NIGHT_TYPE_NONE; + Theme.saveAutoNightThemeConfig(); + Theme.cancelAutoNightThemeCallbacks(); + } + int[] pos = new int[2]; + Switch s = ((DrawerActionCheckCell) view).checkBox; + s.getLocationInWindow(pos); + pos[0] += s.getMeasuredWidth() / 2; + pos[1] += s.getMeasuredHeight() / 2; + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.needSetDayNightTheme, themeInfo, false, pos, -1); + } else if (id == 13) { + presentFragment(new ProxyListActivity()); + drawerLayoutContainer.closeDrawer(false); + } } else { int id = drawerLayoutAdapter.getId(position); if (id == 2) { @@ -562,7 +622,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa presentFragment(new ChannelCreateActivity(args)); } else { presentFragment(new ActionIntroActivity(ActionIntroActivity.ACTION_TYPE_CHANNEL_CREATE)); - preferences.edit().putBoolean("channel_intro", true).commit(); + preferences.edit().putBoolean("channel_intro", true).apply(); } drawerLayoutContainer.closeDrawer(false); } else if (id == 6) { @@ -576,7 +636,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa presentFragment(fragment); drawerLayoutContainer.closeDrawer(false); } else if (id == 9) { - Browser.openUrl(LaunchActivity.this, LocaleController.getString("TelegramFaqUrl", R.string.TelegramFaqUrl)); + Browser.openUrl(LaunchActivity.this, NekoXConfig.FAQ_URL); drawerLayoutContainer.closeDrawer(false); } else if (id == 10) { presentFragment(new CallLogActivity()); @@ -618,6 +678,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.didSetNewWallpapper); NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.notificationsCountUpdated); NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.screenStateChanged); + NotificationCenter.getGlobalInstance().addObserver(drawerLayoutAdapter, NotificationCenter.proxySettingsChanged); if (actionBarLayout.fragmentsStack.isEmpty()) { if (!UserConfig.getInstance(currentAccount).isClientActivated()) { @@ -745,7 +806,26 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa FileLog.e(e); } MediaController.getInstance().setBaseActivity(this, true); - AndroidUtilities.startAppCenter(this); + UpdateUtil.checkUpdate(this); + UIUtil.runOnIoDispatcher(() -> { + + for (SubInfo subInfo : SubManager.getSubList().find()) { + + if (subInfo == null) continue; + + try { + + subInfo.proxies = subInfo.reloadProxies(); + subInfo.lastFetch = System.currentTimeMillis(); + + SubManager.getSubList().update(subInfo, true); + + } catch (SubInfo.AllTriesFailed allTriesFailed) { + } + + } + + }); } private void checkSystemBarColors() { @@ -765,6 +845,10 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } public void switchToAccount(int account, boolean removeAll) { + switchToAccount(account, removeAll, false); + } + + public void switchToAccount(int account, boolean removeAll, boolean afterLogin) { if (account == UserConfig.selectedAccount) { return; } @@ -806,6 +890,9 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (UserConfig.getInstance(account).unacceptedTermsOfService != null) { showTosActivity(account, UserConfig.getInstance(account).unacceptedTermsOfService); } + if (afterLogin) { + PrivacyUtil.postCheckAll(this, account); + } updateCurrentConnectionState(currentAccount); } @@ -1117,15 +1204,14 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (!TextUtils.isEmpty(text)) { Matcher m = locationRegex.matcher(text); if (m.find()) { - String lines[] = text.split("\\n"); + String[] lines = text.split("\\n"); String venueTitle = null; String venueAddress = null; - if (lines[0].equals("My Position")){ + if (lines[0].equals("My Position")) { // Use normal GeoPoint message (user position) - } - else if(!lines[0].contains("geo:")){ + } else if (!lines[0].contains("geo:")) { venueTitle = lines[0]; - if(!lines[1].contains("geo:")){ + if (!lines[1].contains("geo:")) { venueAddress = lines[1]; } } @@ -1151,11 +1237,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa parcelable = Uri.parse(parcelable.toString()); } Uri uri = (Uri) parcelable; - if (uri != null) { - if (AndroidUtilities.isInternalUri(uri)) { - error = true; - } - } if (!error) { if (uri != null && (type != null && type.startsWith("image/") || uri.toString().toLowerCase().endsWith(".jpg"))) { if (photoPathsArray == null) { @@ -1642,6 +1723,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa open_settings = 3; } else if (url.contains("folders")) { open_settings = 4; + } else if (url.contains("nekox")) { + open_settings = 101; } else if (url.contains("neko")) { open_settings = 100; } @@ -1651,6 +1734,17 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } catch (Exception e) { FileLog.e(e); } + } else if (url.startsWith("tg:user") || url.startsWith("tg://user")) { + try { + url = url.replace("tg:user", "tg://telegram.org").replace("tg://user", "tg://telegram.org"); + data = Uri.parse(url); + int userId = Utilities.parseInt(data.getQueryParameter("id")); + if (userId != 0) { + push_user_id = userId; + } + } catch (Exception e) { + FileLog.e(e); + } } else { unsupportedUrl = url.replace("tg://", "").replace("tg:", ""); int index; @@ -1849,6 +1943,12 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa fragment = new FiltersSetupActivity(); } else if (open_settings == 100) { fragment = new NekoSettingsActivity(); + } else if (open_settings == 101) { + if (NekoXConfig.developerMode) { + fragment = new NekoXSettingActivity(); + } else { + fragment = new NekoSettingsActivity(); + } } else { fragment = null; } @@ -1949,7 +2049,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.didReceiveSmsCode, code); } else { AlertDialog.Builder builder = new AlertDialog.Builder(LaunchActivity.this); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); builder.setMessage(AndroidUtilities.replaceTags(LocaleController.formatString("OtherLoginCode", R.string.OtherLoginCode, code))); builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); showAlertDialog(builder); @@ -2178,7 +2278,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } else { AlertDialog.Builder builder = new AlertDialog.Builder(LaunchActivity.this); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); if (error.text.startsWith("FLOOD_WAIT")) { builder.setMessage(LocaleController.getString("FloodWait", R.string.FloodWait)); } else { @@ -2232,7 +2332,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } else { AlertDialog.Builder builder = new AlertDialog.Builder(LaunchActivity.this); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); if (error.text.startsWith("FLOOD_WAIT")) { builder.setMessage(LocaleController.getString("FloodWait", R.string.FloodWait)); } else if (error.text.equals("USERS_TOO_MUCH")) { @@ -2359,7 +2459,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } if (response instanceof TLRPC.TL_langPackLanguage) { TLRPC.TL_langPackLanguage res = (TLRPC.TL_langPackLanguage) response; - showAlertDialog(AlertsCreator.createLanguageAlert(LaunchActivity.this, res)); + AlertsCreator.createLanguageAlert(LaunchActivity.this, res).show(); } else if (error != null) { if ("LANG_CODE_NOT_SUPPORTED".equals(error.text)) { showAlertDialog(AlertsCreator.createSimpleAlert(LaunchActivity.this, LocaleController.getString("LanguageUnsupportedError", R.string.LanguageUnsupportedError))); @@ -2539,49 +2639,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } - public void checkAppUpdate(boolean force) { - if (!force && BuildVars.DEBUG_VERSION) { - return; - } - if (!force && Math.abs(System.currentTimeMillis() - UserConfig.getInstance(0).lastUpdateCheckTime) < 24 * 60 * 60 * 1000) { - return; - } - TLRPC.TL_help_getAppUpdate req = new TLRPC.TL_help_getAppUpdate(); - try { - req.source = ApplicationLoader.applicationContext.getPackageManager().getInstallerPackageName(ApplicationLoader.applicationContext.getPackageName()); - } catch (Exception ignore) { - - } - if (req.source == null) { - req.source = ""; - } - final int accountNum = currentAccount; - ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> { - UserConfig.getInstance(0).lastUpdateCheckTime = System.currentTimeMillis(); - UserConfig.getInstance(0).saveConfig(false); - if (response instanceof TLRPC.TL_help_appUpdate) { - final TLRPC.TL_help_appUpdate res = (TLRPC.TL_help_appUpdate) response; - AndroidUtilities.runOnUIThread(() -> { - if (res.can_not_skip) { - UserConfig.getInstance(0).pendingAppUpdate = res; - UserConfig.getInstance(0).pendingAppUpdateBuildVersion = BuildVars.BUILD_VERSION; - try { - PackageInfo packageInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); - UserConfig.getInstance(0).pendingAppUpdateInstallTime = Math.max(packageInfo.lastUpdateTime, packageInfo.firstInstallTime); - } catch (Exception e) { - FileLog.e(e); - UserConfig.getInstance(0).pendingAppUpdateInstallTime = 0; - } - UserConfig.getInstance(0).saveConfig(false); - showUpdateActivity(accountNum, res, false); - } else { - (new UpdateAppAlertDialog(LaunchActivity.this, res, accountNum)).show(); - } - }); - } - }); - } - public AlertDialog showAlertDialog(AlertDialog.Builder builder) { try { if (visibleDialog != null) { @@ -2605,12 +2662,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } localeDialog = null; } else if (visibleDialog == proxyErrorDialog) { - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); - editor.putBoolean("proxy_enabled", false); - editor.putBoolean("proxy_enabled_calls", false); - editor.commit(); - ConnectionsManager.setProxySettings(false, "", 1080, "", "", ""); + SharedConfig.setProxyEnable(false); NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); proxyErrorDialog = null; } @@ -2804,6 +2856,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.didSetPasscode); NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.notificationsCountUpdated); NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.screenStateChanged); + NotificationCenter.getGlobalInstance().removeObserver(drawerLayoutAdapter, NotificationCenter.proxySettingsChanged); + } public void presentFragment(BaseFragment fragment) { @@ -2928,7 +2982,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa private void showPermissionErrorAlert(String message) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); builder.setMessage(message); builder.setNegativeButton(LocaleController.getString("PermissionOpenSettings", R.string.PermissionOpenSettings), (dialog, which) -> { try { @@ -2953,13 +3007,18 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa ApplicationLoader.mainInterfacePausedStageQueueTime = 0; }); onPasscodePause(); - actionBarLayout.onPause(); - if (AndroidUtilities.isTablet()) { - rightActionBarLayout.onPause(); - layersActionBarLayout.onPause(); - } - if (passcodeView != null) { - passcodeView.onPause(); + try { + if (actionBarLayout != null) { + actionBarLayout.onPause(); + } + if (AndroidUtilities.isTablet()) { + rightActionBarLayout.onPause(); + layersActionBarLayout.onPause(); + } + if (passcodeView != null) { + passcodeView.onPause(); + } + } catch (Exception ignored) { } ConnectionsManager.getInstance(currentAccount).setAppPaused(true, false); if (PhotoViewer.hasInstance() && PhotoViewer.getInstance().isVisible()) { @@ -3061,6 +3120,9 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } ConnectionsManager.getInstance(currentAccount).setAppPaused(false, false); updateCurrentConnectionState(currentAccount); + if (NekoConfig.disableProxyWhenVpnEnabled && SharedConfig.proxyEnabled && ProxyUtil.isVPNEnabled()) { + SharedConfig.setProxyEnable(false); + } if (PhotoViewer.hasInstance() && PhotoViewer.getInstance().isVisible()) { PhotoViewer.getInstance().onResume(); } @@ -3076,7 +3138,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } else if (UserConfig.getInstance(0).pendingAppUpdate != null) { showUpdateActivity(UserConfig.selectedAccount, UserConfig.getInstance(0).pendingAppUpdate, true); } - checkAppUpdate(false); } @Override @@ -3141,7 +3202,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa return; } AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); if (reason != 2 && reason != 3) { builder.setNegativeButton(LocaleController.getString("MoreInfo", R.string.MoreInfo), (dialogInterface, i) -> { if (!mainFragmentsStack.isEmpty()) { @@ -3180,7 +3241,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } else if (id == NotificationCenter.wasUnableToFindCurrentLocation) { final HashMap waitingForLocation = (HashMap) args[0]; AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); builder.setNegativeButton(LocaleController.getString("ShareYouLocationUnableManually", R.string.ShareYouLocationUnableManually), (dialogInterface, i) -> { if (mainFragmentsStack.isEmpty()) { @@ -3211,7 +3272,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } } else if (id == NotificationCenter.didSetPasscode) { - if (SharedConfig.passcodeHash.length() > 0 && !SharedConfig.allowScreenCapture) { + if (SharedConfig.passcodeHash.length() > 0 && !SharedConfig.allowScreenCapture && !NekoXConfig.disableFlagSecure) { try { getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); } catch (Exception e) { @@ -3290,7 +3351,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa themeSwitchImageView.setVisibility(View.VISIBLE); float finalRadius = (float) Math.max(Math.sqrt((w - pos[0]) * (w - pos[0]) + (h - pos[1]) * (h - pos[1])), Math.sqrt(pos[0] * pos[0] + (h - pos[1]) * (h - pos[1]))); Animator anim = ViewAnimationUtils.createCircularReveal(drawerLayoutContainer, pos[0], pos[1], 0, finalRadius); - anim.setDuration(400); + anim.setDuration(100); anim.setInterpolator(Easings.easeInOutQuad); anim.addListener(new AnimatorListenerAdapter() { @Override @@ -3465,7 +3526,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa freeSpace = statFs.getAvailableBlocksLong() * statFs.getBlockSizeLong(); } if (freeSpace < 1024 * 1024 * 100) { - preferences.edit().putLong("last_space_check", System.currentTimeMillis()).commit(); + preferences.edit().putLong("last_space_check", System.currentTimeMillis()).apply(); AndroidUtilities.runOnUIThread(() -> { try { AlertsCreator.createFreeSpaceDialog(LaunchActivity.this).show(); @@ -3532,7 +3593,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa }); localeDialog = showAlertDialog(builder); SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - preferences.edit().putString("language_showed2", systemLang).commit(); + preferences.edit().putString("language_showed2", systemLang).apply(); } catch (Exception e) { FileLog.e(e); } @@ -3799,33 +3860,36 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa @Override public void onBackPressed() { - if (passcodeView.getVisibility() == View.VISIBLE) { - finish(); - return; - } - if (SecretMediaViewer.hasInstance() && SecretMediaViewer.getInstance().isVisible()) { - SecretMediaViewer.getInstance().closePhoto(true, false); - } else if (PhotoViewer.hasInstance() && PhotoViewer.getInstance().isVisible()) { - PhotoViewer.getInstance().closePhoto(true, false); - } else if (ArticleViewer.hasInstance() && ArticleViewer.getInstance().isVisible()) { - ArticleViewer.getInstance().close(true, false); - } else if (drawerLayoutContainer.isDrawerOpened()) { - drawerLayoutContainer.closeDrawer(false); - } else if (AndroidUtilities.isTablet()) { - if (layersActionBarLayout.getVisibility() == View.VISIBLE) { - layersActionBarLayout.onBackPressed(); - } else { - boolean cancel = false; - if (rightActionBarLayout.getVisibility() == View.VISIBLE && !rightActionBarLayout.fragmentsStack.isEmpty()) { - BaseFragment lastFragment = rightActionBarLayout.fragmentsStack.get(rightActionBarLayout.fragmentsStack.size() - 1); - cancel = !lastFragment.onBackPressed(); - } - if (!cancel) { - actionBarLayout.onBackPressed(); - } + try { + if (passcodeView != null && passcodeView.getVisibility() == View.VISIBLE) { + finish(); + return; } - } else { - actionBarLayout.onBackPressed(); + if (SecretMediaViewer.hasInstance() && SecretMediaViewer.getInstance().isVisible()) { + SecretMediaViewer.getInstance().closePhoto(true, false); + } else if (PhotoViewer.hasInstance() && PhotoViewer.getInstance().isVisible()) { + PhotoViewer.getInstance().closePhoto(true, false); + } else if (ArticleViewer.hasInstance() && ArticleViewer.getInstance().isVisible()) { + ArticleViewer.getInstance().close(true, false); + } else if (drawerLayoutContainer.isDrawerOpened()) { + drawerLayoutContainer.closeDrawer(false); + } else if (AndroidUtilities.isTablet()) { + if (layersActionBarLayout != null && layersActionBarLayout.getVisibility() == View.VISIBLE) { + layersActionBarLayout.onBackPressed(); + } else if (rightActionBarLayout != null) { + boolean cancel = false; + if (rightActionBarLayout.getVisibility() == View.VISIBLE && !rightActionBarLayout.fragmentsStack.isEmpty()) { + BaseFragment lastFragment = rightActionBarLayout.fragmentsStack.get(rightActionBarLayout.fragmentsStack.size() - 1); + cancel = !lastFragment.onBackPressed(); + } + if (!cancel) { + actionBarLayout.onBackPressed(); + } + } + } else { + actionBarLayout.onBackPressed(); + } + } catch (Exception ignored) { } } @@ -3961,7 +4025,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (ArticleViewer.hasInstance() && ArticleViewer.getInstance().isVisible()) { ArticleViewer.getInstance().close(false, true); } - if (AndroidUtilities.isTablet()) { + if (AndroidUtilities.isTablet() && layersActionBarLayout != null) { drawerLayoutContainer.setAllowOpenDrawer(!(fragment instanceof LoginActivity || fragment instanceof CountrySelectActivity) && layersActionBarLayout.getVisibility() != View.VISIBLE, true); if (fragment instanceof DialogsActivity) { DialogsActivity dialogsActivity = (DialogsActivity) fragment; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java index b1d5b63b7..e6cb97c4f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java @@ -8,34 +8,57 @@ package org.telegram.ui; +import android.Manifest; +import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.graphics.Canvas; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; import android.os.SystemClock; +import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.json.JSONArray; +import org.json.JSONObject; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.BuildConfig; +import org.telegram.messenger.BuildVars; import org.telegram.messenger.DownloadController; +import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; +import org.telegram.messenger.SendMessagesHelper; import org.telegram.messenger.SharedConfig; +import org.telegram.messenger.browser.Browser; import org.telegram.tgnet.ConnectionsManager; import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenu; +import org.telegram.ui.ActionBar.ActionBarMenuItem; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ActionBar.ThemeDescription; import org.telegram.ui.Cells.HeaderCell; @@ -45,14 +68,39 @@ import org.telegram.ui.Cells.TextInfoPrivacyCell; import org.telegram.ui.Cells.TextSettingsCell; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RecyclerListView; +import org.telegram.ui.Components.URLSpanNoUnderline; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - +import java.io.File; import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import kotlin.Unit; +import okhttp3.HttpUrl; +import tw.nekomimi.nekogram.BottomBuilder; +import tw.nekomimi.nekogram.RelayBatonSettingsActivity; +import tw.nekomimi.nekogram.ShadowsocksRSettingsActivity; +import tw.nekomimi.nekogram.ShadowsocksSettingsActivity; +import tw.nekomimi.nekogram.SubSettingsActivity; +import tw.nekomimi.nekogram.VmessSettingsActivity; +import tw.nekomimi.nekogram.parts.ProxyChecksKt; +import tw.nekomimi.nekogram.sub.SubInfo; +import tw.nekomimi.nekogram.sub.SubManager; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.FileUtil; +import tw.nekomimi.nekogram.utils.ProxyUtil; +import tw.nekomimi.nekogram.utils.ThreadUtil; +import tw.nekomimi.nekogram.utils.UIUtil; public class ProxyListActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate { @@ -72,20 +120,21 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente private int connectionsHeaderRow; private int proxyStartRow; private int proxyEndRow; - private int proxyAddRow; private int proxyDetailRow; private int callsRow; private int callsDetailRow; + private ActionBarMenuItem otherItem; + public class TextDetailProxyCell extends FrameLayout { private TextView textView; private TextView valueTextView; - private ImageView checkImageView; private SharedConfig.ProxyInfo currentInfo; private Drawable checkDrawable; private int color; + private Pattern urlPattern; public TextDetailProxyCell(Context context) { super(context); @@ -111,14 +160,6 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente valueTextView.setPadding(0, 0, 0, 0); addView(valueTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, (LocaleController.isRTL ? 56 : 21), 35, (LocaleController.isRTL ? 21 : 56), 0)); - checkImageView = new ImageView(context); - checkImageView.setImageResource(R.drawable.profile_info); - checkImageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText3), PorterDuff.Mode.MULTIPLY)); - checkImageView.setScaleType(ImageView.ScaleType.CENTER); - checkImageView.setContentDescription(LocaleController.getString("Edit", R.string.Edit)); - addView(checkImageView, LayoutHelper.createFrame(48, 48, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.TOP, 8, 8, 8, 0)); - checkImageView.setOnClickListener(v -> presentFragment(new ProxySettingsActivity(currentInfo))); - setWillNotDraw(false); } @@ -127,8 +168,40 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(64) + 1, MeasureSpec.EXACTLY)); } + @SuppressLint("SetTextI18n") public void setProxy(SharedConfig.ProxyInfo proxyInfo) { - textView.setText(proxyInfo.address + ":" + proxyInfo.port); + + String title = proxyInfo.getTitle(); + + SpannableStringBuilder stringBuilder = null; + try { + if (urlPattern == null) { + urlPattern = Pattern.compile("@[a-zA-Z\\d_]{1,32}"); + } + Matcher matcher = urlPattern.matcher(title); + while (matcher.find()) { + if (stringBuilder == null) { + stringBuilder = new SpannableStringBuilder(title); + textView.setMovementMethod(new AndroidUtilities.LinkMovementMethodMy()); + } + int start = matcher.start(); + int end = matcher.end(); + if (title.charAt(start) != '@') { + start++; + } + URLSpanNoUnderline url = new URLSpanNoUnderline(title.subSequence(start + 1, end).toString()) { + @Override + public void onClick(View widget) { + MessagesController.getInstance(currentAccount).openByUserName(getURL(), ProxyListActivity.this, 1); + } + }; + stringBuilder.setSpan(url, start, end, 0); + } + } catch (Exception e) { + FileLog.e(e); + } + + textView.setText(stringBuilder == null ? title : stringBuilder); currentInfo = proxyInfo; } @@ -169,8 +242,9 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente valueTextView.setTag(colorKey); valueTextView.setTextColor(color); if (checkDrawable != null) { - checkDrawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + checkDrawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } + } public void setChecked(boolean checked) { @@ -179,7 +253,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente checkDrawable = getResources().getDrawable(R.drawable.proxy_check).mutate(); } if (checkDrawable != null) { - checkDrawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + checkDrawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } if (LocaleController.isRTL) { valueTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, checkDrawable, null); @@ -207,11 +281,21 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente } } + public ProxyListActivity() { + } + + private String alert; + + public ProxyListActivity(String alert) { + this.alert = alert; + } + + private LinkedList proxyList = SharedConfig.getProxyList(); + @Override public boolean onFragmentCreate() { super.onFragmentCreate(); - SharedConfig.loadProxyList(); currentConnectionState = ConnectionsManager.getInstance(currentAccount).getConnectionState(); NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.proxySettingsChanged); @@ -219,7 +303,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.didUpdateConnectionState); final SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - useProxySettings = preferences.getBoolean("proxy_enabled", false) && !SharedConfig.proxyList.isEmpty(); + useProxySettings = SharedConfig.proxyEnabled && !SharedConfig.proxyList.isEmpty(); useProxyForCalls = preferences.getBoolean("proxy_enabled_calls", false); updateRows(true); @@ -233,6 +317,148 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.proxySettingsChanged); NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.proxyCheckDone); NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.didUpdateConnectionState); + + if (currentCheck != null) currentCheck.shutdownNow(); + + } + + private int menu_add = 1; + private int menu_add_input_socks = 2; + private int menu_add_input_telegram = 3; + private int menu_add_input_vmess = 4; + private int menu_add_input_ss = 7; + private int menu_add_input_ssr = 8; + private int menu_add_input_rb = 17; + + private int menu_add_import_from_clipboard = 5; + private int menu_add_scan_qr = 6; + private int menu_other = 9; + private int menu_retest_ping = 10; + private int menu_reorder_by_ping = 11; + private int menu_export_json = 12; + private int menu_import_json = 13; + private int menu_delete_all = 14; + private int menu_delete_unavailable = 15; + private int menu_sub = 16; + + public void processProxyList(ArrayList files) { + + for (String proxyListFilePath : files) { + + File proxyListFile = new File(proxyListFilePath); + + processProxyListFile(getParentActivity(), proxyListFile); + + } + + } + + public static String processProxyListFile(Context ctx, File proxyListFile) { + + try { + + if (proxyListFile.length() > 2 * 1024 * 1024L) { + + throw new IllegalArgumentException("file too large."); + + } + + JSONObject proxyRootObject = new JSONObject(FileUtil.readUtf8String(proxyListFile)); + + int version = proxyRootObject.optInt("nekox_proxy_list_version", 1); + + if (version == 1) { + + if (proxyRootObject.isNull("proxies")) { + + throw new IllegalArgumentException("proxies array not found."); + + } + + JSONArray proxyArray = proxyRootObject.getJSONArray("proxies"); + + if (proxyArray.length() == 0) { + + throw new IllegalArgumentException("Empty proxy list."); + + } + + LinkedList imported = new LinkedList<>(); + LinkedHashMap errors = new LinkedHashMap<>(); + + for (int index = 0; index < proxyArray.length(); index++) { + + String proxyUrl = proxyArray.getString(index); + + try { + + imported.add(ProxyUtil.importInBackground(proxyUrl).getTitle()); + + } catch (Exception ex) { + + errors.put(proxyUrl.length() < 15 ? proxyUrl : (proxyUrl.substring(0, 15) + "..."), ex.getMessage()); + + } + + } + + StringBuilder status = new StringBuilder(); + + if (!imported.isEmpty()) { + + status.append(LocaleController.getString("ImportedProxies", R.string.ImportedProxies)); + + for (String success : imported) { + + status.append("\n").append(success); + + } + + + if (!errors.isEmpty()) { + + status.append("\n\n"); + + } + + } + + if (!errors.isEmpty()) { + + status.append(LocaleController.getString("", R.string.ErrorsInImport)); + + for (Map.Entry error : errors.entrySet()) { + + status.append("\n").append(error.getKey()).append(": ").append(error.getValue()); + + } + + } + + if (imported.isEmpty()) { + + AlertUtil.showSimpleAlert(ctx, status.toString()); + + } else { + + return status.toString(); + + } + + } else { + + throw new IllegalArgumentException("invalid proxy list version " + version + "."); + + } + + } catch (Exception e) { + + AlertUtil.showSimpleAlert(ctx, LocaleController.getString("InvalidProxyFile", R.string.InvalidProxyFile) + proxyListFile.getPath() + "\n\n" + e.getMessage()); + + } + + return null; + } @Override @@ -248,10 +474,210 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente public void onItemClick(int id) { if (id == -1) { finishFragment(); + } else if (id == menu_retest_ping) { + checkProxyList(true); + } else if (id == menu_reorder_by_ping) { + SharedConfig.proxyList = new LinkedList<>(new TreeSet<>(SharedConfig.getProxyList())); + SharedConfig.saveProxyList(); + updateRows(true); + } else if (id == menu_export_json) { + File cacheFile = new File(ApplicationLoader.applicationContext.getExternalCacheDir(), "Proxy-List-" + new Date().toLocaleString() + ".nekox.json"); + + try { + + JSONObject listRoot = new JSONObject(); + + listRoot.put("nekox_proxy_list_version", 1); + + JSONArray proxyArray = new JSONArray(); + + for (SharedConfig.ProxyInfo info : SharedConfig.getProxyList()) { + + if (info.subId <= 1) { + + continue; + + } + + proxyArray.put(info.toUrl()); + + } + + if (proxyArray.length() == 0) { + AlertUtil.showSimpleAlert(getParentActivity(), LocaleController.getString("NoProxy", R.string.NoProxy)); + return; + } + + listRoot.put("proxies", proxyArray); + + FileUtil.writeUtf8String(listRoot.toString(4), cacheFile); + } catch (Exception e) { + return; + } + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("*/*"); + if (Build.VERSION.SDK_INT >= 24) { + try { + intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(getParentActivity(), BuildConfig.APPLICATION_ID + ".provider", cacheFile)); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (Exception ignore) { + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(cacheFile)); + } + } else { + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(cacheFile)); + } + getParentActivity().startActivityForResult(Intent.createChooser(intent, LocaleController.getString("ShareFile", R.string.ShareFile)), 500); + } else if (id == menu_import_json) { + try { + if (Build.VERSION.SDK_INT >= 23 && getParentActivity().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + + getParentActivity().requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 4); + return; + } + } catch (Throwable ignore) { + } + DocumentSelectActivity fragment = new DocumentSelectActivity(false); + fragment.setMaxSelectedFiles(-1); + fragment.setAllowPhoto(false); + fragment.setDelegate(new DocumentSelectActivity.DocumentSelectActivityDelegate() { + + @Override + public void didSelectFiles(DocumentSelectActivity activity, ArrayList files, String caption, boolean notify, int scheduleDate) { + activity.finishFragment(); + processProxyList(files); + } + + @Override + public void didSelectPhotos(ArrayList photos, boolean notify, int scheduleDate) { + } + + @Override + public void startDocumentSelectActivity() { + } + }); + presentFragment(fragment); + } else if (id == menu_delete_all) { + AlertUtil.showConfirm(getParentActivity(), + LocaleController.getString("DeleteAllServer", R.string.DeleteAllServer), + R.drawable.baseline_delete_24, LocaleController.getString("Delete", R.string.Delete), + true, () -> { + SharedConfig.deleteAllProxy(); + updateRows(true); + }); + } else if (id == menu_delete_unavailable) { + AlertUtil.showConfirm(getParentActivity(), + LocaleController.getString("DeleteUnavailableServer", R.string.DeleteUnavailableServer), + R.drawable.baseline_delete_24, LocaleController.getString("Delete", R.string.Delete), + true, () -> { + deleteUnavailableProxy(); + }); + } else if (id == menu_sub) { + showSubDialog(); } } }); + ActionBarMenu menu = actionBar.createMenu(); + + ActionBarMenuItem addItem = menu.addItem(menu_add, R.drawable.add); + + addItem.addSubItem(menu_add_import_from_clipboard, LocaleController.getString("ImportProxyFromClipboard", R.string.ImportProxyFromClipboard)).setOnClickListener((v) -> ProxyUtil.importFromClipboard()); + addItem.addSubItem(menu_add_scan_qr, LocaleController.getString("ScanQRCode", R.string.ScanQRCode)).setOnClickListener((v) -> { + + if (Build.VERSION.SDK_INT >= 23) { + if (getParentActivity().checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 22); + return; + } + } + + CameraScanActivity.showAsSheet(this, new CameraScanActivity.CameraScanActivityDelegate() { + + @Override + public void didFindQr(String text) { + + BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); + + boolean isUrl = false; + + try { + + HttpUrl.parse(text); + + isUrl = true; + + Browser.openUrl(getParentActivity(), text); + + return; + + } catch (Exception ignored) { + } + + builder.setTitle(text); + + builder.setItems(new String[]{ + + LocaleController.getString("Copy", R.string.Copy), + LocaleController.getString("Cancel", R.string.Cancel) + + }, (v, i) -> { + + if (i == 0) { + + if (Build.VERSION.SDK_INT >= 23) { + if (getParentActivity().checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 22); + return; + } + } + + CameraScanActivity.showAsSheet(ProxyListActivity.this, new CameraScanActivity.CameraScanActivityDelegate() { + + @Override + public void didFindQr(String text) { + + ProxyUtil.showLinkAlert(getParentActivity(), text); + + } + }); + + } + + }); + + showDialog(builder.create()); + + } + + }); + + }); + + addItem.addSubItem(menu_add_input_socks, LocaleController.getString("AddProxySocks5", R.string.AddProxySocks5)).setOnClickListener((v) -> presentFragment(new ProxySettingsActivity(0))); + addItem.addSubItem(menu_add_input_telegram, LocaleController.getString("AddProxyTelegram", R.string.AddProxyTelegram)).setOnClickListener((v) -> presentFragment(new ProxySettingsActivity(1))); + + if (!BuildVars.isMini) { + + addItem.addSubItem(menu_add_input_vmess, LocaleController.getString("AddProxyVmess", R.string.AddProxyVmess)).setOnClickListener((v) -> presentFragment(new VmessSettingsActivity())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + addItem.addSubItem(menu_add_input_ss, LocaleController.getString("AddProxySS", R.string.AddProxySS)).setOnClickListener((v) -> presentFragment(new ShadowsocksSettingsActivity())); + addItem.addSubItem(menu_add_input_ssr, LocaleController.getString("AddProxySSR", R.string.AddProxySSR)).setOnClickListener((v) -> presentFragment(new ShadowsocksRSettingsActivity())); + } + // addItem.addSubItem(menu_add_input_rb, LocaleController.getString("AddProxyRB", R.string.AddProxyRB)).setOnClickListener((v) -> presentFragment(new RelayBatonSettingsActivity())); + + } + + menu.addItem(menu_sub, R.drawable.msg_list); + + otherItem = menu.addItem(menu_other, R.drawable.ic_ab_other); + otherItem.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); + otherItem.addSubItem(menu_retest_ping, LocaleController.getString("RetestPing", R.string.RetestPing)); + otherItem.addSubItem(menu_reorder_by_ping, LocaleController.getString("ReorderByPing", R.string.ReorderByPing)); + otherItem.addSubItem(menu_export_json, LocaleController.getString("ExportProxies", R.string.ExportProxies)); + otherItem.addSubItem(menu_import_json, LocaleController.getString("ImportProxies", R.string.ImportProxies)); + otherItem.addSubItem(menu_delete_all, LocaleController.getString("DeleteAllServer", R.string.DeleteAllServer)); + otherItem.addSubItem(menu_delete_unavailable, LocaleController.getString("DeleteUnavailableServer", R.string.DeleteUnavailableServer)); + listAdapter = new ListAdapter(context); fragmentView = new FrameLayout(context); @@ -268,54 +694,23 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente if (position == useProxyRow) { if (SharedConfig.currentProxy == null) { if (!SharedConfig.proxyList.isEmpty()) { - SharedConfig.currentProxy = SharedConfig.proxyList.get(0); - - if (!useProxySettings) { - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); - editor.putString("proxy_ip", SharedConfig.currentProxy.address); - editor.putString("proxy_pass", SharedConfig.currentProxy.password); - editor.putString("proxy_user", SharedConfig.currentProxy.username); - editor.putInt("proxy_port", SharedConfig.currentProxy.port); - editor.putString("proxy_secret", SharedConfig.currentProxy.secret); - editor.commit(); - } + SharedConfig.setCurrentProxy(SharedConfig.proxyList.get(0)); } else { - presentFragment(new ProxySettingsActivity()); + addProxy(); return; } } - useProxySettings = !useProxySettings; - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); + useProxySettings = !useProxySettings; TextCheckCell textCheckCell = (TextCheckCell) view; textCheckCell.setChecked(useProxySettings); - if (!useProxySettings) { - RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(callsRow); - if (holder != null) { - textCheckCell = (TextCheckCell) holder.itemView; - textCheckCell.setChecked(false); - } - useProxyForCalls = false; - } - SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); - editor.putBoolean("proxy_enabled", useProxySettings); - editor.commit(); - - ConnectionsManager.setProxySettings(useProxySettings, SharedConfig.currentProxy.address, SharedConfig.currentProxy.port, SharedConfig.currentProxy.username, SharedConfig.currentProxy.password, SharedConfig.currentProxy.secret); NotificationCenter.getGlobalInstance().removeObserver(ProxyListActivity.this, NotificationCenter.proxySettingsChanged); - NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + SharedConfig.setProxyEnable(useProxySettings); NotificationCenter.getGlobalInstance().addObserver(ProxyListActivity.this, NotificationCenter.proxySettingsChanged); - for (int a = proxyStartRow; a < proxyEndRow; a++) { - RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(a); - if (holder != null) { - TextDetailProxyCell cell = (TextDetailProxyCell) holder.itemView; - cell.updateStatus(); - } - } + updateRows(true); } else if (position == callsRow) { useProxyForCalls = !useProxyForCalls; @@ -323,74 +718,226 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente textCheckCell.setChecked(useProxyForCalls); SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); editor.putBoolean("proxy_enabled_calls", useProxyForCalls); - editor.commit(); + editor.apply(); } else if (position >= proxyStartRow && position < proxyEndRow) { SharedConfig.ProxyInfo info = SharedConfig.proxyList.get(position - proxyStartRow); useProxySettings = true; - SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); - editor.putString("proxy_ip", info.address); - editor.putString("proxy_pass", info.password); - editor.putString("proxy_user", info.username); - editor.putInt("proxy_port", info.port); - editor.putString("proxy_secret", info.secret); - editor.putBoolean("proxy_enabled", useProxySettings); - if (!info.secret.isEmpty()) { - useProxyForCalls = false; - editor.putBoolean("proxy_enabled_calls", false); - } - editor.commit(); - SharedConfig.currentProxy = info; - for (int a = proxyStartRow; a < proxyEndRow; a++) { - RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(a); - if (holder != null) { - TextDetailProxyCell cell = (TextDetailProxyCell) holder.itemView; - cell.setChecked(cell.currentInfo == info); - cell.updateStatus(); - } - } - updateRows(false); + SharedConfig.setCurrentProxy(info); + updateRows(true); RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(useProxyRow); if (holder != null) { TextCheckCell textCheckCell = (TextCheckCell) holder.itemView; textCheckCell.setChecked(true); } - ConnectionsManager.setProxySettings(useProxySettings, SharedConfig.currentProxy.address, SharedConfig.currentProxy.port, SharedConfig.currentProxy.username, SharedConfig.currentProxy.password, SharedConfig.currentProxy.secret); - } else if (position == proxyAddRow) { - presentFragment(new ProxySettingsActivity()); } }); listView.setOnItemLongClickListener((view, position) -> { if (position >= proxyStartRow && position < proxyEndRow) { final SharedConfig.ProxyInfo info = SharedConfig.proxyList.get(position - proxyStartRow); - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setMessage(LocaleController.getString("DeleteProxy", R.string.DeleteProxy)); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), (dialog, which) -> { - SharedConfig.deleteProxy(info); - if (SharedConfig.currentProxy == null) { - useProxyForCalls = false; - useProxySettings = false; - } - NotificationCenter.getGlobalInstance().removeObserver(ProxyListActivity.this, NotificationCenter.proxySettingsChanged); - NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); - NotificationCenter.getGlobalInstance().addObserver(ProxyListActivity.this, NotificationCenter.proxySettingsChanged); - updateRows(false); - if (listAdapter != null) { - listAdapter.notifyItemRemoved(position); - if (SharedConfig.currentProxy == null) { - listAdapter.notifyItemChanged(useProxyRow, ListAdapter.PAYLOAD_CHECKED_CHANGED); - listAdapter.notifyItemChanged(callsRow, ListAdapter.PAYLOAD_CHECKED_CHANGED); + + BottomSheet.Builder builder = new BottomSheet.Builder(context); + + builder.setItems(new String[]{ + + info.subId == 1 ? null : LocaleController.getString("EditProxy", R.string.EditProxy), + info.subId == 1 ? null : LocaleController.getString("ShareProxy", R.string.ShareProxy), + info.subId == 1 ? null : LocaleController.getString("ShareQRCode", R.string.ShareQRCode), + info.subId == 1 ? null : LocaleController.getString("CopyLink", R.string.CopyLink), + LocaleController.getString("ProxyDelete", R.string.ProxyDelete), + LocaleController.getString("Cancel", R.string.Cancel) + + }, new int[]{ + + R.drawable.group_edit, + R.drawable.baseline_share_24, + R.drawable.wallet_qr, + R.drawable.baseline_link_24, + R.drawable.baseline_delete_24, + R.drawable.baseline_cancel_24 + + }, (v, i) -> { + + if (i == 0) { + + if (info instanceof SharedConfig.VmessProxy) { + presentFragment(new VmessSettingsActivity((SharedConfig.VmessProxy) info)); + } else if (info instanceof SharedConfig.ShadowsocksProxy) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + presentFragment(new ShadowsocksSettingsActivity((SharedConfig.ShadowsocksProxy) info)); + } + } else if (info instanceof SharedConfig.ShadowsocksRProxy) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + presentFragment(new ShadowsocksRSettingsActivity((SharedConfig.ShadowsocksRProxy) info)); + } + } else if (info instanceof SharedConfig.RelayBatonProxy) { + presentFragment(new RelayBatonSettingsActivity((SharedConfig.RelayBatonProxy) info)); + } else { + presentFragment(new ProxySettingsActivity(info)); } + + } else if (i == 1) { + + ProxyUtil.shareProxy(getParentActivity(), info, 0); + + } else if (i == 2) { + + ProxyUtil.shareProxy(getParentActivity(), info, 2); + + } else if (i == 3) { + + ProxyUtil.shareProxy(getParentActivity(), info, 1); + + } else if (i == 4) { + + AlertUtil.showConfirm(getParentActivity(), + LocaleController.getString("DeleteProxy", R.string.DeleteProxy), + R.drawable.baseline_delete_24, LocaleController.getString("Delete", R.string.Delete), + true, () -> { + + SharedConfig.deleteProxy(info); + if (SharedConfig.currentProxy == null) { + SharedConfig.setProxyEnable(false); + } + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); + + }); + } + }); + showDialog(builder.create()); + return true; } return false; }); + if (alert != null) { + + AlertUtil.showSimpleAlert(context, alert); + alert = null; + + } + return fragmentView; + + } + + @SuppressLint("NewApi") private void addProxy() { + + BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); + + builder.setItems(new String[]{ + + LocaleController.getString("AddProxySocks5", R.string.AddProxySocks5), + LocaleController.getString("AddProxyTelegram", R.string.AddProxyTelegram), + LocaleController.getString("AddProxyVmess", R.string.AddProxyVmess), + Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? null : LocaleController.getString("AddProxySS", R.string.AddProxySS), + Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? null : LocaleController.getString("AddProxySSR", R.string.AddProxySSR), + LocaleController.getString("AddProxyRB", R.string.AddProxyRB), + LocaleController.getString("ImportProxyFromClipboard", R.string.ImportProxyFromClipboard), + LocaleController.getString("ScanQRCode", R.string.ScanQRCode) + + }, (v, i) -> { + + if (i == 0) { + + presentFragment(new ProxySettingsActivity(0)); + + } else if (i == 1) { + + presentFragment(new ProxySettingsActivity(1)); + + } else if (i == 2) { + + presentFragment(new VmessSettingsActivity()); + + } else if (i == 3) { + + presentFragment(new ShadowsocksSettingsActivity()); + + } else if (i == 4) { + + presentFragment(new ShadowsocksRSettingsActivity()); + + } else if (i == 5) { + + presentFragment(new RelayBatonSettingsActivity()); + + } else if (i == 6) { + + ProxyUtil.importFromClipboard(); + + } else { + + CameraScanActivity.showAsSheet(this, new CameraScanActivity.CameraScanActivityDelegate() { + + @Override + public void didFindQr(String text) { + + BottomSheet.Builder builder = new BottomSheet.Builder(getParentActivity()); + + boolean isUrl = false; + + try { + + HttpUrl.parse(text); + + isUrl = true; + + Browser.openUrl(getParentActivity(), text); + + return; + + } catch (Exception ignored) { + } + + builder.setTitle(text); + + builder.setItems(new String[]{ + + LocaleController.getString("Copy", R.string.Copy), + LocaleController.getString("Cancel", R.string.Cancel) + + }, (v, i) -> { + + if (i == 0) { + + if (Build.VERSION.SDK_INT >= 23) { + if (getParentActivity().checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 22); + return; + } + } + + CameraScanActivity.showAsSheet(ProxyListActivity.this, new CameraScanActivity.CameraScanActivityDelegate() { + + @Override + public void didFindQr(String text) { + + ProxyUtil.showLinkAlert(getParentActivity(), text); + + } + }); + + } + + }); + + showDialog(builder.create()); + + + } + + }); + + } + + }); + + builder.show(); + } private void updateRows(boolean notify) { @@ -406,51 +953,287 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente proxyStartRow = -1; proxyEndRow = -1; } - proxyAddRow = rowCount++; proxyDetailRow = rowCount++; if (SharedConfig.currentProxy == null || SharedConfig.currentProxy.secret.isEmpty()) { boolean change = callsRow == -1; callsRow = rowCount++; callsDetailRow = rowCount++; - if (!notify && change) { - listAdapter.notifyItemChanged(proxyDetailRow); - listAdapter.notifyItemRangeInserted(proxyDetailRow + 1, 2); - } + UIUtil.runOnUIThread(() -> { + if (!notify && change) { + listAdapter.notifyItemChanged(proxyDetailRow); + listAdapter.notifyItemRangeInserted(proxyDetailRow + 1, 2); + } + }); } else { boolean change = callsRow != -1; callsRow = -1; callsDetailRow = -1; if (!notify && change) { - listAdapter.notifyItemChanged(proxyDetailRow); - listAdapter.notifyItemRangeRemoved(proxyDetailRow + 1, 2); + UIUtil.runOnUIThread(() -> { + listAdapter.notifyItemChanged(proxyDetailRow); + listAdapter.notifyItemRangeRemoved(proxyDetailRow + 1, 2); + }); } } - checkProxyList(); + checkProxyList(false); if (notify && listAdapter != null) { - listAdapter.notifyDataSetChanged(); + UIUtil.runOnUIThread(() -> { + try { + listView.clearAnimation(); + listView.getRecycledViewPool().clear(); + listAdapter.notifyDataSetChanged(); + } catch (Exception e) { + FileLog.e(e); + } + }); } } - private void checkProxyList() { - for (int a = 0, count = SharedConfig.proxyList.size(); a < count; a++) { - final SharedConfig.ProxyInfo proxyInfo = SharedConfig.proxyList.get(a); - if (proxyInfo.checking || SystemClock.elapsedRealtime() - proxyInfo.availableCheckTime < 2 * 60 * 1000) { - continue; + private ExecutorService currentCheck; + + private void checkProxyList(boolean force) { + + if (currentCheck == null) { + + currentCheck = Executors.newFixedThreadPool(3); + + } + + ProxyChecksKt.checkProxyList(this, force, currentCheck); + + } + + private void deleteUnavailableProxy() { + + for (SharedConfig.ProxyInfo info : SharedConfig.getProxyList()) { + + if (info.subId != 0) continue; + + checkSingleProxy(info, 1, () -> { + + deleteUnavailableProxy(info); + + }); + + } + + } + + private void deleteUnavailableProxy(SharedConfig.ProxyInfo proxyInfo) { + + if (!proxyInfo.available) { + + SharedConfig.deleteProxy(proxyInfo); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxyCheckDone); + + } + + } + + public void checkSingleProxy(SharedConfig.ProxyInfo proxyInfo, int repeat, Runnable callback) { + + proxyInfo.checking = true; + + UIUtil.runOnUIThread(() -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxyCheckDone, proxyInfo)); + + UIUtil.runOnIoDispatcher(() -> { + + if (proxyInfo instanceof SharedConfig.ExternalSocks5Proxy && !((SharedConfig.ExternalSocks5Proxy) proxyInfo).isStarted()) { + + try { + + ((SharedConfig.ExternalSocks5Proxy) proxyInfo).start(); + + } catch (Exception e) { + + FileLog.e(e); + AlertUtil.showToast(e); + + } + + ThreadUtil.sleep(233L); + } - proxyInfo.checking = true; + proxyInfo.proxyCheckPingId = ConnectionsManager.getInstance(currentAccount).checkProxy(proxyInfo.address, proxyInfo.port, proxyInfo.username, proxyInfo.password, proxyInfo.secret, time -> AndroidUtilities.runOnUIThread(() -> { proxyInfo.availableCheckTime = SystemClock.elapsedRealtime(); - proxyInfo.checking = false; if (time == -1) { - proxyInfo.available = false; - proxyInfo.ping = 0; + if (repeat > 0) { + checkSingleProxy(proxyInfo, repeat - 1, callback); + } else { + proxyInfo.checking = false; + proxyInfo.available = false; + proxyInfo.ping = 0; + if (proxyInfo instanceof SharedConfig.ExternalSocks5Proxy && proxyInfo != SharedConfig.currentProxy) { + ((SharedConfig.ExternalSocks5Proxy) proxyInfo).stop(); + } + if (callback != null) { + UIUtil.runOnUIThread(callback); + } + } } else { + proxyInfo.checking = false; proxyInfo.ping = time; proxyInfo.available = true; + if (proxyInfo instanceof SharedConfig.ExternalSocks5Proxy && proxyInfo != SharedConfig.currentProxy) { + ((SharedConfig.ExternalSocks5Proxy) proxyInfo).stop(); + } + if (callback != null) { + UIUtil.runOnUIThread(callback); + } } - NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxyCheckDone, proxyInfo); })); + + }); + + } + + private void showSubDialog() { + + BottomBuilder builder = new BottomBuilder(getParentActivity()); + + builder.addTitle(LocaleController.getString("ProxySubscription", R.string.ProxySubscription)); + + HashMap toChange = new HashMap<>(); + + for (SubInfo sub : SubManager.getSubList().find()) { + + TextCheckCell subItem = builder.addCheckItem(sub.name, sub.enable, true, (it) -> { + + boolean curr = (toChange.containsKey(sub) ? toChange.get(sub) : sub.enable); + + if (curr != sub.enable) { + + toChange.remove(sub); + + } else { + + toChange.put(sub, !sub.enable); + + } + + it.setChecked(!curr); + + return Unit.INSTANCE; + + }); + + subItem.setOnLongClickListener((it) -> { + + if (sub.internal) return false; + + builder.dismiss(); + + presentFragment(new SubSettingsActivity(sub)); + + return true; + + }); + } + + builder.addButton(LocaleController.getString("Add", R.string.Add), false, true, (it) -> { + + builder.dismiss(); + + presentFragment(new SubSettingsActivity()); + + return Unit.INSTANCE; + + }); + + builder.addButton(LocaleController.getString("Update", R.string.Update), (it) -> { + + AlertDialog pro = AlertUtil.showProgress(getParentActivity(), LocaleController.getString("SubscriptionUpdating", R.string.SubscriptionUpdating)); + AtomicBoolean canceled = new AtomicBoolean(); + pro.setOnCancelListener((__) -> { + canceled.set(true); + }); + pro.show(); + + UIUtil.runOnIoDispatcher(() -> { + + for (SubInfo subInfo : SubManager.getSubList().find()) { + + try { + + subInfo.proxies = subInfo.reloadProxies(); + subInfo.lastFetch = System.currentTimeMillis(); + + } catch (SubInfo.AllTriesFailed allTriesFailed) { + + if (canceled.get()) return; + + AlertUtil.showSimpleAlert(getParentActivity(), "All tries failed: " + allTriesFailed.toString().trim()); + + continue; + + } + + SubManager.getSubList().update(subInfo, true); + + if (canceled.get()) return; + + } + + SharedConfig.reloadProxyList(); + + updateRows(true); + + UIUtil.runOnUIThread(() -> { + + builder.dismiss(); + + pro.dismiss(); + + }); + + + }); + + return Unit.INSTANCE; + + }); + + builder.addButton(LocaleController.getString("OK", R.string.OK), (it) -> { + + builder.dismiss(); + + if (!toChange.isEmpty()) { + + AlertDialog pro = AlertUtil.showProgress(getParentActivity()); + pro.setCanCacnel(false); + pro.show(); + + UIUtil.runOnIoDispatcher(() -> { + + for (Map.Entry toChangeE : toChange.entrySet()) { + + toChangeE.getKey().enable = toChangeE.getValue(); + + SubManager.getSubList().update(toChangeE.getKey(), true); + + } + + SharedConfig.reloadProxyList(); + + UIUtil.runOnUIThread(() -> updateRows(true)); + + ThreadUtil.sleep(233L); + + UIUtil.runOnUIThread(pro::dismiss); + + }); + + } + + return Unit.INSTANCE; + + }); + + builder.show(); + } @Override @@ -461,9 +1244,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente @Override public void onResume() { super.onResume(); - if (listAdapter != null) { - listAdapter.notifyDataSetChanged(); - } + updateRows(true); } @Override @@ -478,7 +1259,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente int idx = SharedConfig.proxyList.indexOf(SharedConfig.currentProxy); if (idx >= 0) { RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(idx + proxyStartRow); - if (holder != null) { + if (holder != null && holder.itemView instanceof TextDetailProxyCell) { TextDetailProxyCell cell = (TextDetailProxyCell) holder.itemView; cell.updateStatus(); } @@ -487,13 +1268,17 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente } } else if (id == NotificationCenter.proxyCheckDone) { if (listView != null) { - SharedConfig.ProxyInfo proxyInfo = (SharedConfig.ProxyInfo) args[0]; - int idx = SharedConfig.proxyList.indexOf(proxyInfo); - if (idx >= 0) { - RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(idx + proxyStartRow); - if (holder != null) { - TextDetailProxyCell cell = (TextDetailProxyCell) holder.itemView; - cell.updateStatus(); + if (args.length == 0) { + updateRows(true); + } else { + SharedConfig.ProxyInfo proxyInfo = (SharedConfig.ProxyInfo) args[0]; + int idx = SharedConfig.proxyList.indexOf(proxyInfo); + if (idx >= 0) { + RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(idx + proxyStartRow); + if (holder != null && holder.itemView instanceof TextDetailProxyCell) { + TextDetailProxyCell cell = (TextDetailProxyCell) holder.itemView; + cell.updateStatus(); + } } } } @@ -526,14 +1311,6 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente } break; } - case 1: { - TextSettingsCell textCell = (TextSettingsCell) holder.itemView; - textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); - if (position == proxyAddRow) { - textCell.setText(LocaleController.getString("AddProxy", R.string.AddProxy), false); - } - break; - } case 2: { HeaderCell headerCell = (HeaderCell) holder.itemView; if (position == connectionsHeaderRow) { @@ -544,7 +1321,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente case 3: { TextCheckCell checkCell = (TextCheckCell) holder.itemView; if (position == useProxyRow) { - checkCell.setTextAndCheck(LocaleController.getString("UseProxySettings", R.string.UseProxySettings), useProxySettings, false); + checkCell.setTextAndCheck(LocaleController.getString("UseProxySettings", R.string.UseProxySettings), useProxySettings, true); } else if (position == callsRow) { checkCell.setTextAndCheck(LocaleController.getString("UseProxyForCalls", R.string.UseProxyForCalls), useProxyForCalls, false); } @@ -599,7 +1376,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { int position = holder.getAdapterPosition(); - return position == useProxyRow || position == callsRow || position == proxyAddRow || position >= proxyStartRow && position < proxyEndRow; + return position == useProxyRow || position == callsRow || position >= proxyStartRow && position < proxyEndRow; } @Override @@ -638,8 +1415,6 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente public int getItemViewType(int position) { if (position == useProxyDetailRow || position == proxyDetailRow) { return 0; - } else if (position == proxyAddRow) { - return 1; } else if (position == useProxyRow || position == callsRow) { return 3; } else if (position == connectionsHeaderRow) { @@ -650,6 +1425,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente return 4; } } + } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProxySettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProxySettingsActivity.java index 94837757d..991a48e73 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProxySettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProxySettingsActivity.java @@ -12,7 +12,6 @@ import android.animation.ValueAnimator; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Canvas; import android.graphics.PorterDuff; @@ -41,8 +40,6 @@ import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; -import androidx.core.graphics.ColorUtils; - import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesController; @@ -56,8 +53,6 @@ import org.telegram.ui.ActionBar.ActionBarMenuItem; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ActionBar.ThemeDescription; -import org.telegram.ui.Cells.HeaderCell; -import org.telegram.ui.Cells.RadioCell; import org.telegram.ui.Cells.ShadowSectionCell; import org.telegram.ui.Cells.TextInfoPrivacyCell; import org.telegram.ui.Cells.TextSettingsCell; @@ -67,7 +62,6 @@ import org.telegram.ui.Components.LayoutHelper; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.net.URLEncoder; import java.util.ArrayList; public class ProxySettingsActivity extends BaseFragment { @@ -80,18 +74,17 @@ public class ProxySettingsActivity extends BaseFragment { private final static int FIELD_USER = 2; private final static int FIELD_PASSWORD = 3; private final static int FIELD_SECRET = 4; + private final static int FIELD_REMARKS = 5; private EditTextBoldCursor[] inputFields; private ScrollView scrollView; private LinearLayout linearLayout2; private LinearLayout inputFieldsContainer; - private HeaderCell headerCell; private ShadowSectionCell[] sectionCell = new ShadowSectionCell[3]; private TextInfoPrivacyCell[] bottomCells = new TextInfoPrivacyCell[2]; - private TextSettingsCell shareCell; private TextSettingsCell pasteCell; private ActionBarMenuItem doneItem; - private RadioCell[] typeCell = new RadioCell[2]; + // private RadioCell[] typeCell = new RadioCell[2]; private int currentType = -1; private int pasteType = -1; @@ -135,7 +128,7 @@ public class ProxySettingsActivity extends BaseFragment { addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : 21, 0, LocaleController.isRTL ? 21 : 23, 0)); checkImage = new ImageView(context); - checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.MULTIPLY)); + checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.SRC_IN)); checkImage.setImageResource(R.drawable.sticker_added); addView(checkImage, LayoutHelper.createFrame(19, 14, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 21, 0, 21, 0)); } @@ -163,10 +156,11 @@ public class ProxySettingsActivity extends BaseFragment { } } - public ProxySettingsActivity() { + public ProxySettingsActivity(int type) { super(); currentProxyInfo = new SharedConfig.ProxyInfo("", 1080, "", "", ""); addingNewProxy = true; + currentType = type; } public ProxySettingsActivity(SharedConfig.ProxyInfo proxyInfo) { @@ -210,6 +204,7 @@ public class ProxySettingsActivity extends BaseFragment { } currentProxyInfo.address = inputFields[FIELD_IP].getText().toString(); currentProxyInfo.port = Utilities.parseInt(inputFields[FIELD_PORT].getText().toString()); + currentProxyInfo.setRemarks(inputFields[FIELD_REMARKS].getText().toString()); if (currentType == 0) { currentProxyInfo.secret = ""; currentProxyInfo.username = inputFields[FIELD_USER].getText().toString(); @@ -222,14 +217,11 @@ public class ProxySettingsActivity extends BaseFragment { SharedPreferences preferences = MessagesController.getGlobalMainSettings(); SharedPreferences.Editor editor = preferences.edit(); - boolean enabled; if (addingNewProxy) { SharedConfig.addProxy(currentProxyInfo); - SharedConfig.currentProxy = currentProxyInfo; - editor.putBoolean("proxy_enabled", true); - enabled = true; + SharedConfig.setCurrentProxy(currentProxyInfo); } else { - enabled = preferences.getBoolean("proxy_enabled", false); + SharedConfig.setProxyEnable(false); SharedConfig.saveProxyList(); } if (addingNewProxy || SharedConfig.currentProxy == currentProxyInfo) { @@ -238,13 +230,21 @@ public class ProxySettingsActivity extends BaseFragment { editor.putString("proxy_user", currentProxyInfo.username); editor.putInt("proxy_port", currentProxyInfo.port); editor.putString("proxy_secret", currentProxyInfo.secret); - ConnectionsManager.setProxySettings(enabled, currentProxyInfo.address, currentProxyInfo.port, currentProxyInfo.username, currentProxyInfo.password, currentProxyInfo.secret); + if (currentProxyInfo instanceof SharedConfig.VmessProxy) { + editor.putString("vmess_link", ((SharedConfig.VmessProxy) currentProxyInfo).bean.toString()); + } else if (currentProxyInfo instanceof SharedConfig.ShadowsocksProxy) { + editor.putString("vmess_link", ((SharedConfig.ShadowsocksProxy) currentProxyInfo).bean.toString()); + } else if (currentProxyInfo instanceof SharedConfig.ShadowsocksRProxy) { + editor.putString("vmess_link", ((SharedConfig.ShadowsocksRProxy) currentProxyInfo).bean.toString()); + } + ConnectionsManager.setProxySettings(SharedConfig.proxyEnabled, currentProxyInfo.address, currentProxyInfo.port, currentProxyInfo.username, currentProxyInfo.password, currentProxyInfo.secret); } - editor.commit(); + editor.apply(); NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged); finishFragment(); + } } }); @@ -265,23 +265,7 @@ public class ProxySettingsActivity extends BaseFragment { linearLayout2.setOrientation(LinearLayout.VERTICAL); scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - final View.OnClickListener typeCellClickListener = view -> setProxyType((Integer) view.getTag(), true); - - for (int a = 0; a < 2; a++) { - typeCell[a] = new RadioCell(context); - typeCell[a].setBackground(Theme.getSelectorDrawable(true)); - typeCell[a].setTag(a); - if (a == 0) { - typeCell[a].setText(LocaleController.getString("UseProxySocks5", R.string.UseProxySocks5), a == currentType, true); - } else if (a == 1) { - typeCell[a].setText(LocaleController.getString("UseProxyTelegram", R.string.UseProxyTelegram), a == currentType, false); - } - linearLayout2.addView(typeCell[a], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 50)); - typeCell[a].setOnClickListener(typeCellClickListener); - } - sectionCell[0] = new ShadowSectionCell(context); - linearLayout2.addView(sectionCell[0], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); inputFieldsContainer = new LinearLayout(context); inputFieldsContainer.setOrientation(LinearLayout.VERTICAL); @@ -293,8 +277,8 @@ public class ProxySettingsActivity extends BaseFragment { } linearLayout2.addView(inputFieldsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - inputFields = new EditTextBoldCursor[5]; - for (int a = 0; a < 5; a++) { + inputFields = new EditTextBoldCursor[6]; + for (int a = 0; a < 6; a++) { FrameLayout container = new FrameLayout(context); inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); @@ -409,6 +393,10 @@ public class ProxySettingsActivity extends BaseFragment { inputFields[a].setHintText(LocaleController.getString("UseProxySecret", R.string.UseProxySecret)); inputFields[a].setText(currentProxyInfo.secret); break; + case FIELD_REMARKS: + inputFields[a].setHintText(LocaleController.getString("ProxyRemarks", R.string.ProxyRemarks)); + inputFields[a].setText(currentProxyInfo.getRemarks()); + break; } inputFields[a].setSelection(inputFields[a].length()); @@ -488,64 +476,6 @@ public class ProxySettingsActivity extends BaseFragment { linearLayout2.addView(sectionCell[2], 1, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); sectionCell[2].setVisibility(View.GONE); - shareCell = new TextSettingsCell(context); - shareCell.setBackgroundDrawable(Theme.getSelectorDrawable(true)); - shareCell.setText(LocaleController.getString("ShareFile", R.string.ShareFile), false); - shareCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)); - linearLayout2.addView(shareCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - shareCell.setOnClickListener(v -> { - StringBuilder params = new StringBuilder(); - String address = inputFields[FIELD_IP].getText().toString(); - String password = inputFields[FIELD_PASSWORD].getText().toString(); - String user = inputFields[FIELD_USER].getText().toString(); - String port = inputFields[FIELD_PORT].getText().toString(); - String secret = inputFields[FIELD_SECRET].getText().toString(); - String url; - try { - if (!TextUtils.isEmpty(address)) { - params.append("server=").append(URLEncoder.encode(address, "UTF-8")); - } - if (!TextUtils.isEmpty(port)) { - if (params.length() != 0) { - params.append("&"); - } - params.append("port=").append(URLEncoder.encode(port, "UTF-8")); - } - if (currentType == 1) { - url = "https://t.me/proxy?"; - if (params.length() != 0) { - params.append("&"); - } - params.append("secret=").append(URLEncoder.encode(secret, "UTF-8")); - } else { - url = "https://t.me/socks?"; - if (!TextUtils.isEmpty(user)) { - if (params.length() != 0) { - params.append("&"); - } - params.append("user=").append(URLEncoder.encode(user, "UTF-8")); - } - if (!TextUtils.isEmpty(password)) { - if (params.length() != 0) { - params.append("&"); - } - params.append("pass=").append(URLEncoder.encode(password, "UTF-8")); - } - } - } catch (Exception ignore) { - return; - } - if (params.length() == 0) { - return; - } - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_TEXT, url + params.toString()); - Intent chooserIntent = Intent.createChooser(shareIntent, LocaleController.getString("ShareLink", R.string.ShareLink)); - chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - getParentActivity().startActivity(chooserIntent); - }); - sectionCell[1] = new ShadowSectionCell(context); sectionCell[1].setBackgroundDrawable(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); linearLayout2.addView(sectionCell[1], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); @@ -556,8 +486,19 @@ public class ProxySettingsActivity extends BaseFragment { shareDoneProgress = 1f; checkShareDone(false); - currentType = -1; - setProxyType(TextUtils.isEmpty(currentProxyInfo.secret) ? 0 : 1, false); + if (currentType == -1) { + + setProxyType(TextUtils.isEmpty(currentProxyInfo.secret) ? 0 : 1, false); + + } else { + + int t = currentType; + + currentType = -1; + + setProxyType(t, false); + + } pasteType = -1; pasteString = null; @@ -665,7 +606,6 @@ public class ProxySettingsActivity extends BaseFragment { shareDoneAnimator.setDuration(200); shareDoneAnimator.addUpdateListener(a -> { shareDoneProgress = AndroidUtilities.lerp(shareDoneProgressAnimValues, a.getAnimatedFraction()); - shareCell.setTextColor(ColorUtils.blendARGB(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2), Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4), shareDoneProgress)); doneItem.setAlpha(shareDoneProgress / 2f + 0.5f); }); } @@ -675,17 +615,15 @@ public class ProxySettingsActivity extends BaseFragment { shareDoneAnimator.start(); } else { shareDoneProgress = enabled ? 1f : 0f; - shareCell.setTextColor(enabled ? Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4) : Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2)); doneItem.setAlpha(enabled ? 1f : .5f); } - shareCell.setEnabled(enabled); doneItem.setEnabled(enabled); shareDoneEnabled = enabled; } } private void checkShareDone(boolean animated) { - if (shareCell == null || doneItem == null || inputFields[FIELD_IP] == null || inputFields[FIELD_PORT] == null) { + if (doneItem == null || inputFields[FIELD_IP] == null || inputFields[FIELD_PORT] == null) { return; } setShareDoneEnabled(inputFields[FIELD_IP].length() != 0 && Utilities.parseInt(inputFields[FIELD_PORT].getText().toString()) != 0, animated); @@ -749,8 +687,6 @@ public class ProxySettingsActivity extends BaseFragment { ((View) inputFields[FIELD_PASSWORD].getParent()).setVisibility(View.GONE); ((View) inputFields[FIELD_USER].getParent()).setVisibility(View.GONE); } - typeCell[0].setChecked(currentType == 0, animated); - typeCell[1].setChecked(currentType == 1, animated); } } @@ -765,9 +701,6 @@ public class ProxySettingsActivity extends BaseFragment { @Override public ArrayList getThemeDescriptions() { final ThemeDescription.ThemeDescriptionDelegate delegate = () -> { - if (shareCell != null && (shareDoneAnimator == null || !shareDoneAnimator.isRunning())) { - shareCell.setTextColor(shareDoneEnabled ? Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4) : Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2)); - } if (inputFields != null) { for (int i = 0; i < inputFields.length; i++) { inputFields[i].setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), @@ -788,23 +721,10 @@ public class ProxySettingsActivity extends BaseFragment { arrayList.add(new ThemeDescription(inputFieldsContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); - arrayList.add(new ThemeDescription(shareCell, ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_windowBackgroundWhite)); - arrayList.add(new ThemeDescription(shareCell, ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_listSelector)); - arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteBlueText4)); - arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteGrayText2)); - arrayList.add(new ThemeDescription(pasteCell, ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_windowBackgroundWhite)); arrayList.add(new ThemeDescription(pasteCell, ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_listSelector)); arrayList.add(new ThemeDescription(pasteCell, 0, new Class[]{TextSettingsCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueText4)); - for (int a = 0; a < typeCell.length; a++) { - arrayList.add(new ThemeDescription(typeCell[a], ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_windowBackgroundWhite)); - arrayList.add(new ThemeDescription(typeCell[a], ThemeDescription.FLAG_SELECTORWHITE, null, null, null, null, Theme.key_listSelector)); - arrayList.add(new ThemeDescription(typeCell[a], 0, new Class[]{RadioCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); - arrayList.add(new ThemeDescription(typeCell[a], ThemeDescription.FLAG_CHECKBOX, new Class[]{RadioCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackground)); - arrayList.add(new ThemeDescription(typeCell[a], ThemeDescription.FLAG_CHECKBOXCHECK, new Class[]{RadioCell.class}, new String[]{"radioButton"}, null, null, null, Theme.key_radioBackgroundChecked)); - } - if (inputFields != null) { for (int a = 0; a < inputFields.length; a++) { arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); @@ -819,8 +739,6 @@ public class ProxySettingsActivity extends BaseFragment { arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); } - arrayList.add(new ThemeDescription(headerCell, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); - arrayList.add(new ThemeDescription(headerCell, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); for (int a = 0; a < sectionCell.length; a++) { if (sectionCell[a] != null) { arrayList.add(new ThemeDescription(sectionCell[a], ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/GuardedProcessPool.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/GuardedProcessPool.kt new file mode 100644 index 000000000..ecc11cc45 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/GuardedProcessPool.kt @@ -0,0 +1,118 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package tw.nekomimi.nekogram + +import android.annotation.SuppressLint +import android.os.Build +import android.os.SystemClock +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import androidx.annotation.MainThread +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import org.telegram.messenger.ApplicationLoader +import org.telegram.messenger.FileLog +import java.io.File +import java.io.IOException +import java.io.InputStream +import kotlin.concurrent.thread + +class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope { + companion object { + private const val TAG = "GuardedProcessPool" + } + + private inner class Guard(private val cmd: List) { + private lateinit var process: Process + + private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try { + input.bufferedReader().forEachLine(logger) + } catch (_: IOException) { } // ignore + + fun start() { + process = ProcessBuilder(cmd).directory(ApplicationLoader.applicationContext.cacheDir).start() + } + + suspend fun looper(onRestartCallback: (suspend () -> Unit)?) { + var running = true + val cmdName = File(cmd.first()).nameWithoutExtension + val exitChannel = Channel() + try { + while (true) { + thread(name = "stderr-$cmdName") { + streamLogger(process.errorStream) { + FileLog.e("[$cmdName]$it") + } + } + thread(name = "stdout-$cmdName") { + streamLogger(process.inputStream) { + FileLog.d("[$cmdName]$it") + } + // this thread also acts as a daemon thread for waitFor + runBlocking { exitChannel.send(process.waitFor()) } + } + val startTime = SystemClock.elapsedRealtime() + val exitCode = exitChannel.receive() + running = false + when { + SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException( + "$cmdName exits too fast (exit code: $exitCode)") + exitCode == 128 + OsConstants.SIGKILL -> FileLog.w("$cmdName was killed") + else -> FileLog.e(IOException("$cmdName unexpectedly exits with code $exitCode")) + } + start() + running = true + onRestartCallback?.invoke() + } + } catch (e: IOException) { + FileLog.w("error occurred. stop guard: " + cmd.joinToString(" ")) + GlobalScope.launch(Dispatchers.Main) { onFatal(e) } + } finally { + if (running) withContext(NonCancellable) { + process.destroy() // kill the process + if (Build.VERSION.SDK_INT >= 26) { + if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext + process.destroyForcibly() // Force to kill the process if it's still alive + } + exitChannel.receive() + } // otherwise process already exited, nothing to be done + } + } + } + + override val coroutineContext = Dispatchers.Main.immediate + Job() + + @MainThread + fun start(cmd: List, onRestartCallback: (suspend () -> Unit)? = null) { + FileLog.d("start process: " + cmd.joinToString (" ")) + Guard(cmd).apply { + start() // if start fails, IOException will be thrown directly + launch { looper(onRestartCallback) } + } + } + + @MainThread + fun close(scope: CoroutineScope) { + cancel() + coroutineContext[Job]!!.also { job -> scope.launch { job.join() } } + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ProxyManager.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ProxyManager.kt new file mode 100644 index 000000000..980f44ac4 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ProxyManager.kt @@ -0,0 +1,51 @@ +package tw.nekomimi.nekogram + +import java.net.InetSocketAddress +import java.net.ServerSocket +import kotlin.random.Random + +object ProxyManager { + + @JvmStatic + fun mkPort(): Int { + + var port: Int + + do { + + port = mkNewPort() + + } while (!isProxyAvailable(port)) + + return port + + } + + private fun mkNewPort() = Random.nextInt(2048, 32768) + + @JvmStatic + fun isProxyAvailable(port: Int): Boolean { + + if (port !in 2048 until 32768) return false + + runCatching { + + val server = ServerSocket() + + server.bind(InetSocketAddress("127.0.0.1",port)) + + server.close() + + Thread.sleep(1000L) + + }.onFailure { + + return false + + } + + return true + + } + +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/RelayBatonLoader.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/RelayBatonLoader.kt new file mode 100644 index 000000000..80dbd01c9 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/RelayBatonLoader.kt @@ -0,0 +1,191 @@ +package tw.nekomimi.nekogram + +import android.annotation.SuppressLint +import com.v2ray.ang.V2RayConfig +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.telegram.messenger.ApplicationLoader +import org.telegram.messenger.FileLog +import tw.nekomimi.nekogram.utils.FileUtil +import java.io.File +import kotlin.concurrent.thread +import kotlin.properties.Delegates + +class RelayBatonLoader { + + companion object { + + @SuppressLint("AuthLeak") + const val publicServer = "rb://anonymous:nya@net.neko.services" + + } + + lateinit var bean: Bean + var port by Delegates.notNull() + var rbProcess: GuardedProcessPool? = null + + fun initConfig(bean: Bean, port: Int) { + + this.bean = bean + this.port = port + + } + + fun start() { + + stop() + + val cacheCfg = File(ApplicationLoader.applicationContext.cacheDir, "config.toml") + + cacheCfg.writeText(bean.toToml(port)) + + val geoip = File(ApplicationLoader.applicationContext.cacheDir, "GeoLite2-Country.mmdb") + + if (!geoip.isFile) { + + geoip.createNewFile() + + geoip.outputStream().use { out -> + + ApplicationLoader.applicationContext.assets.open("GeoLite2-Country.mmdb").use { + + it.copyTo(out) + + } + + } + + } + + rbProcess = GuardedProcessPool { + + FileLog.e(it) + + }.apply { + + runCatching { + + start(listOf(FileUtil.extLib("relaybaton").path, "client")) { + + cacheCfg.delete() + + } + + }.onFailure { + + cacheCfg.delete() + + FileLog.e(it) + + } + + } + + } + + fun stop() { + + if (rbProcess != null) { + + val proc = rbProcess!! + + thread { + + runCatching { + + runBlocking { proc.close(this) } + + } + + } + + rbProcess = null + + } + + } + + data class Bean( + var server: String = "", + var username: String = "", + var password: String = "", + var esni: Boolean = true, + var remarks: String? = null + ) { + + override fun equals(other: Any?): Boolean { + return super.equals(other) || (other is Bean && hash == other.hash) + } + + val hash = (server + username + password).hashCode() + + fun toToml(port: Int) = """ +[log] +file="./log.txt" +level="error" + +[dns] +type="dot" +server="cloudflare-dns.com" +addr="1.0.0.1:853" +local_resolve=false + +[clients] +port=$port + [[clients.client]] + id="1" + server="$server" + username="$username" + password="$password" + esni=$esni + timeout=15 + +[routes] +geoip_file="GeoLite2-Country.mmdb" + [[routes.route]] + type="default" + cond="" + target="1" +""" + + companion object { + + fun parse(url: String): Bean { + + // ss-android style + + val link = url.replace(V2RayConfig.RB_PROTOCOL, "https://").toHttpUrlOrNull() ?: error("invalid relaybaton link $url") + + return Bean( + link.host, + link.username, + link.password, + link.queryParameter("esni").takeIf { it in arrayOf("true", "false") }?.toBoolean() ?: true, + link.fragment + ) + + } + + } + + override fun toString(): String { + + val url = HttpUrl.Builder() + .scheme("https") + .username(username) + .password(password) + .host(server) + + if (!remarks.isNullOrBlank()) url.fragment(remarks) + + if (!esni) url.addQueryParameter("esni","false") + + return url.build().toString().replace("https://", V2RayConfig.RB_PROTOCOL) + + } + + } + + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/RelayBatonSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/RelayBatonSettingsActivity.java new file mode 100644 index 000000000..73b71ac39 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/RelayBatonSettingsActivity.java @@ -0,0 +1,389 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package tw.nekomimi.nekogram; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.os.Build; +import android.text.InputType; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.github.shadowsocks.plugin.PluginConfiguration; +import com.github.shadowsocks.plugin.PluginContract; +import com.github.shadowsocks.plugin.PluginList; +import com.github.shadowsocks.plugin.PluginManager; +import com.github.shadowsocks.plugin.PluginOptions; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; +import org.telegram.messenger.Utilities; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.TextCheckCell; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Components.EditTextBoldCursor; +import org.telegram.ui.Components.LayoutHelper; + +import java.util.ArrayList; + +import cn.hutool.core.util.StrUtil; +import kotlin.Unit; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.PopupBuilder; + +public class RelayBatonSettingsActivity extends BaseFragment { + + private EditTextBoldCursor[] inputFields; + + private EditTextBoldCursor serverField; + private EditTextBoldCursor usernameField; + private EditTextBoldCursor passwordField; + private TextCheckCell esniField; + private EditTextBoldCursor remarksField; + + private ScrollView scrollView; + private LinearLayout linearLayout2; + private LinearLayout inputFieldsContainer; + + private TextInfoPrivacyCell bottomCell; + + private SharedConfig.RelayBatonProxy currentProxyInfo; + private RelayBatonLoader.Bean currentBean; + + private boolean ignoreOnTextChange; + + private static final int done_button = 1; + + public class TypeCell extends FrameLayout { + + private TextView textView; + private ImageView checkImage; + private boolean needDivider; + + public TypeCell(Context context) { + super(context); + + setWillNotDraw(false); + + textView = new TextView(context); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : 21, 0, LocaleController.isRTL ? 21 : 23, 0)); + + checkImage = new ImageView(context); + checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.SRC_IN)); + checkImage.setImageResource(R.drawable.sticker_added); + addView(checkImage, LayoutHelper.createFrame(19, 14, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 21, 0, 21, 0)); + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(50) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + + public void setValue(String name, boolean checked, boolean divider) { + textView.setText(name); + checkImage.setVisibility(checked ? VISIBLE : INVISIBLE); + needDivider = divider; + } + + public void setTypeChecked(boolean value) { + checkImage.setVisibility(value ? VISIBLE : INVISIBLE); + } + + @Override + protected void onDraw(Canvas canvas) { + if (needDivider) { + canvas.drawLine(LocaleController.isRTL ? 0 : AndroidUtilities.dp(20), getMeasuredHeight() - 1, getMeasuredWidth() - (LocaleController.isRTL ? AndroidUtilities.dp(20) : 0), getMeasuredHeight() - 1, Theme.dividerPaint); + } + } + } + + public RelayBatonSettingsActivity() { + super(); + currentBean = new RelayBatonLoader.Bean(); + } + + public RelayBatonSettingsActivity(SharedConfig.RelayBatonProxy proxyInfo) { + super(); + currentProxyInfo = proxyInfo; + currentBean = proxyInfo.bean; + } + + + @Override + public void onResume() { + super.onResume(); + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + } + + @Override + public View createView(Context context) { + actionBar.setTitle(LocaleController.getString("ProxyDetails", R.string.ProxyDetails)); + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(false); + if (AndroidUtilities.isTablet()) { + actionBar.setOccupyStatusBar(false); + } + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == done_button) { + + if (getParentActivity() == null) { + return; + } + + if (StrUtil.isBlank(serverField.getText())) { + + serverField.requestFocus(); + AndroidUtilities.showKeyboard(serverField); + + return; + + } + + if (StrUtil.isBlank(usernameField.getText())) { + + usernameField.requestFocus(); + AndroidUtilities.showKeyboard(usernameField); + + return; + + } + + if (StrUtil.isBlank(passwordField.getText())) { + + passwordField.requestFocus(); + AndroidUtilities.showKeyboard(passwordField); + + return; + + } + + + currentBean.setServer(serverField.getText().toString()); + currentBean.setUsername(usernameField.getText().toString()); + currentBean.setPassword(passwordField.getText().toString()); + currentBean.setEsni(esniField.isChecked()); + currentBean.setRemarks(remarksField.getText().toString()); + + if (currentProxyInfo == null) { + currentProxyInfo = new SharedConfig.RelayBatonProxy(currentBean); + SharedConfig.addProxy(currentProxyInfo); + SharedConfig.setCurrentProxy(currentProxyInfo); + } else { + currentProxyInfo.proxyCheckPingId = 0; + currentProxyInfo.availableCheckTime = 0; + currentProxyInfo.ping = 0; + SharedConfig.saveProxyList(); + SharedConfig.setProxyEnable(false); + } + + finishFragment(); + + } + } + }); + + ActionBarMenuItem doneItem = actionBar.createMenu().addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); + doneItem.setContentDescription(LocaleController.getString("Done", R.string.Done)); + + fragmentView = new FrameLayout(context); + FrameLayout frameLayout = (FrameLayout) fragmentView; + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + + scrollView = new ScrollView(context); + scrollView.setFillViewport(true); + AndroidUtilities.setScrollViewEdgeEffectColor(scrollView, Theme.getColor(Theme.key_actionBarDefault)); + frameLayout.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + inputFieldsContainer = new LinearLayout(context); + inputFieldsContainer.setOrientation(LinearLayout.VERTICAL); + inputFieldsContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // bring to front for transitions + inputFieldsContainer.setElevation(AndroidUtilities.dp(1f)); + inputFieldsContainer.setOutlineProvider(null); + } + linearLayout2.addView(inputFieldsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + inputFields = new EditTextBoldCursor[4]; + + for (int a = 0; a < 4; a++) { + FrameLayout container = new FrameLayout(context); + EditTextBoldCursor cursor = mkCursor(); + inputFields[a] = cursor; + cursor.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_EXTRACT_UI); + switch (a) { + case 0: + serverField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress)); + cursor.setText(currentBean.getServer()); + break; + case 1: + usernameField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyUsername", R.string.UseProxyUsername)); + cursor.setText(currentBean.getUsername()); + break; + case 2: + passwordField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword)); + cursor.setText(currentBean.getPassword()); + break; + case 3: + remarksField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("ProxyRemarks", R.string.ProxyRemarks)); + cursor.setText(currentBean.getRemarks()); + break; + } + cursor.setSelection(cursor.length()); + + cursor.setPadding(0, 0, 0, 0); + container.addView(cursor, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 17, a == 0 ? 12 : 0, 17, 0)); + + } + + inputFieldsContainer.addView((View) serverField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) usernameField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) passwordField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + FrameLayout container = new FrameLayout(context); + esniField = new TextCheckCell(context); + esniField.setBackground(Theme.getSelectorDrawable(false)); + esniField.setTextAndCheck(LocaleController.getString("ESNI", R.string.ESNI), currentBean.getEsni(), false); + esniField.setOnClickListener((v) -> esniField.setChecked(!esniField.isChecked())); + container.addView(esniField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + inputFieldsContainer.addView((View) remarksField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + bottomCell = new TextInfoPrivacyCell(context); + bottomCell.setBackground(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + bottomCell.setText(LocaleController.getString("ProxyInfoSS", R.string.ProxyInfoSS)); + linearLayout2.addView(bottomCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + return fragmentView; + + } + + EditTextBoldCursor mkCursor() { + + EditTextBoldCursor cursor = new EditTextBoldCursor(getParentActivity()); + cursor.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + cursor.setHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + cursor.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setBackground(null); + cursor.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setCursorSize(AndroidUtilities.dp(20)); + cursor.setCursorWidth(1.5f); + cursor.setSingleLine(true); + cursor.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + cursor.setHeaderHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader)); + cursor.setTransformHintToHeader(true); + cursor.setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + return cursor; + + } + + @Override + protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { + if (isOpen && !backward && currentProxyInfo == null) { + serverField.requestFocus(); + AndroidUtilities.showKeyboard(serverField); + } + } + + @Override + public ArrayList getThemeDescriptions() { + final ThemeDescription.ThemeDescriptionDelegate delegate = () -> { + if (inputFields != null) { + for (int i = 0; i < inputFields.length; i++) { + inputFields[i].setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), + Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), + Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + } + } + }; + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(scrollView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_actionBarDefaultSearch)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_actionBarDefaultSearchPlaceholder)); + arrayList.add(new ThemeDescription(inputFieldsContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); + + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteBlueText4)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteGrayText2)); + + + if (inputFields != null) { + for (int a = 0; a < inputFields.length; a++) { + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR | ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_CURSORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputField)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputFieldActivated)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteRedText3)); + } + } else { + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + } + + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); + arrayList.add(new ThemeDescription(bottomCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4)); + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_LINKCOLOR, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText)); + + return arrayList; + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksLoader.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksLoader.kt new file mode 100644 index 000000000..8c29a02a7 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksLoader.kt @@ -0,0 +1,259 @@ +package tw.nekomimi.nekogram + +import android.annotation.SuppressLint +import cn.hutool.core.codec.Base64 +import com.github.shadowsocks.plugin.PluginConfiguration +import com.github.shadowsocks.plugin.PluginManager +import com.v2ray.ang.V2RayConfig.SS_PROTOCOL +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.json.JSONObject +import org.telegram.messenger.ApplicationLoader +import org.telegram.messenger.FileLog +import tw.nekomimi.nekogram.utils.AlertUtil +import tw.nekomimi.nekogram.utils.FileUtil +import java.io.File +import kotlin.concurrent.thread +import kotlin.properties.Delegates + +@SuppressLint("NewApi") +class ShadowsocksLoader { + + lateinit var bean: Bean + var port by Delegates.notNull() + var shadowsocksProcess: GuardedProcessPool? = null + + fun initConfig(bean: Bean, port: Int) { + + this.bean = bean + this.port = port + + } + + fun start() { + + stop() + + val cacheCfg = File(ApplicationLoader.applicationContext.cacheDir, "ss_cfg_${bean.hash}.json") + + shadowsocksProcess = GuardedProcessPool { + + FileLog.e(it) + + }.apply { + + runCatching { + + cacheCfg.writeText(bean.toJson().toString()) + + start(listOf(FileUtil.extLib("ss-local").path, + "--local-addr", "127.0.0.1:$port", + "--config", cacheCfg.path)) { + + cacheCfg.delete() + + } + + }.onFailure { it -> + + AlertUtil.showToast("${it.javaClass.simpleName}: ${it.message}") + + cacheCfg.delete() + + FileLog.e(it) + + } + + } + + } + + fun stop() { + + if (shadowsocksProcess != null) { + + val proc = shadowsocksProcess!! + + thread { + + runCatching { + + runBlocking { proc.close(this) } + + } + + } + + shadowsocksProcess = null + + } + + } + + data class Bean( + var host: String = "", + var remotePort: Int = 443, + var password: String = "", + var method: String = "aes-256-cfb", + var plugin: String = "", + var remarks: String? = null + ) { + + init { + + if (method == "plain") method = "none" + + val pl = PluginConfiguration(plugin) + + if (pl.selected.contains("v2ray") && pl.selected != "v2ray") { + + pl.pluginsOptions["v2ray"] = pl.getOptions() + pl.pluginsOptions.remove(pl.selected) + pl.selected = "v2ray" + + // reslove v2ray plugin + + } + + } + + override fun equals(other: Any?): Boolean { + return super.equals(other) || (other is Bean && hash == other.hash) + } + + /* + init { + + if (method !in methods) error("method $method not supported") + + } + */ + + val hash = (host + remotePort + password + method).hashCode() + + val pluginInitResult by lazy { + PluginManager.init(PluginConfiguration(plugin ?: "")) + } + + fun toJson(): JSONObject = JSONObject().apply { + put("server", host) + put("server_port", remotePort) + put("password", password) + put("method", method) + put("ipv6", true) + if (pluginInitResult != null) { + put("plugin", pluginInitResult!!.first) + put("plugin_opts", pluginInitResult!!.second.toString()) + } + } + + companion object { + + fun parse(url: String): Bean { + + if (url.contains("@")) { + + // ss-android style + + val link = url.replace(SS_PROTOCOL, "https://").toHttpUrlOrNull() + ?: error("invalid ss-android link $url") + + if (link.password.isNotBlank()) { + + return Bean( + link.host, + link.port, + link.password, + link.username, + link.queryParameter("plugin") ?: "", + link.fragment + ) + + } + + val methodAndPswd = Base64.decodeStr(link.username) + + return Bean( + link.host, + link.port, + methodAndPswd.substringAfter(":"), + methodAndPswd.substringBefore(":"), + link.queryParameter("plugin") ?: "", + link.fragment + ) + + } else { + + // v2rayNG style + + var v2Url = url + + if (v2Url.contains("#")) v2Url = v2Url.substringBefore("#") + + val link = ("https://" + Base64.decodeStr(v2Url.substringAfter(SS_PROTOCOL))).toHttpUrlOrNull() + ?: error("invalid v2rayNG link $url") + + return Bean( + link.host, + link.port, + link.password, + link.username, + "", + link.fragment + ) + + } + + } + + } + + override fun toString(): String { + + val url = HttpUrl.Builder() + .scheme("https") + .encodedUsername(Base64.encodeUrlSafe("$method:$password")) + .host(host) + .port(remotePort) + + if (!remarks.isNullOrBlank()) url.fragment(remarks) + + if (plugin.isNotBlank()) url.addQueryParameter("plugin", plugin) + + return url.build().toString().replace("https://", "ss://") + + } + + } + + companion object { + + val methods = arrayOf( + + "none", + "rc4-md5", + "aes-128-cfb", + "aes-192-cfb", + "aes-256-cfb", + "aes-128-ctr", + "aes-192-ctr", + "aes-256-ctr", + "bf-cfb", + "camellia-128-cfb", + "camellia-192-cfb", + "camellia-256-cfb", + "salsa20", + "chacha20", + "chacha20-ietf", + "aes-128-gcm", + "aes-192-gcm", + "aes-256-gcm", + "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305" + + ) + + } + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksRLoader.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksRLoader.kt new file mode 100644 index 000000000..a8fbd58e6 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksRLoader.kt @@ -0,0 +1,242 @@ +package tw.nekomimi.nekogram + +import cn.hutool.core.codec.Base64 +import com.v2ray.ang.V2RayConfig.SSR_PROTOCOL +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.json.JSONObject +import org.telegram.messenger.ApplicationLoader +import org.telegram.messenger.FileLog +import tw.nekomimi.nekogram.utils.FileUtil +import java.io.File +import java.util.* +import kotlin.concurrent.thread +import kotlin.properties.Delegates + +class ShadowsocksRLoader { + + lateinit var bean: Bean + var port by Delegates.notNull() + var shadowsocksProcess: GuardedProcessPool? = null + + fun initConfig(bean: Bean, port: Int) { + + this.bean = bean + this.port = port + + } + + fun start() { + + stop() + + val cacheCfg = File(ApplicationLoader.applicationContext.cacheDir, "ssr_cfg_${bean.hash}.json") + + cacheCfg.writeText(bean.toJson().toString()) + + shadowsocksProcess = GuardedProcessPool { + + FileLog.e(it) + + }.apply { + + runCatching { + + start(listOf(FileUtil.extLib("ssr-local").path, + "-b", "127.0.0.1", + "--host", bean.host, + "-t", "600", + "-c", cacheCfg.path, + "-l", port.toString())) { + + cacheCfg.delete() + + } + + }.onFailure { + + cacheCfg.delete() + + FileLog.e(it) + + } + + } + + } + + fun stop() { + + if (shadowsocksProcess != null) { + + val proc = shadowsocksProcess!! + + thread { + + runCatching { + + runBlocking { proc.close(this) } + + } + + } + + shadowsocksProcess = null + + } + + } + + data class Bean( + var host: String = "", + var remotePort: Int = 443, + var password: String = "", + var protocol: String = "origin", + var protocol_param: String = "", + var obfs: String = "plain", + var obfs_param: String = "", + var method: String = "aes-256-cfb", + var remarks: String? = null + ) { + + val hash = (host + remotePort + password + protocol + obfs + method).hashCode() + + override fun equals(other: Any?): Boolean { + return super.equals(other) || (other is Bean && hash == other.hash) + } + + /* + init { + + if (method !in methods) error("method $method not supported") + if (protocol !in protocols) error("protocol $protocol not supported") + if (obfs !in obfses) error("obfs $obfs not supported") + + } + */ + + fun toJson(): JSONObject = JSONObject().apply { + put("server", host) + put("server_port", remotePort) + put("password", password) + put("method", method) + put("protocol", protocol) + put("protocol_param", protocol_param) + put("obfs", obfs) + put("obfs_param", obfs_param) + put("remarks", remarks) + put("route", "all") + put("remote_dns", "8.8.8.8:53") + put("ipv6", true) + put("metered", false) + put("proxy_apps", JSONObject().apply { + put("enabled", false) + }) + put("udpdns", false) + } + + companion object { + + fun parse(url: String): Bean { + + val params = Base64.decodeStr(url.substringAfter(SSR_PROTOCOL)).split(":") + + val bean = Bean(params[0], + params[1].toInt(), + protocol = params[2], + method = params[3], + obfs = params[4], + password = Base64.decodeStr(params[5].substringBefore("/"))) + + val httpUrl = ("https://localhost" + params[5].substringAfter("/")).toHttpUrl() + + runCatching { + + bean.obfs_param = Base64.decodeStr(httpUrl.queryParameter("obfsparam")!!) + + } + + runCatching { + + bean.protocol_param = Base64.decodeStr(httpUrl.queryParameter("protoparam")!!) + + } + + runCatching { + + val remarks = httpUrl.queryParameter("remarks") + + if (remarks?.isNotBlank() == true) { + + bean.remarks = Base64.decodeStr(remarks) + + } + + } + + return bean + + } + + } + + override fun toString(): String { + + return "ssr://" + Base64.encodeUrlSafe("%s:%d:%s:%s:%s:%s/?obfsparam=%s&protoparam=%s&remarks=%s".format(Locale.ENGLISH, host, remotePort, protocol, method, obfs, + Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, password)), + Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, obfs_param)), + Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, protocol_param)), + Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, remarks ?: "")))) + } + + } + + companion object { + + val methods = arrayOf( + + "none", + "table", + "rc4", + "rc4-md5", + "rc4-md5-6", + "aes-128-cfb", + "aes-192-cfb", + "aes-256-cfb", + "aes-128-ctr", + "aes-192-ctr", + "aes-256-ctr", + "bf-cfb", + "camellia-128-cfb", + "camellia-192-cfb", + "camellia-256-cfb", + "salsa20", + "chacha20", + "chacha20-ietf" + + ) + + val protocols = arrayOf( + "origin", + "verify_simple", + "verify_sha1", + "auth_sha1", + "auth_sha1_v2", + "auth_sha1_v4", + "auth_aes128_sha1", + "auth_aes128_md5", + "auth_chain_a", + "auth_chain_b" + ) + + val obfses = arrayOf( + "plain", + "http_simple", + "http_post", + "tls_simple", + "tls1.2_ticket_auth" + ) + + } + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksRSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksRSettingsActivity.java new file mode 100644 index 000000000..2d94e36a3 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksRSettingsActivity.java @@ -0,0 +1,469 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package tw.nekomimi.nekogram; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.os.Build; +import android.text.InputType; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; +import org.telegram.messenger.Utilities; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Components.EditTextBoldCursor; +import org.telegram.ui.Components.LayoutHelper; + +import java.util.ArrayList; + +import cn.hutool.core.util.StrUtil; +import kotlin.Unit; +import tw.nekomimi.nekogram.utils.PopupBuilder; + +public class ShadowsocksRSettingsActivity extends BaseFragment { + + private EditTextBoldCursor[] inputFields; + + private EditTextBoldCursor ipField; + private EditTextBoldCursor portField; + private EditTextBoldCursor passwordField; + private TextSettingsCell methodField; + + private TextSettingsCell protocolField; + private EditTextBoldCursor protocolParamField; + + private TextSettingsCell obfsField; + private EditTextBoldCursor obfsParamField; + + private EditTextBoldCursor remarksField; + + private ScrollView scrollView; + private LinearLayout linearLayout2; + private LinearLayout inputFieldsContainer; + + private TextInfoPrivacyCell bottomCell; + + private SharedConfig.ShadowsocksRProxy currentProxyInfo; + private ShadowsocksRLoader.Bean currentBean; + + private boolean ignoreOnTextChange; + + private static final int done_button = 1; + + public class TypeCell extends FrameLayout { + + private TextView textView; + private ImageView checkImage; + private boolean needDivider; + + public TypeCell(Context context) { + super(context); + + setWillNotDraw(false); + + textView = new TextView(context); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : 21, 0, LocaleController.isRTL ? 21 : 23, 0)); + + checkImage = new ImageView(context); + checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.SRC_IN)); + checkImage.setImageResource(R.drawable.sticker_added); + addView(checkImage, LayoutHelper.createFrame(19, 14, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 21, 0, 21, 0)); + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(50) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + + public void setValue(String name, boolean checked, boolean divider) { + textView.setText(name); + checkImage.setVisibility(checked ? VISIBLE : INVISIBLE); + needDivider = divider; + } + + public void setTypeChecked(boolean value) { + checkImage.setVisibility(value ? VISIBLE : INVISIBLE); + } + + @Override + protected void onDraw(Canvas canvas) { + if (needDivider) { + canvas.drawLine(LocaleController.isRTL ? 0 : AndroidUtilities.dp(20), getMeasuredHeight() - 1, getMeasuredWidth() - (LocaleController.isRTL ? AndroidUtilities.dp(20) : 0), getMeasuredHeight() - 1, Theme.dividerPaint); + } + } + } + + public ShadowsocksRSettingsActivity() { + super(); + currentBean = new ShadowsocksRLoader.Bean(); + } + + public ShadowsocksRSettingsActivity(SharedConfig.ShadowsocksRProxy proxyInfo) { + super(); + currentProxyInfo = proxyInfo; + currentBean = proxyInfo.bean; + } + + + @Override + public void onResume() { + super.onResume(); + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + } + + @Override + public View createView(Context context) { + actionBar.setTitle(LocaleController.getString("ProxyDetails", R.string.ProxyDetails)); + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(false); + if (AndroidUtilities.isTablet()) { + actionBar.setOccupyStatusBar(false); + } + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == done_button) { + + if (getParentActivity() == null) { + return; + } + + if (StrUtil.isBlank(ipField.getText())) { + + ipField.requestFocus(); + AndroidUtilities.showKeyboard(ipField); + + return; + + } + + if (StrUtil.isBlank(portField.getText())) { + + portField.requestFocus(); + AndroidUtilities.showKeyboard(portField); + + return; + + } + + if (StrUtil.isBlank(passwordField.getText())) { + + passwordField.requestFocus(); + AndroidUtilities.showKeyboard(passwordField); + + return; + + } + + currentBean.setHost(ipField.getText().toString()); + currentBean.setRemotePort(Utilities.parseInt(portField.getText().toString())); + currentBean.setPassword(passwordField.getText().toString()); + currentBean.setMethod(methodField.getValueTextView().getText().toString()); + currentBean.setProtocol(protocolField.getValueTextView().getText().toString()); + currentBean.setProtocol_param(protocolParamField.getText().toString()); + currentBean.setObfs(obfsField.getValueTextView().getText().toString()); + currentBean.setObfs_param(obfsParamField.getText().toString()); + currentBean.setRemarks(remarksField.getText().toString()); + + if (currentProxyInfo == null) { + currentProxyInfo = new SharedConfig.ShadowsocksRProxy(currentBean); + SharedConfig.addProxy(currentProxyInfo); + SharedConfig.setCurrentProxy(currentProxyInfo); + } else { + currentProxyInfo.proxyCheckPingId = 0; + currentProxyInfo.availableCheckTime = 0; + currentProxyInfo.ping = 0; + SharedConfig.saveProxyList(); + SharedConfig.setProxyEnable(false); + } + + finishFragment(); + + } + } + }); + + ActionBarMenuItem doneItem = actionBar.createMenu().addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); + doneItem.setContentDescription(LocaleController.getString("Done", R.string.Done)); + + fragmentView = new FrameLayout(context); + FrameLayout frameLayout = (FrameLayout) fragmentView; + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + + scrollView = new ScrollView(context); + scrollView.setFillViewport(true); + AndroidUtilities.setScrollViewEdgeEffectColor(scrollView, Theme.getColor(Theme.key_actionBarDefault)); + frameLayout.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + inputFieldsContainer = new LinearLayout(context); + inputFieldsContainer.setOrientation(LinearLayout.VERTICAL); + inputFieldsContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // bring to front for transitions + inputFieldsContainer.setElevation(AndroidUtilities.dp(1f)); + inputFieldsContainer.setOutlineProvider(null); + } + linearLayout2.addView(inputFieldsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + inputFields = new EditTextBoldCursor[6]; + + for (int a = 0; a < 6; a++) { + FrameLayout container = new FrameLayout(context); + EditTextBoldCursor cursor = mkCursor(); + inputFields[a] = cursor; + cursor.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_EXTRACT_UI); + switch (a) { + case 0: + ipField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress)); + cursor.setText(currentBean.getHost()); + break; + case 1: + portField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_NUMBER); + cursor.setHintText(LocaleController.getString("UseProxyPort", R.string.UseProxyPort)); + cursor.setText("" + currentBean.getRemotePort()); + break; + case 2: + passwordField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword)); + cursor.setText(currentBean.getPassword()); + break; + case 3: + protocolParamField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("SSRProtocolParams", R.string.SSRProtocolParams)); + cursor.setText(currentBean.getProtocol_param()); + break; + case 4: + obfsParamField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("SSRObfsParam", R.string.SSRObfsParam)); + cursor.setText(currentBean.getObfs_param()); + break; + case 5: + remarksField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("ProxyRemarks", R.string.ProxyRemarks)); + cursor.setText(currentBean.getRemarks()); + break; + } + cursor.setSelection(cursor.length()); + + cursor.setPadding(0, 0, 0, 0); + container.addView(cursor, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 17, a == 0 ? 12 : 0, 17, 0)); + + } + + inputFieldsContainer.addView((View) ipField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) portField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) passwordField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + FrameLayout container = new FrameLayout(context); + + methodField = new TextSettingsCell(context); + methodField.setBackground(Theme.getSelectorDrawable(false)); + methodField.setTextAndValue(LocaleController.getString("SSMethod", R.string.SSMethod), currentBean.getMethod(), false); + container.addView(methodField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + + methodField.setOnClickListener((v) -> { + + PopupBuilder select = new PopupBuilder(v); + + select.setItems(ShadowsocksRLoader.Companion.getMethods(), (__,value) -> { + + methodField.getValueTextView().setText(value); + + return Unit.INSTANCE; + + }); + + select.show(); + + }); + + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + container = new FrameLayout(context); + protocolField = new TextSettingsCell(context); + protocolField.setBackground(Theme.getSelectorDrawable(false)); + protocolField.setTextAndValue(LocaleController.getString("SSRProtocol", R.string.SSRProtocol), currentBean.getProtocol(), false); + container.addView(protocolField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + + protocolField.setOnClickListener((v) -> { + + PopupBuilder select = new PopupBuilder(v); + + select.setItems(ShadowsocksRLoader.Companion.getProtocols(), (__,value) -> { + + protocolField.getValueTextView().setText(value); + + return Unit.INSTANCE; + + }); + + select.show(); + + }); + + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + inputFieldsContainer.addView((View) protocolParamField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + container = new FrameLayout(context); + obfsField = new TextSettingsCell(context); + obfsField.setBackground(Theme.getSelectorDrawable(false)); + obfsField.setTextAndValue(LocaleController.getString("SSRObfs", R.string.SSRObfs), currentBean.getObfs(), false); + container.addView(obfsField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + + obfsField.setOnClickListener((v) -> { + + PopupBuilder select = new PopupBuilder(v); + + select.setItems(ShadowsocksRLoader.Companion.getObfses(), (__,value) -> { + + obfsField.getValueTextView().setText(value); + + return Unit.INSTANCE; + + }); + + select.show(); + + }); + + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + inputFieldsContainer.addView((View) obfsParamField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + inputFieldsContainer.addView((View) remarksField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + bottomCell = new TextInfoPrivacyCell(context); + bottomCell.setBackground(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + bottomCell.setText(LocaleController.getString("ProxyInfoSSR", R.string.ProxyInfoSSR)); + linearLayout2.addView(bottomCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + return fragmentView; + + } + + EditTextBoldCursor mkCursor() { + + EditTextBoldCursor cursor = new EditTextBoldCursor(getParentActivity()); + cursor.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + cursor.setHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + cursor.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setBackground(null); + cursor.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setCursorSize(AndroidUtilities.dp(20)); + cursor.setCursorWidth(1.5f); + cursor.setSingleLine(true); + cursor.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + cursor.setHeaderHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader)); + cursor.setTransformHintToHeader(true); + cursor.setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + return cursor; + + } + + @Override + protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { + if (isOpen && !backward && currentProxyInfo == null) { + ipField.requestFocus(); + AndroidUtilities.showKeyboard(ipField); + } + } + + @Override + public ArrayList getThemeDescriptions() { + final ThemeDescription.ThemeDescriptionDelegate delegate = () -> { + if (inputFields != null) { + for (int i = 0; i < inputFields.length; i++) { + inputFields[i].setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), + Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), + Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + } + } + }; + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(scrollView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_actionBarDefaultSearch)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_actionBarDefaultSearchPlaceholder)); + arrayList.add(new ThemeDescription(inputFieldsContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); + + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteBlueText4)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteGrayText2)); + + + if (inputFields != null) { + for (int a = 0; a < inputFields.length; a++) { + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR | ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_CURSORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputField)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputFieldActivated)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteRedText3)); + } + } else { + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + } + + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); + arrayList.add(new ThemeDescription(bottomCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4)); + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_LINKCOLOR, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText)); + + return arrayList; + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksSettingsActivity.java new file mode 100644 index 000000000..f6a5c64e7 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/ShadowsocksSettingsActivity.java @@ -0,0 +1,570 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package tw.nekomimi.nekogram; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.os.Build; +import android.text.InputType; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.RequiresApi; + +import com.github.shadowsocks.plugin.PluginConfiguration; +import com.github.shadowsocks.plugin.PluginContract; +import com.github.shadowsocks.plugin.PluginList; +import com.github.shadowsocks.plugin.PluginManager; +import com.github.shadowsocks.plugin.PluginOptions; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; +import org.telegram.messenger.Utilities; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Components.EditTextBoldCursor; +import org.telegram.ui.Components.LayoutHelper; + +import java.util.ArrayList; + +import cn.hutool.core.util.StrUtil; +import kotlin.Unit; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.PopupBuilder; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class ShadowsocksSettingsActivity extends BaseFragment { + + private EditTextBoldCursor[] inputFields; + + private EditTextBoldCursor ipField; + private EditTextBoldCursor portField; + private EditTextBoldCursor passwordField; + private TextSettingsCell methodField; + private TextSettingsCell pluginField; + private TextSettingsCell pluginOptsField; + private EditTextBoldCursor remarksField; + + private ScrollView scrollView; + private LinearLayout linearLayout2; + private LinearLayout inputFieldsContainer; + + private TextInfoPrivacyCell bottomCell; + + private SharedConfig.ShadowsocksProxy currentProxyInfo; + private ShadowsocksLoader.Bean currentBean; + private PluginConfiguration plugin; + + private boolean ignoreOnTextChange; + + private static final int done_button = 1; + + public class TypeCell extends FrameLayout { + + private TextView textView; + private ImageView checkImage; + private boolean needDivider; + + public TypeCell(Context context) { + super(context); + + setWillNotDraw(false); + + textView = new TextView(context); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : 21, 0, LocaleController.isRTL ? 21 : 23, 0)); + + checkImage = new ImageView(context); + checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.SRC_IN)); + checkImage.setImageResource(R.drawable.sticker_added); + addView(checkImage, LayoutHelper.createFrame(19, 14, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 21, 0, 21, 0)); + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(50) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + + public void setValue(String name, boolean checked, boolean divider) { + textView.setText(name); + checkImage.setVisibility(checked ? VISIBLE : INVISIBLE); + needDivider = divider; + } + + public void setTypeChecked(boolean value) { + checkImage.setVisibility(value ? VISIBLE : INVISIBLE); + } + + @Override + protected void onDraw(Canvas canvas) { + if (needDivider) { + canvas.drawLine(LocaleController.isRTL ? 0 : AndroidUtilities.dp(20), getMeasuredHeight() - 1, getMeasuredWidth() - (LocaleController.isRTL ? AndroidUtilities.dp(20) : 0), getMeasuredHeight() - 1, Theme.dividerPaint); + } + } + } + + public ShadowsocksSettingsActivity() { + super(); + currentBean = new ShadowsocksLoader.Bean(); + plugin = new PluginConfiguration(""); + } + + public ShadowsocksSettingsActivity(SharedConfig.ShadowsocksProxy proxyInfo) { + super(); + currentProxyInfo = proxyInfo; + currentBean = proxyInfo.bean; + plugin = new PluginConfiguration(currentBean.getPlugin()); + } + + + @Override + public void onResume() { + super.onResume(); + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + } + + @Override + public View createView(Context context) { + actionBar.setTitle(LocaleController.getString("ProxyDetails", R.string.ProxyDetails)); + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(false); + if (AndroidUtilities.isTablet()) { + actionBar.setOccupyStatusBar(false); + } + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == done_button) { + + if (getParentActivity() == null) { + return; + } + + if (StrUtil.isBlank(ipField.getText())) { + + ipField.requestFocus(); + AndroidUtilities.showKeyboard(ipField); + + return; + + } + + if (StrUtil.isBlank(portField.getText())) { + + portField.requestFocus(); + AndroidUtilities.showKeyboard(portField); + + return; + + } + + if (StrUtil.isBlank(passwordField.getText()) && !"plain".equals(methodField.getTextView().getText().toString().toLowerCase())) { + + passwordField.requestFocus(); + AndroidUtilities.showKeyboard(passwordField); + + return; + + } + + currentBean.setHost(ipField.getText().toString()); + currentBean.setRemotePort(Utilities.parseInt(portField.getText().toString())); + currentBean.setPassword(passwordField.getText().toString()); + currentBean.setMethod(methodField.getValueTextView().getText().toString()); + currentBean.setPlugin(plugin.toString()); + currentBean.setRemarks(remarksField.getText().toString()); + + if (currentProxyInfo == null) { + currentProxyInfo = new SharedConfig.ShadowsocksProxy(currentBean); + SharedConfig.addProxy(currentProxyInfo); + SharedConfig.setCurrentProxy(currentProxyInfo); + } else { + currentProxyInfo.proxyCheckPingId = 0; + currentProxyInfo.availableCheckTime = 0; + currentProxyInfo.ping = 0; + SharedConfig.saveProxyList(); + SharedConfig.setProxyEnable(false); + } + + finishFragment(); + + } + } + }); + + ActionBarMenuItem doneItem = actionBar.createMenu().addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); + doneItem.setContentDescription(LocaleController.getString("Done", R.string.Done)); + + fragmentView = new FrameLayout(context); + FrameLayout frameLayout = (FrameLayout) fragmentView; + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + + scrollView = new ScrollView(context); + scrollView.setFillViewport(true); + AndroidUtilities.setScrollViewEdgeEffectColor(scrollView, Theme.getColor(Theme.key_actionBarDefault)); + frameLayout.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + inputFieldsContainer = new LinearLayout(context); + inputFieldsContainer.setOrientation(LinearLayout.VERTICAL); + inputFieldsContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // bring to front for transitions + inputFieldsContainer.setElevation(AndroidUtilities.dp(1f)); + inputFieldsContainer.setOutlineProvider(null); + } + linearLayout2.addView(inputFieldsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + inputFields = new EditTextBoldCursor[4]; + + for (int a = 0; a < 4; a++) { + FrameLayout container = new FrameLayout(context); + EditTextBoldCursor cursor = mkCursor(); + inputFields[a] = cursor; + cursor.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_EXTRACT_UI); + switch (a) { + case 0: + ipField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress)); + cursor.setText(currentBean.getHost()); + break; + case 1: + portField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_NUMBER); + cursor.setHintText(LocaleController.getString("UseProxyPort", R.string.UseProxyPort)); + cursor.setText("" + currentBean.getRemotePort()); + break; + case 2: + passwordField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyPassword", R.string.UseProxyPassword)); + cursor.setText(currentBean.getPassword()); + break; + case 3: + remarksField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("ProxyRemarks", R.string.ProxyRemarks)); + cursor.setText(currentBean.getRemarks()); + break; + } + cursor.setSelection(cursor.length()); + + cursor.setPadding(0, 0, 0, 0); + container.addView(cursor, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 17, a == 0 ? 12 : 0, 17, 0)); + + } + + inputFieldsContainer.addView((View) ipField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) portField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) passwordField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + FrameLayout container = new FrameLayout(context); + + methodField = new TextSettingsCell(context); + methodField.setBackground(Theme.getSelectorDrawable(false)); + methodField.setTextAndValue(LocaleController.getString("SSMethod", R.string.SSMethod), currentBean.getMethod(), false); + container.addView(methodField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + methodField.setOnClickListener((v) -> { + + PopupBuilder select = new PopupBuilder(v); + + select.setItems(ShadowsocksLoader.Companion.getMethods(), (__, value) -> { + + methodField.getValueTextView().setText(value); + + return Unit.INSTANCE; + + }); + + select.show(); + + }); + + container = new FrameLayout(context); + pluginField = new TextSettingsCell(context); + pluginField.setBackground(Theme.getSelectorDrawable(false)); + pluginField.setTextAndValue(LocaleController.getString("SSPlugin", R.string.SSPlugin), plugin.getSelectedName(), false); + container.addView(pluginField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + pluginField.setOnClickListener((v) -> { + + PluginList plugins = PluginManager.fetchPlugins(); + + PopupBuilder select = new PopupBuilder(v); + + try { + + select.setItems(plugins.getLookupNames(), (index, __) -> { + + String pluginId = plugins.get(index).getId(); + + plugin.setSelected(pluginId); + + ((View) pluginOptsField.getParent()).setVisibility(StrUtil.isBlank(pluginId) ? View.GONE : View.VISIBLE); + + pluginField.getValueTextView().setText(plugin.getSelectedName()); + pluginOptsField.getValueTextView().setText(plugin.getOptions(pluginId).toString()); + + return Unit.INSTANCE; + + }); + + select.show(); + + } catch (Exception e) { + + AlertUtil.showSimpleAlert(getParentActivity(),e.getMessage()); + + } + + }); + + container = new FrameLayout(context); + pluginOptsField = new TextSettingsCell(context); + pluginOptsField.setBackground(Theme.getSelectorDrawable(false)); + pluginOptsField.setTextAndValue(LocaleController.getString("SSPluginOpts", R.string.SSPluginOpts), plugin.getOptions().toString(), false); + container.addView(pluginOptsField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + pluginOptsField.setOnClickListener((v) -> { + + Intent intent = PluginManager.buildIntent(plugin.getSelected(), PluginContract.ACTION_CONFIGURE); + intent.putExtra(PluginContract.EXTRA_OPTIONS, plugin.getOptions().toString()); + + if (intent.resolveActivity(getParentActivity().getPackageManager()) == null) { + showPluginEditor(); + } else { + startActivityForResult(intent, 1919); + } + + }); + + ((View) pluginOptsField.getParent()).setVisibility(StrUtil.isBlank(plugin.getSelected()) ? View.GONE : View.VISIBLE); + + inputFieldsContainer.addView((View) remarksField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + bottomCell = new TextInfoPrivacyCell(context); + bottomCell.setBackground(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + bottomCell.setText(LocaleController.getString("ProxyInfoSS", R.string.ProxyInfoSS)); + linearLayout2.addView(bottomCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + return fragmentView; + + } + + @Override public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { + + if (requestCode == 1919) { + + if (resultCode == Activity.RESULT_OK) { + + String options = data.getStringExtra(PluginContract.EXTRA_OPTIONS); + + if (options != null) { + + onPreferenceChange(options); + + } + + } else if (resultCode == PluginContract.RESULT_FALLBACK) { + + showPluginEditor(); + + } + + } else if (requestCode == 810) { + + if (resultCode == Activity.RESULT_OK) { + + CharSequence helpMessage = data.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE); + + if (StrUtil.isBlank(helpMessage)) { + + helpMessage = "No Help :("; + + } + + AlertUtil.showSimpleAlert(getParentActivity(),LocaleController.getString("BotHelp",R.string.BotHelp),helpMessage.toString()); + + } else { + + AlertUtil.showSimpleAlert(getParentActivity(),"Get Help Message Error :("); + + } + + } + + } + + private void showPluginEditor() { + + BottomBuilder builder = new BottomBuilder(getParentActivity()); + + builder.addTitle(LocaleController.getString("SSPluginOpts", R.string.SSPluginOpts)); + + EditText options = builder.addEditText(); + options.setSingleLine(false); + options.setGravity(Gravity.TOP | LocaleController.generateFlagStart()); + options.setMinLines(3); + options.setText(plugin.getOptions().toString()); + + Intent intent = PluginManager.buildIntent(plugin.getSelected(), PluginContract.ACTION_HELP); + intent.putExtra(PluginContract.EXTRA_OPTIONS, plugin.getOptions().toString()); + if (intent.resolveActivity(getParentActivity().getPackageManager()) != null) { + + builder.addButton(LocaleController.getString("BotHelp", R.string.BotHelp), false,true, (it) -> { + + getParentActivity().startActivityForResult(intent, 810); + + return Unit.INSTANCE; + + }); + + builder.addCancelButton(false); + + } else { + + builder.addCancelButton(); + + } + + builder.addOkButton((it) -> { + + onPreferenceChange(options.getText().toString()); + + builder.dismiss(); + + return Unit.INSTANCE; + + }); + + builder.show(); + + } + + private void onPreferenceChange(String newValue) { + String selected = plugin.getSelected(); + plugin.getPluginsOptions().put(selected, new PluginOptions(selected, newValue)); + pluginOptsField.getValueTextView().setText(newValue); + } + + EditTextBoldCursor mkCursor() { + + EditTextBoldCursor cursor = new EditTextBoldCursor(getParentActivity()); + cursor.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + cursor.setHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + cursor.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setBackground(null); + cursor.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setCursorSize(AndroidUtilities.dp(20)); + cursor.setCursorWidth(1.5f); + cursor.setSingleLine(true); + cursor.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + cursor.setHeaderHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader)); + cursor.setTransformHintToHeader(true); + cursor.setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + return cursor; + + } + + @Override + protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { + if (isOpen && !backward && currentProxyInfo == null) { + ipField.requestFocus(); + AndroidUtilities.showKeyboard(ipField); + } + } + + @Override + public ArrayList getThemeDescriptions() { + final ThemeDescription.ThemeDescriptionDelegate delegate = () -> { + if (inputFields != null) { + for (int i = 0; i < inputFields.length; i++) { + inputFields[i].setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), + Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), + Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + } + } + }; + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(scrollView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_actionBarDefaultSearch)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_actionBarDefaultSearchPlaceholder)); + arrayList.add(new ThemeDescription(inputFieldsContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); + + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteBlueText4)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteGrayText2)); + + + if (inputFields != null) { + for (int a = 0; a < inputFields.length; a++) { + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR | ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_CURSORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputField)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputFieldActivated)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteRedText3)); + } + } else { + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + } + + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); + arrayList.add(new ThemeDescription(bottomCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4)); + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_LINKCOLOR, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText)); + + return arrayList; + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/SubSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/SubSettingsActivity.java new file mode 100644 index 000000000..93ee92d87 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/SubSettingsActivity.java @@ -0,0 +1,348 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package tw.nekomimi.nekogram; + +import android.content.Context; +import android.os.Build; +import android.text.InputType; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenu; +import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Components.EditTextBoldCursor; +import org.telegram.ui.Components.LayoutHelper; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +import cn.hutool.core.util.StrUtil; +import kotlin.collections.ArraysKt; +import kotlin.collections.CollectionsKt; +import tw.nekomimi.nekogram.sub.SubInfo; +import tw.nekomimi.nekogram.sub.SubManager; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.UIUtil; + +public class SubSettingsActivity extends BaseFragment { + + private EditText[] inputFields; + + private EditText urlsField; + private EditTextBoldCursor remarksField; + + private ScrollView scrollView; + private LinearLayout linearLayout2; + private LinearLayout inputFieldsContainer; + + private SubInfo subInfo; + + private boolean ignoreOnTextChange; + + private static int done_button = 1; + private static int menu_delete = 2; + + public SubSettingsActivity() { + super(); + subInfo = new SubInfo(); + subInfo.id = 0L; + } + + public SubSettingsActivity(SubInfo subInfo) { + super(); + this.subInfo = subInfo; + } + + @Override + public void onResume() { + super.onResume(); + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + } + + @Override + public View createView(Context context) { + actionBar.setTitle(LocaleController.getString("ProxySubDetails", R.string.ProxySubDetails)); + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(false); + if (AndroidUtilities.isTablet()) { + actionBar.setOccupyStatusBar(false); + } + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == done_button) { + + if (getParentActivity() == null) { + return; + } + + if (StrUtil.isBlank(urlsField.getText())) { + + urlsField.requestFocus(); + AndroidUtilities.showKeyboard(urlsField); + + return; + + } + + if (StrUtil.isBlank(remarksField.getText())) { + + remarksField.requestFocus(); + AndroidUtilities.showKeyboard(remarksField); + + return; + + } + + subInfo.urls = ArraysKt.toList(urlsField.getText().toString().split("\n")); + subInfo.name = remarksField.getText().toString(); + + doGetProxies(); + + } else if (id == menu_delete) { + + AlertUtil.showConfirm(getParentActivity(), + LocaleController.getString("SubscriptionDelete", R.string.SubscriptionDelete), + R.drawable.baseline_delete_24, LocaleController.getString("Delete", R.string.Delete), + true, () -> { + + AlertDialog pro = AlertUtil.showProgress(getParentActivity()); + + pro.show(); + + UIUtil.runOnIoDispatcher(() -> { + + SubManager.getSubList().remove(subInfo); + + SharedConfig.reloadProxyList(); + + UIUtil.runOnUIThread(() -> { + + pro.dismiss(); + + finishFragment(); + + }); + + }); + + }); + + + } + } + }); + + ActionBarMenu menu = actionBar.createMenu(); + + if (subInfo.id != 0) { + + menu.addItem(menu_delete, R.drawable.baseline_delete_24, AndroidUtilities.dp(56)).setContentDescription(LocaleController.getString("Delete", R.string.Delete)); + + } + + menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)).setContentDescription(LocaleController.getString("Done", R.string.Done)); + + fragmentView = new FrameLayout(context); + FrameLayout frameLayout = (FrameLayout) fragmentView; + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + + scrollView = new ScrollView(context); + scrollView.setFillViewport(true); + AndroidUtilities.setScrollViewEdgeEffectColor(scrollView, Theme.getColor(Theme.key_actionBarDefault)); + frameLayout.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + inputFieldsContainer = new LinearLayout(context); + inputFieldsContainer.setOrientation(LinearLayout.VERTICAL); + inputFieldsContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // bring to front for transitions + inputFieldsContainer.setElevation(AndroidUtilities.dp(1f)); + inputFieldsContainer.setOutlineProvider(null); + } + linearLayout2.addView(inputFieldsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + inputFields = new EditText[2]; + + urlsField = new EditText(context); + inputFields[0] = urlsField; + urlsField.setImeOptions(InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE); + urlsField.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + + urlsField.setHint(LocaleController.getString("SubscriptionUrls", R.string.SubscriptionUrls)); + urlsField.setText(CollectionsKt.joinToString(subInfo.urls, "\n", "", "", -1, "", null)); + urlsField.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + urlsField.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + urlsField.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + urlsField.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); + urlsField.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader)); + urlsField.setSingleLine(false); + urlsField.setMinLines(6); + + inputFieldsContainer.addView(urlsField, LayoutHelper.createLinear(-1, -2, 17, 0, 17, 0)); + + FrameLayout container = new FrameLayout(context); + + remarksField = mkCursor(); + inputFields[1] = remarksField; + remarksField.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + remarksField.setHintText(LocaleController.getString("ProxyRemarks", R.string.ProxyRemarks)); + remarksField.setText(subInfo.name); + remarksField.setSelection(remarksField.length()); + + container.addView(remarksField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 17, 0, 17, 0)); + + inputFieldsContainer.addView((View) remarksField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + return fragmentView; + + } + + EditTextBoldCursor mkCursor() { + + EditTextBoldCursor cursor = new EditTextBoldCursor(getParentActivity()); + cursor.setSingleLine(true); + cursor.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + cursor.setHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + cursor.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setBackground(null); + cursor.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setCursorSize(AndroidUtilities.dp(20)); + cursor.setCursorWidth(1.5f); + cursor.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + cursor.setHeaderHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader)); + cursor.setTransformHintToHeader(true); + cursor.setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + return cursor; + + } + + public void doGetProxies() { + + AlertDialog pro = AlertUtil.showProgress(getParentActivity(), LocaleController.getString("SubscriptionUpdating", R.string.SubscriptionUpdating)); + + AtomicBoolean canceled = new AtomicBoolean(); + + pro.setOnCancelListener((it) -> { + + canceled.set(true); + + }); + + pro.show(); + + UIUtil.runOnIoDispatcher(() -> { + + try { + + subInfo.proxies = subInfo.reloadProxies(); + subInfo.lastFetch = System.currentTimeMillis(); + + } catch (SubInfo.AllTriesFailed allTriesFailed) { + + if (canceled.get()) return; + + UIUtil.runOnUIThread(pro::dismiss); + + AlertUtil.showSimpleAlert(getParentActivity(), "tries failed: " + allTriesFailed.toString().trim()); + + return; + + } + + if (subInfo.id == 0) subInfo.id = SubManager.getCount() + 10; + + do { + + try { + SubManager.getSubList().update(subInfo, true); + break; + } catch (Exception ignored) { + } + subInfo.id ++; + + } while (true); + + SharedConfig.reloadProxyList(); + + UIUtil.runOnUIThread(() -> { + + pro.dismiss(); + + finishFragment(); + + }); + + }); + + } + + @Override + public ArrayList getThemeDescriptions() { + final ThemeDescription.ThemeDescriptionDelegate delegate = () -> { + if (inputFields != null) { + for (int i = 0; i < inputFields.length; i++) { + inputFields[i].setText(Theme.getColor(Theme.key_windowBackgroundWhiteInputField)); + } + } + }; + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(scrollView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_actionBarDefaultSearch)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_actionBarDefaultSearchPlaceholder)); + arrayList.add(new ThemeDescription(inputFieldsContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); + + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteBlueText4)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteGrayText2)); + + if (inputFields != null) { + for (int a = 0; a < inputFields.length; a++) { + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR | ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_CURSORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputField)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputFieldActivated)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteRedText3)); + } + } else { + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + } + + return arrayList; + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/VmessLoader.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/VmessLoader.kt new file mode 100644 index 000000000..ba9dfb480 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/VmessLoader.kt @@ -0,0 +1,450 @@ +package tw.nekomimi.nekogram + +import cn.hutool.core.codec.Base64 +import com.google.gson.Gson +import com.v2ray.ang.V2RayConfig +import com.v2ray.ang.V2RayConfig.SOCKS_PROTOCOL +import com.v2ray.ang.V2RayConfig.SS_PROTOCOL +import com.v2ray.ang.V2RayConfig.VMESS1_PROTOCOL +import com.v2ray.ang.V2RayConfig.VMESS_PROTOCOL +import com.v2ray.ang.dto.AngConfig.VmessBean +import com.v2ray.ang.dto.VmessQRCode +import com.v2ray.ang.util.Utils +import com.v2ray.ang.util.V2rayConfigUtil +import libv2ray.Libv2ray +import libv2ray.V2RayPoint +import libv2ray.V2RayVPNServiceSupportsSet +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.telegram.messenger.FileLog +import org.telegram.messenger.LocaleController +import org.telegram.messenger.R +import kotlin.concurrent.thread +import kotlin.random.Random + +class VmessLoader { + + private val point = Libv2ray.newV2RayPoint(EmptyCallback(), true) + + companion object { + + fun parseVmess1Link(server: String): VmessBean { + + val lnk = ("https://" + server.substringAfter(VMESS1_PROTOCOL)).toHttpUrl() + + val bean = VmessBean() + + bean.configType = V2RayConfig.EConfigType.Vmess + bean.address = lnk.host + bean.port = lnk.port + bean.id = lnk.username + bean.remarks = lnk.fragment ?: "" + + lnk.queryParameterNames.forEach { + + when (it) { + + "tls" -> if (lnk.queryParameter(it) == "true") bean.streamSecurity = "tls" + + "network" -> { + + bean.network = lnk.queryParameter(it)!! + + if (bean.network in arrayOf("http", "ws")) { + + bean.path = Utils.urlDecode(lnk.encodedPath) + + } + + } + + "header" -> { + + bean.headerType = lnk.queryParameter(it)!! + + } + + } + + } + + return bean + + } + + @JvmStatic + fun parseVmessLink(server: String): VmessBean { + + try { + if (server.isBlank()) error("empty link") + + var vmess = VmessBean() + + if (server.startsWith(VMESS_PROTOCOL)) { + + val indexSplit = server.indexOf("?") + if (indexSplit > 0) { + vmess = resolveSimpleVmess1(server) + } else { + + var result = server.replace(VMESS_PROTOCOL, "") + result = Base64.decodeStr(result) + if (result.isBlank()) { + error("invalid url format") + } + + if (result.contains("= vmess")) { + + vmess = resolveSomeIOSAppShitCsvLink(result) + + } else { + + val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java) + if (vmessQRCode.add.isBlank() + || vmessQRCode.port.isBlank() + || vmessQRCode.id.isBlank() + || vmessQRCode.aid.isBlank() + || vmessQRCode.net.isBlank() + ) { + error("invalid protocol") + } + + vmess.configType = V2RayConfig.EConfigType.Vmess + vmess.security = "auto" + vmess.network = "tcp" + vmess.headerType = "none" + + vmess.configVersion = Utils.parseInt(vmessQRCode.v) + vmess.remarks = vmessQRCode.ps + vmess.address = vmessQRCode.add + vmess.port = Utils.parseInt(vmessQRCode.port) + vmess.id = vmessQRCode.id + vmess.alterId = Utils.parseInt(vmessQRCode.aid) + vmess.network = vmessQRCode.net + vmess.headerType = vmessQRCode.type + vmess.requestHost = vmessQRCode.host + vmess.path = vmessQRCode.path + vmess.streamSecurity = vmessQRCode.tls + } + } + + upgradeServerVersion(vmess) + + return vmess + } else if (server.startsWith(VMESS1_PROTOCOL)) { + + return parseVmess1Link(server) + + } else if (server.startsWith(SS_PROTOCOL)) { + var result = server.replace(SS_PROTOCOL, "") + val indexSplit = result.indexOf("#") + if (indexSplit > 0) { + try { + vmess.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + } catch (e: Exception) { + e.printStackTrace() + } + + result = result.substring(0, indexSplit) + } + + //part decode + val indexS = result.indexOf("@") + if (indexS > 0) { + result = Base64.decodeStr(result.substring(0, indexS)) + result.substring(indexS, result.length) + } else { + result = Base64.decodeStr(result) + } + + val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex() + val match = legacyPattern.matchEntire(result) ?: error("invalid protocol") + vmess.security = match.groupValues[1].toLowerCase() + vmess.id = match.groupValues[2] + vmess.address = match.groupValues[3] + if (vmess.address.firstOrNull() == '[' && vmess.address.lastOrNull() == ']') + vmess.address = vmess.address.substring(1, vmess.address.length - 1) + vmess.port = match.groupValues[4].toInt() + + return vmess + + } else if (server.startsWith(SOCKS_PROTOCOL)) { + var result = server.replace(SOCKS_PROTOCOL, "") + val indexSplit = result.indexOf("#") + if (indexSplit > 0) { + try { + vmess.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length)) + } catch (e: Exception) { + e.printStackTrace() + } + + result = result.substring(0, indexSplit) + } + + //part decode + val indexS = result.indexOf(":") + if (indexS < 0) { + result = Base64.decodeStr(result) + } + + val legacyPattern = "^(.+?):(\\d+?)$".toRegex() + val match = legacyPattern.matchEntire(result) ?: error("invalid protocol") + vmess.address = match.groupValues[1] + if (vmess.address.firstOrNull() == '[' && vmess.address.lastOrNull() == ']') + vmess.address = vmess.address.substring(1, vmess.address.length - 1) + vmess.port = match.groupValues[2].toInt() + + return vmess + } else { + error("invalid protocol") + } + } catch (e: Exception) { + + throw IllegalArgumentException(e) + + } + + } + + private fun resolveSomeIOSAppShitCsvLink(csv: String): VmessBean { + + val args = csv.split(",") + + val bean = VmessBean() + + bean.configType = V2RayConfig.EConfigType.Vmess + bean.address = args[1] + bean.port = args[2].toInt() + bean.security = args[3] + bean.id = args[4].replace("\"", "") + + args.subList(5, args.size).forEach { + + when { + + it == "over-tls=true" -> { + + bean.streamSecurity = "tls" + + } + + it.startsWith("tls-host=") -> { + + bean.requestHost = it.substringAfter("=") + + } + + it.startsWith("obfs=") -> { + + bean.network = it.substringAfter("=") + + } + + it.startsWith("obfs-path=") || it.contains("Host:") -> { + + runCatching { + + bean.path = it + .substringAfter("obfs-path=\"") + .substringBefore("\"obfs") + + } + + runCatching { + + bean.requestHost = it + .substringAfter("Host:") + .substringBefore("[") + + } + + } + + } + + } + + return bean + + } + + /** + * upgrade + */ + private fun upgradeServerVersion(vmess: VmessBean): Int { + try { + if (vmess.configVersion == 2) { + return 0 + } + + when (vmess.network) { + "kcp" -> { + } + "ws" -> { + var path = "" + var host = "" + val lstParameter = vmess.requestHost.split(";") + if (lstParameter.size > 0) { + path = lstParameter.get(0).trim() + } + if (lstParameter.size > 1) { + path = lstParameter.get(0).trim() + host = lstParameter.get(1).trim() + } + vmess.path = path + vmess.requestHost = host + } + "h2" -> { + var path = "" + var host = "" + val lstParameter = vmess.requestHost.split(";") + if (lstParameter.isNotEmpty()) { + path = lstParameter[0].trim() + } + if (lstParameter.size > 1) { + path = lstParameter[0].trim() + host = lstParameter[1].trim() + } + vmess.path = path + vmess.requestHost = host + } + else -> { + } + } + vmess.configVersion = 2 + return 0 + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + } + + private fun resolveSimpleVmess1(server: String): VmessBean { + + val vmess = VmessBean() + + var result = server.replace(VMESS_PROTOCOL, "") + val indexSplit = result.indexOf("?") + if (indexSplit > 0) { + result = result.substring(0, indexSplit) + } + result = Base64.decodeStr(result) + + val arr1 = result.split('@') + if (arr1.count() != 2) { + return vmess + } + val arr21 = arr1[0].split(':') + val arr22 = arr1[1].split(':') + if (arr21.count() != 2 || arr21.count() != 2) { + return vmess + } + + vmess.address = arr22[0] + vmess.port = Utils.parseInt(arr22[1]) + vmess.security = arr21[0] + vmess.id = arr21[1] + + vmess.security = "chacha20-poly1305" + vmess.network = "tcp" + vmess.headerType = "none" + vmess.remarks = "" + vmess.alterId = 0 + + return vmess + } + + } + + lateinit var bean: VmessBean + + fun initConfig(config: VmessBean) { + + this.bean = config + + } + + fun start(): Int { + + var retry = 3 + + do { + + try { + + val port = Random.nextInt(4096, 32768) + + val conf = V2rayConfigUtil.getV2rayConfig(bean, port).content + + runCatching { + + Libv2ray.testConfig(conf) + + }.onFailure { + + FileLog.e(it) + + return -1 + + } + + point.configureFileContent = conf + point.domainName = V2rayConfigUtil.currDomain + + point.runLoop(true) + + return port + + } catch (e: Throwable) { + + retry-- + + if (retry <= 0) { + + FileLog.e(e) + + return -1 + + } + + } + + } while (true) + + } + + fun stop() { + + thread { + + runCatching { + + point.stopLoop() + + } + + } + + } + + class EmptyCallback : V2RayVPNServiceSupportsSet { + override fun onEmitStatus(p0: Long, p1: String?): Long { + return 0 + } + + override fun setup(p0: String?): Long { + return 0 + } + + override fun prepare(): Long { + return 0 + } + + override fun shutdown(): Long { + return 0 + } + + override fun protect(p0: Long): Boolean { + return true + } + } + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/VmessSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/VmessSettingsActivity.java new file mode 100644 index 000000000..75e455d99 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/VmessSettingsActivity.java @@ -0,0 +1,505 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package tw.nekomimi.nekogram; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.os.Build; +import android.text.InputType; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.v2ray.ang.V2RayConfig; +import com.v2ray.ang.dto.AngConfig; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; +import org.telegram.messenger.Utilities; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; +import org.telegram.ui.Cells.TextCheckCell; +import org.telegram.ui.Cells.TextInfoPrivacyCell; +import org.telegram.ui.Cells.TextSettingsCell; +import org.telegram.ui.Components.EditTextBoldCursor; +import org.telegram.ui.Components.LayoutHelper; + +import java.util.ArrayList; + +import cn.hutool.core.util.StrUtil; +import kotlin.Unit; +import tw.nekomimi.nekogram.utils.PopupBuilder; + +public class VmessSettingsActivity extends BaseFragment { + + private EditTextBoldCursor[] inputFields; + + private EditTextBoldCursor ipField; + private EditTextBoldCursor portField; + private EditTextBoldCursor userIdField; + private EditTextBoldCursor alterIdField; + private TextSettingsCell securityField; + private TextSettingsCell networkField; + private TextSettingsCell headTypeField; + private EditTextBoldCursor requestHostField; + private EditTextBoldCursor pathField; + private TextCheckCell useTlsField; + private EditTextBoldCursor remarksField; + + private ScrollView scrollView; + private LinearLayout linearLayout2; + private LinearLayout inputFieldsContainer; + + private TextInfoPrivacyCell bottomCell; + private ActionBarMenuItem doneItem; + + private SharedConfig.VmessProxy currentProxyInfo; + private AngConfig.VmessBean currentBean; + + private boolean ignoreOnTextChange; + + private static final int done_button = 1; + + private static String[] securitySet = { "chacha20-poly1305","aes-128-gcm","auto","none" }; + private static String[] networkSet = { "tcp","kcp","ws","h2","quic" }; + private static String[] headTypeSet = { "none","http","srtp","utp","wechat-video","dtls","wireguard" }; + + public class TypeCell extends FrameLayout { + + private TextView textView; + private ImageView checkImage; + private boolean needDivider; + + public TypeCell(Context context) { + super(context); + + setWillNotDraw(false); + + textView = new TextView(context); + textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setLines(1); + textView.setMaxLines(1); + textView.setSingleLine(true); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : 21, 0, LocaleController.isRTL ? 21 : 23, 0)); + + checkImage = new ImageView(context); + checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.SRC_IN)); + checkImage.setImageResource(R.drawable.sticker_added); + addView(checkImage, LayoutHelper.createFrame(19, 14, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 21, 0, 21, 0)); + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(50) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + + public void setValue(String name, boolean checked, boolean divider) { + textView.setText(name); + checkImage.setVisibility(checked ? VISIBLE : INVISIBLE); + needDivider = divider; + } + + public void setTypeChecked(boolean value) { + checkImage.setVisibility(value ? VISIBLE : INVISIBLE); + } + + @Override + protected void onDraw(Canvas canvas) { + if (needDivider) { + canvas.drawLine(LocaleController.isRTL ? 0 : AndroidUtilities.dp(20), getMeasuredHeight() - 1, getMeasuredWidth() - (LocaleController.isRTL ? AndroidUtilities.dp(20) : 0), getMeasuredHeight() - 1, Theme.dividerPaint); + } + } + } + + public VmessSettingsActivity() { + super(); + currentBean = new AngConfig.VmessBean(); + currentBean.setConfigType(V2RayConfig.EConfigType.Vmess); + } + + public VmessSettingsActivity(SharedConfig.VmessProxy proxyInfo) { + super(); + currentProxyInfo = proxyInfo; + currentBean = proxyInfo.bean; + } + + + @Override + public void onResume() { + super.onResume(); + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + } + + @Override + public View createView(Context context) { + actionBar.setTitle(LocaleController.getString("ProxyDetails", R.string.ProxyDetails)); + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(false); + if (AndroidUtilities.isTablet()) { + actionBar.setOccupyStatusBar(false); + } + + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == done_button) { + + if (getParentActivity() == null) { + return; + } + + if (StrUtil.isBlank(ipField.getText())) { + + ipField.requestFocus(); + AndroidUtilities.showKeyboard(ipField); + + return; + + } + + if (StrUtil.isBlank(portField.getText())) { + + portField.requestFocus(); + AndroidUtilities.showKeyboard(portField); + + return; + + } + + if (StrUtil.isBlank(userIdField.getText())) { + + userIdField.requestFocus(); + AndroidUtilities.showKeyboard(userIdField); + + return; + + } + + if (StrUtil.isBlank(alterIdField.getText())) { + + alterIdField.requestFocus(); + AndroidUtilities.showKeyboard(alterIdField); + + return; + + } + + currentBean.setAddress(ipField.getText().toString()); + currentBean.setPort(Utilities.parseInt(portField.getText().toString())); + currentBean.setId(userIdField.getText().toString()); + currentBean.setAlterId(Utilities.parseInt(alterIdField.getText().toString())); + currentBean.setSecurity(securityField.getValueTextView().getText().toString()); + currentBean.setNetwork(networkField.getValueTextView().getText().toString()); + currentBean.setHeaderType(headTypeField.getValueTextView().getText().toString()); + currentBean.setRequestHost(requestHostField.getText().toString()); + currentBean.setPath(pathField.getText().toString()); + currentBean.setStreamSecurity(useTlsField.isChecked() ? "tls" : ""); + currentBean.setRemarks(remarksField.getText().toString()); + + if (currentProxyInfo == null) { + currentProxyInfo = new SharedConfig.VmessProxy(currentBean); + SharedConfig.addProxy(currentProxyInfo); + SharedConfig.setCurrentProxy(currentProxyInfo); + } else { + currentProxyInfo.proxyCheckPingId = 0; + currentProxyInfo.availableCheckTime = 0; + currentProxyInfo.ping = 0; + SharedConfig.saveProxyList(); + SharedConfig.setProxyEnable(false); + } + + finishFragment(); + + } + } + }); + + doneItem = actionBar.createMenu().addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); + doneItem.setContentDescription(LocaleController.getString("Done", R.string.Done)); + + fragmentView = new FrameLayout(context); + FrameLayout frameLayout = (FrameLayout) fragmentView; + fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + + scrollView = new ScrollView(context); + scrollView.setFillViewport(true); + AndroidUtilities.setScrollViewEdgeEffectColor(scrollView, Theme.getColor(Theme.key_actionBarDefault)); + frameLayout.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + linearLayout2 = new LinearLayout(context); + linearLayout2.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + inputFieldsContainer = new LinearLayout(context); + inputFieldsContainer.setOrientation(LinearLayout.VERTICAL); + inputFieldsContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // bring to front for transitions + inputFieldsContainer.setElevation(AndroidUtilities.dp(1f)); + inputFieldsContainer.setOutlineProvider(null); + } + linearLayout2.addView(inputFieldsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + inputFields = new EditTextBoldCursor[7]; + + for (int a = 0; a < 7; a++) { + FrameLayout container = new FrameLayout(context); + EditTextBoldCursor cursor = mkCursor(); + inputFields[a] = cursor; + cursor.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_EXTRACT_UI); + switch (a) { + case 0: + ipField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress)); + cursor.setText(currentBean.getAddress()); + break; + case 1: + portField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_NUMBER); + cursor.setHintText(LocaleController.getString("UseProxyPort", R.string.UseProxyPort)); + cursor.setText("" + currentBean.getPort()); + break; + case 2: + userIdField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("VmessUserId", R.string.VmessUserId)); + cursor.setText(currentBean.getId()); + break; + case 3: + alterIdField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_NUMBER); + cursor.setHintText(LocaleController.getString("VmessAlterId", R.string.VmessAlterId)); + cursor.setText("" + currentBean.getAlterId()); + break; + case 4: + requestHostField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("VmessRequestHost", R.string.VmessRequestHost)); + cursor.setText(currentBean.getRequestHost()); + break; + case 5: + pathField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("VmessPath", R.string.VmessPath)); + cursor.setText(currentBean.getPath()); + break; + case 6: + remarksField = cursor; + cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + cursor.setHintText(LocaleController.getString("ProxyRemarks", R.string.ProxyRemarks)); + cursor.setText(currentBean.getRemarks()); + break; + } + cursor.setSelection(cursor.length()); + + cursor.setPadding(0, 0, 0, 0); + container.addView(cursor, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 17, a == 0 ? 12 : 0, 17, 0)); + + } + + inputFieldsContainer.addView((View) ipField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) portField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) userIdField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) alterIdField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + FrameLayout container = new FrameLayout(context); + + securityField = new TextSettingsCell(context); + securityField.setBackground(Theme.getSelectorDrawable(false)); + securityField.setTextAndValue(LocaleController.getString("VmessSecurity", R.string.VmessSecurity), currentBean.getSecurity(), false); + container.addView(securityField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + + securityField.setOnClickListener((v) -> { + + PopupBuilder select = new PopupBuilder(v); + + select.setItems(securitySet, (__,value) -> { + + securityField.getValueTextView().setText(value); + + return Unit.INSTANCE; + + }); + + select.show(); + + }); + + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + container = new FrameLayout(context); + networkField = new TextSettingsCell(context); + networkField.setBackground(Theme.getSelectorDrawable(false)); + networkField.setTextAndValue(LocaleController.getString("VmessNetwork", R.string.VmessNetwork), currentBean.getNetwork(), false); + container.addView(networkField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + + networkField.setOnClickListener((v) -> { + + PopupBuilder select = new PopupBuilder(v); + + select.setItems(networkSet, (__,value) -> { + + networkField.getValueTextView().setText(value); + + return Unit.INSTANCE; + + }); + + select.show(); + + }); + + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + container = new FrameLayout(context); + headTypeField = new TextSettingsCell(context); + headTypeField.setBackground(Theme.getSelectorDrawable(false)); + headTypeField.setTextAndValue(LocaleController.getString("VmessHeadType", R.string.VmessHeadType), currentBean.getHeaderType(), false); + container.addView(headTypeField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + + headTypeField.setOnClickListener((v) -> { + + PopupBuilder select = new PopupBuilder(v); + + select.setItems(headTypeSet, (__,value) -> { + + headTypeField.getValueTextView().setText(value); + + return Unit.INSTANCE; + + }); + + select.show(); + + }); + + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + inputFieldsContainer.addView((View) requestHostField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + inputFieldsContainer.addView((View) pathField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + container = new FrameLayout(context); + useTlsField = new TextCheckCell(context); + useTlsField.setBackground(Theme.getSelectorDrawable(false)); + useTlsField.setTextAndCheck(LocaleController.getString("VmessTls", R.string.VmessTls), !StrUtil.isBlank(currentBean.getStreamSecurity()), false); + container.addView(useTlsField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0)); + + useTlsField.setOnClickListener((v) -> useTlsField.setChecked(!useTlsField.isChecked())); + + inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + inputFieldsContainer.addView((View) remarksField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64)); + + bottomCell = new TextInfoPrivacyCell(context); + bottomCell.setBackground(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + bottomCell.setText(LocaleController.getString("ProxyInfoVmess", R.string.ProxyInfoVmess)); + linearLayout2.addView(bottomCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + + return fragmentView; + + } + + EditTextBoldCursor mkCursor() { + + EditTextBoldCursor cursor = new EditTextBoldCursor(getParentActivity()); + cursor.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + cursor.setHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); + cursor.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setBackground(null); + cursor.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); + cursor.setCursorSize(AndroidUtilities.dp(20)); + cursor.setCursorWidth(1.5f); + cursor.setSingleLine(true); + cursor.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); + cursor.setHeaderHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader)); + cursor.setTransformHintToHeader(true); + cursor.setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + return cursor; + + } + + @Override + protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { + if (isOpen && !backward && currentProxyInfo == null) { + ipField.requestFocus(); + AndroidUtilities.showKeyboard(ipField); + } + } + + @Override + public ArrayList getThemeDescriptions() { + final ThemeDescription.ThemeDescriptionDelegate delegate = () -> { + if (inputFields != null) { + for (int i = 0; i < inputFields.length; i++) { + inputFields[i].setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), + Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), + Theme.getColor(Theme.key_windowBackgroundWhiteRedText3)); + } + } + }; + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(scrollView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_actionBarDefaultSearch)); + arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_actionBarDefaultSearchPlaceholder)); + arrayList.add(new ThemeDescription(inputFieldsContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); + + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteBlueText4)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteGrayText2)); + + + if (inputFields != null) { + for (int a = 0; a < inputFields.length; a++) { + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR | ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); + arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_CURSORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputField)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputFieldActivated)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteRedText3)); + } + } else { + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText)); + } + + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); + arrayList.add(new ThemeDescription(bottomCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4)); + arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_LINKCOLOR, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText)); + + return arrayList; + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/parts/ProxyChecks.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/parts/ProxyChecks.kt new file mode 100644 index 000000000..9f8541b19 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/parts/ProxyChecks.kt @@ -0,0 +1,67 @@ +package tw.nekomimi.nekogram.parts + +import android.os.SystemClock +import cn.hutool.core.thread.ThreadUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.telegram.messenger.AndroidUtilities +import org.telegram.messenger.NotificationCenter +import org.telegram.messenger.SharedConfig +import org.telegram.messenger.SharedConfig.ExternalSocks5Proxy +import org.telegram.ui.ProxyListActivity +import java.util.concurrent.ExecutorService +import java.util.concurrent.atomic.AtomicBoolean + +@JvmOverloads +fun ProxyListActivity.checkProxyList(force: Boolean, context: ExecutorService) { + + GlobalScope.launch(Dispatchers.IO) { + + SharedConfig.proxyList.toList().forEach { + + if (it.checking || SystemClock.elapsedRealtime() - it.availableCheckTime < 2 * 60 * 1000L && !force) { + + return@forEach + + } + + it.checking = true + + runCatching { + + context.execute { + + runCatching { + + val lock = AtomicBoolean() + + checkSingleProxy(it, if (it is ExternalSocks5Proxy) 3 else 0) { + + AndroidUtilities.runOnUIThread { + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxyCheckDone, it) + + } + + lock.set(true) + + } + + while (!lock.get()) ThreadUtil.sleep(100L) + + } + + } + + }.onFailure { + + return@launch + + } + + } + + } + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/sub/SubInfo.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/sub/SubInfo.java new file mode 100644 index 000000000..c71562ad9 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/sub/SubInfo.java @@ -0,0 +1,153 @@ +package tw.nekomimi.nekogram.sub; + +import androidx.annotation.NonNull; + +import org.dizitart.no2.Document; +import org.dizitart.no2.NitriteId; +import org.dizitart.no2.mapper.Mappable; +import org.dizitart.no2.mapper.NitriteMapper; +import org.dizitart.no2.objects.Id; +import org.dizitart.no2.objects.Index; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.xml.transform.ErrorListener; + +import cn.hutool.core.util.StrUtil; +import tw.nekomimi.nekogram.RelayBatonLoader; +import tw.nekomimi.nekogram.utils.HttpUtil; +import tw.nekomimi.nekogram.utils.ProxyUtil; + +@Index("id") +@SuppressWarnings("unchecked") +public class SubInfo implements Mappable { + + @Id + public long id; + public String name; + public List urls = new LinkedList<>(); + public List proxies = new LinkedList<>(); + public Long lastFetch = -1L; + public boolean enable = true; + public boolean internal; + + public String displayName() { + + if (id == 1) return LocaleController.getString("PublicPrefix", R.string.PublicPrefix); + + if (name.length() < 10) return name; + + return name.substring(0,10) + "..."; + + } + + public List reloadProxies() throws AllTriesFailed { + + HashMap exceptions = new HashMap<>(); + + for (String url : urls) { + + try { + + String source = HttpUtil.get(url); + + return ProxyUtil.parseProxies(source); + + } catch (Exception e) { + + exceptions.put(url,e); + + } + + } + + throw new AllTriesFailed(exceptions); + + } + + public static class AllTriesFailed extends IOException { + + public AllTriesFailed(HashMap exceptions) { + this.exceptions = exceptions; + } + + public HashMap exceptions; + + @NonNull @Override public String toString() { + + StringBuilder errors = new StringBuilder(); + + for (Map.Entry e : exceptions.entrySet()) { + + errors.append(e.getKey()).append(": "); + + errors.append(e.getValue().getClass().getSimpleName()); + + if (!StrUtil.isBlank(e.getValue().getMessage())) { + + errors.append(" ( "); + errors.append(e.getValue().getMessage()); + errors.append(" )"); + + } + + errors.append("\n\n"); + + } + + return errors.toString(); + + } + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SubInfo subInfo = (SubInfo) o; + return id == subInfo.id; + } + + @Override + public Document write(NitriteMapper mapper) { + + Document document = new Document(); + + document.put("id", id); + document.put("name", name); + document.put("urls", urls); + document.put("proxies",proxies); + document.put("lastFetch", lastFetch); + document.put("enable", enable); + document.put("internal", internal); + + return document; + } + + @Override + public void read(NitriteMapper mapper, Document document) { + + id = document.get("id", Long.class); + name = document.get("name", String.class); + urls = (List) document.get("urls"); + proxies = (List) document.get("proxies"); + + lastFetch = document.get("lastFetch",Long.class); + enable = document.get("enable",Boolean.class); + internal = document.get("internal",Boolean.class); + + } + + + +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/sub/SubManager.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/sub/SubManager.kt new file mode 100644 index 000000000..ab1e981b2 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/sub/SubManager.kt @@ -0,0 +1,43 @@ +package tw.nekomimi.nekogram.sub + +import org.dizitart.no2.objects.filters.ObjectFilters +import org.telegram.messenger.LocaleController +import org.telegram.messenger.R +import tw.nekomimi.nekogram.database.mkDatabase + +object SubManager { + + val database by lazy { mkDatabase("proxy_sub") } + + @JvmStatic val count get() = subList.find().totalCount() + + @JvmStatic + val subList by lazy { + + database.getRepository("proxy_sub", SubInfo::class.java).apply { + + val public = find(ObjectFilters.eq("id", 1L)).firstOrDefault() + + update(SubInfo().apply { + + name = LocaleController.getString("NekoXProxy", R.string.NekoXProxy) + enable = public?.enable ?: true + + urls = listOf( + "https://gitlab.com/nekohasekai/nekox-proxy-list/-/raw/master/proxy_list", + "https://nekox-dev.github.io/ProxyList/proxy_list", + "https://gitee.com/nekoshizuku/AwesomeRepo/raw/master/proxy_list" + ) + + id = 1L + internal = true + + proxies = public?.proxies ?: listOf() + + }, true) + + } + + } + +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/utils/ProxyUtil.kt b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/utils/ProxyUtil.kt new file mode 100644 index 000000000..ea02abfd5 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/utils/ProxyUtil.kt @@ -0,0 +1,482 @@ +package tw.nekomimi.nekogram.utils + +import android.Manifest +import android.app.Activity +import android.content.ClipboardManager +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.os.Build +import android.os.Environment +import android.util.Base64 +import android.view.Gravity +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.Toast +import com.google.zxing.* +import com.google.zxing.common.GlobalHistogramBinarizer +import com.google.zxing.qrcode.QRCodeReader +import com.google.zxing.qrcode.QRCodeWriter +import com.v2ray.ang.V2RayConfig.RB_PROTOCOL +import com.v2ray.ang.V2RayConfig.SSR_PROTOCOL +import com.v2ray.ang.V2RayConfig.SS_PROTOCOL +import com.v2ray.ang.V2RayConfig.VMESS1_PROTOCOL +import com.v2ray.ang.V2RayConfig.VMESS_PROTOCOL +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.telegram.messenger.* +import org.telegram.messenger.browser.Browser +import org.telegram.ui.ActionBar.BottomSheet +import java.io.File +import java.net.NetworkInterface +import java.util.* +import kotlin.collections.HashMap + +object ProxyUtil { + + @JvmStatic + fun isVPNEnabled(): Boolean { + + val networkList = mutableListOf() + + runCatching { + + Collections.list(NetworkInterface.getNetworkInterfaces()).forEach { + + if (it.isUp) networkList.add(it.name) + + } + + } + + return networkList.contains("tun0") + + } + + @JvmStatic + fun parseProxies(_text: String): MutableList { + + val text = runCatching { + + String(Base64.decode(_text, Base64.NO_PADDING)) + + }.recover { + + _text + + }.getOrThrow() + + val proxies = mutableListOf() + + text.split('\n').map { it.split(" ") }.forEach { + + it.forEach { line -> + + if (line.startsWith("tg://proxy") || + line.startsWith("tg://socks") || + line.startsWith("https://t.me/proxy") || + line.startsWith("https://t.me/socks") || + line.startsWith(VMESS_PROTOCOL) || + line.startsWith(VMESS1_PROTOCOL) || + line.startsWith(SS_PROTOCOL) || + line.startsWith(SSR_PROTOCOL) /*|| + line.startsWith(RB_PROTOCOL)*/) { + + runCatching { proxies.add(SharedConfig.parseProxyInfo(line).toUrl()) } + + } + + } + + } + + if (proxies.isEmpty()) error("no proxy link found") + + return proxies + + } + + @JvmStatic + fun importFromClipboard() { + + var text = (ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).primaryClip?.getItemAt(0)?.text?.toString() + + if (text != null) { + + runCatching { + + text = String(Base64.decode(text, Base64.NO_PADDING)) + + } + + } + + val proxies = mutableListOf() + + var error = false + + text?.split('\n')?.map { it.split(" ") }?.forEach { + + it.forEach { line -> + + if (line.startsWith("tg://proxy") || + line.startsWith("tg://socks") || + line.startsWith("https://t.me/proxy") || + line.startsWith("https://t.me/socks") || + line.startsWith(VMESS_PROTOCOL) || + line.startsWith(VMESS1_PROTOCOL) || + line.startsWith(SS_PROTOCOL) || + line.startsWith(SSR_PROTOCOL) /*|| + line.startsWith(RB_PROTOCOL)*/) { + + runCatching { proxies.add(SharedConfig.parseProxyInfo(line)) }.onFailure { + + AlertUtil.showToast(LocaleController.getString("BrokenLink", R.string.BrokenLink) + ": ${it.message ?: it.javaClass.simpleName}") + + } + + } + + } + + } + + if (proxies.isNullOrEmpty()) { + + if (!error) AlertUtil.showToast(LocaleController.getString("BrokenLink", R.string.BrokenLink)) + + return + + } + + proxies.forEach { + + SharedConfig.addProxy(it) + + } + + UIUtil.runOnUIThread { + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged) + + } + + } + + @JvmStatic + fun importProxy(ctx: Context, link: String): Boolean { + + runCatching { + + if (link.startsWith(VMESS_PROTOCOL) || link.startsWith(VMESS1_PROTOCOL)) { + + AndroidUtilities.showVmessAlert(ctx, SharedConfig.VmessProxy(link)) + + } else if (link.startsWith(SS_PROTOCOL)) { + + AndroidUtilities.showShadowsocksAlert(ctx, SharedConfig.ShadowsocksProxy(link)) + + } else if (link.startsWith(SSR_PROTOCOL)) { + + AndroidUtilities.showShadowsocksRAlert(ctx, SharedConfig.ShadowsocksRProxy(link)) + + } else if (link.startsWith(RB_PROTOCOL)) { + + AndroidUtilities.showRelayBatonAlert(ctx, SharedConfig.RelayBatonProxy(link)) + + } else { + + val url = link.toHttpUrlOrNull()!! + + AndroidUtilities.showProxyAlert(ctx, + url.queryParameter("server"), + url.queryParameter("port"), + url.queryParameter("user"), + url.queryParameter("pass"), + url.queryParameter("secret"), + url.fragment) + + + } + + return true + + }.onFailure { + + FileLog.w("$it") + + AlertUtil.showToast("${LocaleController.getString("BrokenLink", R.string.BrokenLink)}: ${it.message}") + + } + + return false + + } + + @JvmStatic + fun importInBackground(link: String): SharedConfig.ProxyInfo { + + val info = runCatching { + + if (link.startsWith(VMESS_PROTOCOL) || link.startsWith(VMESS1_PROTOCOL)) { + + SharedConfig.VmessProxy(link) + + } else if (link.startsWith(SS_PROTOCOL)) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + + error(LocaleController.getString("MinApi21Required", R.string.MinApi21Required)) + + } + + SharedConfig.ShadowsocksProxy(link) + + } else if (link.startsWith(SSR_PROTOCOL)) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + + error(LocaleController.getString("MinApi21Required", R.string.MinApi21Required)) + + } + + SharedConfig.ShadowsocksRProxy(link) + + } else { + + SharedConfig.ProxyInfo.fromUrl(link) + + } + + }.getOrThrow() + + if (!(SharedConfig.addProxy(info) === info)) { + + error("already exists") + + } + + return info + + } + + @JvmStatic + fun shareProxy(ctx: Activity, info: SharedConfig.ProxyInfo, type: Int) { + + val url = info.toUrl(); + + if (type == 1) { + + AndroidUtilities.addToClipboard(url) + + Toast.makeText(ctx, LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_LONG).show() + + } else if (type == 0) { + + val shareIntent = Intent(Intent.ACTION_SEND) + + shareIntent.type = "text/plain" + + shareIntent.putExtra(Intent.EXTRA_TEXT, url) + + val chooserIntent = Intent.createChooser(shareIntent, LocaleController.getString("ShareLink", R.string.ShareLink)) + + chooserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + ctx.startActivity(chooserIntent) + + } else { + + showQrDialog(ctx, url) + + } + + } + + @JvmStatic + fun getOwnerActivity(ctx: Context): Activity { + + if (ctx is Activity) return ctx + + if (ctx is ContextWrapper) return getOwnerActivity(ctx.baseContext) + + error("unable cast ${ctx.javaClass.name} to activity") + + } + + @JvmStatic + fun showQrDialog(ctx: Context, text: String) { + + val code = createQRCode(text) + + ctx.setTheme(R.style.Theme_TMessages) + + android.app.AlertDialog.Builder(ctx).setView(LinearLayout(ctx).apply { + + addView(LinearLayout(ctx).apply { + + gravity = Gravity.CENTER + + val width = AndroidUtilities.dp(330f) + + addView(ImageView(ctx).apply { + + setImageBitmap(code) + + scaleType = ImageView.ScaleType.FIT_XY + + setOnLongClickListener { + + BottomSheet.Builder(ctx).setItems(arrayOf( + + LocaleController.getString("SaveToGallery", R.string.SaveToGallery), + LocaleController.getString("Cancel", R.string.Cancel) + + ), intArrayOf( + + R.drawable.baseline_image_24, + R.drawable.baseline_cancel_24 + + )) { _, i -> + + if (i == 0) { + + if (Build.VERSION.SDK_INT >= 23 && ctx.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + + getOwnerActivity(ctx).requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 4) + + return@setItems + + } + + val saveTo = File(Environment.getExternalStorageDirectory(), "${Environment.DIRECTORY_PICTURES}/share_${text.hashCode()}.jpg") + + saveTo.parentFile?.mkdirs() + + runCatching { + + saveTo.createNewFile() + + saveTo.outputStream().use { + + code?.compress(Bitmap.CompressFormat.JPEG, 100, it); + + } + + AndroidUtilities.addMediaToGallery(saveTo.path) + + } + + } + + }.show() + + return@setOnLongClickListener true + + } + + }, LinearLayout.LayoutParams(width, width)) + + }, LinearLayout.LayoutParams(-1, -1).apply { + + gravity = Gravity.CENTER + + }) + + }).show() + + } + + fun createQRCode(text: String, size: Int = 800): Bitmap? { + try { + val hints = HashMap() + hints[EncodeHintType.CHARACTER_SET] = "utf-8" + //hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H + val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints) + val pixels = IntArray(size * size) + for (y in 0 until size) { + for (x in 0 until size) { + if (bitMatrix.get(x, y)) { + pixels[y * size + x] = 0xff000000.toInt() + } else { + pixels[y * size + x] = 0xffffffff.toInt() + } + } + } + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + bitmap.setPixels(pixels, 0, size, 0, 0, size, size) + return bitmap + } catch (e: WriterException) { + e.printStackTrace() + return null + } + } + + val qrReader = QRCodeReader() + + @JvmStatic + fun tryReadQR(ctx: Activity, bitmap: Bitmap) { + + val intArray = IntArray(bitmap.getWidth() * bitmap.getHeight()) + bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()) + val source = RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray) + + try { + + val result = qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source))) + + if (result == null || result.text.isBlank()) { + + AlertUtil.showToast(LocaleController.getString("NoQrFound", R.string.NoQrFound)) + + } else { + + showLinkAlert(ctx, result.text) + + } + + } catch (ex: NoSuchMethodError) { + + AlertUtil.showSimpleAlert(ctx, "很抱歉, 這是一個已知的問題, 但您現在無法掃碼, 因爲您正在使用糟糕的Android系統, 直到 Google Zxing 為您的設備做出優化.") + + } + + } + + @JvmStatic + fun showLinkAlert(ctx: Activity, text: String) { + + val builder = BottomSheet.Builder(ctx) + + var isUrl = false + + runCatching { + text.toHttpUrlOrNull()!! + if (Browser.isInternalUrl(text, booleanArrayOf(false))) { + Browser.openUrl(ctx, text) + return + } + isUrl = true + } + + builder.setTitle(text) + + builder.setItems(arrayOf( + if (isUrl) LocaleController.getString("Open", R.string.OpenUrlTitle) else null, + LocaleController.getString("Copy", R.string.Copy), + LocaleController.getString("Cancel", R.string.Cancel) + )) { _, i -> + if (i == 0) { + Browser.openUrl(ctx, text) + } else if (i == 1) { + AndroidUtilities.addToClipboard(text) + Toast.makeText(ApplicationLoader.applicationContext, LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_LONG).show() + } + } + + builder.show() + + } + +}