This commit is contained in:
世界 2020-06-25 15:28:55 +00:00
parent 1cec9e7b36
commit 8727ccbd79
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
44 changed files with 9475 additions and 784 deletions

View File

@ -0,0 +1,33 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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!!
}

View File

@ -0,0 +1,103 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
* android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider"&gt;
* &lt;intent-filter&gt;
* &lt;category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" /&gt;
* &lt;/intent-filter&gt;
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*/
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<out String>?, selection: String?, selectionArgs: Array<out String>?,
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 <manifest>
* - android:extractNativeLibs="true" for <application>
*
* 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<out String>?): Int =
throw UnsupportedOperationException()
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
throw UnsupportedOperationException()
}

View File

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

View File

@ -0,0 +1,56 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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
}
}

View File

@ -0,0 +1,41 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.plugin
import android.graphics.drawable.Drawable
abstract class Plugin {
abstract val id: String
open val idAliases get() = emptyArray<String>()
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()
}

View File

@ -0,0 +1,73 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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<String, PluginOptions>, var selected: String) {
private constructor(plugins: List<PluginOptions>) : 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<PluginOptions>()
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) }
}
}

View File

@ -0,0 +1,142 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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=<http></http>|tls> Enable obfuscating: HTTP or TLS (Experimental).
* obfs-host=<host_name> Hostname for obfuscating (Experimental)."
*
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
</host_name> */
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 <manifest>
* - android:extractNativeLibs="true" for <application>
*
* 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"
}

View File

@ -0,0 +1,52 @@
/*******************************************************************************
* *
* Copyright (C) 2020 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2020 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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<Plugin>() {
init {
add(NoPlugin)
addAll(ApplicationLoader.applicationContext.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA).map { NativePlugin(it) })
}
val lookup = mutableMapOf<String, Plugin>().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()
}

View File

@ -0,0 +1,183 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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<String, String>) = 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<String, PluginOptions>? {
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<String, PluginOptions>? {
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}")
}
}

View File

@ -0,0 +1,111 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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<String, String?> {
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)
}

View File

@ -0,0 +1,55 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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<String> 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
}

View File

@ -0,0 +1,159 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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.
*
*
* `
* <someelement><br></br>
* &nbsp;&nbsp;<acommandline executable="/executable/to/run"><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument value="argument 1" /><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument line="argument_1 argument_2 argument_3" /><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument value="argument 4" /><br></br>
* &nbsp;&nbsp;</acommandline><br></br>
* </someelement><br></br>
` *
*
* 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>?): 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<String>) = 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<String> {
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<String>()
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()
}
}

View File

@ -0,0 +1,136 @@
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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 <T> Iterable<T>.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 <T> 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
}

View File

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

View File

@ -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<VmessBean>,
var subItem: ArrayList<SubItemBean>
) {
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 = "")
}

View File

@ -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<InboundBean>,
var outbounds: ArrayList<OutboundBean>,
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<String>)
}
data class OutboundBean(val tag: String,
var protocol: String,
var settings: OutSettingsBean?,
var streamSettings: StreamSettingsBean?,
var mux: MuxBean?) {
data class OutSettingsBean(var vnext: List<VnextBean>?,
var servers: List<ServersBean>?,
var response: Response) {
data class VnextBean(var address: String,
var port: Int,
var users: List<UsersBean>) {
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<String> = 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<String>)
data class DnsBean(var servers: List<Any>?=null,
var hosts: Map<String, String>?=null
) {
data class ServersBean(var address: String = "",
var port: Int = 0,
var domains: List<String>?)
}
data class RoutingBean(var domainStrategy: String,
var rules: ArrayList<RulesBean>) {
data class RulesBean(var type: String = "",
var ip: ArrayList<String>? = null,
var domain: ArrayList<String>? = null,
var outboundTag: String = "",
var port: String? = null,
var inboundTag: ArrayList<String>? = null)
}
data class PolicyBean(var levels: Map<String, LevelBean>,
var system: Any?=null) {
data class LevelBean(
var handshake: Int? = null,
var connIdle: Int? = null,
var uplinkOnly: Int? = null,
var downlinkOnly: Int? = null)
}
}

View File

@ -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 = "")

View File

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

View File

@ -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<String>()
// vmess.requestHost
// .split(",")
// .forEach {
// arrHost.add(it)
// }
// requestObj.optJSONObject("headers")
// .put("Host", arrHost)
//
// }
if (!TextUtils.isEmpty(vmess.requestHost)) {
val arrHost = ArrayList<String>()
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<String>()
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 ""
}
}

View File

@ -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 {

View File

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

View File

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

View File

@ -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"));

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<Void, Void, String[]> {

View File

@ -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<String, MessageObject> waitingForLocation = (HashMap<String, MessageObject>) 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;

View File

@ -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<ThemeDescription> 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));

View File

@ -0,0 +1,118 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
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<String>) {
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<Int>()
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<String>, 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() } }
}
}

View File

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

View File

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

View File

@ -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<ThemeDescription> 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<ThemeDescription> 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;
}
}

View File

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

View File

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

View File

@ -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<ThemeDescription> 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<ThemeDescription> 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;
}
}

View File

@ -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<ThemeDescription> 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<ThemeDescription> 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;
}
}

View File

@ -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<ThemeDescription> 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<ThemeDescription> 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;
}
}

View File

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

View File

@ -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<ThemeDescription> 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<ThemeDescription> 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;
}
}

View File

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

View File

@ -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<String> urls = new LinkedList<>();
public List<String> 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<String> reloadProxies() throws AllTriesFailed {
HashMap<String,Exception> 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<String,Exception> exceptions) {
this.exceptions = exceptions;
}
public HashMap<String,Exception> exceptions;
@NonNull @Override public String toString() {
StringBuilder errors = new StringBuilder();
for (Map.Entry<String, Exception> 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<String>) document.get("urls");
proxies = (List<String>) document.get("proxies");
lastFetch = document.get("lastFetch",Long.class);
enable = document.get("enable",Boolean.class);
internal = document.get("internal",Boolean.class);
}
}

View File

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

View File

@ -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<String>()
runCatching {
Collections.list(NetworkInterface.getNetworkInterfaces()).forEach {
if (it.isUp) networkList.add(it.name)
}
}
return networkList.contains("tun0")
}
@JvmStatic
fun parseProxies(_text: String): MutableList<String> {
val text = runCatching {
String(Base64.decode(_text, Base64.NO_PADDING))
}.recover {
_text
}.getOrThrow()
val proxies = mutableListOf<String>()
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<SharedConfig.ProxyInfo>()
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<EncodeHintType, Any>()
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()
}
}