65
README.md
65
README.md
@@ -65,8 +65,20 @@ Default settings are picked to suit general use cases and maximize compatibility
|
||||
|
||||
* Keep Wi-Fi alive: Acquire Wi-Fi locks when repeater, temporary hotspot or system VPN hotspot is activated.
|
||||
- Choose "System default" to save battery life;
|
||||
- Choose "On" (default) if repeater/hotspot turns itself off automatically or stops working after a while;
|
||||
- Choose "High Performance Mode" to minimize packet loss and latency (will consume more power).
|
||||
- (prior to Android 10) Choose "On" (default) if repeater/hotspot turns itself off automatically or stops working after a while;
|
||||
- (prior to Android 10) Choose "High Performance Mode" to minimize packet loss and latency (will consume more power);
|
||||
- (since Android 10) Choose "Disable power save" to decrease packet latency.
|
||||
An example use case is when a voice connection needs to be kept active even after the device screen goes off.
|
||||
Using this mode may improve the call quality.
|
||||
Requires support from the hardware.
|
||||
- (since Android 10) Choose "Low latency mode" to optimize for reduced packet latency, and this might result in:
|
||||
1. Reduced battery life.
|
||||
2. Reduced throughput.
|
||||
3. Reduced frequency of Wi-Fi scanning.
|
||||
This may cause the device not roaming or switching to the AP with highest signal quality, and location accuracy may be reduced.
|
||||
Example use cases are real time gaming or virtual reality applications where low latency is a key factor for user experience.
|
||||
Requires support from the hardware.
|
||||
Note: Requires this app running in foreground with screen on.
|
||||
* Start repeater on boot: Self explanatory.
|
||||
* Network status monitor mode: This option controls how the app monitors connected devices as well as interface changes
|
||||
(when custom upstream is used).
|
||||
@@ -107,23 +119,27 @@ _a.k.a. things that can go wrong if this app doesn't work._
|
||||
This is a list of stuff that might impact this app's functionality if unavailable.
|
||||
This is only meant to be an index. You can read more in the source code.
|
||||
|
||||
Undocumented API list:
|
||||
Non-public API list:
|
||||
|
||||
* (since API 24) [`Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#28703)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112695)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112696)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112697)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112882)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112972)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112974)
|
||||
* [`Landroid/net/wifi/p2p/WifiP2pGroup;->getNetworkId()I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123194)
|
||||
* [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/Collection;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123239)
|
||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123431)
|
||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123450)
|
||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123458)
|
||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123459)
|
||||
* [`Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#299587)
|
||||
* (since API 24) [`Landroid/bluetooth/BluetoothPan;->isTetheringOn()Z,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#32103)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;-><init>()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123103)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringFailed()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123104)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager$OnStartTetheringCallback;->onTetheringStarted()V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123105)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123309)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123408)
|
||||
* (since API 24) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123410)
|
||||
* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apBand:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131529)
|
||||
* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#131530)
|
||||
* [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132289)
|
||||
* [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,system-api,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#132358)
|
||||
* (deprecated since API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z`
|
||||
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pGroup;->getNetworkId()I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134440)
|
||||
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pGroupList;->getGroupList()Ljava/util/Collection;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134487)
|
||||
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->deletePersistentGroup(Landroid/net/wifi/p2p/WifiP2pManager$Channel;ILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134703)
|
||||
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->requestPersistentGroupInfo(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/p2p/WifiP2pManager$PersistentGroupInfoListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134728)
|
||||
* (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->setWifiP2pChannels(Landroid/net/wifi/p2p/WifiP2pManager$Channel;IILandroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134737)
|
||||
* [`Landroid/net/wifi/p2p/WifiP2pManager;->startWps(Landroid/net/wifi/p2p/WifiP2pManager$Channel;Landroid/net/wifi/WpsInfo;Landroid/net/wifi/p2p/WifiP2pManager$ActionListener;)V,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134738)
|
||||
* (prior to API 29) [`Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,greylist-max-p`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#335306)
|
||||
|
||||
Undocumented system configurations:
|
||||
|
||||
@@ -134,21 +150,22 @@ Undocumented system configurations:
|
||||
|
||||
Other:
|
||||
|
||||
* (since API 27) [`Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#144760) is assumed to be `"tether_offload_disabled"`.
|
||||
* (since API 29) `android.net.wifi.p2p.WifiP2pConfig` needs to be parcelized in a very specific order.
|
||||
* (since API 27) [`Landroid/provider/Settings$Global;->TETHER_OFFLOAD_DISABLED:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#158331) is assumed to be `"tether_offload_disabled"`.
|
||||
* (since API 27) `com.android.server.connectivity.tethering.OffloadHardwareInterface.DEFAULT_TETHER_OFFLOAD_DISABLED` is assumed to be 0.
|
||||
* Several constants in `ConnectivityManager` is assumed to be defined as in `TetheringManager.kt`;
|
||||
* Following broadcasts are assumed to be sticky:
|
||||
- [`Landroid/net/ConnectivityManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#112743) is assumed to be `android.net.conn.TETHER_STATE_CHANGED`.
|
||||
- [`Landroid/net/wifi/p2p/WifiP2pManager;->WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/aa21a6e/appcompat/hiddenapi-flags.csv#123415) is assumed to be `android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED`;
|
||||
- [`Landroid/net/ConnectivityManager;->ACTION_TETHER_STATE_CHANGED:Ljava/lang/String;,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#123163) is assumed to be `android.net.conn.TETHER_STATE_CHANGED`.
|
||||
- (prior to API 29) [`Landroid/net/wifi/p2p/WifiP2pManager;->WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION:Ljava/lang/String;,greylist-max-o`](https://android.googlesource.com/platform/prebuilts/runtime/+/3d07e5c/appcompat/hiddenapi-flags.csv#134686) is assumed to be `android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED`;
|
||||
* Activity `com.android.settings/.Settings$TetherSettingsActivity` is assumed to be exported.
|
||||
|
||||
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000
|
||||
respectively; `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000) for API 27-.
|
||||
For `ip rule` priorities, `RULE_PRIORITY_SECURE_VPN` and `RULE_PRIORITY_TETHERING` is assumed to be 12000 and 18000 respectively;
|
||||
(prior to API 24) `RULE_PRIORITY_DEFAULT_NETWORK` is assumed to be 22000 (or at least > 18000).
|
||||
DHCP server like `dnsmasq` is assumed to run and send DHCP packets as root.
|
||||
|
||||
Undocumented system binaries are all bundled and executable:
|
||||
|
||||
* Since API 24: `iptables-save`;
|
||||
* (since API 24) `iptables-save`;
|
||||
* `echo`;
|
||||
* `ip` (`link monitor neigh rule` with proper output format);
|
||||
* `ndc` (`ipfwd` with proper output format since API 23, `nat`);
|
||||
@@ -157,7 +174,7 @@ Undocumented system binaries are all bundled and executable:
|
||||
|
||||
If some of these are unavailable, you can alternatively install a recent version (v1.28.1 or higher) of Busybox.
|
||||
|
||||
Wi-Fi driver `wpa_supplicant`:
|
||||
(prior to API 29) Wi-Fi driver `wpa_supplicant`:
|
||||
|
||||
* P2P configuration file is assumed to be saved to [`/data/vendor/wifi/wpa/p2p_supplicant.conf` or `/data/misc/wifi/p2p_supplicant.conf`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/0b4856b6dc451e290f1f64f6af17e010be78c073/wpa_supplicant/hidl/1.1/supplicant.cpp#26) and have reasonable format;
|
||||
* Android system is expected to restart `wpa_supplicant` after it crashes.
|
||||
|
||||
@@ -12,10 +12,10 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||
classpath 'com.android.tools.build:gradle:3.5.0-beta04'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'io.fabric.tools:gradle:1.28.1'
|
||||
classpath 'io.fabric.tools:gradle:1.29.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ style:
|
||||
OptionalUnit:
|
||||
active: true
|
||||
OptionalWhenBraces:
|
||||
active: true
|
||||
active: false
|
||||
PreferToOverPairSyntax:
|
||||
active: false
|
||||
ProtectedMemberInFinalClass:
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
android.enableJetifier=true
|
||||
android.enableR8=true
|
||||
android.enableR8.fullMode=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
@@ -9,8 +9,7 @@ if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Fdro
|
||||
}
|
||||
|
||||
android {
|
||||
buildToolsVersion "28.0.3"
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
@@ -18,10 +17,10 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "be.mygod.vpnhotspot"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 29
|
||||
resConfigs "ru", "zh-rCN"
|
||||
versionCode 124
|
||||
versionName "2.3.5"
|
||||
versionCode 204
|
||||
versionName '2.4.4'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
@@ -65,39 +64,39 @@ androidExtensions {
|
||||
}
|
||||
|
||||
def aux = [
|
||||
'com.crashlytics.sdk.android:crashlytics:2.9.9',
|
||||
'com.google.firebase:firebase-core:16.0.8',
|
||||
'com.crashlytics.sdk.android:crashlytics:2.10.1',
|
||||
'com.google.firebase:firebase-core:16.0.9',
|
||||
]
|
||||
def lifecycleVersion = '2.0.0'
|
||||
def roomVersion = '2.1.0-alpha07'
|
||||
def lifecycleVersion = '2.1.0-beta01'
|
||||
def roomVersion = '2.1.0-rc01'
|
||||
dependencies {
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
implementation 'androidx.core:core-ktx:1.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.1.0-rc01'
|
||||
implementation 'androidx.emoji:emoji:1.0.0'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||
implementation 'androidx.preference:preference:1.1.0-alpha04'
|
||||
implementation 'androidx.preference:preference:1.1.0-beta01'
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
implementation 'com.android.billingclient:billing:1.2.2'
|
||||
implementation 'com.github.luongvo:BadgeView:1.1.5'
|
||||
implementation 'com.android.billingclient:billing:2.0.1'
|
||||
implementation 'com.github.topjohnwu.libsu:core:2.5.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha07'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.linkedin.dexmaker:dexmaker:2.25.0'
|
||||
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation 'net.glxn.qrgen:android:2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
|
||||
for (dep in aux) {
|
||||
freedomImplementation dep
|
||||
googleImplementation dep
|
||||
}
|
||||
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.1-beta01'
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_USB"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.TETHER_PRIVILEGED"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
@@ -39,6 +41,8 @@
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<!-- Required since API 29 -->
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
|
||||
@@ -3,17 +3,18 @@ package be.mygod.vpnhotspot
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.UiModeManager
|
||||
import android.content.ClipboardManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.provider.FontRequest
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.emoji.text.FontRequestEmojiCompatConfig
|
||||
import androidx.preference.PreferenceManager
|
||||
import be.mygod.vpnhotspot.net.DhcpWorkaround
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.util.DeviceStorageApp
|
||||
@@ -32,7 +33,8 @@ class App : Application() {
|
||||
app = this
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
deviceStorage = DeviceStorageApp(this)
|
||||
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager.getDefaultSharedPreferencesName(this))
|
||||
// alternative to PreferenceManager.getDefaultSharedPreferencesName(this)
|
||||
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
|
||||
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
||||
} else deviceStorage = this
|
||||
DebugHelper.init()
|
||||
@@ -48,10 +50,11 @@ class App : Application() {
|
||||
override fun onFailed(throwable: Throwable?) = Timber.d(throwable)
|
||||
})
|
||||
})
|
||||
EBegFragment.init()
|
||||
if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ServiceNotification.updateNotificationChannels()
|
||||
}
|
||||
@@ -69,6 +72,7 @@ class App : Application() {
|
||||
}
|
||||
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
|
||||
val connectivity by lazy { getSystemService<ConnectivityManager>()!! }
|
||||
val clipboard by lazy { getSystemService<ClipboardManager>()!! }
|
||||
val uiMode by lazy { getSystemService<UiModeManager>()!! }
|
||||
val wifi by lazy { getSystemService<WifiManager>()!! }
|
||||
|
||||
|
||||
@@ -1,40 +1,66 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.Spinner
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.util.launchUrl
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import com.android.billingclient.api.*
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_ebeg.view.*
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/java/org/sufficientlysecure/donations/DonationsFragment.java
|
||||
*/
|
||||
class EBegFragment : AppCompatDialogFragment(), PurchasesUpdatedListener, BillingClientStateListener,
|
||||
SkuDetailsResponseListener, ConsumeResponseListener {
|
||||
@Parcelize
|
||||
data class MessageArg(@StringRes val title: Int, @StringRes val message: Int) : Parcelable
|
||||
class MessageDialogFragment : AlertDialogFragment<MessageArg, Empty>() {
|
||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||
setTitle(arg.title)
|
||||
setMessage(arg.message)
|
||||
setNeutralButton(R.string.donations__button_close, null)
|
||||
class EBegFragment : AppCompatDialogFragment(), SkuDetailsResponseListener {
|
||||
companion object : BillingClientStateListener, PurchasesUpdatedListener, ConsumeResponseListener {
|
||||
private lateinit var billingClient: BillingClient
|
||||
|
||||
fun init() {
|
||||
billingClient = BillingClient.newBuilder(app).apply {
|
||||
enablePendingPurchases()
|
||||
}.setListener(this).build().also { it.startConnection(this) }
|
||||
}
|
||||
|
||||
override fun onBillingSetupFinished(billingResult: BillingResult?) {
|
||||
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
billingClient.queryPurchases(BillingClient.SkuType.INAPP).apply {
|
||||
if (responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
onPurchasesUpdated(this.billingResult, purchasesList)
|
||||
}
|
||||
}
|
||||
} else Timber.e("onBillingSetupFinished: ${billingResult?.responseCode}")
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
Timber.e("onBillingServiceDisconnected")
|
||||
billingClient.startConnection(this)
|
||||
}
|
||||
|
||||
override fun onPurchasesUpdated(billingResult: BillingResult?, purchases: MutableList<Purchase>?) {
|
||||
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
|
||||
// directly consume in-app purchase, so that people can donate multiple times
|
||||
for (purchase in purchases) if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
|
||||
billingClient.consumeAsync(ConsumeParams.newBuilder().apply {
|
||||
setPurchaseToken(purchase.purchaseToken)
|
||||
}.build(), this)
|
||||
}
|
||||
} else Timber.e("onPurchasesUpdated: ${billingResult?.responseCode}")
|
||||
}
|
||||
|
||||
override fun onConsumeResponse(billingResult: BillingResult?, purchaseToken: String?) {
|
||||
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
SmartSnackbar.make(R.string.donations__thanks_dialog).show()
|
||||
} else Timber.e("onConsumeResponse: ${billingResult?.responseCode}")
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var billingClient: BillingClient
|
||||
private lateinit var googleSpinner: Spinner
|
||||
private var skus: MutableList<SkuDetails>? = null
|
||||
set(value) {
|
||||
@@ -53,62 +79,27 @@ class EBegFragment : AppCompatDialogFragment(), PurchasesUpdatedListener, Billin
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
dialog!!.setTitle(R.string.settings_misc_donate)
|
||||
googleSpinner = view.donations__google_android_market_spinner
|
||||
onBillingServiceDisconnected()
|
||||
billingClient.querySkuDetailsAsync(
|
||||
SkuDetailsParams.newBuilder().apply {
|
||||
setSkusList(listOf("donate001", "donate002", "donate005", "donate010", "donate020", "donate050",
|
||||
"donate100", "donate200", "donatemax"))
|
||||
setType(BillingClient.SkuType.INAPP)
|
||||
}.build(), this)
|
||||
view.donations__google_android_market_donate_button.setOnClickListener {
|
||||
val sku = skus?.getOrNull(googleSpinner.selectedItemPosition)
|
||||
if (sku == null) {
|
||||
openDialog(R.string.donations__google_android_market_not_supported_title,
|
||||
R.string.donations__google_android_market_not_supported)
|
||||
} else billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(sku).build())
|
||||
if (sku != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply {
|
||||
setSkuDetails(sku)
|
||||
}.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
|
||||
}
|
||||
@Suppress("ConstantConditionIf")
|
||||
if (BuildConfig.DONATIONS) (view.donations__more_stub.inflate() as Button)
|
||||
.setOnClickListener { requireContext().launchUrl("https://mygod.be/donate/") }
|
||||
}
|
||||
|
||||
private fun openDialog(@StringRes title: Int, @StringRes message: Int) {
|
||||
val fragmentManager = fragmentManager
|
||||
if (fragmentManager == null) SmartSnackbar.make(message).show() else try {
|
||||
MessageDialogFragment().withArg(MessageArg(title, message)).show(fragmentManager, "MessageDialogFragment")
|
||||
} catch (e: IllegalStateException) {
|
||||
SmartSnackbar.make(message).show()
|
||||
override fun onSkuDetailsResponse(billingResult: BillingResult?, skuDetailsList: MutableList<SkuDetails>?) {
|
||||
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) skus = skuDetailsList else {
|
||||
Timber.e("onSkuDetailsResponse: ${billingResult?.responseCode}")
|
||||
SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
skus = null
|
||||
billingClient = BillingClient.newBuilder(context ?: return).setListener(this).build()
|
||||
.also { it.startConnection(this) }
|
||||
}
|
||||
|
||||
override fun onBillingSetupFinished(responseCode: Int) {
|
||||
if (responseCode == BillingClient.BillingResponse.OK) {
|
||||
billingClient.querySkuDetailsAsync(
|
||||
SkuDetailsParams.newBuilder().apply {
|
||||
setSkusList(listOf("donate001", "donate002", "donate005", "donate010", "donate020", "donate050",
|
||||
"donate100", "donate200", "donatemax"))
|
||||
setType(BillingClient.SkuType.INAPP)
|
||||
}.build(), this)
|
||||
} else Timber.e("onBillingSetupFinished: $responseCode")
|
||||
}
|
||||
|
||||
override fun onSkuDetailsResponse(responseCode: Int, skuDetailsList: MutableList<SkuDetails>?) {
|
||||
if (responseCode == BillingClient.BillingResponse.OK) skus = skuDetailsList
|
||||
else Timber.e("onSkuDetailsResponse: $responseCode")
|
||||
}
|
||||
|
||||
override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList<Purchase>?) {
|
||||
if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
|
||||
// directly consume in-app purchase, so that people can donate multiple times
|
||||
purchases.forEach { billingClient.consumeAsync(it.purchaseToken, this) }
|
||||
} else Timber.e("onPurchasesUpdated: $responseCode")
|
||||
}
|
||||
|
||||
override fun onConsumeResponse(responseCode: Int, purchaseToken: String?) {
|
||||
if (responseCode == BillingClient.BillingResponse.OK) {
|
||||
openDialog(R.string.donations__thanks_dialog_title, R.string.donations__thanks_dialog)
|
||||
dismissAllowingStateLoss()
|
||||
} else Timber.e("onConsumeResponse: $responseCode")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
||||
private var routingManager: RoutingManager? = null
|
||||
private var receiverRegistered = false
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
val ifaces = intent.localOnlyTetheredIfaces
|
||||
val ifaces = intent.localOnlyTetheredIfaces ?: return@broadcastReceiver
|
||||
DebugHelper.log(TAG, "onTetherStateChangedLocked: $ifaces")
|
||||
check(ifaces.size <= 1)
|
||||
val iface = ifaces.singleOrNull()
|
||||
|
||||
@@ -2,28 +2,25 @@ package be.mygod.vpnhotspot
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.get
|
||||
import androidx.lifecycle.observe
|
||||
import be.mygod.vpnhotspot.client.ClientViewModel
|
||||
import be.mygod.vpnhotspot.client.ClientsFragment
|
||||
import be.mygod.vpnhotspot.databinding.ActivityMainBinding
|
||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import q.rorbin.badgeview.QBadgeView
|
||||
|
||||
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var badge: QBadgeView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -31,16 +28,17 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
||||
binding.lifecycleOwner = this
|
||||
binding.navigation.setOnNavigationItemSelectedListener(this)
|
||||
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
||||
badge = QBadgeView(this)
|
||||
badge.bindTarget((binding.navigation.getChildAt(0) as BottomNavigationMenuView).getChildAt(1))
|
||||
badge.badgeBackgroundColor = ContextCompat.getColor(this, R.color.colorSecondary)
|
||||
badge.badgeTextColor = ContextCompat.getColor(this, R.color.primary_text_default_material_light)
|
||||
badge.badgeGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
badge.setGravityOffset(16f, 0f, true)
|
||||
val model = ViewModelProviders.of(this).get<ClientViewModel>()
|
||||
if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class)
|
||||
model.clients.observe(this, Observer { badge.badgeNumber = it.size })
|
||||
model.clients.observe(this) {
|
||||
if (it.isNotEmpty()) binding.navigation.showBadge(R.id.navigation_clients).apply {
|
||||
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary)
|
||||
badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light)
|
||||
number = it.size
|
||||
} else binding.navigation.removeBadge(R.id.navigation_clients)
|
||||
}
|
||||
SmartSnackbar.Register(lifecycle, binding.fragmentHolder)
|
||||
WifiDoubleLock.ActivityListener(this)
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.net.NetworkInfo
|
||||
import android.net.wifi.p2p.WifiP2pDevice
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.net.wifi.p2p.WifiP2pInfo
|
||||
import android.net.wifi.p2p.WifiP2pManager
|
||||
import android.net.wifi.WpsInfo
|
||||
import android.net.wifi.p2p.*
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.getSystemService
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
|
||||
@@ -21,10 +20,8 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
||||
import be.mygod.vpnhotspot.util.StickyEvent0
|
||||
import be.mygod.vpnhotspot.util.StickyEvent1
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
import be.mygod.vpnhotspot.util.intentFilter
|
||||
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
|
||||
import be.mygod.vpnhotspot.util.*
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
@@ -35,7 +32,14 @@ import java.lang.reflect.InvocationTargetException
|
||||
class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
companion object {
|
||||
private const val TAG = "RepeaterService"
|
||||
const val KEY_OPERATING_CHANNEL = "service.repeater.oc"
|
||||
private const val KEY_NETWORK_NAME = "service.repeater.networkName"
|
||||
private const val KEY_PASSPHRASE = "service.repeater.passphrase"
|
||||
private const val KEY_OPERATING_BAND = "service.repeater.band"
|
||||
private const val KEY_OPERATING_CHANNEL = "service.repeater.oc"
|
||||
/**
|
||||
* Placeholder for bypassing networkName check.
|
||||
*/
|
||||
private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot"
|
||||
|
||||
/**
|
||||
* This is only a "ServiceConnection" to system service and its impact on system is minimal.
|
||||
@@ -49,12 +53,24 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
}
|
||||
}
|
||||
val supported get() = p2pManager != null
|
||||
@Deprecated("Not initialized and no use at all since API 29")
|
||||
var persistentSupported = false
|
||||
|
||||
val operatingChannel: Int get() {
|
||||
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
||||
return if (result in 1..165) result else 0
|
||||
}
|
||||
var networkName: String?
|
||||
get() = app.pref.getString(KEY_NETWORK_NAME, null)
|
||||
set(value) = app.pref.edit { putString(KEY_NETWORK_NAME, value) }
|
||||
var passphrase: String?
|
||||
get() = app.pref.getString(KEY_PASSPHRASE, null)
|
||||
set(value) = app.pref.edit { putString(KEY_PASSPHRASE, value) }
|
||||
var operatingBand: Int
|
||||
@SuppressLint("InlinedApi") get() = app.pref.getInt(KEY_OPERATING_BAND, WifiP2pConfig.GROUP_OWNER_BAND_AUTO)
|
||||
set(value) = app.pref.edit { putInt(KEY_OPERATING_BAND, value) }
|
||||
var operatingChannel: Int
|
||||
get() {
|
||||
val result = app.pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
||||
return if (result in 1..165) result else 0
|
||||
}
|
||||
set(value) = app.pref.edit { putString(KEY_OPERATING_CHANNEL, value.toString()) }
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
@@ -71,16 +87,16 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
groupChanged(value)
|
||||
}
|
||||
val groupChanged = StickyEvent1 { group }
|
||||
@Deprecated("Not initialized and no use at all since API 29")
|
||||
var thisDevice: WifiP2pDevice? = null
|
||||
|
||||
@Deprecated("WPS was deprecated RIP")
|
||||
fun startWps(pin: String? = null) {
|
||||
val channel = channel
|
||||
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||
else @Suppress("DEPRECATION") if (active) p2pManager.startWps(channel, android.net.wifi.WpsInfo().apply {
|
||||
setup = if (pin == null) android.net.wifi.WpsInfo.PBC else {
|
||||
else if (active) p2pManager.startWps(channel, WpsInfo().apply {
|
||||
setup = if (pin == null) WpsInfo.PBC else {
|
||||
this.pin = pin
|
||||
android.net.wifi.WpsInfo.KEYPAD
|
||||
WpsInfo.KEYPAD
|
||||
}
|
||||
}, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = SmartSnackbar.make(
|
||||
@@ -94,18 +110,6 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
fun shutdown() {
|
||||
if (active) removeGroup()
|
||||
}
|
||||
|
||||
fun resetCredentials() {
|
||||
val channel = channel
|
||||
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||
else p2pManager.deletePersistentGroup(channel, (group ?: return).netId,
|
||||
object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = SmartSnackbar.make(R.string.repeater_reset_credentials_success)
|
||||
.shortToast().show()
|
||||
override fun onFailure(reason: Int) = SmartSnackbar.make(
|
||||
formatReason(R.string.repeater_reset_credentials_failure, reason)).show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private val p2pManager get() = RepeaterService.p2pManager!!
|
||||
@@ -119,11 +123,12 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
if (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0) ==
|
||||
WifiP2pManager.WIFI_P2P_STATE_DISABLED) clean() // ignore P2P enabled
|
||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> onP2pConnectionChanged(
|
||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
|
||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO),
|
||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
|
||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO)!!,
|
||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)!!)
|
||||
}
|
||||
}
|
||||
@Deprecated("No longer used since API 29")
|
||||
@Suppress("DEPRECATION")
|
||||
private val deviceListener = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> binder.thisDevice =
|
||||
@@ -132,6 +137,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
}
|
||||
}
|
||||
private var routingManager: RoutingManager? = null
|
||||
private var persistNextGroup = false
|
||||
|
||||
var status = Status.IDLE
|
||||
private set(value) {
|
||||
@@ -154,13 +160,17 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
onChannelDisconnected()
|
||||
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
|
||||
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
|
||||
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
|
||||
registerReceiver(deviceListener, intentFilter(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION,
|
||||
WifiP2pManagerHelper.WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION))
|
||||
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = binder
|
||||
|
||||
@Deprecated("No longer used since API 29")
|
||||
@Suppress("DEPRECATION")
|
||||
private fun setOperatingChannel(oc: Int = operatingChannel) = try {
|
||||
val channel = channel
|
||||
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||
@@ -183,17 +193,21 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
channel = null
|
||||
if (status != Status.DESTROYED) try {
|
||||
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
|
||||
setOperatingChannel()
|
||||
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") setOperatingChannel()
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
handler.postDelayed(this::onChannelDisconnected, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("No longer used since API 29")
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == KEY_OPERATING_CHANNEL) setOperatingChannel()
|
||||
}
|
||||
|
||||
@Deprecated("No longer used since API 29")
|
||||
@Suppress("DEPRECATION")
|
||||
private fun onPersistentGroupsChanged() {
|
||||
val channel = channel ?: return
|
||||
val device = binder.thisDevice ?: return
|
||||
@@ -232,34 +246,86 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
||||
receiverRegistered = true
|
||||
p2pManager.requestGroupInfo(channel) {
|
||||
when {
|
||||
it == null -> doStart()
|
||||
it.isGroupOwner -> if (routingManager == null) doStart(it)
|
||||
else -> {
|
||||
Timber.i("Removing old group ($it)")
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = doStart()
|
||||
override fun onFailure(reason: Int) =
|
||||
startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
|
||||
})
|
||||
try {
|
||||
p2pManager.requestGroupInfo(channel) {
|
||||
when {
|
||||
it == null -> doStart()
|
||||
it.isGroupOwner -> if (routingManager == null) doStart(it)
|
||||
else -> {
|
||||
Timber.i("Removing old group ($it)")
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = doStart()
|
||||
override fun onFailure(reason: Int) =
|
||||
startFailure(formatReason(R.string.repeater_remove_old_group_failure, reason))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Timber.w(e)
|
||||
startFailure(e.readableMessage)
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
/**
|
||||
* startService Step 2 (if a group isn't already available)
|
||||
*/
|
||||
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onFailure(reason: Int) = startFailure(formatReason(R.string.repeater_create_group_failure, reason))
|
||||
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
|
||||
})
|
||||
private fun doStart() {
|
||||
val listener = object : WifiP2pManager.ActionListener {
|
||||
override fun onFailure(reason: Int) {
|
||||
startFailure(formatReason(R.string.repeater_create_group_failure, reason))
|
||||
}
|
||||
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
|
||||
}
|
||||
val channel = channel ?: return listener.onFailure(WifiP2pManager.BUSY)
|
||||
val networkName = networkName
|
||||
val passphrase = passphrase
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < 29 || networkName == null || passphrase == null) {
|
||||
persistNextGroup = true
|
||||
p2pManager.createGroup(channel, listener)
|
||||
} else p2pManager.createGroup(channel, WifiP2pConfig.Builder().apply {
|
||||
setNetworkName(PLACEHOLDER_NETWORK_NAME)
|
||||
setPassphrase(passphrase)
|
||||
operatingChannel.let { oc ->
|
||||
if (oc == 0) setGroupOperatingBand(operatingBand)
|
||||
else setGroupOperatingFrequency(channelToFrequency(oc))
|
||||
}
|
||||
}.build().run {
|
||||
useParcel { p ->
|
||||
p.writeParcelable(this, 0)
|
||||
val end = p.dataPosition()
|
||||
p.setDataPosition(0)
|
||||
val creator = p.readString()
|
||||
val deviceAddress = p.readString()
|
||||
val wps = p.readParcelable<WpsInfo>(javaClass.classLoader)
|
||||
val long = p.readLong()
|
||||
check(p.readString() == PLACEHOLDER_NETWORK_NAME)
|
||||
check(p.readString() == passphrase)
|
||||
val int = p.readInt()
|
||||
check(p.dataPosition() == end)
|
||||
p.setDataPosition(0)
|
||||
p.writeString(creator)
|
||||
p.writeString(deviceAddress)
|
||||
p.writeParcelable(wps, 0)
|
||||
p.writeLong(long)
|
||||
p.writeString(networkName)
|
||||
p.writeString(passphrase)
|
||||
p.writeInt(int)
|
||||
p.setDataPosition(0)
|
||||
p.readParcelable<WifiP2pConfig>(javaClass.classLoader)
|
||||
}
|
||||
}, listener)
|
||||
} catch (e: SecurityException) {
|
||||
Timber.w(e)
|
||||
startFailure(e.readableMessage)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Used during step 2, also called when connection changed
|
||||
*/
|
||||
private fun onP2pConnectionChanged(info: WifiP2pInfo, net: NetworkInfo?, group: WifiP2pGroup) {
|
||||
DebugHelper.log(TAG, "P2P connection changed: $info\n$net\n$group")
|
||||
private fun onP2pConnectionChanged(info: WifiP2pInfo, group: WifiP2pGroup) {
|
||||
DebugHelper.log(TAG, "P2P connection changed: $info\n$group")
|
||||
when {
|
||||
!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner -> {
|
||||
if (routingManager != null) clean() // P2P shutdown, else other groups changing before start, ignore
|
||||
@@ -276,6 +342,11 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
*/
|
||||
private fun doStart(group: WifiP2pGroup) {
|
||||
binder.group = group
|
||||
if (persistNextGroup) {
|
||||
networkName = group.networkName
|
||||
passphrase = group.passphrase
|
||||
persistNextGroup = false
|
||||
}
|
||||
check(routingManager == null)
|
||||
routingManager = RoutingManager.LocalOnly(this, group.`interface`!!).apply { start() }
|
||||
status = Status.ACTIVE
|
||||
@@ -320,8 +391,10 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
if (status != Status.IDLE) binder.shutdown()
|
||||
clean() // force clean to prevent leakage
|
||||
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||
unregisterReceiver(deviceListener)
|
||||
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
|
||||
app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||
unregisterReceiver(deviceListener)
|
||||
}
|
||||
status = Status.DESTROYED
|
||||
if (Build.VERSION.SDK_INT >= 27) channel?.close()
|
||||
super.onDestroy()
|
||||
|
||||
@@ -12,6 +12,7 @@ import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
||||
import be.mygod.vpnhotspot.net.monitor.IpMonitor
|
||||
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
|
||||
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
@@ -26,6 +27,7 @@ import java.net.SocketException
|
||||
|
||||
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
WifiDoubleLock.mode = WifiDoubleLock.mode // handle complicated default value and possible system upgrades
|
||||
preferenceManager.preferenceDataStore = SharedPreferenceDataStore(app.pref)
|
||||
RoutingManager.masqueradeMode = RoutingManager.masqueradeMode // flush default value
|
||||
addPreferencesFromResource(R.xml.pref_settings)
|
||||
|
||||
@@ -48,7 +48,7 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
synchronized(downstreams) {
|
||||
val toRemove = downstreams.toMutableMap() // make a copy
|
||||
for (iface in intent.tetheredIfaces) {
|
||||
for (iface in intent.tetheredIfaces ?: return@synchronized) {
|
||||
val downstream = toRemove.remove(iface) ?: continue
|
||||
if (downstream.monitor) downstream.start()
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
if (start()) check(downstreams.put(iface, this) == null) else destroy()
|
||||
}
|
||||
}
|
||||
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.let { iface ->
|
||||
intent.getStringExtra(EXTRA_ADD_INTERFACE_MONITOR)?.also { iface ->
|
||||
val downstream = downstreams[iface]
|
||||
if (downstream == null) Downstream(this, iface, true).apply {
|
||||
start()
|
||||
@@ -98,7 +98,7 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
downstreams[iface] = this
|
||||
} else downstream.monitor = true
|
||||
}
|
||||
downstreams.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.destroy()
|
||||
intent.getStringExtra(EXTRA_REMOVE_INTERFACE)?.also { downstreams.remove(it)?.destroy() }
|
||||
updateNotification() // call this first just in case we are shutting down immediately
|
||||
onDownstreamsChangedLocked()
|
||||
} else if (downstreams.isEmpty()) stopSelf(startId)
|
||||
|
||||
@@ -3,7 +3,7 @@ package be.mygod.vpnhotspot.client
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.StrikethroughSpan
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.map
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
@@ -27,7 +27,7 @@ open class Client(val mac: Long, val iface: String) {
|
||||
|
||||
val ip = TreeMap<InetAddress, IpNeighbour.State>(InetAddressComparator)
|
||||
val macString by lazy { mac.macToString() }
|
||||
private val record = AppDatabase.instance.clientRecordDao.lookupSync(mac)
|
||||
private val record = AppDatabase.instance.clientRecordDao.lookupOrDefaultSync(mac)
|
||||
private val macIface get() = SpannableStringBuilder(makeMacSpan(macString)).apply {
|
||||
append('%')
|
||||
append(iface)
|
||||
@@ -37,24 +37,22 @@ open class Client(val mac: Long, val iface: String) {
|
||||
val blocked get() = record.value?.blocked == true
|
||||
|
||||
open val icon get() = TetherType.ofInterface(iface).icon
|
||||
val title = Transformations.map(record) { record ->
|
||||
val title = record.map { record ->
|
||||
/**
|
||||
* we hijack the get title process to check if we need to perform MacLookup,
|
||||
* as record might not be initialized in other more appropriate places
|
||||
*/
|
||||
SpannableStringBuilder(if (record?.nickname.isNullOrEmpty()) {
|
||||
if (record?.macLookupPending != false) MacLookup.perform(mac)
|
||||
SpannableStringBuilder(if (record.nickname.isEmpty()) {
|
||||
if (record.macLookupPending) MacLookup.perform(mac)
|
||||
macIface
|
||||
} else emojize(record?.nickname)).apply {
|
||||
if (record?.blocked == true) {
|
||||
setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
|
||||
}
|
||||
} else emojize(record.nickname)).apply {
|
||||
if (record.blocked) setSpan(StrikethroughSpan(), 0, length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
|
||||
}
|
||||
}
|
||||
val titleSelectable = Transformations.map(record) { it?.nickname.isNullOrEmpty() }
|
||||
val description = Transformations.map(record) { record ->
|
||||
val titleSelectable = record.map { it.nickname.isEmpty() }
|
||||
val description = record.map { record ->
|
||||
SpannableStringBuilder().apply {
|
||||
if (!record?.nickname.isNullOrEmpty()) appendln(macIface)
|
||||
if (record.nickname.isNotEmpty()) appendln(macIface)
|
||||
ip.entries.forEach { (ip, state) ->
|
||||
append(makeIpSpan(ip))
|
||||
appendln(app.getText(when (state) {
|
||||
|
||||
@@ -20,7 +20,8 @@ import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback {
|
||||
private var tetheredInterfaces = emptySet<String>()
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
tetheredInterfaces = intent.tetheredIfaces.toSet() + intent.localOnlyTetheredIfaces
|
||||
tetheredInterfaces = (intent.tetheredIfaces ?: return@broadcastReceiver).toSet() +
|
||||
(intent.localOnlyTetheredIfaces ?: return@broadcastReceiver)
|
||||
populateClients()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.get
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
@@ -219,8 +219,9 @@ class ClientsFragment : Fragment() {
|
||||
binding.swipeRefresher.setOnRefreshListener {
|
||||
IpNeighbourMonitor.instance?.flush()
|
||||
}
|
||||
ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this,
|
||||
Observer { adapter.submitList(it.toMutableList()) })
|
||||
ViewModelProviders.of(requireActivity()).get<ClientViewModel>().clients.observe(this) {
|
||||
adapter.submitList(it.toMutableList())
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ object MacLookup {
|
||||
try {
|
||||
val response = conn.inputStream.bufferedReader().readText()
|
||||
val obj = JSONObject(response).getJSONObject("result")
|
||||
obj.optString("error", null)?.also { throw UnexpectedError(mac, it) }
|
||||
obj.opt("error")?.also { throw UnexpectedError(mac, it.toString()) }
|
||||
val company = obj.getString("company")
|
||||
val match = extractCountry(mac, response, obj)
|
||||
val result = if (match != null) {
|
||||
@@ -71,9 +71,9 @@ object MacLookup {
|
||||
}
|
||||
|
||||
private fun extractCountry(mac: Long, response: String, obj: JSONObject): MatchResult? {
|
||||
obj.optString("country")?.let { countryCodeRegex.matchEntire(it) }?.also { return it }
|
||||
countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
|
||||
val address = obj.optString("address")
|
||||
if (address.isNullOrBlank()) return null
|
||||
if (address.isBlank()) return null
|
||||
countryCodeRegex.find(address)?.also { return it }
|
||||
Timber.w(UnexpectedError(mac, response))
|
||||
return null
|
||||
|
||||
@@ -7,14 +7,10 @@ import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Typeface
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.provider.Settings
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.getSystemService
|
||||
@@ -31,6 +27,11 @@ import java.net.NetworkInterface
|
||||
|
||||
@TargetApi(26)
|
||||
class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager(), ServiceConnection {
|
||||
companion object {
|
||||
val permission = if (Build.VERSION.SDK_INT >= 29)
|
||||
Manifest.permission.ACCESS_FINE_LOCATION else Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
|
||||
View.OnClickListener {
|
||||
init {
|
||||
@@ -43,10 +44,8 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
||||
val binder = manager.binder
|
||||
if (binder?.iface != null) binder.stop() else {
|
||||
val context = manager.parent.requireContext()
|
||||
if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) !=
|
||||
PackageManager.PERMISSION_GRANTED) {
|
||||
manager.parent.requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
|
||||
TetheringFragment.START_LOCAL_ONLY_HOTSPOT)
|
||||
if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
manager.parent.requestPermissions(arrayOf(permission), TetheringFragment.START_LOCAL_ONLY_HOTSPOT)
|
||||
return
|
||||
}
|
||||
/**
|
||||
@@ -75,15 +74,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
||||
private val lookup: Map<String, NetworkInterface> get() = parent.ifaceLookup
|
||||
|
||||
override val icon get() = R.drawable.ic_action_perm_scan_wifi
|
||||
override val title: CharSequence get() {
|
||||
val configuration = binder?.configuration ?: return parent.getString(R.string.tethering_temp_hotspot)
|
||||
return SpannableStringBuilder("${configuration.SSID} - ").apply {
|
||||
val start = length
|
||||
append(configuration.preSharedKey)
|
||||
setSpan(if (Build.VERSION.SDK_INT >= 28) TypefaceSpan(Typeface.MONOSPACE) else
|
||||
TypefaceSpan("monospace"), start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
override val title: CharSequence get() = parent.getString(R.string.tethering_temp_hotspot)
|
||||
override val text: CharSequence get() {
|
||||
return lookup[binder?.iface ?: return ""]?.formatAddresses() ?: ""
|
||||
}
|
||||
@@ -98,7 +89,7 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
||||
|
||||
override val type get() = VIEW_TYPE_LOCAL_ONLY_HOTSPOT
|
||||
private val data = Data()
|
||||
private var binder: LocalOnlyHotspotService.Binder? = null
|
||||
internal var binder: LocalOnlyHotspotService.Binder? = null
|
||||
|
||||
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
||||
viewHolder as ViewHolder
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.p2p.WifiP2pConfig
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
@@ -21,10 +25,8 @@ import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.get
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import be.mygod.vpnhotspot.*
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
|
||||
import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pDialogFragment
|
||||
import be.mygod.vpnhotspot.net.wifi.configuration.*
|
||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import be.mygod.vpnhotspot.util.formatAddresses
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
@@ -51,7 +53,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
else -> false
|
||||
}
|
||||
|
||||
val ssid @Bindable get() = binder?.group?.networkName ?: ""
|
||||
val title: CharSequence @Bindable get() {
|
||||
if (Build.VERSION.SDK_INT >= 29) binder?.group?.frequency?.let {
|
||||
return parent.getString(R.string.repeater_channel, it, frequencyToChannel(it))
|
||||
}
|
||||
return parent.getString(R.string.title_repeater)
|
||||
}
|
||||
val addresses: CharSequence @Bindable get() {
|
||||
return try {
|
||||
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
||||
@@ -59,12 +66,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
""
|
||||
}
|
||||
}
|
||||
var oc: CharSequence
|
||||
@Bindable get() {
|
||||
val oc = RepeaterService.operatingChannel
|
||||
return if (oc in 1..165) oc.toString() else ""
|
||||
}
|
||||
set(value) = app.pref.edit().putString(RepeaterService.KEY_OPERATING_CHANNEL, value.toString()).apply()
|
||||
|
||||
fun onStatusChanged() {
|
||||
notifyPropertyChanged(BR.switchEnabled)
|
||||
@@ -72,8 +73,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
notifyPropertyChanged(BR.addresses)
|
||||
}
|
||||
fun onGroupChanged(group: WifiP2pGroup? = null) {
|
||||
notifyPropertyChanged(BR.ssid)
|
||||
p2pInterface = group?.`interface`
|
||||
if (Build.VERSION.SDK_INT >= 29) notifyPropertyChanged(BR.title)
|
||||
notifyPropertyChanged(BR.addresses)
|
||||
}
|
||||
|
||||
@@ -82,6 +83,12 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
when (binder?.service?.status) {
|
||||
RepeaterService.Status.IDLE -> {
|
||||
val context = parent.requireContext()
|
||||
if (Build.VERSION.SDK_INT >= 29 && context.checkSelfPermission(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
parent.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||
TetheringFragment.START_REPEATER)
|
||||
return
|
||||
}
|
||||
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
||||
}
|
||||
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
||||
@@ -92,22 +99,6 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
fun wps() {
|
||||
if (binder?.active == true) WpsDialogFragment().show(parent, TetheringFragment.REPEATER_WPS)
|
||||
}
|
||||
|
||||
fun editConfigurations() {
|
||||
val group = binder?.group
|
||||
if (group != null) try {
|
||||
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
|
||||
holder.config = config
|
||||
WifiP2pDialogFragment().withArg(WifiP2pDialogFragment.Arg(WifiConfiguration().apply {
|
||||
SSID = group.networkName
|
||||
preSharedKey = config.psk
|
||||
})).show(parent, TetheringFragment.REPEATER_EDIT_CONFIGURATION)
|
||||
return
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
}
|
||||
SmartSnackbar.make(R.string.repeater_configure_failure).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@@ -128,6 +119,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("No longer used since API 29")
|
||||
@Suppress("DEPRECATION")
|
||||
class ConfigHolder : ViewModel() {
|
||||
var config: P2pSupplicantConfiguration? = null
|
||||
}
|
||||
@@ -140,6 +133,7 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
private val data = Data()
|
||||
internal var binder: RepeaterService.Binder? = null
|
||||
private var p2pInterface: String? = null
|
||||
@Suppress("DEPRECATION")
|
||||
private val holder = ViewModelProviders.of(parent).get<ConfigHolder>()
|
||||
|
||||
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
||||
@@ -168,19 +162,61 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
||||
}
|
||||
}
|
||||
|
||||
fun onEditResult(which: Int, data: Intent?) {
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> try {
|
||||
val master = holder.config ?: return
|
||||
val config = AlertDialogFragment.getRet<WifiP2pDialogFragment.Arg>(data!!).configuration
|
||||
val configuration: WifiConfiguration? get() {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
val networkName = RepeaterService.networkName
|
||||
val passphrase = RepeaterService.passphrase
|
||||
if (networkName != null && passphrase != null) {
|
||||
return newWifiApConfiguration(networkName, passphrase).apply {
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
|
||||
apBand = when (RepeaterService.operatingBand) {
|
||||
WifiP2pConfig.GROUP_OWNER_BAND_AUTO -> AP_BAND_ANY
|
||||
WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> AP_BAND_2GHZ
|
||||
WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> AP_BAND_5GHZ
|
||||
else -> throw IllegalArgumentException("Unknown operatingBand")
|
||||
}
|
||||
apChannel = RepeaterService.operatingChannel
|
||||
}
|
||||
}
|
||||
} else @Suppress("DEPRECATION") {
|
||||
val group = binder?.group
|
||||
if (group != null) try {
|
||||
val config = P2pSupplicantConfiguration(group, binder?.thisDevice?.deviceAddress)
|
||||
holder.config = config
|
||||
return newWifiApConfiguration(group.networkName, config.psk).apply {
|
||||
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) // is not actually used
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
apBand = AP_BAND_ANY
|
||||
apChannel = RepeaterService.operatingChannel
|
||||
}
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.w(e)
|
||||
}
|
||||
}
|
||||
SmartSnackbar.make(R.string.repeater_configure_failure).show()
|
||||
return null
|
||||
}
|
||||
fun updateConfiguration(config: WifiConfiguration) {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
RepeaterService.networkName = config.SSID
|
||||
RepeaterService.passphrase = config.preSharedKey
|
||||
RepeaterService.operatingBand = when (config.apBand) {
|
||||
AP_BAND_ANY -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO
|
||||
AP_BAND_2GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ
|
||||
AP_BAND_5GHZ -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ
|
||||
else -> throw IllegalArgumentException("Unknown apBand")
|
||||
}
|
||||
} else @Suppress("DEPRECATION") holder.config?.let { master ->
|
||||
if (binder?.group?.networkName != config.SSID || master.psk != config.preSharedKey) try {
|
||||
master.update(config.SSID, config.preSharedKey)
|
||||
binder!!.group = null
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
SmartSnackbar.make(e).show()
|
||||
}
|
||||
DialogInterface.BUTTON_NEUTRAL -> binder!!.resetCredentials()
|
||||
holder.config = null
|
||||
}
|
||||
holder.config = null
|
||||
if (Build.VERSION.SDK_INT >= 23) RepeaterService.operatingChannel = config.apChannel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,18 @@ import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
|
||||
@RequiresApi(24)
|
||||
abstract class TetherListeningTileService : KillableTileService() {
|
||||
protected var tethered: List<String> = emptyList()
|
||||
protected var tethered: List<String>? = null
|
||||
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
tethered = intent.tetheredIfaces
|
||||
tethered = intent.tetheredIfaces ?: return@broadcastReceiver
|
||||
updateTile()
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
val intent = registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
if (intent != null) tethered = intent.tetheredIfaces
|
||||
tethered = registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
?.tetheredIfaces
|
||||
updateTile()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -23,24 +24,31 @@ import be.mygod.vpnhotspot.net.TetherType
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
||||
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||
import be.mygod.vpnhotspot.net.wifi.configuration.WifiApDialogFragment
|
||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
import be.mygod.vpnhotspot.util.isNotGone
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import timber.log.Timber
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
|
||||
class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClickListener {
|
||||
class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
const val START_REPEATER = 4
|
||||
const val START_LOCAL_ONLY_HOTSPOT = 1
|
||||
const val REPEATER_EDIT_CONFIGURATION = 2
|
||||
const val REPEATER_WPS = 3
|
||||
const val CONFIGURE_REPEATER = 2
|
||||
const val CONFIGURE_AP = 4
|
||||
}
|
||||
|
||||
inner class ManagerAdapter : ListAdapter<Manager, RecyclerView.ViewHolder>(Manager) {
|
||||
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
|
||||
private val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
|
||||
internal val localOnlyHotspotManager by lazy @TargetApi(26) { LocalOnlyHotspotManager(this@TetheringFragment) }
|
||||
private val tetherManagers by lazy @TargetApi(24) {
|
||||
listOf(TetherManager.Wifi(this@TetheringFragment),
|
||||
TetherManager.Usb(this@TetheringFragment),
|
||||
@@ -91,8 +99,9 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
|
||||
var binder: TetheringService.Binder? = null
|
||||
private val adapter = ManagerAdapter()
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
adapter.update(intent.tetheredIfaces, intent.localOnlyTetheredIfaces,
|
||||
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER))
|
||||
adapter.update(intent.tetheredIfaces ?: return@broadcastReceiver,
|
||||
intent.localOnlyTetheredIfaces ?: return@broadcastReceiver,
|
||||
intent.getStringArrayListExtra(TetheringManager.EXTRA_ERRORED_TETHER) ?: return@broadcastReceiver)
|
||||
}
|
||||
|
||||
private fun updateMonitorList(canMonitor: List<String> = emptyList()) {
|
||||
@@ -100,13 +109,47 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
|
||||
item.isNotGone = canMonitor.isNotEmpty()
|
||||
item.subMenu.apply {
|
||||
clear()
|
||||
canMonitor.sorted().forEach { add(it).setOnMenuItemClickListener(this@TetheringFragment) }
|
||||
for (iface in canMonitor.sorted()) add(iface).setOnMenuItemClickListener {
|
||||
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, iface))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
ContextCompat.startForegroundService(requireContext(), Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE_MONITOR, item?.title ?: return false))
|
||||
return true
|
||||
return when (item?.itemId) {
|
||||
R.id.configuration -> item.subMenu.run {
|
||||
findItem(R.id.configuration_repeater).isNotGone = RepeaterService.supported
|
||||
findItem(R.id.configuration_temp_hotspot).isNotGone =
|
||||
adapter.localOnlyHotspotManager.binder?.configuration != null
|
||||
true
|
||||
}
|
||||
R.id.configuration_repeater -> {
|
||||
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
|
||||
adapter.repeaterManager.configuration ?: return false,
|
||||
p2pMode = true
|
||||
)).show(this, CONFIGURE_REPEATER)
|
||||
true
|
||||
}
|
||||
R.id.configuration_temp_hotspot -> {
|
||||
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
|
||||
adapter.localOnlyHotspotManager.binder?.configuration ?: return false,
|
||||
readOnly = true
|
||||
)).show(this, 0) // read-only, no callback needed
|
||||
true
|
||||
}
|
||||
R.id.configuration_ap -> try {
|
||||
WifiApDialogFragment().withArg(WifiApDialogFragment.Arg(
|
||||
WifiApManager.configuration
|
||||
)).show(this, CONFIGURE_AP)
|
||||
true
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException !is SecurityException) Timber.w(e)
|
||||
SmartSnackbar.make(e.targetException).show()
|
||||
false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@@ -117,13 +160,19 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
|
||||
binding.interfaces.adapter = adapter
|
||||
adapter.update(emptyList(), emptyList(), emptyList())
|
||||
ServiceForegroundConnector(this, this, TetheringService::class)
|
||||
requireActivity().toolbar.inflateMenu(R.menu.toolbar_tethering)
|
||||
requireActivity().toolbar.apply {
|
||||
inflateMenu(R.menu.toolbar_tethering)
|
||||
setOnMenuItemClickListener(this@TetheringFragment)
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
requireActivity().toolbar.menu.clear()
|
||||
requireActivity().toolbar.apply {
|
||||
menu.clear()
|
||||
setOnMenuItemClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -131,19 +180,38 @@ class TetheringFragment : Fragment(), ServiceConnection, MenuItem.OnMenuItemClic
|
||||
if (Build.VERSION.SDK_INT >= 27) ManageBar.Data.notifyChange()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = when (requestCode) {
|
||||
REPEATER_WPS -> adapter.repeaterManager.onWpsResult(resultCode, data)
|
||||
REPEATER_EDIT_CONFIGURATION -> adapter.repeaterManager.onEditResult(resultCode, data)
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
val configuration by lazy { AlertDialogFragment.getRet<WifiApDialogFragment.Arg>(data!!).configuration }
|
||||
when (requestCode) {
|
||||
REPEATER_WPS -> adapter.repeaterManager.onWpsResult(resultCode, data)
|
||||
CONFIGURE_REPEATER -> if (resultCode == DialogInterface.BUTTON_POSITIVE) {
|
||||
adapter.repeaterManager.updateConfiguration(configuration)
|
||||
}
|
||||
CONFIGURE_AP -> if (resultCode == DialogInterface.BUTTON_POSITIVE) try {
|
||||
WifiApManager.configuration = configuration
|
||||
} catch (e: IllegalArgumentException) {
|
||||
SmartSnackbar.make(R.string.configuration_rejected).show()
|
||||
} catch (e: InvocationTargetException) {
|
||||
SmartSnackbar.make(e.targetException).show()
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (requestCode == START_LOCAL_ONLY_HOTSPOT) @TargetApi(26) {
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
when (requestCode) {
|
||||
START_REPEATER -> if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) @TargetApi(29) {
|
||||
val context = requireContext()
|
||||
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
||||
context.startForegroundService(Intent(context, RepeaterService::class.java))
|
||||
}
|
||||
} else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
START_LOCAL_ONLY_HOTSPOT -> {
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) @TargetApi(26) {
|
||||
val context = requireContext()
|
||||
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
||||
}
|
||||
}
|
||||
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
|
||||
@@ -29,7 +29,7 @@ sealed class TetheringTileService : TetherListeningTileService(), TetheringManag
|
||||
protected abstract val labelString: Int
|
||||
protected abstract val tetherType: TetherType
|
||||
protected open val icon get() = tetherType.icon
|
||||
protected val interested get() = tethered.filter { TetherType.ofInterface(it) == tetherType }
|
||||
protected val interested get() = tethered?.filter { TetherType.ofInterface(it) == tetherType }
|
||||
protected var binder: TetheringService.Binder? = null
|
||||
|
||||
protected abstract fun start()
|
||||
@@ -52,19 +52,27 @@ sealed class TetheringTileService : TetherListeningTileService(), TetheringManag
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
binder?.routingsChanged?.remove(this)
|
||||
binder = null
|
||||
}
|
||||
|
||||
override fun updateTile() {
|
||||
qsTile?.run {
|
||||
val interested = interested
|
||||
if (interested.isEmpty()) {
|
||||
state = Tile.STATE_INACTIVE
|
||||
icon = tileOff
|
||||
} else {
|
||||
val binder = binder ?: return
|
||||
state = Tile.STATE_ACTIVE
|
||||
icon = if (interested.all(binder::isActive)) tileOn else tileOff
|
||||
when {
|
||||
interested == null -> {
|
||||
state = Tile.STATE_UNAVAILABLE
|
||||
icon = tileOff
|
||||
}
|
||||
interested.isEmpty() -> {
|
||||
state = Tile.STATE_INACTIVE
|
||||
icon = tileOff
|
||||
}
|
||||
else -> {
|
||||
val binder = binder ?: return
|
||||
state = Tile.STATE_ACTIVE
|
||||
icon = if (interested.all(binder::isActive)) tileOn else tileOff
|
||||
}
|
||||
}
|
||||
label = getText(labelString)
|
||||
updateTile()
|
||||
@@ -88,7 +96,7 @@ sealed class TetheringTileService : TetherListeningTileService(), TetheringManag
|
||||
}
|
||||
}
|
||||
override fun onClick() {
|
||||
val interested = interested
|
||||
val interested = interested ?: return
|
||||
if (interested.isEmpty()) safeInvoker { start() } else {
|
||||
val binder = binder
|
||||
if (binder == null) tapPending = true else {
|
||||
@@ -146,11 +154,14 @@ sealed class TetheringTileService : TetherListeningTileService(), TetheringManag
|
||||
|
||||
override fun updateTile() {
|
||||
qsTile?.run {
|
||||
when (tethering?.active) {
|
||||
val interested = interested
|
||||
if (interested == null) {
|
||||
state = Tile.STATE_UNAVAILABLE
|
||||
icon = tileOff
|
||||
} else when (tethering?.active) {
|
||||
true -> {
|
||||
val binder = binder ?: return
|
||||
state = Tile.STATE_ACTIVE
|
||||
val interested = interested
|
||||
icon = if (interested.isNotEmpty() && interested.all(binder::isActive)) tileOn else tileOff
|
||||
}
|
||||
false -> {
|
||||
@@ -164,18 +175,20 @@ sealed class TetheringTileService : TetherListeningTileService(), TetheringManag
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() = when (tethering?.active) {
|
||||
true -> {
|
||||
val binder = binder
|
||||
if (binder == null) tapPending = true else {
|
||||
val inactive = interested.filterNot(binder::isActive)
|
||||
if (inactive.isEmpty()) safeInvoker { stop() }
|
||||
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||
override fun onClick() {
|
||||
when (tethering?.active) {
|
||||
true -> {
|
||||
val binder = binder
|
||||
if (binder == null) tapPending = true else {
|
||||
val inactive = (interested ?: return).filterNot(binder::isActive)
|
||||
if (inactive.isEmpty()) safeInvoker { stop() }
|
||||
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||
}
|
||||
}
|
||||
false -> safeInvoker { start() }
|
||||
else -> tapPending = true
|
||||
}
|
||||
false -> safeInvoker { start() }
|
||||
else -> tapPending = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.os.Build
|
||||
import android.system.ErrnoException
|
||||
import android.system.OsConstants
|
||||
import androidx.core.os.BuildCompat
|
||||
import be.mygod.vpnhotspot.room.macToLong
|
||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.lang.NumberFormatException
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
@@ -98,7 +97,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: Long, v
|
||||
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
|
||||
.toList()
|
||||
} catch (e: IOException) {
|
||||
if (e !is FileNotFoundException || !BuildCompat.isAtLeastQ() ||
|
||||
if (e !is FileNotFoundException || Build.VERSION.SDK_INT < 29 ||
|
||||
(e.cause as? ErrnoException)?.errno != OsConstants.EACCES) Timber.w(e)
|
||||
}
|
||||
return arpCache
|
||||
|
||||
@@ -11,6 +11,7 @@ import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.util.computeIfAbsentCompat
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
@@ -91,7 +92,12 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
}
|
||||
|
||||
private val hostAddress = try {
|
||||
NetworkInterface.getByName(downstream)!!.interfaceAddresses!!.asSequence().single { it.address is Inet4Address }
|
||||
val addresses = NetworkInterface.getByName(downstream)!!.interfaceAddresses!!
|
||||
.filter { it.address is Inet4Address }
|
||||
if (addresses.size > 1) {
|
||||
Crashlytics.logException(IllegalArgumentException("More than one addresses was found: $addresses"))
|
||||
}
|
||||
addresses.first()
|
||||
} catch (e: Exception) {
|
||||
throw InterfaceNotFoundException(e)
|
||||
}
|
||||
@@ -200,9 +206,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
|
||||
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) = synchronized(this) {
|
||||
val toRemove = HashSet(clients.keys)
|
||||
for (neighbour in neighbours) {
|
||||
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address ||
|
||||
runBlocking { AppDatabase.instance.clientRecordDao.lookup(neighbour.lladdr) }
|
||||
?.blocked == true) continue
|
||||
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address || runBlocking {
|
||||
AppDatabase.instance.clientRecordDao.lookupOrDefault(neighbour.lladdr)
|
||||
}.blocked) continue
|
||||
toRemove.remove(neighbour.ip)
|
||||
try {
|
||||
clients.computeIfAbsentCompat(neighbour.ip) { Client(neighbour.ip, neighbour.lladdr) }
|
||||
|
||||
@@ -3,15 +3,25 @@ package be.mygod.vpnhotspot.net.wifi
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
|
||||
*
|
||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
||||
*/
|
||||
object WifiApManager {
|
||||
private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
WifiConfiguration::class.java, Boolean::class.java)
|
||||
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
||||
private val setWifiApConfiguration by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("setWifiApConfiguration", WifiConfiguration::class.java)
|
||||
}
|
||||
var configuration: WifiConfiguration
|
||||
get() = getWifiApConfiguration.invoke(app.wifi) as WifiConfiguration
|
||||
set(value) {
|
||||
if (setWifiApConfiguration.invoke(app.wifi, value) as? Boolean != true) {
|
||||
throw IllegalArgumentException("setWifiApConfiguration failed")
|
||||
}
|
||||
}
|
||||
|
||||
private val setWifiApEnabled by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
WifiConfiguration::class.java, Boolean::class.java)
|
||||
}
|
||||
/**
|
||||
* Start AccessPoint mode with the specified
|
||||
* configuration. If the radio is already running in
|
||||
@@ -25,11 +35,18 @@ object WifiApManager {
|
||||
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
|
||||
setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean
|
||||
|
||||
/**
|
||||
* Although the functionalities were removed in API 26, it is already not functioning correctly on API 25.
|
||||
*
|
||||
* See also: https://android.googlesource.com/platform/frameworks/base/+/5c0b10a4a9eecc5307bb89a271221f2b20448797%5E%21/
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||
fun start(wifiConfig: WifiConfiguration? = null) {
|
||||
app.wifi.isWifiEnabled = false
|
||||
app.wifi.setWifiApEnabled(wifiConfig, true)
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26")
|
||||
fun stop() {
|
||||
app.wifi.setWifiApEnabled(null, false)
|
||||
|
||||
@@ -3,8 +3,15 @@ package be.mygod.vpnhotspot.net.wifi
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
|
||||
/**
|
||||
@@ -13,8 +20,12 @@ import be.mygod.vpnhotspot.App.Companion.app
|
||||
class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
||||
companion object : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private const val KEY = "service.wifiLock"
|
||||
private val lockType get() =
|
||||
WifiDoubleLock.Mode.valueOf(app.pref.getString(KEY, WifiDoubleLock.Mode.Full.toString()) ?: "").lockType
|
||||
var mode: Mode
|
||||
@Suppress("DEPRECATION")
|
||||
get() = Mode.valueOf(app.pref.getString(KEY, Mode.Full.toString()) ?: "").let {
|
||||
if (it == Mode.Full && Build.VERSION.SDK_INT >= 29) Mode.None else it
|
||||
}
|
||||
set(value) = app.pref.edit { putString(KEY, value.toString()) }
|
||||
private val service by lazy { app.getSystemService<PowerManager>()!! }
|
||||
|
||||
private var holders = mutableSetOf<Any>()
|
||||
@@ -23,7 +34,7 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
||||
fun acquire(holder: Any) = synchronized(this) {
|
||||
if (holders.isEmpty()) {
|
||||
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||
val lockType = lockType
|
||||
val lockType = mode.lockType
|
||||
if (lockType != null) lock = WifiDoubleLock(lockType)
|
||||
}
|
||||
check(holders.add(holder))
|
||||
@@ -40,14 +51,44 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == KEY) synchronized(this) {
|
||||
lock?.close()
|
||||
val lockType = lockType
|
||||
val lockType = mode.lockType
|
||||
lock = if (lockType == null) null else WifiDoubleLock(lockType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Mode(val lockType: Int? = null) {
|
||||
None, Full(WifiManager.WIFI_MODE_FULL), HighPerf(WifiManager.WIFI_MODE_FULL_HIGH_PERF)
|
||||
enum class Mode(val lockType: Int? = null, val keepScreenOn: Boolean = false) {
|
||||
None,
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("This constant was deprecated in API level Q.\n" +
|
||||
"This API is non-functional and will have no impact.")
|
||||
Full(WifiManager.WIFI_MODE_FULL),
|
||||
HighPerf(WifiManager.WIFI_MODE_FULL_HIGH_PERF),
|
||||
@RequiresApi(29)
|
||||
LowLatency(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, true),
|
||||
}
|
||||
|
||||
class ActivityListener(private val activity: ComponentActivity) :
|
||||
DefaultLifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var keepScreenOn: Boolean = false
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
field = value
|
||||
if (value) activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
init {
|
||||
activity.lifecycle.addObserver(this)
|
||||
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||
keepScreenOn = mode.keepScreenOn
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == KEY) keepScreenOn = mode.keepScreenOn
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) = app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
private val wifi = app.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiConfiguration.AuthAlgorithm
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import be.mygod.vpnhotspot.AlertDialogFragment
|
||||
import be.mygod.vpnhotspot.R
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
|
||||
*
|
||||
* This dialog has been deprecated in API 28, but we are still using it since it works better for our purposes.
|
||||
* Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
|
||||
*/
|
||||
class WifiP2pDialogFragment : AlertDialogFragment<WifiP2pDialogFragment.Arg, WifiP2pDialogFragment.Arg>(), TextWatcher {
|
||||
@Parcelize
|
||||
data class Arg(val configuration: WifiConfiguration) : Parcelable
|
||||
|
||||
private lateinit var mView: View
|
||||
private lateinit var mSsid: TextView
|
||||
private lateinit var mPassword: EditText
|
||||
override val ret: Arg? get() {
|
||||
val config = WifiConfiguration()
|
||||
config.SSID = mSsid.text.toString()
|
||||
config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN)
|
||||
if (mPassword.length() != 0) {
|
||||
val password = mPassword.text.toString()
|
||||
config.preSharedKey = password
|
||||
}
|
||||
return Arg(config)
|
||||
}
|
||||
|
||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||
mView = requireActivity().layoutInflater.inflate(R.layout.dialog_wifi_ap, null)
|
||||
setView(mView)
|
||||
setTitle(R.string.repeater_configure)
|
||||
mSsid = mView.ssid
|
||||
mPassword = mView.password
|
||||
setPositiveButton(context.getString(R.string.wifi_save), listener)
|
||||
setNegativeButton(context.getString(R.string.wifi_cancel), null)
|
||||
setNeutralButton(context.getString(R.string.repeater_reset_credentials), listener)
|
||||
mSsid.text = arg.configuration.SSID
|
||||
mSsid.addTextChangedListener(this@WifiP2pDialogFragment)
|
||||
mPassword.setText(arg.configuration.preSharedKey)
|
||||
mPassword.addTextChangedListener(this@WifiP2pDialogFragment)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
validate()
|
||||
}
|
||||
|
||||
private fun validate() {
|
||||
val mSsidString = mSsid.text.toString()
|
||||
val ssidValid = mSsid.length() != 0 && Charset.forName("UTF-8").encode(mSsidString).limit() <= 32
|
||||
val passwordValid = mPassword.length() >= 8
|
||||
mView.password_wrapper.error =
|
||||
if (passwordValid) null else requireContext().getString(R.string.credentials_password_too_short)
|
||||
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).isEnabled = ssidValid && passwordValid
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
|
||||
override fun afterTextChanged(editable: Editable) = validate()
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import java.lang.reflect.Proxy
|
||||
|
||||
object WifiP2pManagerHelper {
|
||||
const val UNSUPPORTED = -2
|
||||
@Deprecated("No longer used since API 29")
|
||||
const val WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION = "android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED"
|
||||
|
||||
/**
|
||||
@@ -24,6 +25,7 @@ object WifiP2pManagerHelper {
|
||||
WifiP2pManager::class.java.getDeclaredMethod("setWifiP2pChannels", WifiP2pManager.Channel::class.java,
|
||||
Int::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
|
||||
}
|
||||
@Deprecated("No longer used since API 29")
|
||||
fun WifiP2pManager.setWifiP2pChannels(c: WifiP2pManager.Channel, lc: Int, oc: Int,
|
||||
listener: WifiP2pManager.ActionListener) {
|
||||
try {
|
||||
@@ -39,18 +41,19 @@ object WifiP2pManagerHelper {
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-4.3_r0.9/wifi/java/android/net/wifi/p2p/WifiP2pManager.java#958
|
||||
*/
|
||||
private val startWps by lazy {
|
||||
WifiP2pManager::class.java.getDeclaredMethod("startWps",
|
||||
WifiP2pManager.Channel::class.java, WpsInfo::class.java, WifiP2pManager.ActionListener::class.java)
|
||||
}
|
||||
fun WifiP2pManager.startWps(c: WifiP2pManager.Channel, wps: WpsInfo, listener: WifiP2pManager.ActionListener) {
|
||||
@JvmStatic
|
||||
val startWps by lazy {
|
||||
try {
|
||||
startWps.invoke(this, c, wps, listener)
|
||||
WifiP2pManager::class.java.getDeclaredMethod("startWps",
|
||||
WifiP2pManager.Channel::class.java, WpsInfo::class.java, WifiP2pManager.ActionListener::class.java)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
DebugHelper.logEvent("NoSuchMethod_startWps")
|
||||
listener.onFailure(UNSUPPORTED)
|
||||
null
|
||||
}
|
||||
}
|
||||
fun WifiP2pManager.startWps(c: WifiP2pManager.Channel, wps: WpsInfo, listener: WifiP2pManager.ActionListener) {
|
||||
startWps!!.invoke(this, c, wps, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Available since Android 4.2.
|
||||
@@ -61,6 +64,7 @@ object WifiP2pManagerHelper {
|
||||
WifiP2pManager::class.java.getDeclaredMethod("deletePersistentGroup",
|
||||
WifiP2pManager.Channel::class.java, Int::class.java, WifiP2pManager.ActionListener::class.java)
|
||||
}
|
||||
@Deprecated("No longer used since API 29")
|
||||
fun WifiP2pManager.deletePersistentGroup(c: WifiP2pManager.Channel, netId: Int,
|
||||
listener: WifiP2pManager.ActionListener) {
|
||||
try {
|
||||
@@ -87,6 +91,7 @@ object WifiP2pManagerHelper {
|
||||
* @param c is the channel created at {@link #initialize}
|
||||
* @param listener for callback when persistent group info list is available. Can be null.
|
||||
*/
|
||||
@Deprecated("No longer used since API 29")
|
||||
fun WifiP2pManager.requestPersistentGroupInfo(c: WifiP2pManager.Channel,
|
||||
listener: (Collection<WifiP2pGroup>) -> Unit) {
|
||||
val proxy = Proxy.newProxyInstance(interfacePersistentGroupInfoListener.classLoader,
|
||||
@@ -109,5 +114,6 @@ object WifiP2pManagerHelper {
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/android-4.2_r1/wifi/java/android/net/wifi/p2p/WifiP2pGroup.java#253
|
||||
*/
|
||||
private val getNetworkId by lazy { WifiP2pGroup::class.java.getDeclaredMethod("getNetworkId") }
|
||||
@Deprecated("No longer used since API 29")
|
||||
val WifiP2pGroup.netId get() = getNetworkId.invoke(this) as Int
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
package be.mygod.vpnhotspot.net.wifi.configuration
|
||||
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.Build
|
||||
@@ -14,6 +14,7 @@ import java.lang.IllegalStateException
|
||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
|
||||
*/
|
||||
@Deprecated("No longer used since API 29")
|
||||
class P2pSupplicantConfiguration(private val group: WifiP2pGroup, ownerAddress: String?) {
|
||||
companion object {
|
||||
private const val TAG = "P2pSupplicantConfiguration"
|
||||
@@ -0,0 +1,214 @@
|
||||
package be.mygod.vpnhotspot.net.wifi.configuration
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ClipData
|
||||
import android.content.DialogInterface
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Base64
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isGone
|
||||
import be.mygod.vpnhotspot.AlertDialogFragment
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.util.QRCodeDialog
|
||||
import be.mygod.vpnhotspot.util.toByteArray
|
||||
import be.mygod.vpnhotspot.util.toParcelable
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.dialog_wifi_ap.view.*
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java
|
||||
*
|
||||
* This dialog has been deprecated in API 28, but we are still using it since it works better for our purposes.
|
||||
* Related: https://android.googlesource.com/platform/packages/apps/Settings/+/defb1183ecb00d6231bac7d934d07f58f90261ea
|
||||
*/
|
||||
class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiApDialogFragment.Arg>(), TextWatcher,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
private val channels by lazy { (1..165).map { BandOption.Channel(it) } }
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Arg(val configuration: WifiConfiguration,
|
||||
val readOnly: Boolean = false,
|
||||
/**
|
||||
* KeyMgmt is enforced to WPA_PSK.
|
||||
* Various values for apBand are allowed according to different rules.
|
||||
*/
|
||||
val p2pMode: Boolean = false) : Parcelable
|
||||
|
||||
@TargetApi(23)
|
||||
private sealed class BandOption {
|
||||
open val apBand get() = AP_BAND_2GHZ
|
||||
open val apChannel get() = 0
|
||||
|
||||
object BandAny : BandOption() {
|
||||
override val apBand get() = AP_BAND_ANY
|
||||
override fun toString() = app.getString(R.string.wifi_ap_choose_auto)
|
||||
}
|
||||
object Band2GHz : BandOption() {
|
||||
override fun toString() = app.getString(R.string.wifi_ap_choose_2G)
|
||||
}
|
||||
object Band5GHz : BandOption() {
|
||||
override val apBand get() = AP_BAND_5GHZ
|
||||
override fun toString() = app.getString(R.string.wifi_ap_choose_5G)
|
||||
}
|
||||
class Channel(override val apChannel: Int) : BandOption() {
|
||||
override fun toString() = "${channelToFrequency(apChannel)} MHz ($apChannel)"
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var dialogView: View
|
||||
private lateinit var bandOptions: MutableList<BandOption>
|
||||
private var started = false
|
||||
private val selectedSecurity get() =
|
||||
if (arg.p2pMode) WifiConfiguration.KeyMgmt.WPA_PSK else dialogView.security.selectedItemPosition
|
||||
override val ret get() = Arg(WifiConfiguration().apply {
|
||||
SSID = dialogView.ssid.text.toString()
|
||||
allowedKeyManagement.set(selectedSecurity)
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
if (dialogView.password.length() != 0) preSharedKey = dialogView.password.text.toString()
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
val bandOption = dialogView.band.selectedItem as BandOption
|
||||
apBand = bandOption.apBand
|
||||
apChannel = bandOption.apChannel
|
||||
}
|
||||
})
|
||||
|
||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||
val activity = requireActivity()
|
||||
dialogView = activity.layoutInflater.inflate(R.layout.dialog_wifi_ap, null)
|
||||
setView(dialogView)
|
||||
if (!arg.readOnly) setPositiveButton(R.string.wifi_save, listener)
|
||||
setNegativeButton(R.string.donations__button_close, null)
|
||||
dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration)
|
||||
dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment)
|
||||
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (arg.p2pMode) dialogView.security_wrapper.isGone = true else dialogView.security.apply {
|
||||
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
||||
WifiConfiguration.KeyMgmt.strings).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) =
|
||||
throw IllegalStateException("Must select something")
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
dialogView.password_wrapper.isGone = position == WifiConfiguration.KeyMgmt.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!arg.readOnly) dialogView.password.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.apply {
|
||||
bandOptions = mutableListOf<BandOption>().apply {
|
||||
if (arg.p2pMode) {
|
||||
add(BandOption.BandAny)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
add(BandOption.Band2GHz)
|
||||
add(BandOption.Band5GHz)
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= 28) add(BandOption.BandAny)
|
||||
add(BandOption.Band2GHz)
|
||||
add(BandOption.Band5GHz)
|
||||
}
|
||||
addAll(channels)
|
||||
}
|
||||
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0, bandOptions).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
setSelection(bandOptions.indexOfFirst { it.apChannel == RepeaterService.operatingChannel })
|
||||
}
|
||||
} else dialogView.band_wrapper.isGone = true
|
||||
populateFromConfiguration(arg.configuration)
|
||||
}
|
||||
|
||||
private fun populateFromConfiguration(configuration: WifiConfiguration) {
|
||||
dialogView.ssid.setText(configuration.SSID)
|
||||
if (!arg.p2pMode) dialogView.security.setSelection(configuration.apKeyManagement)
|
||||
dialogView.password.setText(configuration.preSharedKey)
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
dialogView.band.setSelection(if (configuration.apChannel in 1..165) {
|
||||
bandOptions.indexOfFirst { it.apChannel == configuration.apChannel }
|
||||
} else bandOptions.indexOfFirst { it.apBand == configuration.apBand })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
started = true
|
||||
if (!arg.readOnly) validate()
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is reached only if not arg.readOnly.
|
||||
*/
|
||||
private fun validate() {
|
||||
if (!started) return
|
||||
val ssidValid = dialogView.ssid.length() != 0 &&
|
||||
Charset.forName("UTF-8").encode(dialogView.ssid.text.toString()).limit() <= 32
|
||||
val passwordValid = when (selectedSecurity) {
|
||||
WifiConfiguration.KeyMgmt.WPA_PSK, WPA2_PSK -> dialogView.password.length() >= 8
|
||||
else -> true // do not try to validate
|
||||
}
|
||||
dialogView.password_wrapper.error = if (passwordValid) null else {
|
||||
requireContext().getString(R.string.credentials_password_too_short)
|
||||
}
|
||||
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE).isEnabled = ssidValid && passwordValid
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { }
|
||||
override fun afterTextChanged(editable: Editable) = validate()
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
return when (item?.itemId) {
|
||||
android.R.id.copy -> {
|
||||
app.clipboard.setPrimaryClip(ClipData.newPlainText(null,
|
||||
Base64.encodeToString(ret.configuration.toByteArray(), BASE64_FLAGS)))
|
||||
true
|
||||
}
|
||||
android.R.id.paste -> try {
|
||||
app.clipboard.primaryClip?.getItemAt(0)?.text?.apply {
|
||||
Base64.decode(toString(), BASE64_FLAGS).toParcelable<WifiConfiguration>()
|
||||
?.let { populateFromConfiguration(it) }
|
||||
}
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
SmartSnackbar.make(e).show()
|
||||
false
|
||||
}
|
||||
R.id.share_qr -> {
|
||||
val qrString = try {
|
||||
ret.configuration.toQRString()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
SmartSnackbar.make(e).show()
|
||||
return false
|
||||
}
|
||||
QRCodeDialog().withArg(qrString).show(fragmentManager ?: return false, "QRCodeDialog")
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
super.onClick(dialog, which)
|
||||
if (Build.VERSION.SDK_INT < 23 && arg.p2pMode && which == DialogInterface.BUTTON_POSITIVE) {
|
||||
RepeaterService.operatingChannel = (dialogView.band.selectedItem as BandOption).apChannel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package be.mygod.vpnhotspot.net.wifi.configuration
|
||||
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
|
||||
val WPA2_PSK = WifiConfiguration.KeyMgmt.strings.indexOf("WPA2_PSK")
|
||||
|
||||
/**
|
||||
* apBand and apChannel is available since API 23.
|
||||
*
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-6.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#242
|
||||
*/
|
||||
private val apBandField by lazy { WifiConfiguration::class.java.getDeclaredField("apBand") }
|
||||
private val apChannelField by lazy { WifiConfiguration::class.java.getDeclaredField("apChannel") }
|
||||
|
||||
/**
|
||||
* 2GHz band.
|
||||
*
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-7.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#241
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
const val AP_BAND_2GHZ = 0
|
||||
/**
|
||||
* 5GHz band.
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
const val AP_BAND_5GHZ = 1
|
||||
/**
|
||||
* Device is allowed to choose the optimal band (2Ghz or 5Ghz) based on device capability,
|
||||
* operating country code and current radio conditions.
|
||||
*
|
||||
* Introduced in 9.0, but we will abuse this constant anyway.
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r1/wifi/java/android/net/wifi/WifiConfiguration.java#295
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
const val AP_BAND_ANY = -1
|
||||
|
||||
/**
|
||||
* The band which AP resides on
|
||||
* -1:Any 0:2G 1:5G
|
||||
* By default, 2G is chosen
|
||||
*/
|
||||
var WifiConfiguration.apBand: Int
|
||||
@RequiresApi(23) get() = apBandField.get(this) as Int
|
||||
@RequiresApi(23) set(value) = apBandField.set(this, value)
|
||||
/**
|
||||
* The channel which AP resides on
|
||||
* 2G 1-11
|
||||
* 5G 36,40,44,48,149,153,157,161,165
|
||||
* 0 - find a random available channel according to the apBand
|
||||
*/
|
||||
var WifiConfiguration.apChannel: Int
|
||||
@RequiresApi(23) get() = apChannelField.get(this) as Int
|
||||
@RequiresApi(23) set(value) = apChannelField.set(this, value)
|
||||
|
||||
/**
|
||||
* The frequency which AP resides on (MHz). Resides in range [2412, 5815].
|
||||
*/
|
||||
fun channelToFrequency(channel: Int) = when (channel) {
|
||||
in 1..14 -> 2407 + 5 * channel
|
||||
in 15..165 -> 5000 + 5 * channel
|
||||
else -> throw IllegalArgumentException("Invalid channel $channel")
|
||||
}
|
||||
fun frequencyToChannel(frequency: Int) = when (frequency % 5) {
|
||||
2 -> ((frequency - 2407) / 5).also { check(it in 1..14) { "Invalid 2.4 GHz frequency $frequency" } }
|
||||
0 -> ((frequency - 5000) / 5).also { check(it in 15..165) { "Invalid 5 GHz frequency $frequency" } }
|
||||
else -> throw IllegalArgumentException("Invalid frequency $frequency")
|
||||
}
|
||||
|
||||
val WifiConfiguration.apKeyManagement get() = allowedKeyManagement.nextSetBit(0).let { selected ->
|
||||
check(allowedKeyManagement.nextSetBit(selected + 1) < 0) { "More than 1 key managements supplied" }
|
||||
if (selected < 0) WifiConfiguration.KeyMgmt.NONE else selected // getAuthType returns NONE if nothing is selected
|
||||
}
|
||||
|
||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||
/**
|
||||
* Documentation: https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
|
||||
*/
|
||||
fun WifiConfiguration.toQRString() = StringBuilder("WIFI:").apply {
|
||||
fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
|
||||
var password = true
|
||||
when (apKeyManagement) {
|
||||
WifiConfiguration.KeyMgmt.NONE -> password = false
|
||||
WifiConfiguration.KeyMgmt.WPA_PSK, WifiConfiguration.KeyMgmt.WPA_EAP, WPA2_PSK -> append("T:WPA;")
|
||||
else -> throw IllegalArgumentException("Unsupported authentication type")
|
||||
}
|
||||
append("S:")
|
||||
append(SSID.sanitize())
|
||||
append(';')
|
||||
if (password) {
|
||||
append("P:")
|
||||
append(preSharedKey.sanitize())
|
||||
append(';')
|
||||
}
|
||||
if (hiddenSSID) append("H:true;")
|
||||
append(';')
|
||||
}.toString()
|
||||
|
||||
/**
|
||||
* Based on:
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/android-5.0.0_r1/src/com/android/settings/wifi/WifiApDialog.java#88
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/b1af85d/src/com/android/settings/wifi/tether/WifiTetherSettings.java#162
|
||||
*/
|
||||
fun newWifiApConfiguration(ssid: String, passphrase: String?) = try {
|
||||
WifiApManager.configuration
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException !is SecurityException) Timber.w(e)
|
||||
WifiConfiguration()
|
||||
}.apply {
|
||||
SSID = ssid
|
||||
preSharedKey = passphrase
|
||||
allowedKeyManagement.clear()
|
||||
allowedAuthAlgorithms.clear()
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package be.mygod.vpnhotspot.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.room.*
|
||||
|
||||
@Entity
|
||||
@@ -12,12 +13,12 @@ data class ClientRecord(@PrimaryKey
|
||||
@androidx.room.Dao
|
||||
abstract class Dao {
|
||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||
abstract suspend fun lookup(mac: Long): ClientRecord?
|
||||
|
||||
protected abstract suspend fun lookup(mac: Long): ClientRecord?
|
||||
suspend fun lookupOrDefault(mac: Long) = lookup(mac) ?: ClientRecord(mac)
|
||||
|
||||
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
|
||||
abstract fun lookupSync(mac: Long): LiveData<ClientRecord>
|
||||
protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?>
|
||||
fun lookupOrDefaultSync(mac: Long) = lookupSync(mac).map { it ?: ClientRecord(mac) }
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun updateInternal(value: ClientRecord): Long
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package be.mygod.vpnhotspot.room
|
||||
|
||||
import android.os.Parcel
|
||||
import android.text.TextUtils
|
||||
import androidx.room.TypeConverter
|
||||
import be.mygod.vpnhotspot.util.useParcel
|
||||
import java.net.InetAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -10,27 +10,17 @@ import java.nio.ByteOrder
|
||||
object Converters {
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun persistCharSequence(cs: CharSequence): ByteArray {
|
||||
val p = Parcel.obtain()
|
||||
try {
|
||||
TextUtils.writeToParcel(cs, p, 0)
|
||||
return p.marshall()
|
||||
} finally {
|
||||
p.recycle()
|
||||
}
|
||||
fun persistCharSequence(cs: CharSequence) = useParcel { p ->
|
||||
TextUtils.writeToParcel(cs, p, 0)
|
||||
p.marshall()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun unpersistCharSequence(data: ByteArray): CharSequence {
|
||||
val p = Parcel.obtain()
|
||||
try {
|
||||
p.unmarshall(data, 0, data.size)
|
||||
p.setDataPosition(0)
|
||||
return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)
|
||||
} finally {
|
||||
p.recycle()
|
||||
}
|
||||
fun unpersistCharSequence(data: ByteArray) = useParcel { p ->
|
||||
p.unmarshall(data, 0, data.size)
|
||||
p.setDataPosition(0)
|
||||
TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package be.mygod.vpnhotspot.util
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import be.mygod.vpnhotspot.R
|
||||
import net.glxn.qrgen.android.QRCode
|
||||
|
||||
class QRCodeDialog : DialogFragment() {
|
||||
companion object {
|
||||
private const val KEY_ARG = "arg"
|
||||
}
|
||||
|
||||
fun withArg(arg: String) = apply { arguments = bundleOf(KEY_ARG to arg) }
|
||||
private val arg get() = arguments?.getString(KEY_ARG)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
|
||||
ImageView(context).apply {
|
||||
val size = resources.getDimensionPixelSize(R.dimen.qr_code_size)
|
||||
layoutParams = ViewGroup.LayoutParams(size, size)
|
||||
setImageBitmap((QRCode.from(arg).withSize(size, size) as QRCode).bitmap())
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ object SpanFormatter {
|
||||
i = m.start()
|
||||
val exprEnd = m.end()
|
||||
|
||||
val argTerm = m.group(1)
|
||||
val argTerm = m.group(1)!!
|
||||
val modTerm = m.group(2)
|
||||
|
||||
val cookedArg = when (val typeTerm = m.group(3)) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package be.mygod.vpnhotspot.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.net.InetAddresses
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
@@ -15,7 +19,6 @@ import androidx.databinding.BindingAdapter
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.room.macToString
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import java.lang.RuntimeException
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
@@ -32,6 +35,25 @@ fun Long.toPluralInt(): Int {
|
||||
return (this % 1000000000).toInt() + 1000000000
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
|
||||
try {
|
||||
block(this)
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun Parcelable.toByteArray(parcelableFlags: Int = 0) = useParcel { p ->
|
||||
p.writeParcelable(this, parcelableFlags)
|
||||
p.marshall()
|
||||
}
|
||||
fun <T : Parcelable> ByteArray.toParcelable() = useParcel { p ->
|
||||
p.unmarshall(this, 0, size)
|
||||
p.setDataPosition(0)
|
||||
p.readParcelable<T>(javaClass.classLoader)
|
||||
}
|
||||
|
||||
fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = receiver(context, intent)
|
||||
}
|
||||
@@ -72,12 +94,13 @@ fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableString
|
||||
}
|
||||
}.trimEnd()
|
||||
|
||||
private val parseNumericAddress by lazy {
|
||||
private val parseNumericAddress by lazy @SuppressLint("SoonBlockedPrivateApi") {
|
||||
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
|
||||
isAccessible = true
|
||||
}
|
||||
}
|
||||
fun parseNumericAddress(address: String) = parseNumericAddress.invoke(null, address) as InetAddress
|
||||
fun parseNumericAddress(address: String) = if (Build.VERSION.SDK_INT >= 29)
|
||||
InetAddresses.parseNumericAddress(address) else parseNumericAddress.invoke(null, address) as InetAddress
|
||||
|
||||
fun Context.launchUrl(url: String) {
|
||||
if (app.hasTouch) try {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M17 16.99c-1.35 0-2.2 0.42 -2.95 0.8 -0.65 0.33 -1.18 0.6 -2.05 0.6 -0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.57-0.8-2.95-0.8s-2.2 0.42 -2.95 0.8 c-0.65 0.33 -1.17 0.6 -2.05 0.6 v1.95c1.35 0 2.2-0.42 2.95-0.8 0.65 -0.33 1.17-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.42 2.95-0.8c0.65-0.33 1.18-0.6 2.05-0.6 0.9 0 1.4 0.25 2.05 0.6 0.75 0.38 1.58 0.8 2.95 0.8 v-1.95c-0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.6-0.8-2.95-0.8zm0-4.45c-1.35 0-2.2 0.43 -2.95 0.8 -0.65 0.32 -1.18 0.6 -2.05 0.6 -0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.57-0.8-2.95-0.8s-2.2 0.43 -2.95 0.8 c-0.65 0.32 -1.17 0.6 -2.05 0.6 v1.95c1.35 0 2.2-0.43 2.95-0.8 0.65 -0.35 1.15-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.43 2.95-0.8c0.65-0.35 1.15-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.58 0.8 2.95 0.8 v-1.95c-0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.6-0.8-2.95-0.8zm2.95-8.08c-0.75-0.38-1.58-0.8-2.95-0.8s-2.2 0.42 -2.95 0.8 c-0.65 0.32 -1.18 0.6 -2.05 0.6 -0.9 0-1.4-0.25-2.05-0.6-0.75-0.37-1.57-0.8-2.95-0.8s-2.2 0.42 -2.95 0.8 c-0.65 0.33 -1.17 0.6 -2.05 0.6 v1.93c1.35 0 2.2-0.43 2.95-0.8 0.65 -0.33 1.17-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.43 2.95-0.8c0.65-0.32 1.18-0.6 2.05-0.6 0.9 0 1.4 0.25 2.05 0.6 0.75 0.38 1.58 0.8 2.95 0.8 V5.04c-0.9 0-1.4-0.25-2.05-0.58zM17 8.09c-1.35 0-2.2 0.43 -2.95 0.8 -0.65 0.35 -1.15 0.6 -2.05 0.6 s-1.4-0.25-2.05-0.6c-0.75-0.38-1.57-0.8-2.95-0.8s-2.2 0.43 -2.95 0.8 c-0.65 0.35 -1.15 0.6 -2.05 0.6 v1.95c1.35 0 2.2-0.43 2.95-0.8 0.65 -0.32 1.18-0.6 2.05-0.6s1.4 0.25 2.05 0.6 c0.75 0.38 1.57 0.8 2.95 0.8 s2.2-0.43 2.95-0.8c0.65-0.32 1.18-0.6 2.05-0.6 0.9 0 1.4 0.25 2.05 0.6 0.75 0.38 1.58 0.8 2.95 0.8 V9.49c-0.9 0-1.4-0.25-2.05-0.6-0.75-0.38-1.6-0.8-2.95-0.8z" />
|
||||
</vector>
|
||||
6
mobile/src/main/res/drawable/ic_social_share.xml
Normal file
6
mobile/src/main/res/drawable/ic_social_share.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path android:fillColor="#FF000000" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -10,7 +10,7 @@
|
||||
android:orientation="vertical"
|
||||
tools:context="be.mygod.vpnhotspot.MainActivity">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Source: https://android.googlesource.com/platform/packages/apps/Settings/+/6b4a31c/res/layout/wifi_ap_dialog.xml -->
|
||||
<!-- Copyright (C) 2010 The Android Open Source Project
|
||||
<!--
|
||||
Based on:
|
||||
* https://github.com/material-components/material-components-android/blob/da6096bb8df2ac5b0cabeaa7960501d4083e4ea9/lib/java/com/google/android/material/dialog/res/layout/mtrl_alert_dialog_title.xml
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/6b4a31c/res/layout/wifi_ap_dialog.xml
|
||||
-->
|
||||
<!--
|
||||
Copyright (C) 2018 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
@@ -11,49 +16,97 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="300sp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fadeScrollbars="false">
|
||||
<LinearLayout
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="300sp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusableInTouchMode="true"
|
||||
style="@style/wifi_item">
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ssid_wrapper"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
app:title="@string/configuration_view"/>
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fadeScrollbars="false">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dip"
|
||||
android:layout_marginBottom="8dip">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/ssid"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusableInTouchMode="true"
|
||||
style="@style/wifi_item">
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ssid_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/ssid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_edit_content"
|
||||
android:hint="@string/wifi_ssid"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:maxLength="32" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/security_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_edit_content"
|
||||
android:hint="@string/wifi_ssid"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:maxLength="32" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/password_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_marginTop="8dip"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_label"
|
||||
android:text="@string/wifi_security" />
|
||||
<Spinner
|
||||
android:id="@+id/security"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_content"
|
||||
android:prompt="@string/wifi_security" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/password_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_edit_content"
|
||||
android:singleLine="true"
|
||||
android:hint="@string/wifi_password"
|
||||
android:inputType="textPassword"
|
||||
android:typeface="monospace"
|
||||
android:maxLength="63"
|
||||
android:imeOptions="flagForceAscii" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
android:layout_marginTop="8dip"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_edit_content"
|
||||
android:singleLine="true"
|
||||
android:hint="@string/wifi_password"
|
||||
android:inputType="textPassword"
|
||||
android:typeface="monospace"
|
||||
android:maxLength="63"
|
||||
android:imeOptions="flagForceAscii" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/band_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dip"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_label"
|
||||
android:text="@string/wifi_hotspot_ap_band_title" />
|
||||
<Spinner
|
||||
android:id="@+id/band"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/wifi_item_content"
|
||||
android:prompt="@string/wifi_hotspot_ap_band_title" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<data>
|
||||
<import type="be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper"/>
|
||||
<variable
|
||||
name="data"
|
||||
type="be.mygod.vpnhotspot.manage.RepeaterManager.Data"/>
|
||||
@@ -40,7 +41,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_repeater"
|
||||
android:text="@{data.title}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
|
||||
|
||||
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
|
||||
@@ -69,104 +70,11 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:nextFocusDown="@+id/oc"
|
||||
android:padding="16dp"
|
||||
android:onClick="@{_ -> data.editConfigurations()}">
|
||||
|
||||
<Space
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_device_wifi_lock"
|
||||
android:tint="?android:attr/textColorPrimary"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/wifi_ssid"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
|
||||
|
||||
<be.mygod.vpnhotspot.widget.AutoCollapseTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{data.ssid}"
|
||||
tools:text="…"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_content_wave"
|
||||
android:tint="?android:attr/textColorPrimary"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_service_repeater_oc"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/oc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@={data.oc}"
|
||||
android:inputType="number"
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLength="3"
|
||||
android:hint="@string/settings_service_repeater_oc_summary"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
android:nextFocusUp="@+id/oc"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:padding="16dp"
|
||||
android:onClick="@{_ -> data.wps()}"
|
||||
android:visibility="@{data.serviceStarted}">
|
||||
android:visibility="@{data.serviceStarted && WifiP2pManagerHelper.startWps != null}">
|
||||
|
||||
<Space
|
||||
android:layout_width="40dp"
|
||||
|
||||
18
mobile/src/main/res/menu/toolbar_configuration.xml
Normal file
18
mobile/src/main/res/menu/toolbar_configuration.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@android:id/copy"
|
||||
android:alphabeticShortcut="c"
|
||||
android:icon="?attr/actionModeCopyDrawable"
|
||||
android:title="@android:string/copy"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item android:id="@android:id/paste"
|
||||
android:alphabeticShortcut="v"
|
||||
android:icon="?attr/actionModePasteDrawable"
|
||||
android:title="@android:string/paste"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item android:id="@+id/share_qr"
|
||||
android:icon="@drawable/ic_social_share"
|
||||
android:title="@string/configuration_share"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
||||
@@ -9,4 +9,18 @@
|
||||
app:showAsAction="always">
|
||||
<menu/>
|
||||
</item>
|
||||
|
||||
<item android:id="@+id/configuration"
|
||||
android:icon="@drawable/ic_device_wifi_lock"
|
||||
android:title="@string/configuration_view"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<item android:id="@+id/configuration_repeater"
|
||||
android:title="@string/title_repeater"/>
|
||||
<item android:id="@+id/configuration_temp_hotspot"
|
||||
android:title="@string/tethering_temp_hotspot"/>
|
||||
<item android:id="@+id/configuration_ap"
|
||||
android:title="@string/tethering_manage_wifi"/>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
Values copied from:
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/419086d/res/values-ru/strings.xml
|
||||
-->
|
||||
|
||||
<string name="repeater_configure">Настройка Wi-Fi ретранслятора</string>
|
||||
<string name="repeater_configure_failure">Действительный конфиг не найден. Пожалуйста, сначала запустите ретранслятор.</string>
|
||||
<string name="repeater_reset_credentials">Сброс</string>
|
||||
<string name="repeater_reset_credentials_success">Сброс учетных данных.</string>
|
||||
<string name="repeater_reset_credentials_failure">Не удалось сбросить учетные данные (причина:%s)</string>
|
||||
<string name="repeater_clean_pog_failure">Не удалось удалить избыточную группу P2P (причина: %s)</string>
|
||||
|
||||
<string name="repeater_p2p_unavailable">Wi-Fi директ недоступен, пожалуйста включите Wi-Fi</string>
|
||||
@@ -19,7 +19,9 @@
|
||||
<string name="repeater_failure_reason_unsupported_operation">неподдерживаемая операция</string>
|
||||
<string name="repeater_failure_disconnected">Сервис недоступен. Попробуйте позже</string>
|
||||
|
||||
|
||||
<string name="tethering_manage_usb" msgid="585829947108007917">"USB-модем"</string>
|
||||
<string name="tethering_manage_wifi" msgid="7763495093333664887">"Точка доступа Wi‑Fi"</string>
|
||||
<string name="tethering_manage_bluetooth" msgid="2379175828878753652">"Bluetooth-модем"</string>
|
||||
|
||||
<string name="connected_state_incomplete">" (подключение)"</string>
|
||||
<string name="connected_state_valid">" (доступный)"</string>
|
||||
@@ -44,17 +46,24 @@
|
||||
<string name="exception_interface_not_found">Ошибка: Нисходящий интерфейс не найден</string>
|
||||
<string name="noisy_su_failure">Что-то пошло не так, пожалуйста, проверьте отладочную информацию.</string>
|
||||
|
||||
<string name="configuration_view">Настройка Wi-Fi ретранслятора</string>
|
||||
<string name="wifi_ssid" msgid="5519636102673067319">"Имя сети"</string>
|
||||
<string name="wifi_security" msgid="6603611185592956936">"Защита"</string>
|
||||
<string name="wifi_password" msgid="5948219759936151048">"Пароль"</string>
|
||||
<string name="credentials_password_too_short">Пароль должен содержать не менее 8 символов.</string>
|
||||
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"Диапазон частот Wi-Fi"</string>
|
||||
<string name="wifi_ap_choose_auto" msgid="2677800651271769965">"Авто"</string>
|
||||
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2,4 ГГц"</string>
|
||||
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5,0 ГГц"</string>
|
||||
<string name="wifi_save" msgid="3331121567988522826">"Сохранить"</string>
|
||||
|
||||
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values-ru/donations__strings.xml -->
|
||||
<string name="donations__button_close">Закрыть</string>
|
||||
<string name="donations__description">Считаете это приложение полезным?\nПоддержите его разработку, отправив пожертвование разработчику!</string>
|
||||
<string name="donations__google_android_market">Google Play Store</string>
|
||||
<string name="donations__google_android_market_not_supported_title">In-App пожертвования не поддерживаются.</string>
|
||||
<string name="donations__google_android_market_not_supported">Пожертвования через приложение не поддерживаются. Google Play Store установлен правильно?</string>
|
||||
<string name="donations__google_android_market_description">Google взимает 30% комиссии с каждого пожертвования!</string>
|
||||
<string name="donations__google_android_market_donate_button">Пожертвовать!</string>
|
||||
<string name="donations__google_android_market_text">Сколько?</string>
|
||||
<string name="donations__thanks_dialog_title">Благодарю!</string>
|
||||
<string name="donations__thanks_dialog">Благодарю за пожертвование! Я очень это ценю!</string>
|
||||
</resources>
|
||||
|
||||
13
mobile/src/main/res/values-v29/arrays.xml
Normal file
13
mobile/src/main/res/values-v29/arrays.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="settings_service_wifi_lock">
|
||||
<item>@string/settings_service_wifi_lock_none</item>
|
||||
<item>@string/settings_service_wifi_lock_high_perf_v29</item>
|
||||
<item>@string/settings_service_wifi_lock_low_latency</item>
|
||||
</string-array>
|
||||
<string-array name="settings_service_wifi_lock_values">
|
||||
<item>None</item>
|
||||
<item>HighPerf</item>
|
||||
<item>LowLatency</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -6,17 +6,14 @@
|
||||
<string name="title_clients">已连设备</string>
|
||||
<string name="title_settings">设置选项</string>
|
||||
|
||||
<string name="repeater_channel">无线中继 (%1$d MHz, 频道 %2$d)</string>
|
||||
<string name="repeater_wps">WPS(不安全)</string>
|
||||
<string name="repeater_wps_dialog_title">输入 PIN</string>
|
||||
<string name="repeater_wps_dialog_pbc">一键加密</string>
|
||||
<string name="repeater_wps_success_pbc">请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。</string>
|
||||
<string name="repeater_wps_success_keypad">成功注册 PIN。</string>
|
||||
<string name="repeater_wps_failure">打开 WPS 失败(原因:%s)</string>
|
||||
<string name="repeater_configure">设置 WLAN 中继</string>
|
||||
<string name="repeater_configure_failure">未能找到有效的档案。请尝试先打开中继。</string>
|
||||
<string name="repeater_reset_credentials">重置</string>
|
||||
<string name="repeater_reset_credentials_success">凭据已重置。</string>
|
||||
<string name="repeater_reset_credentials_failure">重置凭据失败(原因:%s)</string>
|
||||
<string name="repeater_clean_pog_failure">删除多余 P2P 群组失败(原因:%s)</string>
|
||||
|
||||
<string name="repeater_p2p_unavailable">Wi\u2011Fi 直连不可用,请打开 Wi\u2011Fi</string>
|
||||
@@ -47,7 +44,7 @@
|
||||
<!--
|
||||
Values copied from:
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/b63de87/res/values-zh-rCN/strings.xml
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/419086d/res/values-zh-rCN/strings.xml
|
||||
* @string/usb_tethering_button_text
|
||||
* @string/wifi_hotspot_checkbox_text
|
||||
* @string/bluetooth_tether_checkbox_text
|
||||
@@ -86,8 +83,6 @@
|
||||
<string name="settings_service_masquerade_none">无</string>
|
||||
<string name="settings_service_masquerade_simple">简易</string>
|
||||
<string name="settings_service_masquerade_netd">Android Netd 服务</string>
|
||||
<string name="settings_service_repeater_oc">Wi\u2011Fi 运行频段 (不稳定)</string>
|
||||
<string name="settings_service_repeater_oc_summary">"自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)"</string>
|
||||
<string name="settings_service_disable_ipv6">禁用 IPv6 共享</string>
|
||||
<string name="settings_service_disable_ipv6_summary">防止 VPN 通过 IPv6 泄漏。</string>
|
||||
<string name="settings_service_repeater_start_on_boot">开机自启动中继</string>
|
||||
@@ -95,6 +90,8 @@
|
||||
<string name="settings_service_wifi_lock_none">系统默认</string>
|
||||
<string name="settings_service_wifi_lock_full">开</string>
|
||||
<string name="settings_service_wifi_lock_high_perf">高性能模式</string>
|
||||
<string name="settings_service_wifi_lock_high_perf_v29">禁用省电</string>
|
||||
<string name="settings_service_wifi_lock_low_latency">低延迟模式</string>
|
||||
<string name="settings_service_ip_monitor">网络状态监听模式</string>
|
||||
<string name="settings_service_ip_monitor_monitor">Netlink 监听</string>
|
||||
<string name="settings_service_ip_monitor_monitor_root">Netlink 监听 (root)</string>
|
||||
@@ -131,20 +128,25 @@
|
||||
<string name="exception_interface_not_found">错误:未找到下游接口</string>
|
||||
<string name="noisy_su_failure">发生异常,详情请查看调试信息。</string>
|
||||
|
||||
<string name="configuration_view">设置 WLAN</string>
|
||||
<string name="configuration_share">使用 QR 码分享</string>
|
||||
<string name="configuration_rejected">Android 系统拒绝使用此配置。(详情参见日志)</string>
|
||||
<string name="wifi_ssid" msgid="5519636102673067319">"网络名称"</string>
|
||||
<string name="wifi_security" msgid="6603611185592956936">"安全性"</string>
|
||||
<string name="wifi_password" msgid="5948219759936151048">"密码"</string>
|
||||
<string name="credentials_password_too_short" msgid="7502749986405522663">"密码至少应包含 8 个字符。"</string>
|
||||
<string name="wifi_hotspot_ap_band_title" msgid="1165801173359290681">"AP 频段"</string>
|
||||
<string name="wifi_ap_choose_auto" msgid="2677800651271769965">"自动"</string>
|
||||
<string name="wifi_ap_choose_2G" msgid="8724267386885036210">"2.4 GHz 频段"</string>
|
||||
<string name="wifi_ap_choose_5G" msgid="8813128641914385634">"5.0 GHz 频段"</string>
|
||||
<string name="wifi_save" msgid="3331121567988522826">"保存"</string>
|
||||
<string name="wifi_cancel" msgid="6763568902542968964">"取消"</string>
|
||||
|
||||
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values-zh/donations__strings.xml -->
|
||||
<string name="donations__button_close">关闭</string>
|
||||
<string name="donations__google_android_market">Google Play 商店</string>
|
||||
<string name="donations__google_android_market_not_supported_title">不支持 In-App 捐赠。</string>
|
||||
<string name="donations__google_android_market_not_supported">不支持 In-App 捐赠。你的 Google Play 商店是否安装正确了呢?</string>
|
||||
<string name="donations__google_android_market_donate_button">捐赠!</string>
|
||||
<string name="donations__google_android_market_text">捐赠多少?</string>
|
||||
<string name="donations__thanks_dialog_title">谢谢!</string>
|
||||
<string name="donations__thanks_dialog">谢谢捐赠!\n非常感谢您!</string>
|
||||
|
||||
<string name="donations__description">觉得此应用很有用?\n捐赠给该开发者以支持此应用的开发!</string>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="listitem_manage_tether_padding_start">56dp</dimen>
|
||||
<dimen name="qr_code_size">250dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
<!--
|
||||
Values copied from:
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/e5ed810/res/values/strings.xml
|
||||
* @string/wifi_tether_configure_ap_text
|
||||
* https://android.googlesource.com/platform/packages/apps/Settings/+/5697a7e/res/values/strings.xml
|
||||
* @string/usb_tethering_button_text
|
||||
* @string/wifi_hotspot_checkbox_text
|
||||
* @string/bluetooth_tether_checkbox_text
|
||||
@@ -15,6 +14,7 @@
|
||||
<string name="title_clients">Clients</string>
|
||||
<string name="title_settings">Settings</string>
|
||||
|
||||
<string name="repeater_channel">Repeater (%1$d MHz, channel %2$d)</string>
|
||||
<string name="repeater_wps">WPS (insecure)</string>
|
||||
<string name="repeater_wps_dialog_title">Enter PIN</string>
|
||||
<string name="repeater_wps_dialog_pbc">Push Button</string>
|
||||
@@ -22,11 +22,7 @@
|
||||
device.</string>
|
||||
<string name="repeater_wps_success_keypad">PIN registered.</string>
|
||||
<string name="repeater_wps_failure">Failed to start WPS (reason: %s)</string>
|
||||
<string name="repeater_configure">Configure Wi\u2011Fi repeater</string>
|
||||
<string name="repeater_configure_failure">Valid config not found. Please start repeater first.</string>
|
||||
<string name="repeater_reset_credentials">Reset</string>
|
||||
<string name="repeater_reset_credentials_success">Credentials reset.</string>
|
||||
<string name="repeater_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
|
||||
<string name="repeater_clean_pog_failure">Failed to remove redundant P2P group (reason: %s)</string>
|
||||
|
||||
<string name="repeater_p2p_unavailable">Wi\u2011Fi direct unavailable, please enable Wi\u2011Fi</string>
|
||||
@@ -92,8 +88,6 @@
|
||||
<string name="settings_service_masquerade_none">None</string>
|
||||
<string name="settings_service_masquerade_simple">Simple</string>
|
||||
<string name="settings_service_masquerade_netd">Android Netd Service</string>
|
||||
<string name="settings_service_repeater_oc">Operating Wi\u2011Fi channel (unstable)</string>
|
||||
<string name="settings_service_repeater_oc_summary">Auto (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)</string>
|
||||
<string name="settings_service_disable_ipv6">Disable IPv6 tethering</string>
|
||||
<string name="settings_service_disable_ipv6_summary">Enabling this option will prevent VPN leaks via IPv6.</string>
|
||||
<string name="settings_service_repeater_start_on_boot">Start repeater on boot</string>
|
||||
@@ -101,6 +95,8 @@
|
||||
<string name="settings_service_wifi_lock_none">System default</string>
|
||||
<string name="settings_service_wifi_lock_full">On</string>
|
||||
<string name="settings_service_wifi_lock_high_perf">High Performance Mode</string>
|
||||
<string name="settings_service_wifi_lock_high_perf_v29">Disable power save</string>
|
||||
<string name="settings_service_wifi_lock_low_latency">Low latency mode</string>
|
||||
<string name="settings_service_ip_monitor">Network status monitor mode</string>
|
||||
<string name="settings_service_ip_monitor_monitor">Netlink monitor</string>
|
||||
<string name="settings_service_ip_monitor_monitor_root">Netlink monitor with root</string>
|
||||
@@ -140,21 +136,26 @@
|
||||
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
|
||||
<string name="noisy_su_failure">Something went wrong, please check the debug information.</string>
|
||||
|
||||
<string name="configuration_view">Wi\u2011Fi configuration</string>
|
||||
<string name="configuration_share">Share via QR code</string>
|
||||
<string name="configuration_rejected">Android system refuses such configuration. (see logcat)</string>
|
||||
<string name="wifi_ssid">Network name</string>
|
||||
<string name="wifi_security">Security</string>
|
||||
<string name="wifi_password">Password</string>
|
||||
<string name="credentials_password_too_short">The password must have at least 8 characters.</string>
|
||||
<string name="wifi_hotspot_ap_band_title">AP Band</string>
|
||||
<string name="wifi_ap_choose_auto">Auto</string>
|
||||
<string name="wifi_ap_choose_2G">2.4 GHz Band</string>
|
||||
<string name="wifi_ap_choose_5G">5.0 GHz Band</string>
|
||||
<string name="wifi_save">Save</string>
|
||||
<string name="wifi_cancel">Cancel</string>
|
||||
|
||||
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml -->
|
||||
<string name="donations__button_close">Close</string>
|
||||
<string name="donations__description">Do you find this application useful?\nSupport its development by sending a donation to the developer!</string>
|
||||
<string name="donations__google_android_market">Google Play Store</string>
|
||||
<string name="donations__google_android_market_not_supported_title">In-App Donations are not supported.</string>
|
||||
<string name="donations__google_android_market_not_supported">In-App Donations are not supported. Is Google Play Store installed correctly?</string>
|
||||
<string name="donations__google_android_market_description">Google charges a fee of 30%</string>
|
||||
<string name="donations__google_android_market_donate_button">Donate!</string>
|
||||
<string name="donations__google_android_market_text">How much?</string>
|
||||
<string name="donations__thanks_dialog_title">Thanks!</string>
|
||||
<string name="donations__thanks_dialog">Thanks for donating!\nI really appreciate this!</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
<!-- https://android.googlesource.com/platform/packages/apps/Settings/+/7efcc35/res/values/styles.xml -->
|
||||
<style name="wifi_item">
|
||||
<item name="android:layout_marginTop">8dip</item>
|
||||
<item name="android:layout_marginStart">8dip</item>
|
||||
<item name="android:layout_marginEnd">8dip</item>
|
||||
<item name="android:paddingStart">8dip</item>
|
||||
@@ -21,6 +20,18 @@
|
||||
<item name="android:orientation">vertical</item>
|
||||
<item name="android:gravity">start</item>
|
||||
</style>
|
||||
<style name="wifi_item_label">
|
||||
<item name="android:paddingStart">8dip</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textAlignment">viewStart</item>
|
||||
<item name="android:textAppearance">@android:style/TextAppearance.Material.Body1</item>
|
||||
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
||||
</style>
|
||||
<style name="wifi_item_content">
|
||||
<item name="android:textAlignment">viewStart</item>
|
||||
<item name="android:textAppearance">@android:style/TextAppearance.Material.Subhead</item>
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
<style name="wifi_item_edit_content">
|
||||
<item name="android:paddingStart">4dip</item>
|
||||
<item name="android:layout_marginStart">4dip</item>
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
app:icon="@drawable/ic_device_wifi_lock"
|
||||
app:entries="@array/settings_service_wifi_lock"
|
||||
app:entryValues="@array/settings_service_wifi_lock_values"
|
||||
app:defaultValue="Full"
|
||||
app:title="@string/settings_service_wifi_lock"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
<SwitchPreference
|
||||
|
||||
Reference in New Issue
Block a user