Merge pull request #110 from Mygod/v2.4

Official v2.4 release
This commit is contained in:
Mygod
2019-06-10 22:58:36 +08:00
committed by GitHub
50 changed files with 1197 additions and 610 deletions

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -363,7 +363,7 @@ style:
OptionalUnit:
active: true
OptionalWhenBraces:
active: true
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:

View File

@@ -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

View File

@@ -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'
}

View File

@@ -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"

View File

@@ -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>()!! }

View File

@@ -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")
}
}

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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() {

View File

@@ -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?) {

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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) }

View File

@@ -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)

View File

@@ -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() }

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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())
}
}

View File

@@ -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)) {

View File

@@ -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 {

View File

@@ -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>

View 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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 &amp;&amp; WifiP2pManagerHelper.startWps != null}">
<Space
android:layout_width="40dp"

View 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>

View File

@@ -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>

View File

@@ -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">"Точка доступа WiFi"</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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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