diff --git a/.gitignore b/.gitignore index 42a354f..139f38e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,13 @@ build/ .idea/ *.swp *.swo +.gradle/ +.gradle-local/ +android/.gradle-local/ +local.properties +android/.idea/ +android/build/ +android/app/build/ # OS Thumbs.db diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..3929a27 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "org.flowseal.tgwsproxy" + compileSdk = 34 + + defaultConfig { + applicationId = "org.flowseal.tgwsproxy" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "0.1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.activity:activity-ktx:1.9.2") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6") + implementation("androidx.lifecycle:lifecycle-service:2.8.6") + implementation("com.google.android.material:material:1.12.0") + + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..dc67027 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +# Intentionally empty for the initial Android shell. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..132b7ef --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt new file mode 100644 index 0000000..b017348 --- /dev/null +++ b/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt @@ -0,0 +1,132 @@ +package org.flowseal.tgwsproxy + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.launch +import org.flowseal.tgwsproxy.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private lateinit var settingsStore: ProxySettingsStore + + private val notificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { granted -> + if (!granted) { + Toast.makeText( + this, + "Без уведомлений Android может скрыть foreground service.", + Toast.LENGTH_LONG, + ).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + settingsStore = ProxySettingsStore(this) + setContentView(binding.root) + + binding.startButton.setOnClickListener { onStartClicked() } + binding.stopButton.setOnClickListener { ProxyForegroundService.stop(this) } + binding.saveButton.setOnClickListener { onSaveClicked(showMessage = true) } + + renderConfig(settingsStore.load()) + requestNotificationPermissionIfNeeded() + observeServiceState() + } + + private fun onSaveClicked(showMessage: Boolean): NormalizedProxyConfig? { + val validation = collectConfigFromForm().validate() + val config = validation.normalized + if (config == null) { + binding.errorText.text = validation.errorMessage + binding.errorText.isVisible = true + return null + } + + binding.errorText.isVisible = false + settingsStore.save(config) + if (showMessage) { + Snackbar.make(binding.root, R.string.settings_saved, Snackbar.LENGTH_SHORT).show() + } + return config + } + + private fun onStartClicked() { + onSaveClicked(showMessage = false) ?: return + ProxyForegroundService.start(this) + Snackbar.make(binding.root, R.string.service_start_requested, Snackbar.LENGTH_SHORT).show() + } + + private fun renderConfig(config: ProxyConfig) { + binding.hostInput.setText(config.host) + binding.portInput.setText(config.portText) + binding.dcIpInput.setText(config.dcIpText) + binding.verboseSwitch.isChecked = config.verbose + } + + private fun collectConfigFromForm(): ProxyConfig { + return ProxyConfig( + host = binding.hostInput.text?.toString().orEmpty(), + portText = binding.portInput.text?.toString().orEmpty(), + dcIpText = binding.dcIpInput.text?.toString().orEmpty(), + verbose = binding.verboseSwitch.isChecked, + ) + } + + private fun observeServiceState() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + ProxyServiceState.isRunning.collect { isRunning -> + binding.statusValue.text = getString( + if (isRunning) R.string.status_running else R.string.status_stopped, + ) + binding.startButton.isEnabled = !isRunning + binding.stopButton.isEnabled = isRunning + } + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + ProxyServiceState.activeConfig.collect { config -> + binding.serviceHint.text = if (config == null) { + getString(R.string.service_hint_idle) + } else { + getString( + R.string.service_hint_running, + config.host, + config.port, + ) + } + } + } + } + } + + private fun requestNotificationPermissionIfNeeded() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return + } + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + ) { + return + } + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } +} diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyConfig.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyConfig.kt new file mode 100644 index 0000000..a8ffcde --- /dev/null +++ b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyConfig.kt @@ -0,0 +1,84 @@ +package org.flowseal.tgwsproxy + +data class ProxyConfig( + val host: String = DEFAULT_HOST, + val portText: String = DEFAULT_PORT.toString(), + val dcIpText: String = DEFAULT_DC_IP_LINES.joinToString("\n"), + val verbose: Boolean = false, +) { + fun validate(): ValidationResult { + val hostValue = host.trim() + if (!isIpv4Address(hostValue)) { + return ValidationResult(errorMessage = "IP-адрес прокси указан некорректно.") + } + + val portValue = portText.trim().toIntOrNull() + ?: return ValidationResult(errorMessage = "Порт должен быть числом.") + if (portValue !in 1..65535) { + return ValidationResult(errorMessage = "Порт должен быть в диапазоне 1-65535.") + } + + val lines = dcIpText + .lineSequence() + .map { it.trim() } + .filter { it.isNotEmpty() } + .toList() + + if (lines.isEmpty()) { + return ValidationResult(errorMessage = "Добавьте хотя бы один DC:IP маппинг.") + } + + for (line in lines) { + val parts = line.split(":", limit = 2) + val dcValue = parts.firstOrNull()?.toIntOrNull() + val ipValue = parts.getOrNull(1)?.trim().orEmpty() + if (parts.size != 2 || dcValue == null || !isIpv4Address(ipValue)) { + return ValidationResult(errorMessage = "Строка \"$line\" должна быть в формате DC:IP.") + } + } + + return ValidationResult( + normalized = NormalizedProxyConfig( + host = hostValue, + port = portValue, + dcIpList = lines, + verbose = verbose, + ) + ) + } + + companion object { + const val DEFAULT_HOST = "127.0.0.1" + const val DEFAULT_PORT = 1080 + val DEFAULT_DC_IP_LINES = listOf( + "2:149.154.167.220", + "4:149.154.167.220", + ) + + private fun isIpv4Address(value: String): Boolean { + val octets = value.split(".") + if (octets.size != 4) { + return false + } + + return octets.all { octet -> + octet.isNotEmpty() && + octet.length <= 3 && + octet.all(Char::isDigit) && + octet.toIntOrNull() in 0..255 + } + } + } +} + +data class ValidationResult( + val normalized: NormalizedProxyConfig? = null, + val errorMessage: String? = null, +) + +data class NormalizedProxyConfig( + val host: String, + val port: Int, + val dcIpList: List, + val verbose: Boolean, +) diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyForegroundService.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyForegroundService.kt new file mode 100644 index 0000000..fa71c27 --- /dev/null +++ b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyForegroundService.kt @@ -0,0 +1,98 @@ +package org.flowseal.tgwsproxy + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat + +class ProxyForegroundService : Service() { + private lateinit var settingsStore: ProxySettingsStore + + override fun onCreate() { + super.onCreate() + settingsStore = ProxySettingsStore(this) + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return when (intent?.action) { + ACTION_STOP -> { + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + START_NOT_STICKY + } + + else -> { + val config = settingsStore.load().validate().normalized + if (config == null) { + stopSelf() + START_NOT_STICKY + } else { + ProxyServiceState.markStarted(config) + startForeground(NOTIFICATION_ID, buildNotification(config)) + START_STICKY + } + } + } + } + + override fun onDestroy() { + ProxyServiceState.markStopped() + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun buildNotification(config: NormalizedProxyConfig): Notification { + val contentText = "SOCKS5 ${config.host}:${config.port} • service shell active" + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.notification_title)) + .setContentText(contentText) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setOngoing(true) + .setOnlyAlertOnce(true) + .build() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + + val manager = getSystemService(NotificationManager::class.java) + val channel = NotificationChannel( + CHANNEL_ID, + getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_LOW, + ).apply { + description = getString(R.string.notification_channel_description) + } + manager.createNotificationChannel(channel) + } + + companion object { + private const val CHANNEL_ID = "proxy_service" + private const val NOTIFICATION_ID = 1001 + private const val ACTION_START = "org.flowseal.tgwsproxy.action.START" + private const val ACTION_STOP = "org.flowseal.tgwsproxy.action.STOP" + + fun start(context: Context) { + val intent = Intent(context, ProxyForegroundService::class.java).apply { + action = ACTION_START + } + androidx.core.content.ContextCompat.startForegroundService(context, intent) + } + + fun stop(context: Context) { + val intent = Intent(context, ProxyForegroundService::class.java).apply { + action = ACTION_STOP + } + context.startService(intent) + } + } +} diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyServiceState.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyServiceState.kt new file mode 100644 index 0000000..1488c9c --- /dev/null +++ b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyServiceState.kt @@ -0,0 +1,22 @@ +package org.flowseal.tgwsproxy + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +object ProxyServiceState { + private val _isRunning = MutableStateFlow(false) + val isRunning: StateFlow = _isRunning + + private val _activeConfig = MutableStateFlow(null) + val activeConfig: StateFlow = _activeConfig + + fun markStarted(config: NormalizedProxyConfig) { + _activeConfig.value = config + _isRunning.value = true + } + + fun markStopped() { + _activeConfig.value = null + _isRunning.value = false + } +} diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxySettingsStore.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxySettingsStore.kt new file mode 100644 index 0000000..e93249d --- /dev/null +++ b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxySettingsStore.kt @@ -0,0 +1,36 @@ +package org.flowseal.tgwsproxy + +import android.content.Context + +class ProxySettingsStore(context: Context) { + private val preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + fun load(): ProxyConfig { + return ProxyConfig( + host = preferences.getString(KEY_HOST, ProxyConfig.DEFAULT_HOST).orEmpty(), + portText = preferences.getInt(KEY_PORT, ProxyConfig.DEFAULT_PORT).toString(), + dcIpText = preferences.getString( + KEY_DC_IP_TEXT, + ProxyConfig.DEFAULT_DC_IP_LINES.joinToString("\n"), + ).orEmpty(), + verbose = preferences.getBoolean(KEY_VERBOSE, false), + ) + } + + fun save(config: NormalizedProxyConfig) { + preferences.edit() + .putString(KEY_HOST, config.host) + .putInt(KEY_PORT, config.port) + .putString(KEY_DC_IP_TEXT, config.dcIpList.joinToString("\n")) + .putBoolean(KEY_VERBOSE, config.verbose) + .apply() + } + + companion object { + private const val PREFS_NAME = "proxy_settings" + private const val KEY_HOST = "host" + private const val KEY_PORT = "port" + private const val KEY_DC_IP_TEXT = "dc_ip_text" + private const val KEY_VERBOSE = "verbose" + } +} diff --git a/android/app/src/main/res/drawable/ic_proxy_app.xml b/android/app/src/main/res/drawable/ic_proxy_app.xml new file mode 100644 index 0000000..3608bba --- /dev/null +++ b/android/app/src/main/res/drawable/ic_proxy_app.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..04e35a2 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..4317b48 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #1E88E5 + #0B1F33 + #F4F8FC + #FFFFFF + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d124aa2 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + TG WS Proxy + Android shell for the local Telegram SOCKS5 proxy. The embedded Python runtime will be wired in the next commit. + Foreground service + Running + Stopped + The Android service shell is ready. Save settings, then start the service. + Foreground service active for %1$s:%2$d. Python proxy bootstrap will be connected next. + Proxy host + Proxy port + DC to IP mappings (one DC:IP per line) + Verbose logging + Save Settings + Start Service + Stop Service + Settings saved + Foreground service start requested + TG WS Proxy + Proxy service + Keeps the Telegram proxy service alive in the foreground. + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..48198c7 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,11 @@ + + + + diff --git a/android/build-local-debug.sh b/android/build-local-debug.sh new file mode 100644 index 0000000..139e58c --- /dev/null +++ b/android/build-local-debug.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [[ -z "${GRADLE_USER_HOME:-}" ]]; then + if [[ -d "$HOME/.gradle" && -w "$HOME/.gradle" ]]; then + export GRADLE_USER_HOME="$HOME/.gradle" + else + export GRADLE_USER_HOME="$ROOT_DIR/.gradle-local" + fi +fi + +mkdir -p "$GRADLE_USER_HOME" + +if [[ -d "$HOME/.local/jdk" ]]; then + export JAVA_HOME="$HOME/.local/jdk" +fi + +if [[ -d "$HOME/android-sdk" ]]; then + export ANDROID_SDK_ROOT="$HOME/android-sdk" +fi + +if [[ -n "${JAVA_HOME:-}" ]]; then + export PATH="$JAVA_HOME/bin:$PATH" +fi + +if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then + export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" +fi + +if [[ -d "$HOME/.local/gradle/gradle-8.7/bin" ]]; then + export PATH="$HOME/.local/gradle/gradle-8.7/bin:$PATH" +fi + +unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy + +GRADLE_BIN="gradle" +if [[ -x "$ROOT_DIR/gradlew" ]]; then + GRADLE_BIN="$ROOT_DIR/gradlew" +fi + +ATTEMPTS="${ATTEMPTS:-5}" +SLEEP_SECONDS="${SLEEP_SECONDS:-15}" +TASK="${1:-assembleDebug}" + +for attempt in $(seq 1 "$ATTEMPTS"); do + echo "==> Android build attempt $attempt/$ATTEMPTS ($TASK)" + if "$GRADLE_BIN" --no-daemon --console=plain "$TASK"; then + exit 0 + fi + + if [[ "$attempt" -lt "$ATTEMPTS" ]]; then + echo "Build failed, retrying in ${SLEEP_SECONDS}s..." + sleep "$SLEEP_SECONDS" + fi +done + +echo "Android build failed after $ATTEMPTS attempts." +exit 1 diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..017d909 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("com.android.application") version "8.5.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.24" apply false +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..84e8962 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official +systemProp.org.gradle.internal.http.connectionTimeout=120000 +systemProp.org.gradle.internal.http.socketTimeout=120000 diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac658cd --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=120000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..94a30ff --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "tg-ws-proxy-android" +include(":app")