diff --git a/mobile/build.gradle b/mobile/build.gradle index db13b973..e5577006 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.19.1' implementation 'com.takisoft.preferencex:preferencex:1.0.0' + implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" baseImplementation 'com.android.billingclient:billing:1.2' baseImplementation 'com.crashlytics.sdk.android:crashlytics:2.9.8' diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 31feceb2..772364d5 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -33,6 +33,7 @@ tools:ignore="ProtectedPermissions"/> + diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt index 166cc96c..41b503ab 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyHotspotService.kt @@ -8,6 +8,7 @@ import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.manage.LocalOnlyHotspotManager import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.TetheringManager +import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.widget.SmartSnackbar import timber.log.Timber @@ -30,6 +31,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() { private val binder = Binder() private var reservation: WifiManager.LocalOnlyHotspotReservation? = null private var routingManager: LocalOnlyInterfaceManager? = null + private var locked = false private var receiverRegistered = false private val receiver = broadcastReceiver { _, intent -> val ifaces = TetheringManager.getLocalOnlyTetheredIfaces(intent.extras ?: return@broadcastReceiver) @@ -63,6 +65,9 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) { if (reservation == null) onFailed(-2) else { this@LocalOnlyHotspotService.reservation = reservation + check(!locked) + WifiDoubleLock.acquire() + locked = true if (!receiverRegistered) { registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) receiverRegistered = true @@ -119,6 +124,10 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() { private fun unregisterReceiver() { routingManager?.stop() routingManager = null + if (locked) { + WifiDoubleLock.release() + locked = false + } if (receiverRegistered) { unregisterReceiver(receiver) IpNeighbourMonitor.unregisterCallback(this) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index ecac92b6..781cc918 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -15,6 +15,7 @@ import androidx.annotation.StringRes import androidx.core.content.getSystemService import androidx.core.os.postDelayed import be.mygod.vpnhotspot.App.Companion.app +import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId @@ -123,6 +124,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere } } private var routingManager: LocalOnlyInterfaceManager? = null + private var locked = false var status = Status.IDLE private set(value) { @@ -261,6 +263,9 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere * startService Step 3 */ private fun doStart(group: WifiP2pGroup) { + check(!locked) + WifiDoubleLock.acquire() + locked = true binder.group = group check(routingManager == null) routingManager = LocalOnlyInterfaceManager(group.`interface`!!) @@ -297,6 +302,10 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere unregisterReceiver() routingManager?.stop() routingManager = null + if (locked) { + WifiDoubleLock.release() + locked = false + } status = Status.IDLE ServiceNotification.stopForeground(this) stopSelf() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index 4a6a1e55..a02ae704 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -5,8 +5,10 @@ import android.content.IntentFilter import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.manage.TetheringFragment import be.mygod.vpnhotspot.net.Routing +import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor +import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.widget.SmartSnackbar import timber.log.Timber @@ -25,6 +27,7 @@ class TetheringService : IpNeighbourMonitoringService() { private val binder = Binder() private val routings = HashMap() + private var locked = false private var receiverRegistered = false private val receiver = broadcastReceiver { _, intent -> val extras = intent.extras ?: return@broadcastReceiver @@ -37,6 +40,10 @@ class TetheringService : IpNeighbourMonitoringService() { override val activeIfaces get() = synchronized(routings) { routings.keys.toList() } private fun updateRoutingsLocked() { + if (locked && routings.keys.all { !TetherType.ofInterface(it).isWifi }) { + WifiDoubleLock.release() + locked = false + } if (routings.isEmpty()) { unregisterReceiver() ServiceNotification.stopForeground(this) @@ -92,7 +99,13 @@ class TetheringService : IpNeighbourMonitoringService() { if (intent != null) { val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE) synchronized(routings) { - if (iface != null) routings[iface] = null + if (iface != null) { + routings[iface] = null + if (TetherType.ofInterface(iface).isWifi && !locked) { + WifiDoubleLock.acquire() + locked = true + } + } routings.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.revert() updateRoutingsLocked() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt index b6101430..99ba30ad 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt @@ -15,6 +15,10 @@ enum class TetherType { BLUETOOTH -> R.drawable.ic_device_bluetooth else -> R.drawable.ic_device_wifi_tethering } + val isWifi get() = when (this) { + WIFI_P2P, WIFI, WIMAX -> true + else -> false + } companion object { private val usbRegexes: List diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiDoubleLock.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiDoubleLock.kt new file mode 100644 index 00000000..b1831503 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiDoubleLock.kt @@ -0,0 +1,61 @@ +package be.mygod.vpnhotspot.net.wifi + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.net.wifi.WifiManager +import android.os.PowerManager +import androidx.core.content.getSystemService +import be.mygod.vpnhotspot.App.Companion.app + +/** + * This mechanism is used to maximize profit. Source: https://stackoverflow.com/a/29657230/2245107 + */ +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 + private val service by lazy { app.getSystemService()!! } + + private var referenceCount = 0 + private var lock: WifiDoubleLock? = null + + fun acquire() = synchronized(this) { + if (referenceCount == 0) { + app.pref.registerOnSharedPreferenceChangeListener(this) + val lockType = lockType + if (lockType != null) lock = WifiDoubleLock(lockType) + } + referenceCount += 1 + } + fun release() = synchronized(this) { + referenceCount -= 1 + if (referenceCount == 0) { + lock?.close() + lock = null + app.pref.unregisterOnSharedPreferenceChangeListener(this) + } + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (key == KEY) synchronized(this) { + lock?.close() + val lockType = 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) + } + + private val wifi = app.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() } + @SuppressLint("WakelockTimeout") + private val power = service.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "vpnhotspot:power").apply { acquire() } + + override fun close() { + wifi.release() + power.release() + } +} diff --git a/mobile/src/main/res/drawable/ic_device_wifi_lock.xml b/mobile/src/main/res/drawable/ic_device_wifi_lock.xml index 8eba49d0..877f0d44 100644 --- a/mobile/src/main/res/drawable/ic_device_wifi_lock.xml +++ b/mobile/src/main/res/drawable/ic_device_wifi_lock.xml @@ -2,7 +2,9 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportHeight="24.0" + android:autoMirrored="true" + android:tint="?attr/colorControlNormal"> diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index b5f96d9d..e04641bb 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -84,6 +84,10 @@ 禁用 IPv6 共享 防止 IPv6 VPN 泄漏。 开机自启动中继 + 保持 Wi\u2011Fi 开启 + 系统默认 + + 高性能模式 备用 DNS 服务器[:端口] 上游网络接口 自动检测系统 VPN diff --git a/mobile/src/main/res/values/arrays.xml b/mobile/src/main/res/values/arrays.xml new file mode 100644 index 00000000..65dbb003 --- /dev/null +++ b/mobile/src/main/res/values/arrays.xml @@ -0,0 +1,13 @@ + + + + @string/settings_service_wifi_lock_none + @string/settings_service_wifi_lock_full + @string/settings_service_wifi_lock_high_perf + + + None + Full + HighPerf + + diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index f6a5a3e6..189fe0c4 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -92,6 +92,10 @@ Disable IPv6 tethering Enabling this option will prevent VPN leaks via IPv6. Start repeater on boot + Keep Wi\u2011Fi alive + System default + On + High Performance Mode Fallback DNS server[:port] Upstream network interface Auto detect system VPN diff --git a/mobile/src/main/res/xml/pref_settings.xml b/mobile/src/main/res/xml/pref_settings.xml index 9f689c49..5caaf5c0 100644 --- a/mobile/src/main/res/xml/pref_settings.xml +++ b/mobile/src/main/res/xml/pref_settings.xml @@ -35,6 +35,14 @@ android:icon="@drawable/ic_image_looks_6" android:title="@string/settings_service_disable_ipv6" android:summary="@string/settings_service_disable_ipv6_summary"/> +