├── .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 |
76 |
77 |
78 |
79 |
80 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/fragment_tethering.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/listitem_client.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
12 |
13 |
14 |
20 |
21 |
29 |
30 |
33 |
34 |
39 |
40 |
47 |
48 |
55 |
56 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/listitem_interface.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
17 |
18 |
26 |
27 |
30 |
31 |
39 |
40 |
46 |
47 |
54 |
55 |
56 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/listitem_manage.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
15 |
16 |
23 |
24 |
27 |
28 |
34 |
35 |
40 |
41 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/listitem_static_ip.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
18 |
19 |
26 |
27 |
30 |
31 |
39 |
40 |
45 |
46 |
53 |
54 |
55 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/preference_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
30 |
31 |
32 |
33 |
39 |
40 |
46 |
47 |
58 |
59 |
60 |
61 |
62 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/preference_widget_edittext_autocomplete.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/mobile/src/main/res/layout/preference_widget_material_switch.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
21 |
29 |
--------------------------------------------------------------------------------
/mobile/src/main/res/menu/navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
26 |
--------------------------------------------------------------------------------
/mobile/src/main/res/menu/popup_client.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/mobile/src/main/res/menu/toolbar_configuration.xml:
--------------------------------------------------------------------------------
1 |
2 |
27 |
--------------------------------------------------------------------------------
/mobile/src/main/res/menu/toolbar_tethering.xml:
--------------------------------------------------------------------------------
1 |
2 |
27 |
--------------------------------------------------------------------------------
/mobile/src/main/res/mipmap/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values-night/bools.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
5 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/dark_colorPrimary
4 | #005005
5 | #AEEA00
6 |
7 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values-v29/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @string/settings_service_wifi_lock_none
5 | - @string/settings_service_wifi_lock_high_perf_v29
6 | - @string/settings_service_wifi_lock_low_latency
7 |
8 |
9 | - None
10 | - HighPerf
11 | - LowLatency
12 |
13 |
14 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values-v29/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/transparent
4 |
5 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values-v30/bools.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @string/wifi_mac_randomization_none
5 | - @string/wifi_mac_randomization_persistent
6 | - @string/wifi_mac_randomization_non_persistent
7 |
8 |
9 |
10 | - @string/settings_service_masquerade_none
11 | - @string/settings_service_masquerade_simple
12 | - @string/settings_service_masquerade_netd
13 |
14 |
15 | - None
16 | - Simple
17 | - Netd
18 |
19 |
20 |
21 | - @string/settings_service_wifi_lock_none
22 | - @string/settings_service_wifi_lock_full
23 | - @string/settings_service_wifi_lock_high_perf
24 |
25 |
26 | - None
27 | - Full
28 | - HighPerf
29 |
30 |
31 |
32 | - @string/settings_service_ip_monitor_monitor
33 | - @string/settings_service_ip_monitor_monitor_root
34 | - @string/settings_service_ip_monitor_poll
35 | - @string/settings_service_ip_monitor_poll_root
36 |
37 |
38 | - Monitor
39 | - MonitorRoot
40 | - Poll
41 | - PollRoot
42 |
43 |
44 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values/bools.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | true
5 |
6 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #4CAF50
4 | #2e7d32
5 |
6 | @color/light_colorPrimary
7 | #087f23
8 | #AEEA00
9 | #6000
10 |
11 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values/dimen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 48dp
4 | 56dp
5 | 264dp
6 |
7 |
--------------------------------------------------------------------------------
/mobile/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
22 |
25 |
26 |
27 |
35 |
42 |
47 |
52 |
56 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/mobile/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/mobile/src/main/res/xml/full_backup_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/mobile/src/main/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/mobile/src/main/res/xml/log_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven("https://jitpack.io")
14 | }
15 | }
16 | include(":mobile")
17 |
--------------------------------------------------------------------------------