├── .circleci └── config.yml ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── detekt.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mobile ├── .gitignore ├── build.gradle.kts ├── google-services.json ├── lint.xml ├── proguard-rules.pro ├── schemas │ └── be.mygod.vpnhotspot.room.AppDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ ├── androidTest │ └── java │ │ └── be │ │ └── mygod │ │ └── vpnhotspot │ │ └── room │ │ └── MigrationTest.kt │ ├── freedom │ └── java │ │ └── be │ │ └── mygod │ │ └── vpnhotspot │ │ └── util │ │ └── UpdateChecker.kt │ ├── google │ ├── AndroidManifest.xml │ └── java │ │ └── be │ │ └── mygod │ │ └── vpnhotspot │ │ └── util │ │ └── UpdateChecker.kt │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ ├── CMakeLists.txt │ └── root.cpp │ ├── ic_launcher-web.png │ ├── java │ └── be │ │ └── mygod │ │ └── vpnhotspot │ │ ├── AlertDialogFragment.kt │ │ ├── App.kt │ │ ├── BootReceiver.kt │ │ ├── EBegFragment.kt │ │ ├── IpNeighbourMonitoringService.kt │ │ ├── LocalOnlyHotspotService.kt │ │ ├── MainActivity.kt │ │ ├── RepeaterService.kt │ │ ├── RoutingManager.kt │ │ ├── ServiceNotification.kt │ │ ├── SettingsPreferenceFragment.kt │ │ ├── StaticIpSetter.kt │ │ ├── TetheringService.kt │ │ ├── client │ │ ├── Client.kt │ │ ├── ClientAddressInfo.kt │ │ ├── ClientViewModel.kt │ │ ├── ClientsFragment.kt │ │ └── MacLookup.kt │ │ ├── manage │ │ ├── BluetoothTethering.kt │ │ ├── Data.kt │ │ ├── InterfaceManager.kt │ │ ├── IpNeighbourMonitoringTileService.kt │ │ ├── LocalOnlyHotspotManager.kt │ │ ├── LocalOnlyHotspotTileService.kt │ │ ├── ManageBar.kt │ │ ├── Manager.kt │ │ ├── RepeaterManager.kt │ │ ├── RepeaterTileService.kt │ │ ├── StaticIpManager.kt │ │ ├── TetherManager.kt │ │ ├── TetheringFragment.kt │ │ └── TetheringTileService.kt │ │ ├── net │ │ ├── DhcpWorkaround.kt │ │ ├── InetAddressComparator.kt │ │ ├── IpNeighbour.kt │ │ ├── MacAddressCompat.kt │ │ ├── RemoveUidInterfaceRuleCommand.kt │ │ ├── Routing.kt │ │ ├── TetherOffloadManager.kt │ │ ├── TetherType.kt │ │ ├── TetheringManagerCompat.kt │ │ ├── VpnFirewallManager.kt │ │ ├── dns │ │ │ ├── DnsForwarder.kt │ │ │ ├── DnsResolverCompat.kt │ │ │ └── VpnProtectedSelectorManager.kt │ │ ├── monitor │ │ │ ├── DefaultNetworkMonitor.kt │ │ │ ├── FallbackUpstreamMonitor.kt │ │ │ ├── InterfaceMonitor.kt │ │ │ ├── IpMonitor.kt │ │ │ ├── IpNeighbourMonitor.kt │ │ │ ├── TetherTimeoutMonitor.kt │ │ │ ├── TrafficRecorder.kt │ │ │ ├── UpstreamMonitor.kt │ │ │ └── VpnMonitor.kt │ │ └── wifi │ │ │ ├── P2pSupplicantConfiguration.kt │ │ │ ├── SoftApCapability.kt │ │ │ ├── SoftApConfigurationCompat.kt │ │ │ ├── SoftApInfo.kt │ │ │ ├── VendorElements.kt │ │ │ ├── WifiApDialogFragment.kt │ │ │ ├── WifiApManager.kt │ │ │ ├── WifiClient.kt │ │ │ ├── WifiDoubleLock.kt │ │ │ ├── WifiP2pManagerHelper.kt │ │ │ └── WifiSsidCompat.kt │ │ ├── preference │ │ ├── AutoCompleteNetworkPreferenceDialogFragment.kt │ │ ├── SharedPreferenceDataStore.kt │ │ ├── SummaryFallbackProvider.kt │ │ └── UpstreamsPreference.kt │ │ ├── room │ │ ├── AppDatabase.kt │ │ ├── ClientRecord.kt │ │ ├── Converters.kt │ │ └── TrafficRecord.kt │ │ ├── root │ │ ├── Jni.kt │ │ ├── LocalOnlyHotspotCallbacks.kt │ │ ├── MiscCommands.kt │ │ ├── RepeaterCommands.kt │ │ ├── RootManager.kt │ │ ├── RoutingCommands.kt │ │ ├── TetheringCommands.kt │ │ └── WifiApCommands.kt │ │ ├── tasker │ │ ├── StateAction.kt │ │ ├── TaskerEvents.kt │ │ ├── TaskerPermissionManager.kt │ │ ├── TetheringActions.kt │ │ └── TetheringState.kt │ │ ├── util │ │ ├── ConstantLookup.kt │ │ ├── CustomTabsUrlSpan.kt │ │ ├── DeviceStorageApp.kt │ │ ├── Events.kt │ │ ├── KillableTileService.kt │ │ ├── QRCodeDialog.kt │ │ ├── RangeInput.kt │ │ ├── RootSession.kt │ │ ├── SelfDismissActivity.kt │ │ ├── ServiceForegroundConnector.kt │ │ ├── Services.kt │ │ ├── UnblockCentral.kt │ │ └── Utils.kt │ │ └── widget │ │ ├── AlwaysAutoCompleteEditText.kt │ │ ├── AutoCollapseTextView.kt │ │ ├── LinkTextView.kt │ │ └── SmartSnackbar.kt │ └── res │ ├── drawable │ ├── ic_action_autorenew.xml │ ├── ic_action_bug_report.xml │ ├── ic_action_build.xml │ ├── ic_action_card_giftcard.xml │ ├── ic_action_code.xml │ ├── ic_action_perm_scan_wifi.xml │ ├── ic_action_settings.xml │ ├── ic_action_settings_backup_restore.xml │ ├── ic_action_settings_ethernet.xml │ ├── ic_action_settings_input_antenna.xml │ ├── ic_action_settings_input_component.xml │ ├── ic_action_update.xml │ ├── ic_action_wifi_protected_setup.xml │ ├── ic_alert_warning.xml │ ├── ic_av_closed_caption.xml │ ├── ic_av_closed_caption_off.xml │ ├── ic_content_add.xml │ ├── ic_content_file_copy.xml │ ├── ic_content_inbox.xml │ ├── ic_content_push_pin.xml │ ├── ic_deployed_code.xml │ ├── ic_device_battery_charging_full.xml │ ├── ic_device_bluetooth.xml │ ├── ic_device_devices.xml │ ├── ic_device_network_wifi.xml │ ├── ic_device_usb.xml │ ├── ic_device_wifi_lock.xml │ ├── ic_device_wifi_tethering.xml │ ├── ic_hardware_device_hub.xml │ ├── ic_image_flash_on.xml │ ├── ic_image_looks_6.xml │ ├── ic_image_remove_red_eye.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_monochrome.xml │ ├── ic_quick_settings_tile_on.xml │ ├── ic_settings_qrcode.xml │ ├── ic_social_people.xml │ ├── ic_toggle_star.xml │ └── toggle_hex.xml │ ├── layout │ ├── activity_main.xml │ ├── dialog_nickname.xml │ ├── dialog_static_ip.xml │ ├── dialog_wifi_ap.xml │ ├── dialog_wps.xml │ ├── fragment_clients.xml │ ├── fragment_ebeg.xml │ ├── fragment_tethering.xml │ ├── listitem_client.xml │ ├── listitem_interface.xml │ ├── listitem_manage.xml │ ├── listitem_repeater.xml │ ├── listitem_static_ip.xml │ ├── preference_material.xml │ ├── preference_widget_edittext_autocomplete.xml │ └── preference_widget_material_switch.xml │ ├── menu │ ├── navigation.xml │ ├── popup_client.xml │ ├── toolbar_configuration.xml │ └── toolbar_tethering.xml │ ├── mipmap │ ├── banner.xml │ └── ic_launcher.xml │ ├── values-es │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-night │ ├── bools.xml │ └── colors.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-v29 │ ├── arrays.xml │ └── colors.xml │ ├── values-v30 │ └── bools.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── bools.xml │ ├── colors.xml │ ├── dimen.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── data_extraction_rules.xml │ ├── full_backup_content.xml │ ├── locales_config.xml │ ├── log_paths.xml │ └── pref_settings.xml └── settings.gradle.kts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | android: circleci/android@2.3.0 5 | 6 | jobs: 7 | test: 8 | executor: 9 | name: android/android-docker 10 | tag: 2024.11.1-ndk 11 | resource-class: large 12 | steps: 13 | - checkout 14 | - android/restore-gradle-cache 15 | - run: 16 | name: Run Build and Tests 17 | command: ./gradlew assembleDebug check --no-daemon 18 | - android/save-gradle-cache 19 | - store_artifacts: 20 | path: mobile/build/outputs/apk 21 | destination: apk 22 | - store_artifacts: 23 | path: mobile/build/reports 24 | destination: reports 25 | 26 | workflows: 27 | test: 28 | jobs: 29 | - test 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Mygod] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx/ 10 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") version "8.9.1" apply false 3 | id("com.github.ben-manes.versions") version "0.52.0" 4 | id("com.google.devtools.ksp") version "2.1.20-2.0.0" apply false 5 | id("org.jetbrains.kotlin.android") version "2.1.20" apply false 6 | } 7 | 8 | buildscript { 9 | dependencies { 10 | classpath("com.google.firebase:firebase-crashlytics-gradle:3.0.3") 11 | classpath("com.google.android.gms:oss-licenses-plugin:0.10.6") 12 | classpath("com.google.gms:google-services:4.4.2") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | comments: 2 | active: false 3 | 4 | complexity: 5 | LabeledExpression: 6 | active: false 7 | TooManyFunctions: 8 | ignoreDeprecated: true 9 | ignoreOverridden: true 10 | 11 | exceptions: 12 | TooGenericExceptionCaught: 13 | active: false 14 | 15 | formatting: 16 | CommentSpacing: 17 | active: false 18 | Indentation: 19 | active: false 20 | 21 | naming: 22 | MemberNameEqualsClassName: 23 | active: false 24 | 25 | style: 26 | MagicNumber: 27 | active: false 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableR8.fullMode=true 13 | android.enableResourceOptimizations=false 14 | android.injected.testOnly=false 15 | android.nonTransitiveRClass=true 16 | android.useAndroidX=true 17 | org.gradle.jvmargs=-Xmx1536m 18 | room.generateKotlin=true 19 | 20 | # When configured, Gradle will run in incubating parallel mode. 21 | # This option should only be used with decoupled projects. More details, visit 22 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 23 | # org.gradle.parallel=true 24 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/VPNHotspot/497795659eb72115549f49923b06fa076b877ffc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug 3 | release/ 4 | -------------------------------------------------------------------------------- /mobile/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "13108846109", 4 | "firebase_url": "https://mygod-vpnhotspot.firebaseio.com", 5 | "project_id": "mygod-vpnhotspot", 6 | "storage_bucket": "mygod-vpnhotspot.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:13108846109:android:63120dcb2e900ed0", 12 | "android_client_info": { 13 | "package_name": "be.mygod.vpnhotspot" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "13108846109-5iemp5pbapg7n5epbk0mv96007m9bhmg.apps.googleusercontent.com", 19 | "client_type": 3 20 | }, 21 | { 22 | "client_id": "13108846109-5iemp5pbapg7n5epbk0mv96007m9bhmg.apps.googleusercontent.com", 23 | "client_type": 3 24 | } 25 | ], 26 | "api_key": [ 27 | { 28 | "current_key": "AIzaSyADp0gTf25H6H3KxDRKwvp0EaWq_XvYUmg" 29 | } 30 | ], 31 | "services": { 32 | "analytics_service": { 33 | "status": 1 34 | }, 35 | "appinvite_service": { 36 | "status": 1, 37 | "other_platform_oauth_client": [] 38 | }, 39 | "ads_service": { 40 | "status": 2 41 | } 42 | } 43 | } 44 | ], 45 | "configuration_version": "1" 46 | } -------------------------------------------------------------------------------- /mobile/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | -keepattributes SourceFile,LineNumberTable 18 | -dontobfuscate 19 | 20 | # If you keep the line number information, uncomment this to 21 | # hide the original source file name. 22 | #-renamesourcefileattribute SourceFile 23 | -dontwarn lombok.Generated 24 | -dontwarn org.slf4j.impl.StaticLoggerBinder 25 | -dontwarn org.xbill.DNS.spi.DnsjavaInetAddressResolverProvider 26 | -dontwarn sun.net.spi.nameservice.NameServiceDescriptor 27 | -------------------------------------------------------------------------------- /mobile/src/androidTest/java/be/mygod/vpnhotspot/room/MigrationTest.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.room 2 | 3 | import androidx.room.testing.MigrationTestHelper 4 | import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import java.io.IOException 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class MigrationTest { 14 | companion object { 15 | private const val TEST_DB = "migration-test" 16 | } 17 | 18 | @get:Rule 19 | val privateDatabase = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), 20 | AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) 21 | 22 | @Test 23 | @Throws(IOException::class) 24 | fun migrate2() { 25 | val db = privateDatabase.createDatabase(TEST_DB, 1) 26 | db.close() 27 | privateDatabase.runMigrationsAndValidate(TEST_DB, 2, true, AppDatabase.Migration2) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mobile/src/freedom/java/be/mygod/vpnhotspot/util/UpdateChecker.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | object UpdateChecker { 4 | fun check() = null 5 | } 6 | -------------------------------------------------------------------------------- /mobile/src/google/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 11 | 12 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /mobile/src/google/java/be/mygod/vpnhotspot/util/UpdateChecker.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.net.Uri 4 | 5 | object UpdateChecker { 6 | fun check() = Uri.parse("https://github.com/Mygod/VPNHotspot/discussions/643") 7 | } 8 | -------------------------------------------------------------------------------- /mobile/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html. 4 | # For more examples on how to use CMake, see https://github.com/android/ndk-samples. 5 | 6 | # Sets the minimum CMake version required for this project. 7 | cmake_minimum_required(VERSION 3.22.1) 8 | 9 | # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, 10 | # Since this is the top level CMakeLists.txt, the project name is also accessible 11 | # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level 12 | # build script scope). 13 | project("vpnhotspot") 14 | 15 | # Creates and names a library, sets it as either STATIC 16 | # or SHARED, and provides the relative paths to its source code. 17 | # You can define multiple libraries, and CMake builds them for you. 18 | # Gradle automatically packages shared libraries with your APK. 19 | # 20 | # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define 21 | # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} 22 | # is preferred for the same purpose. 23 | # 24 | # In order to load a library into your app from Java/Kotlin, you must call 25 | # System.loadLibrary() and pass the name of the library defined here; 26 | # for GameActivity/NativeActivity derived applications, the same library name must be 27 | # used in the AndroidManifest.xml file. 28 | add_library(${CMAKE_PROJECT_NAME} SHARED 29 | # List C/C++ source files with relative paths to this CMakeLists.txt. 30 | root.cpp) 31 | 32 | # Specifies libraries CMake should link to your target library. You 33 | # can link libraries from various origins, such as libraries defined in this 34 | # build script, prebuilt third-party libraries, or Android system libraries. 35 | target_link_libraries(${CMAKE_PROJECT_NAME} 36 | # List libraries link to the target library 37 | android 38 | log) 39 | -------------------------------------------------------------------------------- /mobile/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mygod/VPNHotspot/497795659eb72115549f49923b06fa076b877ffc/mobile/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/AlertDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot 2 | 3 | import android.app.Activity 4 | import android.content.DialogInterface 5 | import android.os.Bundle 6 | import android.os.Parcelable 7 | import androidx.appcompat.app.AlertDialog 8 | import androidx.appcompat.app.AppCompatDialogFragment 9 | import androidx.core.os.bundleOf 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.setFragmentResult 12 | import androidx.fragment.app.setFragmentResultListener 13 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 14 | import kotlinx.parcelize.Parcelize 15 | 16 | /** 17 | * Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java 18 | */ 19 | abstract class AlertDialogFragment : 20 | AppCompatDialogFragment(), DialogInterface.OnClickListener { 21 | companion object { 22 | private const val KEY_RESULT = "result" 23 | private const val KEY_ARG = "arg" 24 | private const val KEY_RET = "ret" 25 | private const val KEY_WHICH = "which" 26 | 27 | fun setResultListener(fragment: Fragment, requestKey: String, 28 | listener: (Int, Ret?) -> Unit) { 29 | fragment.setFragmentResultListener(requestKey) { _, bundle -> 30 | listener(bundle.getInt(KEY_WHICH, Activity.RESULT_CANCELED), bundle.getParcelable(KEY_RET)) 31 | } 32 | } 33 | inline fun , Ret : Parcelable> setResultListener( 34 | fragment: Fragment, noinline listener: (Int, Ret?) -> Unit) = 35 | setResultListener(fragment, T::class.java.name, listener) 36 | } 37 | protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) 38 | 39 | private val resultKey get() = requireArguments().getString(KEY_RESULT) 40 | protected val arg by lazy { requireArguments().getParcelable(KEY_ARG)!! } 41 | protected open val ret: Ret? get() = null 42 | 43 | private fun args() = arguments ?: Bundle().also { arguments = it } 44 | fun arg(arg: Arg) = args().putParcelable(KEY_ARG, arg) 45 | fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey) 46 | 47 | override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = 48 | MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create() 49 | 50 | override fun onClick(dialog: DialogInterface?, which: Int) { 51 | setFragmentResult(resultKey ?: return, Bundle().apply { 52 | putInt(KEY_WHICH, which) 53 | putParcelable(KEY_RET, ret ?: return@apply) 54 | }) 55 | } 56 | 57 | override fun onDismiss(dialog: DialogInterface) { 58 | super.onDismiss(dialog) 59 | setFragmentResult(resultKey ?: return, bundleOf(KEY_WHICH to Activity.RESULT_CANCELED)) 60 | } 61 | } 62 | 63 | @Parcelize 64 | class Empty : Parcelable 65 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/IpNeighbourMonitoringService.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot 2 | 3 | import android.app.Service 4 | import be.mygod.vpnhotspot.net.IpNeighbour 5 | import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor 6 | import java.net.Inet4Address 7 | 8 | abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback { 9 | private var neighbours: Collection = emptyList() 10 | 11 | protected abstract val activeIfaces: List 12 | protected open val inactiveIfaces get() = emptyList() 13 | 14 | override fun onIpNeighbourAvailable(neighbours: Collection) { 15 | this.neighbours = neighbours 16 | updateNotification() 17 | } 18 | protected open fun updateNotification() { 19 | val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) -> 20 | neighbours 21 | .filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID } 22 | .distinctBy { it.lladdr } 23 | .size 24 | } 25 | ServiceNotification.startForeground(this, activeIfaces.associateWith { sizeLookup[it] ?: 0 }, inactiveIfaces, 26 | false) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/StaticIpSetter.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot 2 | 3 | import android.content.Context 4 | import androidx.core.content.edit 5 | import be.mygod.vpnhotspot.App.Companion.app 6 | import be.mygod.vpnhotspot.net.Routing 7 | import be.mygod.vpnhotspot.root.RoutingCommands 8 | import be.mygod.vpnhotspot.util.Event0 9 | import be.mygod.vpnhotspot.util.RootSession 10 | import be.mygod.vpnhotspot.widget.SmartSnackbar 11 | import kotlinx.coroutines.CancellationException 12 | import kotlinx.coroutines.GlobalScope 13 | import kotlinx.coroutines.launch 14 | import kotlinx.parcelize.Parcelize 15 | import timber.log.Timber 16 | import java.io.IOException 17 | import java.net.NetworkInterface 18 | import java.net.SocketException 19 | import java.security.SecureRandom 20 | 21 | @Parcelize 22 | class StaticIpSetter : BootReceiver.Startable { 23 | companion object { 24 | private const val IFACE = "staticip" 25 | private const val KEY = "service.staticIp" 26 | 27 | val ifaceEvent = Event0() 28 | 29 | val iface get() = try { 30 | NetworkInterface.getByName(IFACE) 31 | } catch (_: SocketException) { 32 | null 33 | } catch (e: Exception) { 34 | Timber.w(e) 35 | null 36 | } 37 | 38 | var ips: String 39 | get() { 40 | app.pref.getString(KEY, null)?.let { return it } 41 | val octets = ByteArray(3) 42 | SecureRandom.getInstanceStrong().nextBytes(octets) 43 | return "10.${octets.joinToString(".") { it.toUByte().toString() }}".also { ips = it } 44 | } 45 | set(value) = app.pref.edit { putString(KEY, value) } 46 | 47 | fun enable(enabled: Boolean) = GlobalScope.launch { 48 | val success = try { 49 | RootSession.use { 50 | try { 51 | if (enabled) { 52 | it.exec("${Routing.IP} link add $IFACE type dummy") 53 | ips.lineSequence().forEach { ip -> 54 | it.exec("${Routing.IP} addr add $ip dev $IFACE") 55 | } 56 | it.exec("${Routing.IP} link set $IFACE up") 57 | true 58 | } else { 59 | it.exec("${Routing.IP} link del $IFACE") 60 | false 61 | } 62 | } catch (e: RoutingCommands.UnexpectedOutputException) { 63 | if (Routing.shouldSuppressIpError(e, enabled)) return@use null 64 | Timber.w(IOException("Failed to modify link", e)) 65 | SmartSnackbar.make(e).show() 66 | null 67 | } 68 | } 69 | } catch (_: CancellationException) { 70 | null 71 | } catch (e: Exception) { 72 | Timber.w(e) 73 | SmartSnackbar.make(e).show() 74 | null 75 | } 76 | when (success) { 77 | true -> BootReceiver.add(StaticIpSetter()) 78 | false -> BootReceiver.delete() 79 | null -> { } 80 | } 81 | ifaceEvent() 82 | } 83 | } 84 | 85 | override fun start(context: Context) { 86 | enable(true) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/client/Client.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.client 2 | 3 | import android.net.MacAddress 4 | import android.os.SystemClock 5 | import android.text.SpannableStringBuilder 6 | import android.text.Spanned 7 | import android.text.style.StrikethroughSpan 8 | import androidx.lifecycle.map 9 | import androidx.recyclerview.widget.DiffUtil 10 | import be.mygod.vpnhotspot.App.Companion.app 11 | import be.mygod.vpnhotspot.R 12 | import be.mygod.vpnhotspot.net.InetAddressComparator 13 | import be.mygod.vpnhotspot.net.IpNeighbour 14 | import be.mygod.vpnhotspot.net.TetherType 15 | import be.mygod.vpnhotspot.room.AppDatabase 16 | import be.mygod.vpnhotspot.room.ClientRecord 17 | import be.mygod.vpnhotspot.util.formatTimestamp 18 | import be.mygod.vpnhotspot.util.makeIpSpan 19 | import be.mygod.vpnhotspot.util.makeMacSpan 20 | import java.net.InetAddress 21 | import java.util.Objects 22 | import java.util.TreeMap 23 | 24 | class Client(val mac: MacAddress, val iface: String? = null, val type: TetherType = TetherType.ofInterface(iface)) { 25 | companion object DiffCallback : DiffUtil.ItemCallback() { 26 | override fun areItemsTheSame(oldItem: Client, newItem: Client) = 27 | oldItem.iface == newItem.iface && oldItem.type == newItem.type && oldItem.mac == newItem.mac 28 | override fun areContentsTheSame(oldItem: Client, newItem: Client) = oldItem == newItem 29 | } 30 | 31 | val ip = TreeMap(InetAddressComparator) 32 | val macString by lazy { mac.toString() } 33 | private val record = AppDatabase.instance.clientRecordDao.lookupOrDefaultSync(mac) 34 | private val macIface get() = SpannableStringBuilder(makeMacSpan(macString)).apply { 35 | iface?.let { 36 | append('%') 37 | append(it) 38 | } 39 | } 40 | 41 | val nickname get() = record.value?.nickname ?: "" 42 | val blocked get() = record.value?.blocked == true 43 | 44 | val icon get() = type.icon 45 | val title = record.map { record -> 46 | /** 47 | * we hijack the get title process to check if we need to perform MacLookup, 48 | * as record might not be initialized in other more appropriate places 49 | */ 50 | SpannableStringBuilder(record.nickname.ifEmpty { 51 | if (record.macLookupPending) MacLookup.perform(mac) 52 | macIface 53 | }).apply { 54 | if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) 55 | } 56 | } 57 | val titleSelectable = record.map { it.nickname.isEmpty() } 58 | val description = record.map { record -> 59 | SpannableStringBuilder().apply { 60 | if (record.nickname.isNotEmpty()) appendLine(macIface) 61 | ip.entries.forEach { (ip, info) -> 62 | append(makeIpSpan(ip)) 63 | info.address?.let { append("/${it.prefixLength}") } 64 | append(when (info.state) { 65 | IpNeighbour.State.UNSET -> "" 66 | IpNeighbour.State.INCOMPLETE -> app.getText(R.string.connected_state_incomplete) 67 | IpNeighbour.State.VALID -> app.getText(R.string.connected_state_valid) 68 | IpNeighbour.State.FAILED -> app.getText(R.string.connected_state_failed) 69 | else -> error("Invalid IpNeighbour.State: ${info.state}") 70 | }) 71 | if (info.address != null) { 72 | info.hostname?.let { append(" →“$it”") } 73 | val delta = System.currentTimeMillis() - SystemClock.elapsedRealtime() 74 | append(" ⏳${app.formatTimestamp(info.deprecationTime + delta)}") 75 | } 76 | appendLine() 77 | } 78 | }.trimEnd() 79 | } 80 | 81 | fun obtainRecord() = record.value ?: ClientRecord(mac) 82 | 83 | override fun equals(other: Any?): Boolean { 84 | if (this === other) return true 85 | if (other !is Client) return false 86 | 87 | if (iface != other.iface) return false 88 | if (mac != other.mac) return false 89 | if (type != other.type) return false 90 | if (ip != other.ip) return false 91 | 92 | return true 93 | } 94 | override fun hashCode() = Objects.hash(iface, mac, type, ip) 95 | } 96 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/client/ClientAddressInfo.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.client 2 | 3 | import android.net.LinkAddress 4 | import be.mygod.vpnhotspot.net.IpNeighbour 5 | 6 | data class ClientAddressInfo(var state: IpNeighbour.State = IpNeighbour.State.UNSET, 7 | val address: LinkAddress? = null, val hostname: String? = null) { 8 | companion object { 9 | private val getDeprecationTime by lazy { LinkAddress::class.java.getDeclaredMethod("getDeprecationTime") } 10 | private val getExpirationTime by lazy { LinkAddress::class.java.getDeclaredMethod("getExpirationTime") } 11 | } 12 | val deprecationTime get() = getDeprecationTime(address) as Long 13 | val expirationTime get() = getExpirationTime(address) as Long 14 | } 15 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/Data.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import androidx.databinding.BaseObservable 4 | 5 | abstract class Data : BaseObservable() { 6 | abstract val icon: Int 7 | abstract val title: CharSequence 8 | abstract val text: CharSequence 9 | abstract val active: Boolean 10 | open val selectable get() = true 11 | } 12 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/InterfaceManager.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.content.Intent 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | import be.mygod.vpnhotspot.R 7 | import be.mygod.vpnhotspot.TetheringService 8 | import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding 9 | import be.mygod.vpnhotspot.net.TetherType 10 | import be.mygod.vpnhotspot.util.formatAddresses 11 | import java.util.* 12 | 13 | class InterfaceManager(private val parent: TetheringFragment, val iface: String) : Manager() { 14 | class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root), 15 | View.OnClickListener { 16 | init { 17 | itemView.setOnClickListener(this) 18 | } 19 | 20 | lateinit var iface: String 21 | 22 | override fun onClick(view: View) { 23 | val context = itemView.context 24 | val data = binding.data as Data 25 | if (data.active) context.startService(Intent(context, TetheringService::class.java) 26 | .putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface)) 27 | else context.startForegroundService(Intent(context, TetheringService::class.java) 28 | .putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface))) 29 | } 30 | } 31 | private inner class Data : be.mygod.vpnhotspot.manage.Data() { 32 | override val icon get() = TetherType.ofInterface(iface).icon 33 | override val title get() = if (parent.binder?.monitored(iface) == true) { 34 | parent.getString(R.string.tethering_state_monitored, iface) 35 | } else iface 36 | override val text get() = addresses 37 | override val active get() = parent.binder?.isActive(iface) == true 38 | } 39 | 40 | private val addresses = parent.ifaceLookup[iface] ?.formatAddresses(parent.binder?.isInactive(iface) == true) ?: "" 41 | override val type get() = VIEW_TYPE_INTERFACE 42 | private val data = Data() 43 | 44 | override fun bindTo(viewHolder: RecyclerView.ViewHolder) { 45 | viewHolder as ViewHolder 46 | viewHolder.binding.data = data 47 | viewHolder.iface = iface 48 | } 49 | 50 | override fun isSameItemAs(other: Manager) = when (other) { 51 | is InterfaceManager -> iface == other.iface 52 | else -> false 53 | } 54 | 55 | override fun equals(other: Any?): Boolean { 56 | if (this === other) return true 57 | if (javaClass != other?.javaClass) return false 58 | other as InterfaceManager 59 | if (iface != other.iface) return false 60 | if (addresses != other.addresses) return false 61 | return true 62 | } 63 | override fun hashCode(): Int = Objects.hash(iface, addresses) 64 | } 65 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/IpNeighbourMonitoringTileService.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.service.quicksettings.Tile 4 | import be.mygod.vpnhotspot.R 5 | import be.mygod.vpnhotspot.net.IpNeighbour 6 | import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor 7 | import be.mygod.vpnhotspot.util.KillableTileService 8 | import java.net.Inet4Address 9 | 10 | abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback { 11 | private var neighbours: Collection = emptyList() 12 | abstract fun updateTile() 13 | 14 | override fun onStartListening() { 15 | super.onStartListening() 16 | IpNeighbourMonitor.registerCallback(this) 17 | } 18 | 19 | override fun onStopListening() { 20 | IpNeighbourMonitor.unregisterCallback(this) 21 | super.onStopListening() 22 | } 23 | 24 | protected fun Tile.subtitleDevices(filter: (String) -> Boolean) { 25 | val size = neighbours 26 | .filter { it.ip is Inet4Address && it.state == IpNeighbour.State.VALID && filter(it.dev) } 27 | .distinctBy { it.lladdr } 28 | .size 29 | if (size > 0) subtitle(resources.getQuantityString( 30 | R.plurals.quick_settings_hotspot_secondary_label_num_devices, size, size)) 31 | } 32 | 33 | override fun onIpNeighbourAvailable(neighbours: Collection) { 34 | this.neighbours = neighbours 35 | updateTile() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotManager.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.Manifest 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.ServiceConnection 7 | import android.os.Build 8 | import android.os.IBinder 9 | import android.view.View 10 | import androidx.recyclerview.widget.RecyclerView 11 | import be.mygod.vpnhotspot.App.Companion.app 12 | import be.mygod.vpnhotspot.LocalOnlyHotspotService 13 | import be.mygod.vpnhotspot.R 14 | import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding 15 | import be.mygod.vpnhotspot.util.ServiceForegroundConnector 16 | import be.mygod.vpnhotspot.util.formatAddresses 17 | import java.net.NetworkInterface 18 | 19 | class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection { 20 | companion object { 21 | val permission = when { 22 | Build.VERSION.SDK_INT >= 33 -> Manifest.permission.NEARBY_WIFI_DEVICES 23 | Build.VERSION.SDK_INT >= 29 -> Manifest.permission.ACCESS_FINE_LOCATION 24 | else -> Manifest.permission.ACCESS_COARSE_LOCATION 25 | } 26 | } 27 | 28 | class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root), 29 | View.OnClickListener { 30 | init { 31 | itemView.setOnClickListener(this) 32 | } 33 | 34 | lateinit var manager: LocalOnlyHotspotManager 35 | 36 | override fun onClick(view: View) { 37 | val binder = manager.binder 38 | if (binder?.iface == null) manager.parent.startLocalOnlyHotspot.launch(permission) else binder.stop() 39 | } 40 | } 41 | private inner class Data : be.mygod.vpnhotspot.manage.Data() { 42 | private val lookup: Map get() = parent.ifaceLookup 43 | 44 | override val icon get() = R.drawable.ic_action_perm_scan_wifi 45 | override val title: CharSequence get() = parent.getString(R.string.tethering_temp_hotspot) 46 | override val text: CharSequence get() { 47 | return lookup[binder?.iface ?: return ""]?.formatAddresses() ?: "" 48 | } 49 | override val active get() = binder?.iface != null 50 | override val selectable get() = active 51 | } 52 | 53 | init { 54 | ServiceForegroundConnector(parent, this, LocalOnlyHotspotService::class) 55 | } 56 | 57 | fun start(context: Context) = app.startServiceWithLocation(context) 58 | 59 | override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT 60 | private val data = Data() 61 | internal var binder: LocalOnlyHotspotService.Binder? = null 62 | 63 | override fun bindTo(viewHolder: RecyclerView.ViewHolder) { 64 | viewHolder as ViewHolder 65 | viewHolder.binding.data = data 66 | viewHolder.manager = this 67 | } 68 | 69 | fun update() = data.notifyChange() 70 | 71 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 72 | binder = service as LocalOnlyHotspotService.Binder 73 | service.ifaceChanged[this] = { data.notifyChange() } 74 | } 75 | 76 | override fun onServiceDisconnected(name: ComponentName?) { 77 | binder?.ifaceChanged?.remove(this) 78 | binder = null 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/LocalOnlyHotspotTileService.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.drawable.Icon 7 | import android.os.IBinder 8 | import android.service.quicksettings.Tile 9 | import be.mygod.vpnhotspot.LocalOnlyHotspotService 10 | import be.mygod.vpnhotspot.R 11 | import be.mygod.vpnhotspot.util.stopAndUnbind 12 | 13 | class LocalOnlyHotspotTileService : IpNeighbourMonitoringTileService() { 14 | private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_perm_scan_wifi) } 15 | 16 | private var binder: LocalOnlyHotspotService.Binder? = null 17 | 18 | override fun onStartListening() { 19 | super.onStartListening() 20 | bindService(Intent(this, LocalOnlyHotspotService::class.java), this, Context.BIND_AUTO_CREATE) 21 | } 22 | 23 | override fun onStopListening() { 24 | stopAndUnbind(this) 25 | super.onStopListening() 26 | } 27 | 28 | override fun updateTile() { 29 | val binder = binder ?: return 30 | qsTile?.run { 31 | icon = tile 32 | subtitle(null) 33 | val iface = binder.iface 34 | if (iface.isNullOrEmpty()) { 35 | state = Tile.STATE_INACTIVE 36 | label = getText(R.string.tethering_temp_hotspot) 37 | } else { 38 | state = Tile.STATE_ACTIVE 39 | label = binder.configuration?.ssid?.toString() ?: getText(R.string.tethering_temp_hotspot) 40 | subtitleDevices { it == iface } 41 | } 42 | updateTile() 43 | } 44 | } 45 | 46 | override fun onClick() { 47 | val binder = binder 48 | when { 49 | binder == null -> tapPending = true 50 | binder.iface == null -> { 51 | LocalOnlyHotspotService.dismissHandle = dismissHandle 52 | startForegroundServiceCompat(Intent(this, LocalOnlyHotspotService::class.java)) 53 | } 54 | else -> binder.stop() 55 | } 56 | } 57 | 58 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 59 | binder = service as LocalOnlyHotspotService.Binder 60 | service.ifaceChanged[this] = { updateTile() } 61 | super.onServiceConnected(name, service) 62 | } 63 | 64 | override fun onServiceDisconnected(name: ComponentName?) { 65 | binder?.ifaceChanged?.remove(this) 66 | binder = null 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/ManageBar.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.content.Intent 4 | import android.view.View 5 | import androidx.databinding.BaseObservable 6 | import androidx.recyclerview.widget.RecyclerView 7 | import be.mygod.vpnhotspot.App.Companion.app 8 | import be.mygod.vpnhotspot.databinding.ListitemManageBinding 9 | import be.mygod.vpnhotspot.net.TetherOffloadManager 10 | 11 | object ManageBar : Manager() { 12 | private const val TAG = "ManageBar" 13 | private const val SETTINGS_PACKAGE = "com.android.settings" 14 | private const val SETTINGS_1 = "com.android.settings.Settings\$TetherSettingsActivity" 15 | private const val SETTINGS_2 = "com.android.settings.TetherSettings" 16 | 17 | object Data : BaseObservable() { 18 | val offloadEnabled get() = TetherOffloadManager.enabled 19 | } 20 | class ViewHolder(binding: ListitemManageBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener { 21 | init { 22 | binding.data = Data 23 | binding.root.setOnClickListener(this) 24 | } 25 | 26 | override fun onClick(v: View?) = start(itemView.context::startActivity) 27 | } 28 | 29 | override val type: Int get() = VIEW_TYPE_MANAGE 30 | 31 | fun start(startActivity: (Intent) -> Unit) { 32 | val intent = Intent().setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 33 | try { 34 | startActivity(intent.setClassName(SETTINGS_PACKAGE, SETTINGS_1)) 35 | } catch (e1: RuntimeException) { 36 | try { 37 | startActivity(intent.setClassName(SETTINGS_PACKAGE, SETTINGS_2)) 38 | app.logEvent(TAG) { param(SETTINGS_1, e1.toString()) } 39 | } catch (e2: RuntimeException) { 40 | app.logEvent(TAG) { 41 | param(SETTINGS_1, e1.toString()) 42 | param(SETTINGS_2, e2.toString()) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding 9 | import be.mygod.vpnhotspot.databinding.ListitemManageBinding 10 | import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding 11 | import be.mygod.vpnhotspot.databinding.ListitemStaticIpBinding 12 | 13 | abstract class Manager { 14 | companion object DiffCallback : DiffUtil.ItemCallback() { 15 | const val VIEW_TYPE_INTERFACE = 0 16 | const val VIEW_TYPE_MANAGE = 1 17 | const val VIEW_TYPE_WIFI = 2 18 | const val VIEW_TYPE_USB = 3 19 | const val VIEW_TYPE_BLUETOOTH = 4 20 | const val VIEW_TYPE_ETHERNET = 8 21 | const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6 22 | const val VIEW_TYPE_REPEATER = 7 23 | const val VIEW_TYPE_STATIC_IP = 9 24 | 25 | override fun areItemsTheSame(oldItem: Manager, newItem: Manager) = oldItem.isSameItemAs(newItem) 26 | @SuppressLint("DiffUtilEquals") 27 | override fun areContentsTheSame(oldItem: Manager, newItem: Manager) = oldItem === newItem 28 | 29 | fun createViewHolder(inflater: LayoutInflater, parent: ViewGroup, type: Int) = when (type) { 30 | VIEW_TYPE_INTERFACE -> 31 | InterfaceManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false)) 32 | VIEW_TYPE_MANAGE -> ManageBar.ViewHolder(ListitemManageBinding.inflate(inflater, parent, false)) 33 | VIEW_TYPE_WIFI, 34 | VIEW_TYPE_USB, 35 | VIEW_TYPE_BLUETOOTH, 36 | VIEW_TYPE_ETHERNET -> { 37 | TetherManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false)) 38 | } 39 | VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> { 40 | LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false)) 41 | } 42 | VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false)) 43 | VIEW_TYPE_STATIC_IP -> StaticIpManager.ViewHolder(ListitemStaticIpBinding.inflate(inflater, parent, false)) 44 | else -> throw IllegalArgumentException("Invalid view type") 45 | } 46 | } 47 | 48 | abstract val type: Int 49 | 50 | open fun bindTo(viewHolder: RecyclerView.ViewHolder) { } 51 | 52 | open fun isSameItemAs(other: Manager) = javaClass == other.javaClass 53 | } 54 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/RepeaterTileService.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.drawable.Icon 7 | import android.net.wifi.p2p.WifiP2pGroup 8 | import android.os.IBinder 9 | import android.service.quicksettings.Tile 10 | import be.mygod.vpnhotspot.R 11 | import be.mygod.vpnhotspot.RepeaterService 12 | import be.mygod.vpnhotspot.util.KillableTileService 13 | import be.mygod.vpnhotspot.util.Services 14 | import be.mygod.vpnhotspot.util.stopAndUnbind 15 | 16 | class RepeaterTileService : KillableTileService() { 17 | private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) } 18 | 19 | private var binder: RepeaterService.Binder? = null 20 | 21 | override fun onStartListening() { 22 | super.onStartListening() 23 | if (Services.p2p != null) { 24 | bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE) 25 | } else updateTile() 26 | } 27 | 28 | override fun onStopListening() { 29 | if (Services.p2p != null) stopAndUnbind(this) 30 | super.onStopListening() 31 | } 32 | 33 | override fun onClick() { 34 | val binder = binder 35 | if (binder == null) tapPending = true else when (binder.service.status) { 36 | RepeaterService.Status.ACTIVE -> { 37 | RepeaterService.dismissHandle = dismissHandle 38 | binder.shutdown() 39 | } 40 | RepeaterService.Status.IDLE -> { 41 | RepeaterService.dismissHandle = dismissHandle 42 | startForegroundServiceCompat(Intent(this, RepeaterService::class.java)) 43 | } 44 | else -> { } 45 | } 46 | } 47 | 48 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 49 | binder = service as RepeaterService.Binder 50 | service.statusChanged[this] = { updateTile() } 51 | service.groupChanged[this] = this::updateTile 52 | super.onServiceConnected(name, service) 53 | } 54 | 55 | override fun onServiceDisconnected(name: ComponentName?) { 56 | val binder = binder ?: return 57 | this.binder = null 58 | binder.statusChanged -= this 59 | binder.groupChanged -= this 60 | } 61 | 62 | private fun updateTile(group: WifiP2pGroup? = binder?.group) { 63 | qsTile?.run { 64 | subtitle(null) 65 | when ((binder ?: return).service.status) { 66 | RepeaterService.Status.IDLE -> { 67 | state = Tile.STATE_INACTIVE 68 | label = getText(R.string.title_repeater) 69 | } 70 | RepeaterService.Status.ACTIVE -> { 71 | state = Tile.STATE_ACTIVE 72 | label = group?.networkName 73 | val size = group?.clientList?.size ?: 0 74 | if (size > 0) subtitle(resources.getQuantityString( 75 | R.plurals.quick_settings_hotspot_secondary_label_num_devices, size, size)) 76 | } 77 | else -> { // STARTING or DESTROYED, which should never occur 78 | state = Tile.STATE_UNAVAILABLE 79 | label = getText(R.string.title_repeater) 80 | } 81 | } 82 | icon = tile 83 | updateTile() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/manage/StaticIpManager.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.manage 2 | 3 | import android.content.DialogInterface 4 | import android.os.Bundle 5 | import android.os.Parcelable 6 | import android.text.method.LinkMovementMethod 7 | import android.view.WindowManager 8 | import android.widget.EditText 9 | import androidx.appcompat.app.AlertDialog 10 | import androidx.databinding.BaseObservable 11 | import androidx.databinding.Bindable 12 | import androidx.lifecycle.DefaultLifecycleObserver 13 | import androidx.lifecycle.LifecycleOwner 14 | import androidx.recyclerview.widget.RecyclerView 15 | import be.mygod.vpnhotspot.AlertDialogFragment 16 | import be.mygod.vpnhotspot.BR 17 | import be.mygod.vpnhotspot.R 18 | import be.mygod.vpnhotspot.StaticIpSetter 19 | import be.mygod.vpnhotspot.databinding.ListitemStaticIpBinding 20 | import be.mygod.vpnhotspot.util.formatAddresses 21 | import be.mygod.vpnhotspot.util.showAllowingStateLoss 22 | import kotlinx.parcelize.Parcelize 23 | 24 | class StaticIpManager(private val parent: TetheringFragment) : Manager(), DefaultLifecycleObserver { 25 | class ViewHolder(val binding: ListitemStaticIpBinding) : RecyclerView.ViewHolder(binding.root) { 26 | init { 27 | binding.text.movementMethod = LinkMovementMethod.getInstance() 28 | } 29 | } 30 | 31 | inner class Data : BaseObservable() { 32 | private var iface = StaticIpSetter.iface 33 | val active: Boolean @Bindable get() = iface != null 34 | val addresses: CharSequence @Bindable get() = iface?.formatAddresses() ?: "" 35 | 36 | fun onChanged() { 37 | iface = StaticIpSetter.iface 38 | notifyPropertyChanged(BR.serviceStarted) 39 | notifyPropertyChanged(BR.addresses) 40 | } 41 | 42 | fun configure() = ConfigureDialogFragment().apply { 43 | key() 44 | arg(ConfigureData(StaticIpSetter.ips)) 45 | }.showAllowingStateLoss(parent.parentFragmentManager) 46 | 47 | fun toggle() { 48 | StaticIpSetter.enable(!active) 49 | onChanged() 50 | } 51 | } 52 | 53 | @Parcelize 54 | data class ConfigureData(val ips: String) : Parcelable 55 | class ConfigureDialogFragment : AlertDialogFragment() { 56 | override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { 57 | setTitle(R.string.tethering_static_ip) 58 | setView(R.layout.dialog_static_ip) 59 | setPositiveButton(android.R.string.ok, listener) 60 | setNegativeButton(android.R.string.cancel, null) 61 | } 62 | 63 | override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply { 64 | create() 65 | findViewById(android.R.id.edit)!!.setText(arg.ips) 66 | window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) 67 | } 68 | 69 | override val ret get() = ConfigureData(dialog!!.findViewById(android.R.id.edit)!!.text!!.toString()) 70 | } 71 | 72 | override val type get() = VIEW_TYPE_STATIC_IP 73 | private val data = Data() 74 | 75 | init { 76 | parent.viewLifecycleOwner.lifecycle.addObserver(this) 77 | AlertDialogFragment.setResultListener(parent) { which, ret -> 78 | if (which == DialogInterface.BUTTON_POSITIVE) StaticIpSetter.ips = ret!!.ips.trim() 79 | } 80 | } 81 | 82 | override fun onCreate(owner: LifecycleOwner) { 83 | StaticIpSetter.ifaceEvent[this] = data::onChanged 84 | } 85 | 86 | override fun bindTo(viewHolder: RecyclerView.ViewHolder) { 87 | (viewHolder as ViewHolder).binding.data = data 88 | } 89 | 90 | override fun onDestroy(owner: LifecycleOwner) { 91 | StaticIpSetter.ifaceEvent -= this 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/DhcpWorkaround.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net 2 | 3 | import android.content.SharedPreferences 4 | import be.mygod.vpnhotspot.App.Companion.app 5 | import be.mygod.vpnhotspot.net.Routing.Companion.IP 6 | import be.mygod.vpnhotspot.root.RoutingCommands 7 | import be.mygod.vpnhotspot.util.RootSession 8 | import be.mygod.vpnhotspot.widget.SmartSnackbar 9 | import kotlinx.coroutines.CancellationException 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import timber.log.Timber 13 | import java.io.IOException 14 | 15 | /** 16 | * Assuming RULE_PRIORITY_VPN_OUTPUT_TO_LOCAL = 11000. 17 | * Normally this is used to forward packets from remote to local, but it works anyways. 18 | * It just needs to be before RULE_PRIORITY_SECURE_VPN = 12000. 19 | * It would be great if we can gain better understanding into why this is only needed on some of the devices but not 20 | * others. 21 | * 22 | * Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#57 23 | */ 24 | object DhcpWorkaround : SharedPreferences.OnSharedPreferenceChangeListener { 25 | private const val KEY_ENABLED = "service.dhcpWorkaround" 26 | 27 | init { 28 | app.pref.registerOnSharedPreferenceChangeListener(this) 29 | } 30 | 31 | val shouldEnable get() = app.pref.getBoolean(KEY_ENABLED, false) 32 | fun enable(enabled: Boolean) = GlobalScope.launch { 33 | val action = if (enabled) "add" else "del" 34 | try { 35 | RootSession.use { 36 | try { 37 | // ROUTE_TABLE_LOCAL_NETWORK: https://cs.android.com/android/platform/superproject/+/master:system/netd/server/RouteController.cpp;l=74;drc=b6dc40ac3d566d952d8445fc6ac796109c0cbc87 38 | it.exec("$IP rule $action iif lo uidrange 0-0 lookup 97 priority 11000") 39 | } catch (e: RoutingCommands.UnexpectedOutputException) { 40 | if (Routing.shouldSuppressIpError(e, enabled)) return@use 41 | Timber.w(IOException("Failed to tweak dhcp workaround rule", e)) 42 | SmartSnackbar.make(e).show() 43 | } 44 | } 45 | } catch (_: CancellationException) { 46 | } catch (e: Exception) { 47 | Timber.w(e) 48 | SmartSnackbar.make(e).show() 49 | } 50 | } 51 | 52 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 53 | if (key == KEY_ENABLED) enable(shouldEnable) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/InetAddressComparator.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net 2 | 3 | import java.net.InetAddress 4 | 5 | object InetAddressComparator : Comparator { 6 | override fun compare(o1: InetAddress?, o2: InetAddress?): Int { 7 | if (o1 == null && o2 == null) return 0 8 | val a1 = o1?.address 9 | val a2 = o2?.address 10 | val r = (a1?.size ?: 0).compareTo(a2?.size ?: 0) 11 | return if (r == 0) a1!!.zip(a2!!).map { (l, r) -> l - r }.find { it != 0 } ?: 0 else r 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/MacAddressCompat.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net 2 | 3 | import android.net.MacAddress 4 | import java.nio.ByteBuffer 5 | import java.nio.ByteOrder 6 | 7 | /** 8 | * This used to be a compat support class for [MacAddress]. 9 | * Now it is just a convenient class for backwards compatibility. 10 | */ 11 | @JvmInline 12 | value class MacAddressCompat(val addr: Long) { 13 | companion object { 14 | /** 15 | * The MacAddress zero MAC address. 16 | * 17 | * Not publicly exposed or treated specially since the OUI 00:00:00 is registered. 18 | */ 19 | val ALL_ZEROS_ADDRESS = MacAddress.fromBytes(byteArrayOf(0, 0, 0, 0, 0, 0)) 20 | val ANY_ADDRESS = MacAddress.fromBytes(byteArrayOf(2, 0, 0, 0, 0, 0)) 21 | 22 | fun MacAddress.toLong() = ByteBuffer.allocate(Long.SIZE_BYTES).apply { 23 | order(ByteOrder.LITTLE_ENDIAN) 24 | put(toByteArray()) 25 | rewind() 26 | }.long 27 | } 28 | 29 | fun toPlatform() = MacAddress.fromBytes(ByteBuffer.allocate(8).run { 30 | order(ByteOrder.LITTLE_ENDIAN) 31 | putLong(addr) 32 | array().take(6) 33 | }.toByteArray()) 34 | } 35 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/TetherOffloadManager.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net 2 | 3 | import android.provider.Settings 4 | import be.mygod.vpnhotspot.App.Companion.app 5 | import be.mygod.vpnhotspot.root.SettingsGlobalPut 6 | 7 | /** 8 | * It's hard to change tethering rules with Tethering hardware acceleration enabled for now. 9 | * 10 | * See also: 11 | * android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED 12 | * https://android.googlesource.com/platform/frameworks/base/+/android-8.1.0_r1/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java#45 13 | * https://android.googlesource.com/platform/hardware/qcom/data/ipacfg-mgr/+/master/msm8998/ipacm/src/IPACM_OffloadManager.cpp 14 | */ 15 | object TetherOffloadManager { 16 | private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled" 17 | val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0 18 | suspend fun setEnabled(value: Boolean) = SettingsGlobalPut.int(TETHER_OFFLOAD_DISABLED, if (value) 0 else 1) 19 | } 20 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/dns/VpnProtectedSelectorManager.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.dns 2 | 3 | import android.annotation.SuppressLint 4 | import android.net.VpnService 5 | import io.ktor.network.selector.SelectInterest 6 | import io.ktor.network.selector.Selectable 7 | import io.ktor.network.selector.SelectorManager 8 | import timber.log.Timber 9 | import java.net.ProtocolFamily 10 | import java.nio.channels.spi.SelectorProvider 11 | 12 | class VpnProtectedSelectorManager(private val manager: SelectorManager) : SelectorProvider(), SelectorManager { 13 | companion object { 14 | @SuppressLint("StaticFieldLeak") 15 | private val protector = VpnService() 16 | } 17 | 18 | private fun checkProtect(success: Boolean) { 19 | if (!success) Timber.w(Exception("protect failed")) 20 | } 21 | 22 | override fun openDatagramChannel() = manager.provider.openDatagramChannel().apply { 23 | checkProtect(protector.protect(socket())) 24 | } 25 | override fun openDatagramChannel(family: ProtocolFamily?) = manager.provider.openDatagramChannel(family).apply { 26 | checkProtect(protector.protect(socket())) 27 | } 28 | override fun openPipe() = manager.provider.openPipe() 29 | override fun openSelector() = manager.provider.openSelector() 30 | override fun openServerSocketChannel() = manager.provider.openServerSocketChannel() 31 | override fun openSocketChannel() = manager.provider.openSocketChannel().apply { 32 | checkProtect(protector.protect(socket())) 33 | } 34 | 35 | override val coroutineContext get() = manager.coroutineContext 36 | override val provider get() = this 37 | override fun close() = manager.close() 38 | override fun notifyClosed(selectable: Selectable) = manager.notifyClosed(selectable) 39 | override suspend fun select(selectable: Selectable, interest: SelectInterest) = manager.select(selectable, interest) 40 | } 41 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.monitor 2 | 3 | import android.net.ConnectivityManager 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import android.os.Build 8 | import be.mygod.vpnhotspot.util.Services 9 | import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | 13 | object DefaultNetworkMonitor : UpstreamMonitor() { 14 | private var registered = false 15 | override var currentLinkProperties: LinkProperties? = null 16 | private set 17 | /** 18 | * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: 19 | * https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e 20 | */ 21 | private val networkRequest = globalNetworkRequestBuilder().apply { 22 | addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 23 | addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) 24 | }.build() 25 | private val networkCallback = object : ConnectivityManager.NetworkCallback() { 26 | override fun onAvailable(network: Network) { 27 | val properties = Services.connectivity.getLinkProperties(network) 28 | val callbacks = synchronized(this@DefaultNetworkMonitor) { 29 | currentNetwork = network 30 | currentLinkProperties = properties 31 | callbacks.toList() 32 | } 33 | GlobalScope.launch { callbacks.forEach { it.onAvailable(properties) } } 34 | } 35 | 36 | override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { 37 | val callbacks = synchronized(this@DefaultNetworkMonitor) { 38 | currentNetwork = network 39 | currentLinkProperties = properties 40 | callbacks.toList() 41 | } 42 | GlobalScope.launch { callbacks.forEach { it.onAvailable(properties) } } 43 | } 44 | 45 | override fun onLost(network: Network) { 46 | val callbacks = synchronized(this@DefaultNetworkMonitor) { 47 | currentNetwork = null 48 | currentLinkProperties = null 49 | callbacks.toList() 50 | } 51 | GlobalScope.launch { callbacks.forEach { it.onAvailable() } } 52 | } 53 | } 54 | 55 | override fun registerCallbackLocked(callback: Callback) { 56 | if (registered) { 57 | val currentLinkProperties = currentLinkProperties 58 | if (currentLinkProperties != null) GlobalScope.launch { 59 | callback.onAvailable(currentLinkProperties) 60 | } 61 | } else { 62 | if (Build.VERSION.SDK_INT >= 31) { 63 | Services.connectivity.registerBestMatchingNetworkCallback(networkRequest, networkCallback, 64 | Services.mainHandler) 65 | } else Services.connectivity.requestNetwork(networkRequest, networkCallback, Services.mainHandler) 66 | registered = true 67 | } 68 | } 69 | 70 | override fun destroyLocked() { 71 | if (!registered) return 72 | Services.connectivity.unregisterNetworkCallback(networkCallback) 73 | registered = false 74 | currentLinkProperties = null 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/FallbackUpstreamMonitor.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.monitor 2 | 3 | import android.content.SharedPreferences 4 | import be.mygod.vpnhotspot.App.Companion.app 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.launch 7 | 8 | abstract class FallbackUpstreamMonitor private constructor() : UpstreamMonitor() { 9 | companion object : SharedPreferences.OnSharedPreferenceChangeListener { 10 | const val KEY = "service.upstream.fallback" 11 | 12 | init { 13 | app.pref.registerOnSharedPreferenceChangeListener(this) 14 | } 15 | 16 | private fun generateMonitor(): UpstreamMonitor { 17 | val upstream = app.pref.getString(KEY, null) 18 | return if (upstream.isNullOrEmpty()) DefaultNetworkMonitor else InterfaceMonitor(upstream) 19 | } 20 | private var monitor = generateMonitor() 21 | val currentNetwork get() = monitor.currentNetwork 22 | 23 | fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) } 24 | fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) } 25 | 26 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 27 | if (key == KEY) GlobalScope.launch { // prevent callback called in main 28 | synchronized(this) { 29 | val old = monitor 30 | val callbacks = synchronized(old) { 31 | old.callbacks.toList().also { 32 | old.callbacks.clear() 33 | old.destroyLocked() 34 | } 35 | } 36 | val new = generateMonitor() 37 | monitor = new 38 | for (callback in callbacks) new.registerCallback(callback) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.monitor 2 | 3 | import android.net.ConnectivityManager 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import be.mygod.vpnhotspot.util.Services 8 | import be.mygod.vpnhotspot.util.allInterfaceNames 9 | import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import timber.log.Timber 13 | import java.util.regex.PatternSyntaxException 14 | 15 | class InterfaceMonitor(private val ifaceRegex: String) : UpstreamMonitor() { 16 | private val iface = try { 17 | ifaceRegex.toRegex()::matches 18 | } catch (e: PatternSyntaxException) { 19 | Timber.d(e); 20 | { it == ifaceRegex } 21 | } 22 | private val request = globalNetworkRequestBuilder().apply { 23 | removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) 24 | removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) 25 | removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 26 | }.build() 27 | private var registered = false 28 | 29 | private val available = HashMap() 30 | override val currentLinkProperties: LinkProperties? get() = currentNetwork?.let { available[it] } 31 | private val networkCallback = object : ConnectivityManager.NetworkCallback() { 32 | override fun onAvailable(network: Network) { 33 | val properties = Services.connectivity.getLinkProperties(network) 34 | if (properties?.allInterfaceNames?.any(iface) != true) return 35 | val callbacks = synchronized(this@InterfaceMonitor) { 36 | available[network] = properties 37 | currentNetwork = network 38 | callbacks.toList() 39 | } 40 | GlobalScope.launch { callbacks.forEach { it.onAvailable(properties) } } 41 | } 42 | 43 | override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { 44 | val matched = properties.allInterfaceNames.any(iface) 45 | val (callbacks, newProperties) = synchronized(this@InterfaceMonitor) { 46 | if (matched) { 47 | available[network] = properties 48 | if (currentNetwork == null) currentNetwork = network else if (currentNetwork != network) return 49 | callbacks.toList() to properties 50 | } else { 51 | available.remove(network) 52 | if (currentNetwork != network) return 53 | val nextBest = available.entries.firstOrNull() 54 | currentNetwork = nextBest?.key 55 | callbacks.toList() to nextBest?.value 56 | } 57 | } 58 | GlobalScope.launch { callbacks.forEach { it.onAvailable(newProperties) } } 59 | } 60 | 61 | override fun onLost(network: Network) { 62 | var properties: LinkProperties? = null 63 | val callbacks = synchronized(this@InterfaceMonitor) { 64 | if (available.remove(network) == null || currentNetwork != network) return 65 | val next = available.entries.firstOrNull() 66 | currentNetwork = next?.run { 67 | properties = value 68 | key 69 | } 70 | callbacks.toList() 71 | } 72 | GlobalScope.launch { callbacks.forEach { it.onAvailable(properties) } } 73 | } 74 | } 75 | 76 | override fun registerCallbackLocked(callback: Callback) { 77 | if (registered) { 78 | val currentLinkProperties = currentLinkProperties 79 | if (currentLinkProperties != null) GlobalScope.launch { 80 | callback.onAvailable(currentLinkProperties) 81 | } 82 | } else { 83 | Services.registerNetworkCallback(request, networkCallback) 84 | registered = true 85 | } 86 | } 87 | 88 | override fun destroyLocked() { 89 | if (!registered) return 90 | Services.connectivity.unregisterNetworkCallback(networkCallback) 91 | registered = false 92 | available.clear() 93 | currentNetwork = null 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpNeighbourMonitor.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.monitor 2 | 3 | import be.mygod.vpnhotspot.net.IpDev 4 | import be.mygod.vpnhotspot.net.IpNeighbour 5 | import kotlinx.collections.immutable.PersistentMap 6 | import kotlinx.collections.immutable.persistentMapOf 7 | import kotlinx.collections.immutable.toPersistentMap 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.channels.Channel 10 | import kotlinx.coroutines.channels.actor 11 | import kotlinx.coroutines.channels.onFailure 12 | import kotlinx.coroutines.channels.trySendBlocking 13 | import kotlinx.coroutines.launch 14 | 15 | class IpNeighbourMonitor private constructor() : IpMonitor() { 16 | companion object { 17 | private val callbacks = mutableMapOf() 18 | var instance: IpNeighbourMonitor? = null 19 | var fullMode = false 20 | 21 | /** 22 | * @param full Whether the invalid entries should also be parsed. 23 | * In this case it is more likely to trigger root request on API 29+. 24 | * However, even in light mode, caller should still filter out invalid entries in 25 | * [Callback.onIpNeighbourAvailable] in case the full mode was requested by other callers. 26 | */ 27 | fun registerCallback(callback: Callback, full: Boolean = false) = synchronized(callbacks) { 28 | if (callbacks.put(callback, full) == full) return@synchronized null 29 | fullMode = full || callbacks.any { it.value } 30 | var monitor = instance 31 | if (monitor == null) { 32 | monitor = IpNeighbourMonitor() 33 | instance = monitor 34 | null 35 | } else { 36 | monitor.flushAsync() 37 | monitor.neighbours.values 38 | } 39 | }?.let { GlobalScope.launch { callback.onIpNeighbourAvailable(it) } } 40 | fun unregisterCallback(callback: Callback) = synchronized(callbacks) { 41 | if (callbacks.remove(callback) == null) return@synchronized 42 | fullMode = callbacks.any { it.value } 43 | if (callbacks.isNotEmpty()) return@synchronized 44 | instance?.destroy() 45 | instance = null 46 | } 47 | } 48 | 49 | interface Callback { 50 | fun onIpNeighbourAvailable(neighbours: Collection) 51 | } 52 | 53 | private val aggregator = GlobalScope.actor>(capacity = Channel.CONFLATED) { 54 | for (value in channel) { 55 | val neighbours = value.values 56 | for (callback in synchronized(callbacks) { callbacks.keys.toList() }) { 57 | callback.onIpNeighbourAvailable(neighbours) 58 | } 59 | } 60 | } 61 | private var neighbours = persistentMapOf() 62 | 63 | init { 64 | init() 65 | } 66 | 67 | override val monitoredObject: String get() = "neigh" 68 | 69 | override suspend fun processLine(line: String) { 70 | val old = neighbours 71 | for (neighbour in IpNeighbour.parse(line, fullMode)) neighbours = when (neighbour.state) { 72 | IpNeighbour.State.DELETING -> neighbours.remove(IpDev(neighbour)) 73 | else -> neighbours.put(IpDev(neighbour), neighbour) 74 | } 75 | if (neighbours != old) aggregator.trySendBlocking(neighbours).onFailure { throw it!! } 76 | } 77 | 78 | override suspend fun processLines(lines: Sequence) { 79 | neighbours = mutableMapOf().apply { 80 | for (line in lines) for (neigh in IpNeighbour.parse(line, fullMode)) { 81 | if (neigh.state != IpNeighbour.State.DELETING) this[IpDev(neigh)] = neigh 82 | } 83 | }.toPersistentMap() 84 | aggregator.trySendBlocking(neighbours).onFailure { throw it!! } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.monitor 2 | 3 | import android.content.SharedPreferences 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import be.mygod.vpnhotspot.App.Companion.app 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | 10 | abstract class UpstreamMonitor { 11 | companion object : SharedPreferences.OnSharedPreferenceChangeListener { 12 | const val KEY = "service.upstream" 13 | 14 | init { 15 | app.pref.registerOnSharedPreferenceChangeListener(this) 16 | } 17 | 18 | private fun generateMonitor(): UpstreamMonitor { 19 | val upstream = app.pref.getString(KEY, null) 20 | return if (upstream.isNullOrEmpty()) VpnMonitor else InterfaceMonitor(upstream) 21 | } 22 | private var monitor = generateMonitor() 23 | val currentNetwork get() = monitor.currentNetwork 24 | 25 | fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) } 26 | fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) } 27 | 28 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { 29 | if (key == KEY) GlobalScope.launch { // prevent callback called in main 30 | synchronized(this) { 31 | val old = monitor 32 | val callbacks = synchronized(old) { 33 | old.callbacks.toList().also { 34 | old.callbacks.clear() 35 | old.destroyLocked() 36 | } 37 | } 38 | val new = generateMonitor() 39 | monitor = new 40 | for (callback in callbacks) new.registerCallback(callback) 41 | } 42 | } 43 | } 44 | } 45 | 46 | interface Callback { 47 | /** 48 | * Called if some possibly stacked interface is available 49 | */ 50 | fun onAvailable(properties: LinkProperties? = null) { } 51 | } 52 | 53 | val callbacks = mutableSetOf() 54 | var currentNetwork: Network? = null 55 | protected set 56 | protected abstract val currentLinkProperties: LinkProperties? 57 | protected abstract fun registerCallbackLocked(callback: Callback) 58 | abstract fun destroyLocked() 59 | 60 | fun registerCallback(callback: Callback) { 61 | synchronized(this) { 62 | if (callbacks.add(callback)) registerCallbackLocked(callback) 63 | } 64 | } 65 | fun unregisterCallback(callback: Callback) = synchronized(this) { 66 | if (callbacks.remove(callback) && callbacks.isEmpty()) destroyLocked() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.monitor 2 | 3 | import android.net.ConnectivityManager 4 | import android.net.LinkProperties 5 | import android.net.Network 6 | import android.net.NetworkCapabilities 7 | import be.mygod.vpnhotspot.net.VpnFirewallManager 8 | import be.mygod.vpnhotspot.util.Services 9 | import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import timber.log.Timber 13 | 14 | object VpnMonitor : UpstreamMonitor() { 15 | private val request = globalNetworkRequestBuilder().apply { 16 | addTransportType(NetworkCapabilities.TRANSPORT_VPN) 17 | removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 18 | }.build() 19 | private var registered = false 20 | 21 | private val available = HashMap() 22 | override val currentLinkProperties: LinkProperties? get() = currentNetwork?.let { available[it] } 23 | private val networkCallback = object : ConnectivityManager.NetworkCallback() { 24 | private fun fireCallbacks(properties: LinkProperties?, callbacks: Iterable) = GlobalScope.launch { 25 | if (properties != null) VpnFirewallManager.excludeIfNeeded(this) 26 | callbacks.forEach { it.onAvailable(properties) } 27 | } 28 | 29 | override fun onAvailable(network: Network) { 30 | val properties = Services.connectivity.getLinkProperties(network) 31 | fireCallbacks(properties, synchronized(this@VpnMonitor) { 32 | available[network] = properties 33 | currentNetwork = network 34 | callbacks.toList() 35 | }) 36 | } 37 | 38 | override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { 39 | fireCallbacks(properties, synchronized(this@VpnMonitor) { 40 | available[network] = properties 41 | if (currentNetwork == null) currentNetwork = network 42 | else if (currentNetwork != network) return 43 | callbacks.toList() 44 | }) 45 | } 46 | 47 | override fun onLost(network: Network) { 48 | var properties: LinkProperties? = null 49 | val callbacks = synchronized(this@VpnMonitor) { 50 | if (available.remove(network) == null || currentNetwork != network) return 51 | if (available.isNotEmpty()) { 52 | val next = available.entries.first() 53 | currentNetwork = next.key 54 | Timber.d("Switching to ${next.value} as VPN interface") 55 | properties = next.value 56 | } else currentNetwork = null 57 | callbacks.toList() 58 | } 59 | fireCallbacks(properties, callbacks) 60 | } 61 | } 62 | 63 | override fun registerCallbackLocked(callback: Callback) { 64 | if (registered) { 65 | val currentLinkProperties = currentLinkProperties 66 | if (currentLinkProperties != null) GlobalScope.launch { 67 | callback.onAvailable(currentLinkProperties) 68 | } 69 | } else { 70 | Services.registerNetworkCallback(request, networkCallback) 71 | registered = true 72 | } 73 | } 74 | 75 | override fun destroyLocked() { 76 | if (!registered) return 77 | Services.connectivity.unregisterNetworkCallback(networkCallback) 78 | registered = false 79 | available.clear() 80 | currentNetwork = null 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApCapability.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.wifi 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import android.os.Parcelable 6 | import androidx.annotation.RequiresApi 7 | import be.mygod.vpnhotspot.util.LongConstantLookup 8 | import be.mygod.vpnhotspot.util.UnblockCentral 9 | import timber.log.Timber 10 | 11 | @JvmInline 12 | @RequiresApi(30) 13 | value class SoftApCapability(val inner: Parcelable) { 14 | companion object { 15 | val clazz by lazy { Class.forName("android.net.wifi.SoftApCapability") } 16 | private val getMaxSupportedClients by lazy { clazz.getDeclaredMethod("getMaxSupportedClients") } 17 | private val areFeaturesSupported by lazy { clazz.getDeclaredMethod("areFeaturesSupported", Long::class.java) } 18 | @get:RequiresApi(31) 19 | private val getSupportedChannelList by lazy { 20 | clazz.getDeclaredMethod("getSupportedChannelList", Int::class.java) 21 | } 22 | @get:RequiresApi(31) 23 | @get:TargetApi(33) 24 | private val getCountryCode by lazy { UnblockCentral.getCountryCode(clazz) } 25 | 26 | @RequiresApi(31) 27 | const val SOFTAP_FEATURE_BAND_24G_SUPPORTED = 32L 28 | @RequiresApi(31) 29 | const val SOFTAP_FEATURE_BAND_5G_SUPPORTED = 64L 30 | @RequiresApi(31) 31 | const val SOFTAP_FEATURE_BAND_6G_SUPPORTED = 128L 32 | @RequiresApi(31) 33 | const val SOFTAP_FEATURE_BAND_60G_SUPPORTED = 256L 34 | val featureLookup by lazy { LongConstantLookup(clazz, "SOFTAP_FEATURE_") } 35 | } 36 | 37 | val maxSupportedClients get() = getMaxSupportedClients(inner) as Int 38 | val supportedFeatures: Long get() { 39 | var supportedFeatures = 0L 40 | var probe = 1L 41 | while (probe != 0L) { 42 | if (areFeaturesSupported(inner, probe) as Boolean) supportedFeatures = supportedFeatures or probe 43 | probe += probe 44 | } 45 | return supportedFeatures 46 | } 47 | fun getSupportedChannelList(band: Int) = getSupportedChannelList(inner, band) as IntArray 48 | @get:RequiresApi(31) 49 | val countryCode: String? get() = try { 50 | getCountryCode(inner) as String? 51 | } catch (e: ReflectiveOperationException) { 52 | if (Build.VERSION.SDK_INT >= 33) Timber.w(e) 53 | null 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/SoftApInfo.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.wifi 2 | 3 | import android.annotation.TargetApi 4 | import android.net.MacAddress 5 | import android.os.Parcelable 6 | import androidx.annotation.RequiresApi 7 | import be.mygod.vpnhotspot.util.ConstantLookup 8 | import be.mygod.vpnhotspot.util.UnblockCentral 9 | import timber.log.Timber 10 | 11 | @JvmInline 12 | @RequiresApi(30) 13 | value class SoftApInfo(val inner: Parcelable) { 14 | companion object { 15 | val clazz by lazy { Class.forName("android.net.wifi.SoftApInfo") } 16 | private val getFrequency by lazy { clazz.getDeclaredMethod("getFrequency") } 17 | private val getBandwidth by lazy { clazz.getDeclaredMethod("getBandwidth") } 18 | @get:RequiresApi(31) 19 | private val getBssid by lazy { clazz.getDeclaredMethod("getBssid") } 20 | @get:RequiresApi(31) 21 | private val getWifiStandard by lazy { clazz.getDeclaredMethod("getWifiStandard") } 22 | @get:RequiresApi(31) 23 | private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) } 24 | @get:RequiresApi(31) 25 | private val getAutoShutdownTimeoutMillis by lazy { clazz.getDeclaredMethod("getAutoShutdownTimeoutMillis") } 26 | @get:RequiresApi(35) 27 | val getVendorData by lazy { clazz.getDeclaredMethod("getVendorData") } 28 | 29 | val channelWidthLookup = ConstantLookup("CHANNEL_WIDTH_") { clazz } 30 | } 31 | 32 | val frequency get() = getFrequency(inner) as Int 33 | val bandwidth get() = getBandwidth(inner) as Int 34 | @get:RequiresApi(31) 35 | val bssid get() = getBssid(inner) as MacAddress? 36 | @get:RequiresApi(31) 37 | val wifiStandard get() = getWifiStandard(inner) as Int 38 | @get:RequiresApi(31) 39 | val apInstanceIdentifier get() = try { 40 | getApInstanceIdentifier(inner) as? String 41 | } catch (e: ReflectiveOperationException) { 42 | Timber.w(e) 43 | null 44 | } 45 | @get:RequiresApi(31) 46 | val autoShutdownTimeoutMillis get() = getAutoShutdownTimeoutMillis(inner) as Long 47 | } 48 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/VendorElements.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.wifi 2 | 3 | import android.net.wifi.ScanResult 4 | import androidx.annotation.RequiresApi 5 | import timber.log.Timber 6 | 7 | @RequiresApi(33) 8 | object VendorElements { 9 | fun serialize(input: List) = input.joinToString("\n") { element -> 10 | element.bytes.let { buffer -> 11 | StringBuilder().apply { 12 | @OptIn(ExperimentalStdlibApi::class) 13 | while (buffer.hasRemaining()) append(buffer.get().toHexString()) 14 | }.toString() 15 | }.also { 16 | if (element.id != 221 || element.idExt != 0 || it.isEmpty()) Timber.w(Exception( 17 | "Unexpected InformationElement ${element.id}, ${element.idExt}, $it")) 18 | } 19 | } 20 | 21 | fun deserialize(input: CharSequence?) = (input ?: "").split("\n").mapNotNull { line -> 22 | @OptIn(ExperimentalStdlibApi::class) 23 | if (line.isBlank()) null else ScanResult.InformationElement(221, 0, line.hexToByteArray()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiClient.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.wifi 2 | 3 | import android.annotation.TargetApi 4 | import android.net.MacAddress 5 | import android.os.Parcelable 6 | import androidx.annotation.RequiresApi 7 | import be.mygod.vpnhotspot.util.UnblockCentral 8 | import timber.log.Timber 9 | 10 | @JvmInline 11 | @RequiresApi(30) 12 | value class WifiClient(val inner: Parcelable) { 13 | companion object { 14 | val clazz by lazy { Class.forName("android.net.wifi.WifiClient") } 15 | private val getMacAddress by lazy { clazz.getDeclaredMethod("getMacAddress") } 16 | @get:RequiresApi(31) 17 | private val getApInstanceIdentifier by lazy @TargetApi(31) { UnblockCentral.getApInstanceIdentifier(clazz) } 18 | } 19 | 20 | val macAddress get() = getMacAddress(inner) as MacAddress 21 | @get:RequiresApi(31) 22 | val apInstanceIdentifier get() = try { 23 | getApInstanceIdentifier(inner) as? String 24 | } catch (e: ReflectiveOperationException) { 25 | Timber.w(e) 26 | null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiSsidCompat.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.net.wifi 2 | 3 | import android.net.wifi.WifiSsid 4 | import android.os.Parcelable 5 | import androidx.annotation.RequiresApi 6 | import kotlinx.parcelize.Parcelize 7 | import org.jetbrains.annotations.Contract 8 | import java.nio.ByteBuffer 9 | import java.nio.CharBuffer 10 | import java.nio.charset.Charset 11 | import java.nio.charset.CodingErrorAction 12 | 13 | @Parcelize 14 | data class WifiSsidCompat(val bytes: ByteArray) : Parcelable { 15 | companion object { 16 | private val hexTester = Regex("^(?:[0-9a-f]{2})*$", RegexOption.IGNORE_CASE) 17 | private val qrSanitizer = Regex("([\\\\\":;,])") 18 | 19 | @OptIn(ExperimentalStdlibApi::class) 20 | fun fromHex(hex: String?) = hex?.run { WifiSsidCompat(hexToByteArray()) } 21 | 22 | @Contract("null -> null; !null -> !null") 23 | fun fromUtf8Text(text: String?, truncate: Boolean = false) = text?.toByteArray()?.let { 24 | WifiSsidCompat(if (truncate && it.size > 32) it.sliceArray(0 until 32) else it) 25 | } 26 | 27 | fun toMeCard(text: String) = qrSanitizer.replace(text) { "\\${it.groupValues[1]}" } 28 | 29 | @RequiresApi(33) 30 | fun WifiSsid.toCompat() = WifiSsidCompat(bytes) 31 | } 32 | 33 | init { 34 | require(bytes.size <= 32) { "${bytes.size} > 32" } 35 | } 36 | 37 | @RequiresApi(31) 38 | fun toPlatform() = WifiSsid.fromBytes(bytes) 39 | 40 | fun decode(charset: Charset = Charsets.UTF_8) = CharBuffer.allocate(32).run { 41 | val result = charset.newDecoder().apply { 42 | onMalformedInput(CodingErrorAction.REPORT) 43 | onUnmappableCharacter(CodingErrorAction.REPORT) 44 | }.decode(ByteBuffer.wrap(bytes), this, true) 45 | if (result.isError) null else flip().toString() 46 | } 47 | @OptIn(ExperimentalStdlibApi::class) 48 | val hex get() = bytes.toHexString() 49 | 50 | fun toMeCard(): String { 51 | val utf8 = decode() ?: return hex 52 | return if (hexTester.matches(utf8)) "\"$utf8\"" else toMeCard(utf8) 53 | } 54 | 55 | override fun toString() = String(bytes) 56 | 57 | override fun equals(other: Any?): Boolean { 58 | if (this === other) return true 59 | if (javaClass != other?.javaClass) return false 60 | other as WifiSsidCompat 61 | if (!bytes.contentEquals(other.bytes)) return false 62 | return true 63 | } 64 | 65 | override fun hashCode() = bytes.contentHashCode() 66 | } 67 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/preference/AutoCompleteNetworkPreferenceDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.preference 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.net.LinkProperties 6 | import android.net.Network 7 | import android.net.NetworkCapabilities 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.core.os.bundleOf 11 | import androidx.lifecycle.lifecycleScope 12 | import androidx.lifecycle.withStarted 13 | import androidx.preference.EditTextPreferenceDialogFragmentCompat 14 | import be.mygod.vpnhotspot.R 15 | import be.mygod.vpnhotspot.util.Services 16 | import be.mygod.vpnhotspot.util.allInterfaceNames 17 | import be.mygod.vpnhotspot.util.globalNetworkRequestBuilder 18 | import be.mygod.vpnhotspot.widget.AlwaysAutoCompleteEditText 19 | import kotlinx.coroutines.launch 20 | 21 | class AutoCompleteNetworkPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() { 22 | fun setArguments(key: String) { 23 | arguments = bundleOf(ARG_KEY to key) 24 | } 25 | 26 | private lateinit var editText: AlwaysAutoCompleteEditText 27 | private fun updateAdapter() = editText.setSimpleItems(interfaceNames.flatMap { it.value }.toTypedArray()) 28 | 29 | private val interfaceNames = mutableMapOf>() 30 | private val callback = object : ConnectivityManager.NetworkCallback() { 31 | override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { 32 | interfaceNames[network] = properties.allInterfaceNames 33 | lifecycleScope.launch { 34 | withStarted { updateAdapter() } 35 | } 36 | } 37 | 38 | override fun onLost(network: Network) { 39 | interfaceNames.remove(network) 40 | lifecycleScope.launch { 41 | withStarted { updateAdapter() } 42 | } 43 | } 44 | } 45 | 46 | override fun onCreateDialogView(context: Context) = super.onCreateDialogView(context)!!.apply { 47 | val oldEditText = findViewById(android.R.id.edit)!! 48 | val container = oldEditText.parent as ViewGroup 49 | container.removeView(oldEditText) 50 | container.addView(layoutInflater.inflate(R.layout.preference_widget_edittext_autocomplete, container, false), 51 | oldEditText.layoutParams) 52 | } 53 | 54 | override fun onBindDialogView(view: View) { 55 | super.onBindDialogView(view) 56 | editText = view.findViewById(android.R.id.edit) 57 | editText.hint = (preference.summaryProvider as SummaryFallbackProvider).fallback 58 | } 59 | 60 | override fun onStart() { 61 | super.onStart() 62 | Services.registerNetworkCallback(globalNetworkRequestBuilder().apply { 63 | removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) 64 | removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) 65 | removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 66 | }.build(), callback) 67 | } 68 | 69 | override fun onStop() { 70 | Services.connectivity.unregisterNetworkCallback(callback) 71 | interfaceNames.clear() 72 | updateAdapter() 73 | super.onStop() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/preference/SharedPreferenceDataStore.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.preference 2 | 3 | import android.content.SharedPreferences 4 | import androidx.core.content.edit 5 | import androidx.preference.PreferenceDataStore 6 | 7 | class SharedPreferenceDataStore(private val pref: SharedPreferences) : PreferenceDataStore() { 8 | override fun getBoolean(key: String?, defValue: Boolean) = pref.getBoolean(key, defValue) 9 | override fun getFloat(key: String?, defValue: Float) = pref.getFloat(key, defValue) 10 | override fun getInt(key: String?, defValue: Int) = pref.getInt(key, defValue) 11 | override fun getLong(key: String?, defValue: Long) = pref.getLong(key, defValue) 12 | override fun getString(key: String?, defValue: String?): String? = pref.getString(key, defValue) 13 | override fun getStringSet(key: String?, defValue: MutableSet?): MutableSet? = 14 | pref.getStringSet(key, defValue) 15 | override fun putBoolean(key: String?, value: Boolean) = pref.edit { putBoolean(key, value) } 16 | override fun putFloat(key: String?, value: Float) = pref.edit { putFloat(key, value) } 17 | override fun putInt(key: String?, value: Int) = pref.edit { putInt(key, value) } 18 | override fun putLong(key: String?, value: Long) = pref.edit { putLong(key, value) } 19 | override fun putString(key: String?, value: String?) = pref.edit { putString(key, value) } 20 | override fun putStringSet(key: String?, value: MutableSet?) = pref.edit { putStringSet(key, value) } 21 | } 22 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/preference/SummaryFallbackProvider.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.preference 2 | 3 | import androidx.preference.EditTextPreference 4 | import androidx.preference.Preference 5 | 6 | class SummaryFallbackProvider(preference: Preference) : Preference.SummaryProvider { 7 | val fallback = preference.summary 8 | init { 9 | preference.summaryProvider = this 10 | } 11 | 12 | override fun provideSummary(preference: EditTextPreference) = preference.text.let { 13 | if (it.isNullOrEmpty()) fallback else it 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.preference 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.net.LinkProperties 6 | import android.text.SpannableStringBuilder 7 | import android.text.style.StyleSpan 8 | import android.util.AttributeSet 9 | import androidx.lifecycle.DefaultLifecycleObserver 10 | import androidx.lifecycle.Lifecycle 11 | import androidx.lifecycle.LifecycleOwner 12 | import androidx.lifecycle.lifecycleScope 13 | import androidx.preference.Preference 14 | import be.mygod.vpnhotspot.R 15 | import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor 16 | import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor 17 | import be.mygod.vpnhotspot.util.allRoutes 18 | import be.mygod.vpnhotspot.util.format 19 | import be.mygod.vpnhotspot.util.parseNumericAddress 20 | import kotlinx.coroutines.launch 21 | import timber.log.Timber 22 | 23 | class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs), 24 | DefaultLifecycleObserver { 25 | companion object { 26 | private val internetV4Address = parseNumericAddress("8.8.8.8") 27 | private val internetV6Address = parseNumericAddress("2001:4860:4860::8888") 28 | } 29 | 30 | private open inner class Monitor : UpstreamMonitor.Callback { 31 | protected var currentInterfaces = emptyMap() 32 | val charSequence get() = currentInterfaces.map { (ifname, internet) -> 33 | if (internet) SpannableStringBuilder(ifname).apply { 34 | setSpan(StyleSpan(Typeface.BOLD), 0, length, 0) 35 | } else ifname 36 | }.joinTo(SpannableStringBuilder()).ifEmpty { "∅" } 37 | 38 | override fun onAvailable(properties: LinkProperties?) { 39 | val result = mutableMapOf() 40 | for (route in properties?.allRoutes ?: emptyList()) { 41 | result.compute(route.`interface` ?: continue) { _, internet -> 42 | internet == true || try { 43 | route.matches(internetV4Address) || route.matches(internetV6Address) 44 | } catch (e: RuntimeException) { 45 | Timber.w(e) 46 | false 47 | } 48 | } 49 | } 50 | currentInterfaces = result 51 | onUpdate() 52 | } 53 | } 54 | 55 | private val primary = Monitor() 56 | private val fallback = Monitor() 57 | 58 | fun attachListener(lifecycle: Lifecycle) { 59 | lifecycle.addObserver(this) 60 | onUpdate() 61 | } 62 | 63 | override fun onStart(owner: LifecycleOwner) { 64 | UpstreamMonitor.registerCallback(primary) 65 | FallbackUpstreamMonitor.registerCallback(fallback) 66 | } 67 | override fun onStop(owner: LifecycleOwner) { 68 | UpstreamMonitor.unregisterCallback(primary) 69 | FallbackUpstreamMonitor.unregisterCallback(fallback) 70 | } 71 | 72 | private fun onUpdate() { 73 | (context as LifecycleOwner).lifecycleScope.launch { 74 | summary = context.getText(R.string.settings_service_upstream_monitor_summary).format( 75 | context.resources.configuration.locales[0], primary.charSequence, fallback.charSequence) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/room/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.room 2 | 3 | import androidx.room.Database 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import androidx.room.TypeConverters 7 | import androidx.room.migration.Migration 8 | import androidx.sqlite.db.SupportSQLiteDatabase 9 | import be.mygod.vpnhotspot.App.Companion.app 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | 13 | @Database(entities = [ClientRecord::class, TrafficRecord::class], version = 2) 14 | @TypeConverters(Converters::class) 15 | abstract class AppDatabase : RoomDatabase() { 16 | companion object { 17 | const val DB_NAME = "app.db" 18 | 19 | val instance by lazy { 20 | Room.databaseBuilder(app.deviceStorage, AppDatabase::class.java, DB_NAME).apply { 21 | addMigrations( 22 | Migration2 23 | ) 24 | setQueryExecutor { GlobalScope.launch { it.run() } } 25 | }.build() 26 | } 27 | } 28 | 29 | abstract val clientRecordDao: ClientRecord.Dao 30 | abstract val trafficRecordDao: TrafficRecord.Dao 31 | 32 | object Migration2 : Migration(1, 2) { 33 | override fun migrate(database: SupportSQLiteDatabase) = 34 | database.execSQL("ALTER TABLE `ClientRecord` ADD COLUMN `macLookupPending` INTEGER NOT NULL DEFAULT 1") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/room/ClientRecord.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.room 2 | 3 | import android.net.MacAddress 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.map 6 | import androidx.room.* 7 | import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong 8 | 9 | @Entity 10 | data class ClientRecord(@PrimaryKey 11 | val mac: MacAddress, 12 | var nickname: CharSequence = "", 13 | var blocked: Boolean = false, 14 | var macLookupPending: Boolean = true) { 15 | @androidx.room.Dao 16 | abstract class Dao { 17 | @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") 18 | protected abstract fun lookupBlocking(mac: MacAddress): ClientRecord? 19 | fun lookupOrDefaultBlocking(mac: MacAddress) = lookupBlocking(mac) ?: ClientRecord(mac) 20 | 21 | @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") 22 | protected abstract suspend fun lookup(mac: MacAddress): ClientRecord? 23 | suspend fun lookupOrDefault(mac: MacAddress) = lookup(mac) ?: ClientRecord(mac) 24 | 25 | @Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac") 26 | protected abstract fun lookupSync(mac: MacAddress): LiveData 27 | fun lookupOrDefaultSync(mac: MacAddress) = lookupSync(mac).map { it ?: ClientRecord(mac) } 28 | 29 | @Insert(onConflict = OnConflictStrategy.REPLACE) 30 | protected abstract suspend fun updateInternal(value: ClientRecord): Long 31 | suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac.toLong()) 32 | 33 | @Transaction 34 | open suspend fun upsert(mac: MacAddress, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault( 35 | mac).apply { 36 | operation() 37 | update(this) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/room/Converters.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.room 2 | 3 | import android.net.MacAddress 4 | import android.text.TextUtils 5 | import androidx.room.TypeConverter 6 | import be.mygod.librootkotlinx.useParcel 7 | import be.mygod.vpnhotspot.net.MacAddressCompat 8 | import be.mygod.vpnhotspot.net.MacAddressCompat.Companion.toLong 9 | import timber.log.Timber 10 | import java.net.InetAddress 11 | 12 | object Converters { 13 | @JvmStatic 14 | @TypeConverter 15 | fun persistCharSequence(cs: CharSequence) = useParcel { p -> 16 | TextUtils.writeToParcel(cs, p, 0) 17 | p.marshall() 18 | } 19 | 20 | @JvmStatic 21 | @TypeConverter 22 | fun unpersistCharSequence(data: ByteArray?) = data?.let { 23 | useParcel { p -> 24 | p.unmarshall(data, 0, data.size) 25 | p.setDataPosition(0) 26 | try { 27 | TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p) 28 | } catch (e: RuntimeException) { 29 | Timber.w(e) 30 | null 31 | } 32 | } 33 | } 34 | 35 | @JvmStatic 36 | @TypeConverter 37 | fun persistMacAddress(address: MacAddress) = address.toLong() 38 | 39 | @JvmStatic 40 | @TypeConverter 41 | fun unpersistMacAddress(address: Long) = MacAddressCompat(address).toPlatform() 42 | 43 | @JvmStatic 44 | @TypeConverter 45 | fun persistInetAddress(address: InetAddress): ByteArray = address.address 46 | 47 | @JvmStatic 48 | @TypeConverter 49 | fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data) 50 | } 51 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/room/TrafficRecord.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.room 2 | 3 | import android.net.MacAddress 4 | import android.os.Parcelable 5 | import androidx.room.* 6 | import kotlinx.parcelize.Parcelize 7 | import java.net.InetAddress 8 | 9 | @Entity(foreignKeys = [ForeignKey(entity = TrafficRecord::class, parentColumns = ["id"], childColumns = ["previousId"], 10 | onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.RESTRICT)], 11 | indices = [Index(value = ["previousId"], unique = true)]) 12 | data class TrafficRecord( 13 | /** 14 | * Setting id = null should only be used when a new row is created and not yet inserted into the database. 15 | * 16 | * https://www.sqlite.org/lang_createtable.html#primkeyconst: 17 | * > Unless the column is an INTEGER PRIMARY KEY or the table is a WITHOUT ROWID table or the column is declared 18 | * > NOT NULL, SQLite allows NULL values in a PRIMARY KEY column. 19 | */ 20 | @PrimaryKey(autoGenerate = true) 21 | var id: Long? = null, 22 | val timestamp: Long = System.currentTimeMillis(), 23 | /** 24 | * Foreign key/ID for (possibly non-existent, i.e. default) entry in ClientRecord. 25 | */ 26 | val mac: MacAddress, 27 | /** 28 | * For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case. 29 | */ 30 | val ip: InetAddress, 31 | @Deprecated("This field is no longer used.") 32 | val upstream: String? = null, 33 | val downstream: String, 34 | var sentPackets: Long = 0, 35 | var sentBytes: Long = 0, 36 | var receivedPackets: Long = 0, 37 | var receivedBytes: Long = 0, 38 | /** 39 | * ID for the previous traffic record. 40 | */ 41 | val previousId: Long? = null) { 42 | @androidx.room.Dao 43 | abstract class Dao { 44 | @Insert 45 | protected abstract fun insertInternal(value: TrafficRecord): Long 46 | fun insert(value: TrafficRecord) { 47 | check(value.id == null) 48 | value.id = insertInternal(value) 49 | } 50 | 51 | @Query(""" 52 | SELECT MIN(TrafficRecord.timestamp) AS timestamp, 53 | COUNT(TrafficRecord.id) AS count, 54 | SUM(TrafficRecord.sentPackets) AS sentPackets, 55 | SUM(TrafficRecord.sentBytes) AS sentBytes, 56 | SUM(TrafficRecord.receivedPackets) AS receivedPackets, 57 | SUM(TrafficRecord.receivedBytes) AS receivedBytes 58 | FROM TrafficRecord LEFT JOIN TrafficRecord AS Next ON TrafficRecord.id = Next.previousId 59 | /* We only want to find the last record for each chain so that we don't double count */ 60 | WHERE TrafficRecord.mac = :mac AND Next.id IS NULL 61 | """) 62 | abstract suspend fun queryStats(mac: MacAddress): ClientStats 63 | } 64 | } 65 | 66 | @Parcelize 67 | data class ClientStats( 68 | val timestamp: Long = 0, 69 | val count: Long = 0, 70 | val sentPackets: Long = 0, 71 | val sentBytes: Long = 0, 72 | val receivedPackets: Long = 0, 73 | val receivedBytes: Long = 0 74 | ) : Parcelable 75 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/root/Jni.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.root 2 | 3 | object Jni { 4 | init { 5 | System.loadLibrary("vpnhotspot") 6 | } 7 | external fun removeUidInterfaceRules(path: String?, uid: Int, rules: Long): Boolean 8 | } 9 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/root/LocalOnlyHotspotCallbacks.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.root 2 | 3 | import android.net.wifi.SoftApConfiguration 4 | import android.os.Parcelable 5 | import androidx.annotation.RequiresApi 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @RequiresApi(30) 9 | sealed class LocalOnlyHotspotCallbacks : Parcelable { 10 | @Parcelize 11 | data class OnStarted(val config: SoftApConfiguration) : LocalOnlyHotspotCallbacks() 12 | @Parcelize 13 | class OnStopped : LocalOnlyHotspotCallbacks() { 14 | override fun equals(other: Any?) = other is OnStopped 15 | override fun hashCode() = 0x80acd3ca.toInt() 16 | } 17 | @Parcelize 18 | data class OnFailed(val reason: Int) : LocalOnlyHotspotCallbacks() 19 | } 20 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/root/RoutingCommands.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.root 2 | 3 | import android.os.Parcelable 4 | import be.mygod.librootkotlinx.RootCommand 5 | import be.mygod.librootkotlinx.RootCommandNoResult 6 | import be.mygod.vpnhotspot.net.Routing 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.async 9 | import kotlinx.coroutines.coroutineScope 10 | import kotlinx.coroutines.withContext 11 | import kotlinx.parcelize.Parcelize 12 | import timber.log.Timber 13 | 14 | object RoutingCommands { 15 | @Parcelize 16 | class Clean : RootCommandNoResult { 17 | override suspend fun execute() = withContext(Dispatchers.IO) { 18 | val process = ProcessBuilder("sh").fixPath(true).start() 19 | process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands) 20 | when (val code = process.waitFor()) { 21 | 0 -> { } 22 | else -> Timber.w("Unexpected exit code $code") 23 | } 24 | check(process.waitFor() == 0) 25 | null 26 | } 27 | } 28 | 29 | class UnexpectedOutputException(msg: String, val result: ProcessResult) : RuntimeException(msg) 30 | 31 | @Parcelize 32 | data class ProcessResult(val exit: Int, val out: String, val err: String) : Parcelable { 33 | fun message(command: List, out: Boolean = this.out.isNotEmpty(), 34 | err: Boolean = this.err.isNotEmpty()): String? { 35 | val msg = StringBuilder("${command.joinToString(" ")} exited with $exit") 36 | if (out) msg.append("\n${this.out}") 37 | if (err) msg.append("\n=== stderr ===\n${this.err}") 38 | return if (exit != 0 || out || err) msg.toString() else null 39 | } 40 | 41 | fun check(command: List, out: Boolean = this.out.isNotEmpty(), 42 | err: Boolean = this.err.isNotEmpty()) = message(command, out, err)?.let { msg -> 43 | throw UnexpectedOutputException(msg, this) 44 | } 45 | } 46 | 47 | @Parcelize 48 | data class Process(val command: List, private val redirect: Boolean = false) : RootCommand { 49 | override suspend fun execute() = withContext(Dispatchers.IO) { 50 | val process = ProcessBuilder(command).fixPath(redirect).start() 51 | coroutineScope { 52 | val output = async { process.inputStream.bufferedReader().readText() } 53 | val error = async { if (redirect) "" else process.errorStream.bufferedReader().readText() } 54 | ProcessResult(process.waitFor(), output.await(), error.await()) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/root/TetheringCommands.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.root 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.RequiresApi 5 | import be.mygod.librootkotlinx.RootCommandChannel 6 | import be.mygod.vpnhotspot.net.TetheringManagerCompat 7 | import kotlinx.coroutines.CompletableDeferred 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.channels.ClosedSendChannelException 10 | import kotlinx.coroutines.channels.onClosed 11 | import kotlinx.coroutines.channels.onFailure 12 | import kotlinx.coroutines.channels.produce 13 | import kotlinx.coroutines.launch 14 | import kotlinx.parcelize.Parcelize 15 | 16 | object TetheringCommands { 17 | /** 18 | * This is the only command supported since other callbacks do not require signature permissions. 19 | */ 20 | @Parcelize 21 | data class OnClientsChanged(val clients: List) : Parcelable { 22 | fun dispatch(callback: TetheringManagerCompat.TetheringEventCallback) = callback.onClientsChanged(clients) 23 | } 24 | 25 | @Parcelize 26 | @RequiresApi(30) 27 | class RegisterTetheringEventCallback : RootCommandChannel { 28 | override fun create(scope: CoroutineScope) = scope.produce(capacity = capacity) { 29 | val finish = CompletableDeferred() 30 | val callback = object : TetheringManagerCompat.TetheringEventCallback { 31 | private fun push(parcel: OnClientsChanged) { 32 | trySend(parcel).onClosed { 33 | finish.completeExceptionally(it ?: ClosedSendChannelException("Channel was closed normally")) 34 | return 35 | }.onFailure { throw it!! } 36 | } 37 | 38 | override fun onClientsChanged(clients: Collection) = 39 | push(OnClientsChanged(clients.toList())) 40 | } 41 | TetheringManagerCompat.registerTetheringEventCallback(callback) { 42 | scope.launch { 43 | try { 44 | it.run() 45 | } catch (e: Throwable) { 46 | finish.completeExceptionally(e) 47 | } 48 | } 49 | } 50 | try { 51 | finish.await() 52 | } finally { 53 | TetheringManagerCompat.unregisterTetheringEventCallback(callback) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/tasker/StateAction.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.tasker 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import be.mygod.vpnhotspot.TetheringService 9 | import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoInput 10 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoInput 11 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput 12 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 13 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultErrorWithOutput 14 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess 15 | 16 | class GetStateConfig : AppCompatActivity(), TaskerPluginConfigNoInput { 17 | override val context: Context 18 | get() = this 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | GetStateHelper(this).apply { 23 | onCreate() 24 | finishForTasker() 25 | } 26 | } 27 | } 28 | 29 | class GetStateHelper(config: GetStateConfig) : TaskerPluginConfigHelperNoInput(config) { 30 | override val outputClass: Class = TetheringState::class.java 31 | override val runnerClass: Class = GetStateRunner::class.java 32 | } 33 | 34 | class GetStateRunner : TaskerPluginRunnerActionNoInput() { 35 | override fun run( 36 | context: Context, 37 | input: TaskerInput, 38 | ) = if (context.checkCallingPermission(Manifest.permission.ACCESS_NETWORK_STATE) == 39 | PackageManager.PERMISSION_GRANTED) { 40 | TaskerPluginResultSucess(TetheringState(TetheringService.activeTetherTypes)) 41 | } else TaskerPluginResultErrorWithOutput(SecurityException("Need ACCESS_NETWORK_STATE permission")) 42 | } 43 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/tasker/TaskerEvents.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.tasker 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import be.mygod.vpnhotspot.TetheringService 9 | import com.joaomgcd.taskerpluginlibrary.condition.TaskerPluginRunnerConditionEvent 10 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig 11 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelper 12 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput 13 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 14 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionSatisfied 15 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionUnknown 16 | import timber.log.Timber 17 | 18 | class TetheringEventConfig : AppCompatActivity(), TaskerPluginConfigNoInput { 19 | override val context: Context 20 | get() = this 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | TetheringEventHelper(this).apply { 25 | onCreate() 26 | finishForTasker() 27 | } 28 | } 29 | } 30 | 31 | class TetheringEventHelper(config: TaskerPluginConfig) : TaskerPluginConfigHelper(config) { 32 | override val runnerClass: Class = TetheringEventRunner::class.java 33 | override val inputClass: Class = Unit::class.java 34 | override val outputClass: Class = TetheringState::class.java 35 | } 36 | 37 | class TetheringEventRunner : TaskerPluginRunnerConditionEvent() { 38 | override fun getSatisfiedCondition( 39 | context: Context, 40 | input: TaskerInput, 41 | update: Unit?, 42 | ) = if (context.checkCallingPermission(Manifest.permission.ACCESS_NETWORK_STATE) == 43 | PackageManager.PERMISSION_GRANTED) { 44 | TaskerPluginResultConditionSatisfied( 45 | context = context, 46 | regular = TetheringState(TetheringService.activeTetherTypes), 47 | ) 48 | } else { 49 | Timber.w("TetheringEventRunner needs ACCESS_NETWORK_STATE permission") 50 | TaskerPluginResultConditionUnknown() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/tasker/TaskerPermissionManager.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.tasker 2 | 3 | import android.app.Activity 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import com.joaomgcd.taskerpluginlibrary.TaskerPluginConstants 9 | import net.dinglisch.android.tasker.TaskerPlugin 10 | 11 | object TaskerPermissionManager { 12 | /** 13 | * See also [com.joaomgcd.taskerpluginlibrary.condition.TaskerPluginRunnerCondition.requestQuery]. 14 | */ 15 | fun requestQuery(context: Context, configActivityClass: Class, 16 | permission: String) { 17 | val intentRequest = Intent(TaskerPluginConstants.ACTION_REQUEST_QUERY).apply { 18 | addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 19 | putExtra(TaskerPluginConstants.EXTRA_ACTIVITY, configActivityClass.name) 20 | TaskerPlugin.Event.addPassThroughMessageID(this) 21 | } 22 | val packagesAlreadyHandled = try { 23 | requestQueryThroughServicesAndGetSuccessPackages(context, intentRequest, permission) 24 | } catch (ex: Exception) { 25 | listOf() 26 | } 27 | requestQueryThroughBroadcasts(context, intentRequest, packagesAlreadyHandled, permission) 28 | } 29 | 30 | private fun requestQueryThroughServicesAndGetSuccessPackages(context: Context, intentRequest: Intent, 31 | permission: String): List { 32 | val packageManager = context.packageManager 33 | val intent = Intent(TaskerPluginConstants.ACTION_REQUEST_QUERY) 34 | val resolveInfos = packageManager.queryIntentServices(intent, 0) 35 | val result = arrayListOf() 36 | resolveInfos.forEach { resolveInfo -> 37 | val serviceInfo = resolveInfo.serviceInfo 38 | if (packageManager.checkPermission(permission, serviceInfo.packageName) != 39 | PackageManager.PERMISSION_GRANTED) { 40 | result.add(serviceInfo.packageName) 41 | return@forEach 42 | } 43 | val componentName = ComponentName(serviceInfo.packageName, serviceInfo.name) 44 | intentRequest.component = componentName 45 | try{ 46 | context.startService(intentRequest) 47 | result.add(serviceInfo.packageName) 48 | }catch (t:Throwable){ 49 | //not successful. Don't add to successes 50 | } 51 | } 52 | return result 53 | } 54 | 55 | private fun requestQueryThroughBroadcasts(context: Context, intentRequest: Intent, ignorePackages: List, 56 | permission: String) { 57 | if (ignorePackages.isEmpty()) { 58 | context.sendBroadcast(intentRequest) 59 | return 60 | } 61 | val packageManager = context.packageManager 62 | val intent = Intent(TaskerPluginConstants.ACTION_REQUEST_QUERY) 63 | val resolveInfos = packageManager.queryBroadcastReceivers(intent, 0) 64 | return resolveInfos.forEach { resolveInfo -> 65 | val broadcastInfo = resolveInfo.activityInfo 66 | val applicationInfo = broadcastInfo.applicationInfo 67 | if (ignorePackages.contains(applicationInfo.packageName)) return@forEach 68 | 69 | val componentName = ComponentName(broadcastInfo.packageName, broadcastInfo.name) 70 | intentRequest.component = componentName 71 | context.sendBroadcast(intentRequest, permission) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/tasker/TetheringState.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.tasker 2 | 3 | import be.mygod.vpnhotspot.net.TetherType 4 | import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject 5 | import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable 6 | 7 | @TaskerOutputObject 8 | class TetheringState( 9 | @get:TaskerOutputVariable("wifi") 10 | val wifi: Boolean = false, 11 | @get:TaskerOutputVariable("bluetooth") 12 | val bluetooth: Boolean = false, 13 | @get:TaskerOutputVariable("usb") 14 | val usb: Boolean = false, 15 | @get:TaskerOutputVariable("ethernet") 16 | val ethernet: Boolean = false, 17 | ) { 18 | companion object { 19 | operator fun invoke(types: Set): TetheringState { 20 | return TetheringState( 21 | wifi = types.contains(TetherType.WIFI), 22 | bluetooth = types.contains(TetherType.BLUETOOTH), 23 | usb = types.contains(TetherType.USB) || types.contains(TetherType.NCM), 24 | ethernet = types.contains(TetherType.ETHERNET), 25 | ) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/ConstantLookup.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.os.Build 4 | import androidx.collection.LongSparseArray 5 | import androidx.collection.SparseArrayCompat 6 | import be.mygod.vpnhotspot.App.Companion.app 7 | import be.mygod.vpnhotspot.R 8 | import timber.log.Timber 9 | 10 | class ConstantLookup(private val prefix: String, private val lookup29: Array, 11 | private val clazz: () -> Class<*>) { 12 | val lookup by lazy { 13 | SparseArrayCompat().apply { 14 | for (field in clazz().declaredFields) try { 15 | if (field?.type == Int::class.java && field.name.startsWith(prefix)) put(field.getInt(null), field.name) 16 | } catch (e: Exception) { 17 | Timber.w(e) 18 | } 19 | } 20 | } 21 | 22 | operator fun invoke(reason: Int, trimPrefix: Boolean = false): String { 23 | if (Build.VERSION.SDK_INT >= 30) try { 24 | lookup.get(reason)?.let { return if (trimPrefix) it.substring(prefix.length) else it } 25 | } catch (e: ReflectiveOperationException) { 26 | Timber.w(e) 27 | } 28 | return lookup29.getOrNull(reason)?.let { if (trimPrefix) it else prefix + it } 29 | ?: app.getString(R.string.failure_reason_unknown, reason) 30 | } 31 | } 32 | 33 | fun ConstantLookup(prefix: String, vararg lookup29: String?, clazz: () -> Class<*>) = 34 | ConstantLookup(prefix, lookup29, clazz) 35 | inline fun ConstantLookup(prefix: String, vararg lookup29: String?) = 36 | ConstantLookup(prefix, lookup29) { T::class.java } 37 | 38 | class LongConstantLookup(private val clazz: Class<*>, private val prefix: String) { 39 | private val lookup = LongSparseArray().apply { 40 | for (field in clazz.declaredFields) try { 41 | if (field?.type == Long::class.java && field.name.startsWith(prefix)) put(field.getLong(null), field.name) 42 | } catch (e: Exception) { 43 | Timber.w(e) 44 | } 45 | } 46 | 47 | operator fun invoke(reason: Long, trimPrefix: Boolean = false): String { 48 | try { 49 | lookup.get(reason)?.let { return if (trimPrefix) it.substring(prefix.length) else it } 50 | } catch (e: ReflectiveOperationException) { 51 | Timber.w(e) 52 | } 53 | return app.getString(R.string.failure_reason_unknown, reason) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/CustomTabsUrlSpan.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.text.style.URLSpan 4 | import android.view.View 5 | 6 | class CustomTabsUrlSpan(url: String) : URLSpan(url) { 7 | override fun onClick(widget: View) = widget.context.launchUrl(url) 8 | } 9 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/DeviceStorageApp.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.ComponentCallbacks 6 | import android.content.Context 7 | import android.content.res.Configuration 8 | 9 | @SuppressLint("MissingSuperCall", "Registered") 10 | class DeviceStorageApp(private val app: Application) : Application() { 11 | init { 12 | attachBaseContext(app.createDeviceProtectedStorageContext()) 13 | } 14 | 15 | /** 16 | * Thou shalt not get the REAL underlying application context which would no longer be operating under device 17 | * protected storage. 18 | */ 19 | override fun getApplicationContext(): Context = this 20 | 21 | override fun onCreate() = app.onCreate() 22 | override fun onTerminate() = app.onTerminate() 23 | override fun onConfigurationChanged(newConfig: Configuration) = app.onConfigurationChanged(newConfig) 24 | override fun onLowMemory() = app.onLowMemory() 25 | override fun onTrimMemory(level: Int) = app.onTrimMemory(level) 26 | override fun registerComponentCallbacks(callback: ComponentCallbacks?) = app.registerComponentCallbacks(callback) 27 | override fun unregisterComponentCallbacks(callback: ComponentCallbacks?) = 28 | app.unregisterComponentCallbacks(callback) 29 | override fun registerActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) = 30 | app.registerActivityLifecycleCallbacks(callback) 31 | override fun unregisterActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) = 32 | app.unregisterActivityLifecycleCallbacks(callback) 33 | override fun registerOnProvideAssistDataListener(callback: OnProvideAssistDataListener?) = 34 | app.registerOnProvideAssistDataListener(callback) 35 | override fun unregisterOnProvideAssistDataListener(callback: OnProvideAssistDataListener?) = 36 | app.unregisterOnProvideAssistDataListener(callback) 37 | } 38 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/Events.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | /** 6 | * These class are based off https://github.com/1blustone/kotlin-events. 7 | */ 8 | open class Event0 : ConcurrentHashMap Unit>() { 9 | operator fun invoke() { 10 | for ((_, handler) in this) handler() 11 | } 12 | } 13 | 14 | class StickyEvent0 : Event0() { 15 | override fun put(key: Any, value: () -> Unit): (() -> Unit)? = 16 | super.put(key, value).also { if (it == null) value() } 17 | } 18 | 19 | open class Event1 : ConcurrentHashMap Unit>() { 20 | operator fun invoke(arg: T) { 21 | for ((_, handler) in this) handler(arg) 22 | } 23 | } 24 | 25 | class StickyEvent1(private val fire: () -> T) : Event1() { 26 | override fun put(key: Any, value: (T) -> Unit): ((T) -> Unit)? = 27 | super.put(key, value).also { if (it == null) value(fire()) } 28 | } 29 | 30 | open class Event2 : ConcurrentHashMap Unit>() { 31 | operator fun invoke(arg1: T1, arg2: T2) { 32 | for ((_, handler) in this) handler(arg1, arg2) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/KillableTileService.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.ForegroundServiceStartNotAllowedException 5 | import android.app.PendingIntent 6 | import android.content.ComponentName 7 | import android.content.Intent 8 | import android.content.ServiceConnection 9 | import android.graphics.PixelFormat 10 | import android.os.Build 11 | import android.os.DeadObjectException 12 | import android.os.IBinder 13 | import android.service.quicksettings.Tile 14 | import android.service.quicksettings.TileService 15 | import android.view.View 16 | import android.view.WindowManager 17 | import androidx.core.view.doOnPreDraw 18 | import java.lang.ref.WeakReference 19 | 20 | abstract class KillableTileService : TileService(), ServiceConnection { 21 | protected var tapPending = false 22 | 23 | /** 24 | * Compat helper for setSubtitle. 25 | */ 26 | protected fun Tile.subtitle(value: CharSequence?) { 27 | if (Build.VERSION.SDK_INT >= 29) subtitle = value 28 | } 29 | 30 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 31 | if (tapPending) { 32 | tapPending = false 33 | onClick() 34 | } 35 | } 36 | 37 | override fun onBind(intent: Intent?) = try { 38 | super.onBind(intent) 39 | } catch (e: RuntimeException) { 40 | if (e.cause !is DeadObjectException) throw e 41 | null 42 | } 43 | 44 | protected fun runActivity(intent: Intent) = unlockAndRun { 45 | if (Build.VERSION.SDK_INT < 34) @Suppress("DEPRECATION") @SuppressLint("StartActivityAndCollapseDeprecated") { 46 | startActivityAndCollapse(intent) 47 | } else startActivityAndCollapse(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)) 48 | } 49 | fun dismiss() = runActivity(Intent(this, SelfDismissActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) 50 | @Suppress("LeakingThis") 51 | protected val dismissHandle = WeakReference(this) 52 | override fun onDestroy() { 53 | dismissHandle.clear() 54 | super.onDestroy() 55 | } 56 | 57 | // Workaround on U: https://github.com/zhanghai/MaterialFiles/commit/7a2b228dfef8e5080d4cc887208b1ac5458c160e 58 | protected fun startForegroundServiceCompat(service: Intent) { 59 | try { 60 | startForegroundService(service) 61 | } catch (e: ForegroundServiceStartNotAllowedException) { 62 | if (Build.VERSION.SDK_INT != 34) throw e 63 | val windowManager = getSystemService(WindowManager::class.java) 64 | val view = View(this) 65 | windowManager.addView(view, WindowManager.LayoutParams().apply { 66 | type = WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + 35 67 | format = PixelFormat.TRANSLUCENT 68 | token = UnblockCentral.TileService_mToken.get(this@KillableTileService) as IBinder? 69 | }) 70 | view.doOnPreDraw { 71 | view.post { 72 | view.invalidate() 73 | view.doOnPreDraw { 74 | try { 75 | startForegroundService(service) 76 | } finally { 77 | windowManager.removeView(view) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | typealias TileServiceDismissHandle = WeakReference 86 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/QRCodeDialog.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.ViewGroup 8 | import android.widget.ImageView 9 | import android.widget.Toast 10 | import androidx.core.os.bundleOf 11 | import androidx.fragment.app.DialogFragment 12 | import be.mygod.vpnhotspot.R 13 | import com.google.zxing.BarcodeFormat 14 | import com.google.zxing.EncodeHintType 15 | import com.google.zxing.MultiFormatWriter 16 | import com.google.zxing.WriterException 17 | import timber.log.Timber 18 | import java.nio.charset.StandardCharsets 19 | 20 | class QRCodeDialog : DialogFragment() { 21 | companion object { 22 | private const val KEY_ARG = "arg" 23 | private val iso88591 = StandardCharsets.ISO_8859_1.newEncoder() 24 | } 25 | 26 | fun withArg(arg: String) = apply { arguments = bundleOf(KEY_ARG to arg) } 27 | private val arg get() = arguments?.getString(KEY_ARG) 28 | 29 | /** 30 | * Based on: 31 | * https://android.googlesource.com/platform/packages/apps/Settings/+/0d706f0/src/com/android/settings/wifi/qrcode/QrCodeGenerator.java 32 | * https://android.googlesource.com/platform/packages/apps/Settings/+/8a9ccfd/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java#153 33 | */ 34 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = try { 35 | val size = resources.getDimensionPixelSize(R.dimen.qrcode_size) 36 | val hints = mutableMapOf() 37 | if (!iso88591.canEncode(arg)) hints[EncodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8.name() 38 | val qrBits = MultiFormatWriter().encode(arg, BarcodeFormat.QR_CODE, size, size, hints) 39 | ImageView(context).apply { 40 | layoutParams = ViewGroup.LayoutParams(size, size) 41 | setImageBitmap(Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565).apply { 42 | for (x in 0 until size) for (y in 0 until size) { 43 | setPixel(x, y, if (qrBits.get(x, y)) Color.BLACK else Color.WHITE) 44 | } 45 | }) 46 | } 47 | } catch (e: WriterException) { 48 | Timber.w(e) 49 | Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show() 50 | dismiss() 51 | null 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/RangeInput.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | object RangeInput { 4 | fun toString(input: IntArray) = StringBuilder().apply { 5 | if (input.isEmpty()) return@apply 6 | input.sort() 7 | var pending: Int? = null 8 | var last = input[0] 9 | append(last) 10 | for (channel in input.asSequence().drop(1)) { 11 | if (channel == last + 1) pending = channel else { 12 | pending?.let { 13 | append('-') 14 | append(it) 15 | pending = null 16 | } 17 | append(",\u200b") // zero-width space to save space 18 | append(channel) 19 | } 20 | last = channel 21 | } 22 | pending?.let { 23 | append('-') 24 | append(it) 25 | } 26 | }.toString() 27 | fun toString(input: Set?) = input?.run { toString(toIntArray()) } 28 | 29 | fun fromString(input: CharSequence?, min: Int = 1, max: Int = 999) = mutableSetOf().apply { 30 | if (input == null) return@apply 31 | for (unit in input.split(',')) { 32 | if (unit.isBlank()) continue 33 | val blocks = unit.split('-', limit = 2).map { i -> 34 | i.trim { it == '\u200b' || it.isWhitespace() }.toInt() 35 | } 36 | require(blocks[0] in min..max) { "Out of range: ${blocks[0]}" } 37 | if (blocks.size == 2) { 38 | require(blocks[1] in min..max) { "Out of range: ${blocks[1]}" } 39 | addAll(blocks[0]..blocks[1]) 40 | } else add(blocks[0]) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/RootSession.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import be.mygod.librootkotlinx.RootServer 4 | import be.mygod.vpnhotspot.root.RootManager 5 | import be.mygod.vpnhotspot.root.RoutingCommands 6 | import kotlinx.coroutines.runBlocking 7 | import timber.log.Timber 8 | import java.util.* 9 | import java.util.concurrent.locks.ReentrantLock 10 | import kotlin.concurrent.withLock 11 | 12 | class RootSession : AutoCloseable { 13 | companion object { 14 | private val monitor = ReentrantLock() 15 | 16 | fun use(operation: (RootSession) -> T) = monitor.withLock { operation(RootSession()) } 17 | fun beginTransaction(): Transaction { 18 | monitor.lock() 19 | try { 20 | return RootSession().Transaction() 21 | } catch (e: Exception) { 22 | monitor.unlock() 23 | throw e 24 | } 25 | } 26 | } 27 | 28 | private var server: RootServer? = runBlocking { RootManager.acquire() } 29 | override fun close() { 30 | server?.let { runBlocking { RootManager.release(it) } } 31 | server = null 32 | } 33 | 34 | /** 35 | * Don't care about the results, but still sync. 36 | */ 37 | fun submit(command: String) = execQuiet(command).message(listOf(command))?.let { Timber.v(it) } 38 | 39 | fun execQuiet(command: String, redirect: Boolean = false) = runBlocking { 40 | server!!.execute(RoutingCommands.Process(listOf("sh", "-c", command), redirect)) 41 | } 42 | fun exec(command: String) = execQuiet(command).check(listOf(command)) 43 | 44 | /** 45 | * This transaction is different from what you may have in mind since you can revert it after committing it. 46 | */ 47 | inner class Transaction { 48 | private val revertCommands = LinkedList() 49 | 50 | fun exec(command: String, revert: String? = null) = execQuiet(command, revert).check(listOf(command)) 51 | fun execQuiet(command: String, revert: String? = null): RoutingCommands.ProcessResult { 52 | if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails 53 | return this@RootSession.execQuiet(command) 54 | } 55 | 56 | fun commit() = monitor.unlock() 57 | 58 | fun revert() { 59 | var locked = monitor.isHeldByCurrentThread 60 | try { 61 | if (revertCommands.isEmpty()) return 62 | val shell = if (locked) this@RootSession else { 63 | monitor.lock() 64 | locked = true 65 | RootSession() 66 | } 67 | revertCommands.forEach { shell.submit(it) } 68 | } catch (e: Exception) { // if revert fails, it should fail silently 69 | Timber.d(e) 70 | } finally { 71 | revertCommands.clear() 72 | if (locked) monitor.unlock() // commit 73 | } 74 | } 75 | 76 | fun safeguard(work: Transaction.() -> Unit) = try { 77 | work() 78 | commit() 79 | this 80 | } catch (e: Exception) { 81 | revert() 82 | throw e 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/SelfDismissActivity.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | 6 | class SelfDismissActivity : Activity() { 7 | override fun onCreate(savedInstanceState: Bundle?) { 8 | super.onCreate(savedInstanceState) 9 | finish() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/ServiceForegroundConnector.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.app.Service 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.ServiceConnection 7 | import androidx.fragment.app.Fragment 8 | import androidx.lifecycle.DefaultLifecycleObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import kotlin.reflect.KClass 11 | 12 | /** 13 | * owner also needs to be Context/Fragment. 14 | */ 15 | class ServiceForegroundConnector(private val owner: LifecycleOwner, private val connection: ServiceConnection, 16 | private val clazz: KClass) : DefaultLifecycleObserver { 17 | init { 18 | owner.lifecycle.addObserver(this) 19 | } 20 | 21 | private val context get() = when (owner) { 22 | is Context -> owner 23 | is Fragment -> owner.requireContext() 24 | else -> throw UnsupportedOperationException("Unsupported owner") 25 | } 26 | 27 | override fun onStart(owner: LifecycleOwner) { 28 | val context = context 29 | context.bindService(Intent(context, clazz.java), connection, Context.BIND_AUTO_CREATE) 30 | } 31 | 32 | override fun onStop(owner: LifecycleOwner) = context.stopAndUnbind(connection) 33 | } 34 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.net.ConnectivityManager 6 | import android.net.NetworkRequest 7 | import android.net.TetheringManager 8 | import android.net.wifi.WifiManager 9 | import android.net.wifi.p2p.WifiP2pManager 10 | import android.os.Handler 11 | import android.os.Looper 12 | import androidx.annotation.RequiresApi 13 | import androidx.core.content.getSystemService 14 | import timber.log.Timber 15 | 16 | object Services { 17 | private lateinit var contextInit: () -> Context 18 | val context by lazy { contextInit() } 19 | fun init(context: () -> Context) { 20 | contextInit = context 21 | } 22 | 23 | val mainHandler by lazy { Handler(Looper.getMainLooper()) } 24 | val connectivity by lazy { context.getSystemService()!! } 25 | val p2p by lazy { 26 | try { 27 | context.getSystemService() 28 | } catch (e: RuntimeException) { 29 | Timber.w(e) 30 | null 31 | } 32 | } 33 | val wifi by lazy { context.getSystemService()!! } 34 | @get:RequiresApi(30) 35 | val tethering by lazy { context.getSystemService()!! } 36 | 37 | val netd by lazy @SuppressLint("WrongConstant") { context.getSystemService("netd")!! } 38 | 39 | fun registerNetworkCallback(request: NetworkRequest, networkCallback: ConnectivityManager.NetworkCallback) = 40 | connectivity.registerNetworkCallback(request, networkCallback, mainHandler) 41 | } 42 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/util/UnblockCentral.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.net.MacAddress 5 | import android.net.TetheringManager 6 | import android.net.wifi.SoftApConfiguration 7 | import android.net.wifi.p2p.WifiP2pConfig 8 | import android.service.quicksettings.TileService 9 | import androidx.annotation.RequiresApi 10 | import org.lsposed.hiddenapibypass.HiddenApiBypass 11 | 12 | /** 13 | * The central object for accessing all the useful blocked APIs. Thanks Google! 14 | * 15 | * Lazy cannot be used directly as it will create inner classes. 16 | */ 17 | @SuppressLint("BlockedPrivateApi", "DiscouragedPrivateApi", "SoonBlockedPrivateApi") 18 | object UnblockCentral { 19 | var needInit = true 20 | /** 21 | * Retrieve this property before doing dangerous shit. 22 | */ 23 | private val init by lazy { if (needInit) check(HiddenApiBypass.setHiddenApiExemptions("")) } 24 | 25 | @RequiresApi(33) 26 | fun getCountryCode(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getCountryCode") } 27 | 28 | @RequiresApi(33) 29 | fun setRandomizedMacAddress(clazz: Class<*>) = init.let { 30 | clazz.getDeclaredMethod("setRandomizedMacAddress", MacAddress::class.java) 31 | } 32 | 33 | @get:RequiresApi(31) 34 | val SoftApConfiguration_BAND_TYPES get() = init.let { 35 | SoftApConfiguration::class.java.getDeclaredField("BAND_TYPES").get(null) as IntArray 36 | } 37 | 38 | @RequiresApi(31) 39 | fun getApInstanceIdentifier(clazz: Class<*>) = init.let { clazz.getDeclaredMethod("getApInstanceIdentifier") } 40 | 41 | @get:RequiresApi(29) 42 | val WifiP2pConfig_Builder_mNetworkName by lazy { 43 | init 44 | WifiP2pConfig.Builder::class.java.getDeclaredField("mNetworkName").apply { isAccessible = true } 45 | } 46 | 47 | val TileService_mToken by lazy { 48 | init 49 | TileService::class.java.getDeclaredField("mToken").apply { isAccessible = true } 50 | } 51 | 52 | @get:RequiresApi(30) 53 | val ITetheringConnector by lazy { Class.forName("android.net.ITetheringConnector") } 54 | @get:RequiresApi(30) 55 | val ITetheringConnector_stopTethering by lazy @RequiresApi(30) { 56 | init 57 | ITetheringConnector.getDeclaredMethod("stopTethering", Int::class.java, String::class.java, String::class.java, 58 | Class.forName("android.net.IIntResultListener")) 59 | } 60 | @get:RequiresApi(30) 61 | val TetheringManager_ConnectorConsumer by lazy { Class.forName("android.net.TetheringManager\$ConnectorConsumer") } 62 | @get:RequiresApi(30) 63 | val TetheringManager_getConnector by lazy { 64 | init 65 | TetheringManager::class.java.getDeclaredMethod("getConnector", TetheringManager_ConnectorConsumer).apply { 66 | isAccessible = true 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/widget/AlwaysAutoCompleteEditText.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.widget 2 | 3 | import android.content.Context 4 | import android.graphics.Rect 5 | import android.util.AttributeSet 6 | import android.view.View 7 | import com.google.android.material.textfield.MaterialAutoCompleteTextView 8 | 9 | /** 10 | * Based on: https://gist.github.com/furycomptuers/4961368 11 | */ 12 | class AlwaysAutoCompleteEditText @JvmOverloads constructor( 13 | context: Context, 14 | attrs: AttributeSet? = null, 15 | defStyleAttr: Int = com.google.android.material.R.attr.autoCompleteTextViewStyle, 16 | ) : MaterialAutoCompleteTextView(context, attrs, defStyleAttr) { 17 | override fun enoughToFilter() = true 18 | 19 | override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { 20 | super.onFocusChanged(focused, direction, previouslyFocusedRect) 21 | if (focused && windowVisibility != View.GONE && filter != null) { 22 | performFiltering(text, 0) 23 | showDropDown() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/widget/AutoCollapseTextView.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.widget 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.core.view.isGone 6 | 7 | class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, 8 | defStyleAttr: Int = android.R.attr.textViewStyle) : 9 | LinkTextView(context, attrs, defStyleAttr) { 10 | override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) { 11 | super.onTextChanged(text, start, lengthBefore, lengthAfter) 12 | isGone = text.isNullOrEmpty() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/widget/LinkTextView.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.widget 2 | 3 | import android.content.Context 4 | import android.text.method.LinkMovementMethod 5 | import android.util.AttributeSet 6 | import androidx.appcompat.widget.AppCompatTextView 7 | 8 | open class LinkTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, 9 | defStyleAttr: Int = android.R.attr.textViewStyle) : 10 | AppCompatTextView(context, attrs, defStyleAttr) { 11 | override fun setTextIsSelectable(selectable: Boolean) { 12 | super.setTextIsSelectable(selectable) 13 | movementMethod = LinkMovementMethod.getInstance() // override what was set in setTextIsSelectable 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mobile/src/main/java/be/mygod/vpnhotspot/widget/SmartSnackbar.kt: -------------------------------------------------------------------------------- 1 | package be.mygod.vpnhotspot.widget 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Looper 5 | import android.view.View 6 | import android.widget.Toast 7 | import androidx.annotation.StringRes 8 | import androidx.lifecycle.DefaultLifecycleObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import androidx.lifecycle.findViewTreeLifecycleOwner 11 | import be.mygod.vpnhotspot.App.Companion.app 12 | import be.mygod.vpnhotspot.util.readableMessage 13 | import com.google.android.material.snackbar.Snackbar 14 | import java.util.concurrent.atomic.AtomicReference 15 | 16 | sealed class SmartSnackbar { 17 | companion object { 18 | private val holder = AtomicReference() 19 | 20 | fun make(@StringRes text: Int): SmartSnackbar = make(app.getText(text)) 21 | fun make(text: CharSequence = ""): SmartSnackbar { 22 | val holder = holder.get() 23 | return if (holder == null) @SuppressLint("ShowToast") { 24 | if (Looper.myLooper() == null) Looper.prepare() 25 | ToastWrapper(Toast.makeText(app, text, Toast.LENGTH_LONG)) 26 | } else SnackbarWrapper(Snackbar.make(holder, text, Snackbar.LENGTH_LONG)) 27 | } 28 | fun make(e: Throwable) = make(e.readableMessage) 29 | } 30 | 31 | class Register(private val view: View) : DefaultLifecycleObserver { 32 | init { 33 | view.findViewTreeLifecycleOwner()!!.lifecycle.addObserver(this) 34 | } 35 | 36 | override fun onResume(owner: LifecycleOwner) = holder.set(view) 37 | override fun onPause(owner: LifecycleOwner) { 38 | holder.compareAndSet(view, null) 39 | } 40 | } 41 | 42 | abstract fun show() 43 | open fun action(@StringRes id: Int, listener: (View) -> Unit) { } 44 | open fun shortToast() = this 45 | } 46 | 47 | private class SnackbarWrapper(private val snackbar: Snackbar) : SmartSnackbar() { 48 | override fun show() = snackbar.show() 49 | 50 | override fun action(@StringRes id: Int, listener: (View) -> Unit) { 51 | snackbar.setAction(id, listener) 52 | } 53 | } 54 | 55 | private class ToastWrapper(private val toast: Toast) : SmartSnackbar() { 56 | override fun show() = toast.show() 57 | 58 | override fun shortToast() = apply { toast.duration = Toast.LENGTH_SHORT } 59 | } 60 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_autorenew.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_bug_report.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_build.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_card_giftcard.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_code.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_perm_scan_wifi.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_settings_backup_restore.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_settings_ethernet.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_settings_input_antenna.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_settings_input_component.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_update.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_action_wifi_protected_setup.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_alert_warning.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_av_closed_caption.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_av_closed_caption_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_content_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_content_file_copy.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_content_inbox.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_content_push_pin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_deployed_code.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_device_battery_charging_full.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_device_bluetooth.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_device_devices.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_device_network_wifi.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_device_usb.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_device_wifi_lock.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_device_wifi_tethering.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_hardware_device_hub.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_image_flash_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_image_looks_6.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_image_remove_red_eye.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_quick_settings_tile_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_settings_qrcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_social_people.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/ic_toggle_star.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/src/main/res/drawable/toggle_hex.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/dialog_nickname.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/dialog_static_ip.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/dialog_wps.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/fragment_clients.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 19 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /mobile/src/main/res/layout/fragment_ebeg.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | 22 | 23 | 28 | 29 | 36 | 37 | 43 | 44 | 51 | 52 | 59 | 60 | 68 | 69 |