From 017172f48a4f9fb1fb93e6d6642b38dc4d3118da Mon Sep 17 00:00:00 2001 From: Giovanni Harting <539@idlegandalf.com> Date: Thu, 18 Dec 2025 21:08:56 +0100 Subject: [PATCH] Initial commit: Android Tether API app Exposes phone connection status via HTTP API for tethered devices. Features: - Auto-start/stop server when tethering is enabled/disabled - REST API with /status and /health endpoints - Connection type, signal strength, carrier, battery info - Foreground service with persistent notification Stack: Kotlin 2.3, AGP 8.13, Gradle 8.13, compileSdk 36 --- .gitignore | 16 ++ README.md | 194 +++++++++++++ app/build.gradle.kts | 61 +++++ app/proguard-rules.pro | 8 + app/src/main/AndroidManifest.xml | 49 ++++ .../java/dev/itsh/tetherapi/MainActivity.kt | 210 +++++++++++++++ .../java/dev/itsh/tetherapi/api/ApiServer.kt | 88 ++++++ .../dev/itsh/tetherapi/data/PhoneStatus.kt | 33 +++ .../itsh/tetherapi/provider/StatusProvider.kt | 255 ++++++++++++++++++ .../tetherapi/receiver/TetherStateReceiver.kt | 61 +++++ .../tetherapi/service/TetherApiService.kt | 163 +++++++++++ .../res/drawable/ic_launcher_foreground.xml | 32 +++ app/src/main/res/drawable/ic_notification.xml | 24 ++ .../main/res/drawable/status_indicator.xml | 5 + app/src/main/res/layout/activity_main.xml | 177 ++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/values/colors.xml | 16 ++ app/src/main/res/values/strings.xml | 17 ++ app/src/main/res/values/themes.xml | 16 ++ build.gradle.kts | 4 + gradle.properties | 4 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 +++++++++++++++++ gradlew.bat | 93 +++++++ settings.gradle.kts | 18 ++ 27 files changed, 1809 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/dev/itsh/tetherapi/MainActivity.kt create mode 100644 app/src/main/java/dev/itsh/tetherapi/api/ApiServer.kt create mode 100644 app/src/main/java/dev/itsh/tetherapi/data/PhoneStatus.kt create mode 100644 app/src/main/java/dev/itsh/tetherapi/provider/StatusProvider.kt create mode 100644 app/src/main/java/dev/itsh/tetherapi/receiver/TetherStateReceiver.kt create mode 100644 app/src/main/java/dev/itsh/tetherapi/service/TetherApiService.kt create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml create mode 100644 app/src/main/res/drawable/status_indicator.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9262b1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/app/build +/app/release +*.apk +*.aab +*.jks +*.keystore diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4f94ad --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +# Tether API + +Android app that exposes your phone's connection status via HTTP API, accessible from tethered devices. + +## Features + +- **Auto-start on tether**: Server automatically starts/stops when tethering is enabled/disabled +- **Real-time status**: Connection type (5G/LTE/3G/WIFI), signal strength, carrier, battery level +- **Simple REST API**: JSON responses, easy to integrate with any client +- **No root required**: Works with standard Android permissions + +## Installation + +Build with Android Studio or use the release APK. + +**Required permissions:** +- `READ_PHONE_STATE` - Network connection type and carrier +- `ACCESS_FINE_LOCATION` - Required by Android 10+ for signal strength +- `POST_NOTIFICATIONS` - Service status notification + +## API Reference + +Default port: `8765` + +Common gateway IPs when tethering: +- USB tethering: `192.168.42.1` +- WiFi hotspot: `192.168.43.1` + +### GET /status + +Returns full phone status. + +**Response:** +```json +{ + "connection": { + "type": "LTE", + "signal_dbm": -89, + "signal_bars": 3, + "carrier": "T-Mobile", + "operator": "310260", + "roaming": false + }, + "battery": { + "level": 73, + "charging": false + }, + "network": { + "wifi_ssid": null, + "ip_addresses": ["192.168.42.129"] + }, + "timestamp": 1702915200 +} +``` + +**Fields:** + +| Field | Description | +|-------|-------------| +| `connection.type` | `5G`, `LTE`, `HSPA`, `3G`, `2G`, `WIFI`, `CELLULAR`, `NONE` | +| `connection.signal_dbm` | Signal strength in dBm (-999 if unavailable) | +| `connection.signal_bars` | 0-4 signal bars | +| `connection.carrier` | Network operator name | +| `connection.operator` | MCC+MNC code | +| `connection.roaming` | Whether roaming is active | +| `battery.level` | Battery percentage (0-100) | +| `battery.charging` | Whether device is charging | +| `network.wifi_ssid` | WiFi SSID if connected via WiFi | +| `network.ip_addresses` | List of device IP addresses | +| `timestamp` | Unix timestamp of response | + +### GET /health + +Health check endpoint. + +**Response:** +```json +{"status": "ok"} +``` + +### GET / + +API information. + +**Response:** +```json +{ + "name": "Tether API", + "version": "1.0.0", + "endpoints": [ + {"path": "/status", "method": "GET", "description": "Get phone status"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +## Client Examples + +### Basic curl + +```bash +curl http://192.168.42.1:8765/status +``` + +### Auto-discover phone IP + +```bash +#!/bin/bash +for ip in 192.168.42.1 192.168.43.1 192.168.44.1; do + if curl -s --connect-timeout 1 "http://$ip:8765/health" | grep -q "ok"; then + echo "$ip" + exit 0 + fi +done +exit 1 +``` + +### Waybar module + +**~/.config/waybar/config:** +```json +{ + "custom/phone": { + "exec": "~/.local/bin/phone-status.sh", + "return-type": "json", + "interval": 10, + "format": "{}" + } +} +``` + +**~/.local/bin/phone-status.sh:** +```bash +#!/bin/bash +PHONE_IP="${PHONE_IP:-192.168.42.1}" +PORT="${PORT:-8765}" + +status=$(curl -s --connect-timeout 2 "http://$PHONE_IP:$PORT/status") +if [ -z "$status" ]; then + echo '{"text": "N/A", "tooltip": "Phone not connected", "class": "disconnected"}' + exit 0 +fi + +type=$(echo "$status" | jq -r '.connection.type') +bars=$(echo "$status" | jq -r '.connection.signal_bars') +battery=$(echo "$status" | jq -r '.battery.level') + +case $bars in + 4) signal="▂▄▆█" ;; + 3) signal="▂▄▆░" ;; + 2) signal="▂▄░░" ;; + 1) signal="▂░░░" ;; + *) signal="░░░░" ;; +esac + +echo "{\"text\": \"$type $signal $battery%\", \"tooltip\": \"$type | Signal: $bars/4 | Battery: $battery%\", \"class\": \"$type\"}" +``` + +### Python client + +```python +import requests + +def get_phone_status(ip="192.168.42.1", port=8765): + try: + r = requests.get(f"http://{ip}:{port}/status", timeout=2) + return r.json() + except: + return None + +status = get_phone_status() +if status: + print(f"Connection: {status['connection']['type']}") + print(f"Signal: {status['connection']['signal_bars']}/4") + print(f"Battery: {status['battery']['level']}%") +``` + +### KDE Plasmoid / Generic widget + +Most desktop widgets can execute shell commands. Use the curl/jq approach: + +```bash +curl -s http://192.168.42.1:8765/status | jq -r '"\(.connection.type) \(.connection.signal_bars)/4 \(.battery.level)%"' +``` + +## Building + +1. Open in Android Studio +2. Build > Build APK +3. Install APK on your phone + +## License + +MIT diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..93f2f78 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "dev.itsh.tetherapi" + compileSdk = 36 + + defaultConfig { + applicationId = "dev.itsh.tetherapi" + minSdk = 26 + targetSdk = 36 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + buildFeatures { + viewBinding = true + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.17.0") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.1") + + // NanoHTTPD for lightweight HTTP server + implementation("org.nanohttpd:nanohttpd:2.3.1") + + // Kotlin coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") + + // JSON serialization + implementation("com.google.code.gson:gson:2.11.0") + + // Preferences for settings + implementation("androidx.preference:preference-ktx:1.2.1") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..b4d16f4 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,8 @@ +# NanoHTTPD +-keep class fi.iki.elonen.** { *; } +-keep class org.nanohttpd.** { *; } + +# Gson +-keepattributes Signature +-keepattributes *Annotation* +-keep class dev.itsh.tetherapi.data.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..744d5bf --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/dev/itsh/tetherapi/MainActivity.kt b/app/src/main/java/dev/itsh/tetherapi/MainActivity.kt new file mode 100644 index 0000000..278bb97 --- /dev/null +++ b/app/src/main/java/dev/itsh/tetherapi/MainActivity.kt @@ -0,0 +1,210 @@ +package dev.itsh.tetherapi + +import android.Manifest +import android.content.pm.PackageManager +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import com.google.gson.GsonBuilder +import dev.itsh.tetherapi.databinding.ActivityMainBinding +import dev.itsh.tetherapi.provider.StatusProvider +import dev.itsh.tetherapi.receiver.TetherStateReceiver +import dev.itsh.tetherapi.service.TetherApiService +import java.net.NetworkInterface + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + private val handler = Handler(Looper.getMainLooper()) + private val statusProvider by lazy { StatusProvider(this) } + private val gson = GsonBuilder().setPrettyPrinting().create() + + private val requiredPermissions = buildList { + add(Manifest.permission.READ_PHONE_STATE) + add(Manifest.permission.ACCESS_FINE_LOCATION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(Manifest.permission.POST_NOTIFICATIONS) + } + } + + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val allGranted = permissions.all { it.value } + if (allGranted) { + updateUI() + } else { + showPermissionRationale() + } + } + + private val updateRunnable = object : Runnable { + override fun run() { + updateUI() + handler.postDelayed(this, UPDATE_INTERVAL) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupUI() + loadSettings() + checkPermissions() + } + + override fun onResume() { + super.onResume() + handler.post(updateRunnable) + } + + override fun onPause() { + super.onPause() + handler.removeCallbacks(updateRunnable) + } + + private fun setupUI() { + binding.toggleButton.setOnClickListener { + if (TetherApiService.isRunning) { + TetherApiService.stop(this) + } else { + val port = binding.portInput.text.toString().toIntOrNull() ?: TetherApiService.DEFAULT_PORT + TetherApiService.start(this, port) + } + handler.postDelayed({ updateUI() }, 500) + } + + binding.autoStartSwitch.setOnCheckedChangeListener { _, _ -> + saveSettings() + } + + binding.portInput.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + saveSettings() + } + } + } + + private fun loadSettings() { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + binding.autoStartSwitch.isChecked = prefs.getBoolean(TetherStateReceiver.PREF_AUTO_START, true) + binding.portInput.setText(prefs.getInt(TetherStateReceiver.PREF_PORT, TetherApiService.DEFAULT_PORT).toString()) + } + + private fun saveSettings() { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + val port = binding.portInput.text.toString().toIntOrNull() ?: TetherApiService.DEFAULT_PORT + prefs.edit() + .putBoolean(TetherStateReceiver.PREF_AUTO_START, binding.autoStartSwitch.isChecked) + .putInt(TetherStateReceiver.PREF_PORT, port) + .apply() + } + + private fun checkPermissions() { + val missingPermissions = requiredPermissions.filter { + ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED + } + + if (missingPermissions.isNotEmpty()) { + permissionLauncher.launch(missingPermissions.toTypedArray()) + } + } + + private fun showPermissionRationale() { + AlertDialog.Builder(this) + .setTitle("Permissions Required") + .setMessage( + "This app needs the following permissions:\n\n" + + "• Phone State: To read network connection type and carrier\n" + + "• Location: Required by Android to access cell signal info\n" + + "• Notifications: To show service status" + ) + .setPositiveButton("Grant") { _, _ -> + permissionLauncher.launch(requiredPermissions.toTypedArray()) + } + .setNegativeButton("Cancel", null) + .show() + } + + private fun updateUI() { + val isRunning = TetherApiService.isRunning + val port = TetherApiService.currentPort ?: binding.portInput.text.toString().toIntOrNull() ?: TetherApiService.DEFAULT_PORT + + binding.statusText.text = if (isRunning) { + getString(R.string.status_running) + } else { + getString(R.string.status_stopped) + } + + val statusColor = if (isRunning) { + ContextCompat.getColor(this, R.color.status_running) + } else { + ContextCompat.getColor(this, R.color.status_stopped) + } + (binding.statusIndicator.background as? GradientDrawable)?.setColor(statusColor) + + binding.toggleButton.text = if (isRunning) { + getString(R.string.btn_stop) + } else { + getString(R.string.btn_start) + } + + val gatewayIp = getGatewayIp() + binding.endpointText.text = if (gatewayIp != null) { + "http://$gatewayIp:$port/status" + } else { + "http://[phone-ip]:$port/status" + } + + updateStatusPreview() + } + + private fun updateStatusPreview() { + try { + val status = statusProvider.getStatus() + binding.previewText.text = gson.toJson(status) + } catch (e: Exception) { + binding.previewText.text = "Error: ${e.message}" + } + } + + private fun getGatewayIp(): String? { + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val networkInterface = interfaces.nextElement() + val name = networkInterface.name.lowercase() + + if (name.startsWith("rndis") || name.startsWith("usb") || + name.startsWith("wlan") || name.startsWith("ap") || + name.startsWith("swlan")) { + + val addresses = networkInterface.inetAddresses + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (!address.isLoopbackAddress && address.hostAddress?.contains(".") == true) { + return address.hostAddress + } + } + } + } + } catch (e: Exception) { + // Ignore + } + return null + } + + companion object { + private const val UPDATE_INTERVAL = 2000L + } +} diff --git a/app/src/main/java/dev/itsh/tetherapi/api/ApiServer.kt b/app/src/main/java/dev/itsh/tetherapi/api/ApiServer.kt new file mode 100644 index 0000000..bc730df --- /dev/null +++ b/app/src/main/java/dev/itsh/tetherapi/api/ApiServer.kt @@ -0,0 +1,88 @@ +package dev.itsh.tetherapi.api + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import dev.itsh.tetherapi.provider.StatusProvider +import fi.iki.elonen.NanoHTTPD + +class ApiServer( + private val context: Context, + port: Int +) : NanoHTTPD("0.0.0.0", port) { + + private val statusProvider = StatusProvider(context) + private val gson: Gson = GsonBuilder() + .setPrettyPrinting() + .create() + + override fun serve(session: IHTTPSession): Response { + val uri = session.uri + val method = session.method + + if (method != Method.GET) { + return newFixedLengthResponse( + Response.Status.METHOD_NOT_ALLOWED, + MIME_JSON, + """{"error": "Method not allowed"}""" + ).apply { + addHeader("Access-Control-Allow-Origin", "*") + } + } + + return when (uri) { + "/status" -> handleStatus() + "/health" -> handleHealth() + "/" -> handleRoot() + else -> handleNotFound() + } + } + + private fun handleStatus(): Response { + return try { + val status = statusProvider.getStatus() + val json = gson.toJson(status) + jsonResponse(Response.Status.OK, json) + } catch (e: Exception) { + jsonResponse( + Response.Status.INTERNAL_ERROR, + """{"error": "${e.message ?: "Unknown error"}"}""" + ) + } + } + + private fun handleHealth(): Response { + return jsonResponse(Response.Status.OK, """{"status": "ok"}""") + } + + private fun handleRoot(): Response { + val info = mapOf( + "name" to "Tether API", + "version" to "1.0.0", + "endpoints" to listOf( + mapOf("path" to "/status", "method" to "GET", "description" to "Get phone status"), + mapOf("path" to "/health", "method" to "GET", "description" to "Health check") + ) + ) + return jsonResponse(Response.Status.OK, gson.toJson(info)) + } + + private fun handleNotFound(): Response { + return jsonResponse( + Response.Status.NOT_FOUND, + """{"error": "Not found"}""" + ) + } + + private fun jsonResponse(status: Response.Status, json: String): Response { + return newFixedLengthResponse(status, MIME_JSON, json).apply { + addHeader("Access-Control-Allow-Origin", "*") + addHeader("Access-Control-Allow-Methods", "GET") + addHeader("Cache-Control", "no-cache") + } + } + + companion object { + private const val MIME_JSON = "application/json" + } +} diff --git a/app/src/main/java/dev/itsh/tetherapi/data/PhoneStatus.kt b/app/src/main/java/dev/itsh/tetherapi/data/PhoneStatus.kt new file mode 100644 index 0000000..2a1abf3 --- /dev/null +++ b/app/src/main/java/dev/itsh/tetherapi/data/PhoneStatus.kt @@ -0,0 +1,33 @@ +package dev.itsh.tetherapi.data + +import com.google.gson.annotations.SerializedName + +data class PhoneStatus( + val connection: ConnectionInfo, + val battery: BatteryInfo, + val network: NetworkInfo, + val timestamp: Long +) + +data class ConnectionInfo( + val type: String, + @SerializedName("signal_dbm") + val signalDbm: Int, + @SerializedName("signal_bars") + val signalBars: Int, + val carrier: String, + val operator: String, + val roaming: Boolean +) + +data class BatteryInfo( + val level: Int, + val charging: Boolean +) + +data class NetworkInfo( + @SerializedName("wifi_ssid") + val wifiSsid: String?, + @SerializedName("ip_addresses") + val ipAddresses: List +) diff --git a/app/src/main/java/dev/itsh/tetherapi/provider/StatusProvider.kt b/app/src/main/java/dev/itsh/tetherapi/provider/StatusProvider.kt new file mode 100644 index 0000000..2c55f8c --- /dev/null +++ b/app/src/main/java/dev/itsh/tetherapi/provider/StatusProvider.kt @@ -0,0 +1,255 @@ +package dev.itsh.tetherapi.provider + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager +import android.os.BatteryManager +import android.os.Build +import android.telephony.CellInfo +import android.telephony.CellInfoLte +import android.telephony.CellInfoNr +import android.telephony.CellInfoGsm +import android.telephony.CellInfoWcdma +import android.telephony.CellInfoCdma +import android.telephony.TelephonyManager +import androidx.core.content.ContextCompat +import dev.itsh.tetherapi.data.BatteryInfo +import dev.itsh.tetherapi.data.ConnectionInfo +import dev.itsh.tetherapi.data.NetworkInfo +import dev.itsh.tetherapi.data.PhoneStatus +import java.net.NetworkInterface + +class StatusProvider(private val context: Context) { + + private val telephonyManager: TelephonyManager by lazy { + context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + } + + private val connectivityManager: ConnectivityManager by lazy { + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + } + + private val wifiManager: WifiManager by lazy { + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + } + + fun getStatus(): PhoneStatus { + return PhoneStatus( + connection = getConnectionInfo(), + battery = getBatteryInfo(), + network = getNetworkInfo(), + timestamp = System.currentTimeMillis() / 1000 + ) + } + + private fun getConnectionInfo(): ConnectionInfo { + val hasPhonePermission = ContextCompat.checkSelfPermission( + context, Manifest.permission.READ_PHONE_STATE + ) == PackageManager.PERMISSION_GRANTED + + val hasLocationPermission = ContextCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + val connectionType = getConnectionType() + val (signalDbm, signalBars) = if (hasLocationPermission) { + getSignalStrength() + } else { + Pair(-999, 0) + } + + val carrier = if (hasPhonePermission) { + telephonyManager.networkOperatorName ?: "Unknown" + } else { + "Unknown" + } + + val operator = telephonyManager.networkOperator ?: "" + + val isRoaming = try { + telephonyManager.isNetworkRoaming + } catch (e: Exception) { + false + } + + return ConnectionInfo( + type = connectionType, + signalDbm = signalDbm, + signalBars = signalBars, + carrier = carrier, + operator = operator, + roaming = isRoaming + ) + } + + private fun getConnectionType(): String { + val network = connectivityManager.activeNetwork ?: return "NONE" + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return "NONE" + + return when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "WIFI" + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> getCellularType() + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ETHERNET" + else -> "UNKNOWN" + } + } + + private fun getCellularType(): String { + val hasPermission = ContextCompat.checkSelfPermission( + context, Manifest.permission.READ_PHONE_STATE + ) == PackageManager.PERMISSION_GRANTED + + if (!hasPermission) return "CELLULAR" + + return when (telephonyManager.dataNetworkType) { + TelephonyManager.NETWORK_TYPE_NR -> "5G" + TelephonyManager.NETWORK_TYPE_LTE -> "LTE" + TelephonyManager.NETWORK_TYPE_HSPAP, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_HSUPA -> "HSPA" + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_EVDO_B -> "3G" + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT -> "2G" + TelephonyManager.NETWORK_TYPE_UNKNOWN -> "CELLULAR" + else -> "CELLULAR" + } + } + + @Suppress("DEPRECATION") + private fun getSignalStrength(): Pair { + val hasLocationPermission = ContextCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + if (!hasLocationPermission) { + return Pair(-999, 0) + } + + try { + val cellInfoList: List? = telephonyManager.allCellInfo + if (cellInfoList.isNullOrEmpty()) { + return Pair(-999, 0) + } + + for (cellInfo in cellInfoList) { + if (!cellInfo.isRegistered) continue + + val dbm = when (cellInfo) { + is CellInfoLte -> cellInfo.cellSignalStrength.dbm + is CellInfoNr -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + cellInfo.cellSignalStrength.dbm + } else -999 + is CellInfoGsm -> cellInfo.cellSignalStrength.dbm + is CellInfoWcdma -> cellInfo.cellSignalStrength.dbm + is CellInfoCdma -> cellInfo.cellSignalStrength.dbm + else -> continue + } + + val level = when (cellInfo) { + is CellInfoLte -> cellInfo.cellSignalStrength.level + is CellInfoNr -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + cellInfo.cellSignalStrength.level + } else 0 + is CellInfoGsm -> cellInfo.cellSignalStrength.level + is CellInfoWcdma -> cellInfo.cellSignalStrength.level + is CellInfoCdma -> cellInfo.cellSignalStrength.level + else -> continue + } + + return Pair(dbm, level) + } + } catch (e: SecurityException) { + return Pair(-999, 0) + } catch (e: Exception) { + return Pair(-999, 0) + } + + return Pair(-999, 0) + } + + private fun getBatteryInfo(): BatteryInfo { + val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { filter -> + context.registerReceiver(null, filter) + } + + val level = batteryStatus?.let { intent -> + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + if (level >= 0 && scale > 0) { + (level * 100 / scale) + } else { + -1 + } + } ?: -1 + + val isCharging = batteryStatus?.let { intent -> + val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + } ?: false + + return BatteryInfo( + level = level, + charging = isCharging + ) + } + + @Suppress("DEPRECATION") + private fun getNetworkInfo(): NetworkInfo { + val wifiSsid = try { + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) { + val wifiInfo = wifiManager.connectionInfo + wifiInfo?.ssid?.replace("\"", "")?.takeIf { it != "" } + } else { + null + } + } catch (e: Exception) { + null + } + + val ipAddresses = getIpAddresses() + + return NetworkInfo( + wifiSsid = wifiSsid, + ipAddresses = ipAddresses + ) + } + + private fun getIpAddresses(): List { + val addresses = mutableListOf() + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val networkInterface = interfaces.nextElement() + if (networkInterface.isLoopback || !networkInterface.isUp) continue + + val inetAddresses = networkInterface.inetAddresses + while (inetAddresses.hasMoreElements()) { + val address = inetAddresses.nextElement() + if (!address.isLoopbackAddress && !address.hostAddress.isNullOrEmpty()) { + val hostAddress = address.hostAddress!! + if (!hostAddress.contains(":")) { + addresses.add(hostAddress) + } + } + } + } + } catch (e: Exception) { + // Ignore + } + return addresses + } +} diff --git a/app/src/main/java/dev/itsh/tetherapi/receiver/TetherStateReceiver.kt b/app/src/main/java/dev/itsh/tetherapi/receiver/TetherStateReceiver.kt new file mode 100644 index 0000000..9941e02 --- /dev/null +++ b/app/src/main/java/dev/itsh/tetherapi/receiver/TetherStateReceiver.kt @@ -0,0 +1,61 @@ +package dev.itsh.tetherapi.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.os.Build +import androidx.preference.PreferenceManager +import dev.itsh.tetherapi.service.TetherApiService + +class TetherStateReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION_TETHER_STATE_CHANGED) return + + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val autoStartEnabled = prefs.getBoolean(PREF_AUTO_START, true) + + if (!autoStartEnabled) return + + val isTethering = isTetherActive(context, intent) + val port = prefs.getInt(PREF_PORT, TetherApiService.DEFAULT_PORT) + + if (isTethering && !TetherApiService.isRunning) { + TetherApiService.start(context, port) + } else if (!isTethering && TetherApiService.isRunning) { + TetherApiService.stop(context) + } + } + + @Suppress("DEPRECATION") + private fun isTetherActive(context: Context, intent: Intent): Boolean { + val extras = intent.extras ?: return false + + val activeList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + extras.getStringArrayList("activeArray") + } else { + @Suppress("UNCHECKED_CAST") + extras.get("activeArray") as? ArrayList + } + + if (!activeList.isNullOrEmpty()) { + return true + } + + return try { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val method = cm.javaClass.getDeclaredMethod("getTetheredIfaces") + val tetheredIfaces = method.invoke(cm) as? Array<*> + tetheredIfaces != null && tetheredIfaces.isNotEmpty() + } catch (e: Exception) { + false + } + } + + companion object { + const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED" + const val PREF_AUTO_START = "auto_start_on_tether" + const val PREF_PORT = "api_port" + } +} diff --git a/app/src/main/java/dev/itsh/tetherapi/service/TetherApiService.kt b/app/src/main/java/dev/itsh/tetherapi/service/TetherApiService.kt new file mode 100644 index 0000000..44149c2 --- /dev/null +++ b/app/src/main/java/dev/itsh/tetherapi/service/TetherApiService.kt @@ -0,0 +1,163 @@ +package dev.itsh.tetherapi.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Binder +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import dev.itsh.tetherapi.MainActivity +import dev.itsh.tetherapi.R +import dev.itsh.tetherapi.api.ApiServer + +class TetherApiService : Service() { + + private var apiServer: ApiServer? = null + private val binder = LocalBinder() + + inner class LocalBinder : Binder() { + fun getService(): TetherApiService = this@TetherApiService + } + + override fun onBind(intent: Intent?): IBinder = binder + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val port = intent?.getIntExtra(EXTRA_PORT, DEFAULT_PORT) ?: DEFAULT_PORT + + when (intent?.action) { + ACTION_START -> startServer(port) + ACTION_STOP -> stopServer() + } + + return START_STICKY + } + + private fun startServer(port: Int) { + if (apiServer?.isAlive == true) { + return + } + + try { + apiServer = ApiServer(this, port).apply { + start() + } + + val notification = createNotification(port) + startForeground(NOTIFICATION_ID, notification) + + isRunning = true + currentPort = port + } catch (e: Exception) { + stopSelf() + } + } + + private fun stopServer() { + apiServer?.stop() + apiServer = null + isRunning = false + currentPort = null + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } + + override fun onDestroy() { + stopServer() + super.onDestroy() + } + + private fun createNotificationChannel() { + val channel = NotificationChannel( + CHANNEL_ID, + getString(R.string.service_notification_channel), + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "Shows when Tether API server is running" + setShowBadge(false) + } + + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + + private fun createNotification(port: Int): Notification { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + val stopIntent = Intent(this, TetherApiService::class.java).apply { + action = ACTION_STOP + } + + val stopPendingIntent = PendingIntent.getService( + this, + 1, + stopIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.service_notification_title)) + .setContentText(getString(R.string.service_notification_text, port)) + .setSmallIcon(R.drawable.ic_notification) + .setContentIntent(pendingIntent) + .addAction(0, "Stop", stopPendingIntent) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .build() + } + + companion object { + const val ACTION_START = "dev.itsh.tetherapi.action.START" + const val ACTION_STOP = "dev.itsh.tetherapi.action.STOP" + const val EXTRA_PORT = "dev.itsh.tetherapi.extra.PORT" + const val DEFAULT_PORT = 8765 + + private const val CHANNEL_ID = "tether_api_service" + private const val NOTIFICATION_ID = 1 + + var isRunning = false + private set + + var currentPort: Int? = null + private set + + fun start(context: Context, port: Int = DEFAULT_PORT) { + val intent = Intent(context, TetherApiService::class.java).apply { + action = ACTION_START + putExtra(EXTRA_PORT, port) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + fun stop(context: Context) { + val intent = Intent(context, TetherApiService::class.java).apply { + action = ACTION_STOP + } + context.startService(intent) + } + } +} diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..7738df9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..9d6cda0 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/status_indicator.xml b/app/src/main/res/drawable/status_indicator.xml new file mode 100644 index 0000000..2ce56aa --- /dev/null +++ b/app/src/main/res/drawable/status_indicator.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..417941c --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..a8b92c6 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..cbcb603 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,16 @@ + + + #1976D2 + #1565C0 + #26A69A + #FAFAFA + #FFFFFF + #B00020 + #FFFFFF + #000000 + #000000 + #000000 + #FFFFFF + #4CAF50 + #9E9E9E + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..93b95e0 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,17 @@ + + + Tether API + Tether API Service + Tether API Running + API server active on port %d + Running + Stopped + Start Server + Stop Server + API Endpoint + Port + Auto-start on tether + Location permission is required to access cell signal information on Android 10+ + Phone state permission is required to read network connection information + Notification permission allows showing the service status + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..5f4ebb7 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c14ed1f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("com.android.application") version "8.13.0" apply false + id("org.jetbrains.kotlin.android") version "2.3.0" apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f0a2e55 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8e1ee3125fe0768e9a76ee977ac089eb657005e GIT binary patch literal 45633 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 '} + 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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 + + + +# 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" ) + + 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, 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..a95ac20 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "TetherAPI" +include(":app")