Temporary Wi-Fi hotspot for bypassing tethering limits (#18)
* First draft of temporary hotspot * Refactor with LocalOnlyInterfaceManager * Refactor LocalOnlyHotspotService * Localize * Update strict summary
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
<uses-permission android:name="android.permission.TETHER_PRIVILEGE"/>
|
<uses-permission android:name="android.permission.TETHER_PRIVILEGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@@ -53,6 +54,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service android:name=".LocalOnlyHotspotService">
|
||||||
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".RepeaterService"
|
android:name=".RepeaterService"
|
||||||
android:directBootAware="true">
|
android:directBootAware="true">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
@@ -43,6 +44,7 @@ class App : Application() {
|
|||||||
val handler = Handler()
|
val handler = Handler()
|
||||||
val pref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(deviceContext) }
|
val pref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(deviceContext) }
|
||||||
val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
||||||
|
val wifi by lazy { app.getSystemService(Context.WIFI_SERVICE) as WifiManager }
|
||||||
|
|
||||||
val operatingChannel: Int get() {
|
val operatingChannel: Int get() {
|
||||||
val result = pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
val result = pref.getString(KEY_OPERATING_CHANNEL, null)?.toIntOrNull() ?: 0
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
|
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||||
|
|
||||||
|
abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback {
|
||||||
|
private var neighbours = emptyList<IpNeighbour>()
|
||||||
|
|
||||||
|
protected abstract val activeIfaces: List<String>
|
||||||
|
|
||||||
|
override fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>) {
|
||||||
|
this.neighbours = neighbours.values.toList()
|
||||||
|
}
|
||||||
|
override fun postIpNeighbourAvailable() {
|
||||||
|
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
|
||||||
|
neighbours
|
||||||
|
.filter { it.state != IpNeighbour.State.FAILED }
|
||||||
|
.distinctBy { it.lladdr }
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
ServiceNotification.startForeground(this, activeIfaces.associate { Pair(it, sizeLookup[it] ?: 0) })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Binder
|
||||||
|
import android.support.annotation.RequiresApi
|
||||||
|
import android.widget.Toast
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||||
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
|
|
||||||
|
@RequiresApi(26)
|
||||||
|
class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LocalOnlyHotspotService"
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class HotspotBinder : Binder() {
|
||||||
|
var fragment: TetheringFragment? = null
|
||||||
|
var iface: String? = null
|
||||||
|
val configuration get() = reservation?.wifiConfiguration
|
||||||
|
|
||||||
|
fun stop() = reservation?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val binder = HotspotBinder()
|
||||||
|
private var reservation: WifiManager.LocalOnlyHotspotReservation? = null
|
||||||
|
private var routingManager: LocalOnlyInterfaceManager? = null
|
||||||
|
private var receiverRegistered = false
|
||||||
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
|
val ifaces = TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
|
||||||
|
debugLog(TAG, "onTetherStateChangedLocked: $ifaces")
|
||||||
|
check(ifaces.size <= 1)
|
||||||
|
val iface = ifaces.singleOrNull()
|
||||||
|
binder.iface = iface
|
||||||
|
if (iface == null) {
|
||||||
|
routingManager?.stop()
|
||||||
|
routingManager = null
|
||||||
|
unregisterReceiver()
|
||||||
|
ServiceNotification.stopForeground(this)
|
||||||
|
stopSelf()
|
||||||
|
} else {
|
||||||
|
val routingManager = routingManager
|
||||||
|
if (routingManager == null) {
|
||||||
|
this.routingManager = LocalOnlyInterfaceManager(iface)
|
||||||
|
IpNeighbourMonitor.registerCallback(this)
|
||||||
|
} else check(iface == routingManager.downstream)
|
||||||
|
}
|
||||||
|
app.handler.post { binder.fragment?.adapter?.updateLocalOnlyViewHolder() }
|
||||||
|
}
|
||||||
|
override val activeIfaces get() = listOfNotNull(binder.iface)
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?) = binder
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
// throws IllegalStateException if the caller attempts to start the LocalOnlyHotspot while they
|
||||||
|
// have an outstanding request.
|
||||||
|
// https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiServiceImpl.java#1192
|
||||||
|
try {
|
||||||
|
app.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
|
||||||
|
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
|
||||||
|
if (reservation == null) onFailed(-2) else {
|
||||||
|
this@LocalOnlyHotspotService.reservation = reservation
|
||||||
|
if (!receiverRegistered) {
|
||||||
|
registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
|
receiverRegistered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopped() {
|
||||||
|
debugLog(TAG, "LOHCallback.onStopped")
|
||||||
|
reservation = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailed(reason: Int) {
|
||||||
|
Toast.makeText(this@LocalOnlyHotspotService, getString(R.string.tethering_temp_hotspot_failure,
|
||||||
|
when (reason) {
|
||||||
|
WifiManager.LocalOnlyHotspotCallback.ERROR_NO_CHANNEL ->
|
||||||
|
getString(R.string.tethering_temp_hotspot_failure_no_channel)
|
||||||
|
WifiManager.LocalOnlyHotspotCallback.ERROR_GENERIC ->
|
||||||
|
getString(R.string.tethering_temp_hotspot_failure_generic)
|
||||||
|
WifiManager.LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE ->
|
||||||
|
getString(R.string.tethering_temp_hotspot_failure_incompatible_mode)
|
||||||
|
WifiManager.LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED ->
|
||||||
|
getString(R.string.tethering_temp_hotspot_failure_tethering_disallowed)
|
||||||
|
else -> getString(R.string.failure_reason_unknown, reason)
|
||||||
|
}), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}, app.handler)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
unregisterReceiver()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterReceiver() {
|
||||||
|
if (receiverRegistered) {
|
||||||
|
unregisterReceiver(receiver)
|
||||||
|
IpNeighbourMonitor.unregisterCallback(this)
|
||||||
|
receiverRegistered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.support.v4.content.LocalBroadcastManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
|
import be.mygod.vpnhotspot.net.VpnMonitor
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.SocketException
|
||||||
|
|
||||||
|
class LocalOnlyInterfaceManager(val downstream: String, private val owner: InetAddress? = null) :
|
||||||
|
BroadcastReceiver(), VpnMonitor.Callback {
|
||||||
|
private var routing: Routing? = null
|
||||||
|
private var dns = emptyList<InetAddress>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
LocalBroadcastManager.getInstance(app).registerReceiver(this, intentFilter(App.ACTION_CLEAN_ROUTINGS))
|
||||||
|
VpnMonitor.registerCallback(this) { initRouting() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||||
|
val routing = routing
|
||||||
|
initRouting(ifname, if (routing == null) owner else {
|
||||||
|
routing.stop()
|
||||||
|
check(routing.upstream == null)
|
||||||
|
routing.hostAddress
|
||||||
|
}, dns)
|
||||||
|
}
|
||||||
|
override fun onLost(ifname: String) {
|
||||||
|
val routing = routing ?: return
|
||||||
|
if (!routing.stop()) app.toast(R.string.noisy_su_failure)
|
||||||
|
initRouting(null, routing.hostAddress, emptyList())
|
||||||
|
}
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val routing = routing ?: return
|
||||||
|
routing.started = false
|
||||||
|
initRouting(routing.upstream, routing.hostAddress, dns)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initRouting(upstream: String? = null, owner: InetAddress? = this.owner,
|
||||||
|
dns: List<InetAddress> = this.dns) {
|
||||||
|
try {
|
||||||
|
val routing = Routing(upstream, downstream, owner)
|
||||||
|
this.routing = routing
|
||||||
|
this.dns = dns
|
||||||
|
val strict = app.pref.getBoolean("service.repeater.strict", false)
|
||||||
|
if (strict && upstream == null) return // in this case, nothing to be done
|
||||||
|
if (routing.ipForward() // local only interfaces may not enable ip_forward
|
||||||
|
.rule().forward(strict).masquerade(strict).dnsRedirect(dns).start()) return
|
||||||
|
app.toast(R.string.noisy_su_failure)
|
||||||
|
} catch (e: SocketException) {
|
||||||
|
Toast.makeText(app, e.message, Toast.LENGTH_SHORT).show()
|
||||||
|
routing = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
VpnMonitor.unregisterCallback(this)
|
||||||
|
LocalBroadcastManager.getInstance(app).unregisterReceiver(this)
|
||||||
|
if (routing?.stop() == false) app.toast(R.string.noisy_su_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ssid @Bindable get() = binder?.ssid ?: getText(R.string.repeater_inactive)
|
val ssid @Bindable get() = binder?.ssid ?: getText(R.string.service_inactive)
|
||||||
val addresses @Bindable get(): String {
|
val addresses @Bindable get(): String {
|
||||||
return try {
|
return try {
|
||||||
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
||||||
@@ -160,7 +160,8 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
|||||||
private var p2pInterface: String? = null
|
private var p2pInterface: String? = null
|
||||||
private var tetheredInterfaces = emptySet<String>()
|
private var tetheredInterfaces = emptySet<String>()
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet()
|
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet() +
|
||||||
|
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
|
||||||
adapter.recreate()
|
adapter.recreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import android.support.v4.content.LocalBroadcastManager
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
|
||||||
import be.mygod.vpnhotspot.net.VpnMonitor
|
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
|
||||||
@@ -26,10 +24,8 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
|||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.SocketException
|
|
||||||
|
|
||||||
class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Callback,
|
class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTION_STATUS_CHANGED = "be.mygod.vpnhotspot.RepeaterService.STATUS_CHANGED"
|
const val ACTION_STATUS_CHANGED = "be.mygod.vpnhotspot.RepeaterService.STATUS_CHANGED"
|
||||||
private const val TAG = "RepeaterService"
|
private const val TAG = "RepeaterService"
|
||||||
@@ -110,17 +106,9 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
|
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO),
|
||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO),
|
intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO),
|
||||||
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
|
intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP))
|
||||||
App.ACTION_CLEAN_ROUTINGS -> if (status == Status.ACTIVE) {
|
|
||||||
val routing = routing
|
|
||||||
routing!!.started = false
|
|
||||||
resetup(routing, upstream, dns)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private var routingManager: LocalOnlyInterfaceManager? = null
|
||||||
private var upstream: String? = null
|
|
||||||
private var dns: List<InetAddress> = emptyList()
|
|
||||||
private var routing: Routing? = null
|
|
||||||
|
|
||||||
var status = Status.IDLE
|
var status = Status.IDLE
|
||||||
private set(value) {
|
private set(value) {
|
||||||
@@ -134,7 +122,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
WifiP2pManager.P2P_UNSUPPORTED -> getString(R.string.repeater_failure_reason_p2p_unsupported)
|
WifiP2pManager.P2P_UNSUPPORTED -> getString(R.string.repeater_failure_reason_p2p_unsupported)
|
||||||
WifiP2pManager.BUSY -> getString(R.string.repeater_failure_reason_busy)
|
WifiP2pManager.BUSY -> getString(R.string.repeater_failure_reason_busy)
|
||||||
WifiP2pManager.NO_SERVICE_REQUESTS -> getString(R.string.repeater_failure_reason_no_service_requests)
|
WifiP2pManager.NO_SERVICE_REQUESTS -> getString(R.string.repeater_failure_reason_no_service_requests)
|
||||||
else -> getString(R.string.repeater_failure_reason_unknown, reason)
|
else -> getString(R.string.failure_reason_unknown, reason)
|
||||||
})
|
})
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -176,36 +164,19 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* startService 1st stop
|
* startService Step 1
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (status != Status.IDLE) return START_NOT_STICKY
|
if (status != Status.IDLE) return START_NOT_STICKY
|
||||||
status = Status.STARTING
|
status = Status.STARTING
|
||||||
VpnMonitor.registerCallback(this) { setup() }
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
|
|
||||||
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
showNotification()
|
|
||||||
if (group != null) removeGroup() else clean()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* startService 2nd stop
|
|
||||||
*/
|
|
||||||
private fun setup(ifname: String? = null, dns: List<InetAddress> = emptyList()) {
|
|
||||||
val matcher = WifiP2pManagerHelper.patternNetworkInfo.matcher(
|
val matcher = WifiP2pManagerHelper.patternNetworkInfo.matcher(
|
||||||
loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
|
loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
|
||||||
when {
|
when {
|
||||||
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
|
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
|
||||||
matcher.group(2) == "true" -> {
|
matcher.group(2) == "true" -> {
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
upstream = ifname
|
|
||||||
this.dns = dns
|
|
||||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
||||||
LocalBroadcastManager.getInstance(this)
|
|
||||||
.registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS))
|
|
||||||
receiverRegistered = true
|
receiverRegistered = true
|
||||||
p2pManager.requestGroupInfo(channel, {
|
p2pManager.requestGroupInfo(channel, {
|
||||||
when {
|
when {
|
||||||
@@ -227,72 +198,40 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
}
|
}
|
||||||
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
|
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
|
||||||
}
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
private fun resetup(routing: Routing, ifname: String? = null, dns: List<InetAddress> = emptyList()) =
|
* startService Step 2 (if a group isn't already available)
|
||||||
initRouting(ifname, routing.downstream, routing.hostAddress, dns)
|
*/
|
||||||
|
|
||||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) = when (status) {
|
|
||||||
Status.STARTING -> setup(ifname, dns)
|
|
||||||
Status.ACTIVE -> {
|
|
||||||
val routing = routing!!
|
|
||||||
if (routing.started) {
|
|
||||||
routing.stop()
|
|
||||||
check(routing.upstream == null)
|
|
||||||
}
|
|
||||||
resetup(routing, ifname, dns)
|
|
||||||
while (false) { }
|
|
||||||
}
|
|
||||||
else -> throw IllegalStateException("RepeaterService is in unexpected state when receiving onAvailable")
|
|
||||||
}
|
|
||||||
override fun onLost(ifname: String) {
|
|
||||||
if (routing?.stop() == false)
|
|
||||||
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
|
||||||
upstream = null
|
|
||||||
if (status == Status.ACTIVE) resetup(routing!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
|
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 onFailure(reason: Int) = startFailure(formatReason(R.string.repeater_create_group_failure, reason))
|
||||||
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire
|
override fun onSuccess() { } // wait for WIFI_P2P_CONNECTION_CHANGED_ACTION to fire to go to step 3
|
||||||
})
|
})
|
||||||
private fun doStart(group: WifiP2pGroup) {
|
|
||||||
this.group = group
|
|
||||||
status = Status.ACTIVE
|
|
||||||
showNotification(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* startService 3rd stop (if a group isn't already available), also called when connection changed
|
* Used during step 2, also called when connection changed
|
||||||
*/
|
*/
|
||||||
private fun onP2pConnectionChanged(info: WifiP2pInfo, net: NetworkInfo?, group: WifiP2pGroup) {
|
private fun onP2pConnectionChanged(info: WifiP2pInfo, net: NetworkInfo?, group: WifiP2pGroup) {
|
||||||
debugLog(TAG, "P2P connection changed: $info\n$net\n$group")
|
debugLog(TAG, "P2P connection changed: $info\n$net\n$group")
|
||||||
if (!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner) {
|
if (!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner) {
|
||||||
if (routing != null) clean() // P2P shutdown
|
if (routingManager != null) clean() // P2P shutdown
|
||||||
return
|
} else if (routingManager != null) {
|
||||||
}
|
this.group = group
|
||||||
if (routing == null) try {
|
showNotification(group)
|
||||||
if (initRouting(upstream, group.`interface` ?: return, info.groupOwnerAddress ?: return, dns))
|
} else doStart(group, info.groupOwnerAddress)
|
||||||
doStart(group)
|
|
||||||
} catch (e: SocketException) {
|
|
||||||
startFailure(e.message, group)
|
|
||||||
return
|
|
||||||
} else showNotification(group)
|
|
||||||
this.group = group
|
|
||||||
}
|
}
|
||||||
private fun initRouting(upstream: String?, downstream: String,
|
/**
|
||||||
owner: InetAddress, dns: List<InetAddress>): Boolean {
|
* startService Step 3
|
||||||
val routing = Routing(upstream, downstream, owner)
|
*/
|
||||||
this.routing = routing
|
private fun doStart(group: WifiP2pGroup, ownerAddress: InetAddress? = null) {
|
||||||
this.dns = dns
|
this.group = group
|
||||||
val strict = app.pref.getBoolean("service.repeater.strict", false)
|
routingManager = LocalOnlyInterfaceManager(group.`interface`!!, ownerAddress)
|
||||||
return if (strict && upstream == null || // in this case, nothing to be done
|
status = Status.ACTIVE
|
||||||
routing.ipForward() // Wi-Fi direct doesn't enable ip_forward
|
showNotification(group)
|
||||||
.rule().forward(strict).masquerade(strict).dnsRedirect(dns).start()) true else {
|
}
|
||||||
routing.stop()
|
private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
|
||||||
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
||||||
false
|
showNotification()
|
||||||
}
|
if (group != null) removeGroup() else clean()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showNotification(group: WifiP2pGroup? = null) = ServiceNotification.startForeground(this,
|
private fun showNotification(group: WifiP2pGroup? = null) = ServiceNotification.startForeground(this,
|
||||||
@@ -314,16 +253,13 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
private fun unregisterReceiver() {
|
private fun unregisterReceiver() {
|
||||||
if (receiverRegistered) {
|
if (receiverRegistered) {
|
||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
|
||||||
receiverRegistered = false
|
receiverRegistered = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun clean() {
|
private fun clean() {
|
||||||
VpnMonitor.unregisterCallback(this)
|
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
if (routing?.stop() == false)
|
routingManager?.stop()
|
||||||
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
routingManager = null
|
||||||
routing = null
|
|
||||||
status = Status.IDLE
|
status = Status.IDLE
|
||||||
ServiceNotification.stopForeground(this)
|
ServiceNotification.stopForeground(this)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.annotation.TargetApi
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.databinding.BaseObservable
|
import android.databinding.BaseObservable
|
||||||
import android.databinding.Bindable
|
import android.databinding.Bindable
|
||||||
import android.databinding.DataBindingUtil
|
import android.databinding.DataBindingUtil
|
||||||
@@ -39,12 +42,15 @@ import java.util.*
|
|||||||
class TetheringFragment : Fragment(), ServiceConnection {
|
class TetheringFragment : Fragment(), ServiceConnection {
|
||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_INTERFACE = 0
|
private const val VIEW_TYPE_INTERFACE = 0
|
||||||
|
private const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
|
||||||
private const val VIEW_TYPE_MANAGE = 1
|
private const val VIEW_TYPE_MANAGE = 1
|
||||||
private const val VIEW_TYPE_WIFI = 2
|
private const val VIEW_TYPE_WIFI = 2
|
||||||
private const val VIEW_TYPE_USB = 3
|
private const val VIEW_TYPE_USB = 3
|
||||||
private const val VIEW_TYPE_BLUETOOTH = 4
|
private const val VIEW_TYPE_BLUETOOTH = 4
|
||||||
private const val VIEW_TYPE_WIFI_LEGACY = 5
|
private const val VIEW_TYPE_WIFI_LEGACY = 5
|
||||||
|
|
||||||
|
private const val START_LOCAL_ONLY_HOTSPOT = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PAN Profile
|
* PAN Profile
|
||||||
* From BluetoothProfile.java.
|
* From BluetoothProfile.java.
|
||||||
@@ -55,26 +61,63 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Data(val iface: TetheredInterface) : BaseObservable() {
|
interface Data {
|
||||||
val icon: Int get() = TetherType.ofInterface(iface.name).icon
|
val icon: Int
|
||||||
val active = binder?.isActive(iface.name) == true
|
val title: CharSequence
|
||||||
|
val text: CharSequence
|
||||||
|
val active: Boolean
|
||||||
|
}
|
||||||
|
inner class TetheredData(val iface: TetheredInterface) : Data {
|
||||||
|
override val icon: Int get() = TetherType.ofInterface(iface.name).icon
|
||||||
|
override val title get() = iface.name
|
||||||
|
override val text get() = iface.addresses
|
||||||
|
override val active = tetheringBinder?.isActive(iface.name) == true
|
||||||
|
}
|
||||||
|
inner class LocalHotspotData(private val lookup: Map<String, NetworkInterface>) : Data {
|
||||||
|
override val icon: Int get() {
|
||||||
|
val iface = hotspotBinder?.iface ?: return TetherType.WIFI.icon
|
||||||
|
return TetherType.ofInterface(iface).icon
|
||||||
|
}
|
||||||
|
override val title get() = getString(R.string.tethering_temp_hotspot)
|
||||||
|
override val text by lazy {
|
||||||
|
val binder = hotspotBinder
|
||||||
|
val configuration = binder?.configuration ?: return@lazy getText(R.string.service_inactive)
|
||||||
|
val iface = binder.iface ?: return@lazy getText(R.string.service_inactive)
|
||||||
|
"${configuration.SSID} - ${configuration.preSharedKey}\n${TetheredInterface(iface, lookup).addresses}"
|
||||||
|
}
|
||||||
|
override val active = hotspotBinder?.iface != null
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InterfaceViewHolder(val binding: ListitemInterfaceBinding) : RecyclerView.ViewHolder(binding.root),
|
private open class InterfaceViewHolder(val binding: ListitemInterfaceBinding) :
|
||||||
View.OnClickListener {
|
RecyclerView.ViewHolder(binding.root), View.OnClickListener {
|
||||||
init {
|
init {
|
||||||
itemView.setOnClickListener(this)
|
itemView.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(view: View) {
|
override fun onClick(view: View) {
|
||||||
val context = itemView.context
|
val context = itemView.context
|
||||||
val data = binding.data!!
|
val data = binding.data as TetheredData
|
||||||
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, data.iface.name))
|
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, data.iface.name))
|
||||||
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
|
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE, data.iface.name))
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACE, data.iface.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@RequiresApi(26)
|
||||||
|
private inner class LocalOnlyHotspotViewHolder(binding: ListitemInterfaceBinding) : InterfaceViewHolder(binding) {
|
||||||
|
override fun onClick(view: View) {
|
||||||
|
val binder = hotspotBinder
|
||||||
|
if (binder?.iface != null) binder.stop() else {
|
||||||
|
val context = requireContext()
|
||||||
|
if (context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED) {
|
||||||
|
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
||||||
|
} else {
|
||||||
|
requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), START_LOCAL_ONLY_HOTSPOT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
private class ManageViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
private class ManageViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||||
init {
|
init {
|
||||||
view.setOnClickListener(this)
|
view.setOnClickListener(this)
|
||||||
@@ -160,7 +203,7 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
|
inner class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener {
|
||||||
var enabledTypes = emptySet<TetherType>()
|
var enabledTypes = emptySet<TetherType>()
|
||||||
@Bindable get
|
@Bindable get
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -208,29 +251,40 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
}
|
}
|
||||||
inner class TetheringAdapter :
|
inner class TetheringAdapter :
|
||||||
ListAdapter<TetheredInterface, RecyclerView.ViewHolder>(TetheredInterface.DiffCallback) {
|
ListAdapter<TetheredInterface, RecyclerView.ViewHolder>(TetheredInterface.DiffCallback) {
|
||||||
fun update(data: Set<String>) {
|
private var lookup: Map<String, NetworkInterface> = emptyMap()
|
||||||
val lookup = try {
|
|
||||||
|
fun update(activeIfaces: List<String>, localOnlyIfaces: List<String>) {
|
||||||
|
lookup = try {
|
||||||
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
|
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
|
||||||
} catch (e: SocketException) {
|
} catch (e: SocketException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
emptyMap<String, NetworkInterface>()
|
emptyMap()
|
||||||
}
|
}
|
||||||
this@TetheringFragment.tetherListener.enabledTypes = data.map { TetherType.ofInterface(it) }.toSet()
|
this@TetheringFragment.tetherListener.enabledTypes =
|
||||||
submitList(data.map { TetheredInterface(it, lookup) }.sorted())
|
(activeIfaces + localOnlyIfaces).map { TetherType.ofInterface(it) }.toSet()
|
||||||
|
submitList(activeIfaces.map { TetheredInterface(it, lookup) }.sorted())
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) updateLocalOnlyViewHolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = super.getItemCount() + when (Build.VERSION.SDK_INT) {
|
override fun getItemCount() = super.getItemCount() + if (Build.VERSION.SDK_INT < 24) 2 else 5
|
||||||
in 0 until 24 -> 2
|
override fun getItemViewType(position: Int) = if (Build.VERSION.SDK_INT < 26) {
|
||||||
in 24..25 -> 5
|
when (position - super.getItemCount()) {
|
||||||
else -> 4
|
0 -> VIEW_TYPE_MANAGE
|
||||||
}
|
1 -> if (Build.VERSION.SDK_INT >= 24) VIEW_TYPE_USB else VIEW_TYPE_WIFI_LEGACY
|
||||||
override fun getItemViewType(position: Int) = when (position - super.getItemCount()) {
|
2 -> VIEW_TYPE_WIFI
|
||||||
0 -> VIEW_TYPE_MANAGE
|
3 -> VIEW_TYPE_BLUETOOTH
|
||||||
1 -> if (Build.VERSION.SDK_INT >= 24) VIEW_TYPE_USB else VIEW_TYPE_WIFI_LEGACY
|
4 -> VIEW_TYPE_WIFI_LEGACY
|
||||||
2 -> VIEW_TYPE_WIFI
|
else -> VIEW_TYPE_INTERFACE
|
||||||
3 -> VIEW_TYPE_BLUETOOTH
|
}
|
||||||
4 -> VIEW_TYPE_WIFI_LEGACY
|
} else {
|
||||||
else -> VIEW_TYPE_INTERFACE
|
when (position - super.getItemCount()) {
|
||||||
|
0 -> VIEW_TYPE_LOCAL_ONLY_HOTSPOT
|
||||||
|
1 -> VIEW_TYPE_MANAGE
|
||||||
|
2 -> VIEW_TYPE_USB
|
||||||
|
3 -> VIEW_TYPE_WIFI
|
||||||
|
4 -> VIEW_TYPE_BLUETOOTH
|
||||||
|
else -> VIEW_TYPE_INTERFACE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
@@ -239,22 +293,33 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
VIEW_TYPE_MANAGE -> ManageViewHolder(inflater.inflate(R.layout.listitem_manage, parent, false))
|
VIEW_TYPE_MANAGE -> ManageViewHolder(inflater.inflate(R.layout.listitem_manage, parent, false))
|
||||||
VIEW_TYPE_WIFI, VIEW_TYPE_USB, VIEW_TYPE_BLUETOOTH, VIEW_TYPE_WIFI_LEGACY ->
|
VIEW_TYPE_WIFI, VIEW_TYPE_USB, VIEW_TYPE_BLUETOOTH, VIEW_TYPE_WIFI_LEGACY ->
|
||||||
ManageItemHolder(ListitemManageTetherBinding.inflate(inflater, parent, false), viewType)
|
ManageItemHolder(ListitemManageTetherBinding.inflate(inflater, parent, false), viewType)
|
||||||
|
VIEW_TYPE_LOCAL_ONLY_HOTSPOT -> @TargetApi(26) {
|
||||||
|
LocalOnlyHotspotViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
|
||||||
|
}
|
||||||
else -> throw IllegalArgumentException("Invalid view type")
|
else -> throw IllegalArgumentException("Invalid view type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (holder) {
|
when (holder) {
|
||||||
is InterfaceViewHolder -> holder.binding.data = Data(getItem(position))
|
is LocalOnlyHotspotViewHolder -> holder.binding.data = LocalHotspotData(lookup)
|
||||||
|
is InterfaceViewHolder -> holder.binding.data = TetheredData(getItem(position))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@RequiresApi(26)
|
||||||
|
fun updateLocalOnlyViewHolder() {
|
||||||
|
notifyItemChanged(super.getItemCount())
|
||||||
|
notifyItemChanged(super.getItemCount() + 3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tetherListener = TetherListener()
|
private val tetherListener = TetherListener()
|
||||||
private lateinit var binding: FragmentTetheringBinding
|
private lateinit var binding: FragmentTetheringBinding
|
||||||
private var binder: TetheringService.TetheringBinder? = null
|
private var hotspotBinder: LocalOnlyHotspotService.HotspotBinder? = null
|
||||||
|
private var tetheringBinder: TetheringService.TetheringBinder? = null
|
||||||
val adapter = TetheringAdapter()
|
val adapter = TetheringAdapter()
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
adapter.update(TetheringManager.getTetheredIfaces(intent.extras))
|
adapter.update(TetheringManager.getTetheredIfaces(intent.extras),
|
||||||
|
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
@@ -269,14 +334,23 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
|
||||||
context.bindService(Intent(context, TetheringService::class.java), this, Context.BIND_AUTO_CREATE)
|
context.bindService(Intent(context, TetheringService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
context.bindService(Intent(context, LocalOnlyHotspotService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
val context = requireContext()
|
||||||
|
context.startForegroundService(Intent(context, LocalOnlyHotspotService::class.java))
|
||||||
|
}
|
||||||
|
} else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
val context = requireContext()
|
requireContext().unbindService(this)
|
||||||
context.unbindService(this)
|
|
||||||
context.unregisterReceiver(receiver)
|
|
||||||
super.onStop()
|
super.onStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,14 +359,34 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) = when (service) {
|
||||||
val binder = service as TetheringService.TetheringBinder
|
is TetheringService.TetheringBinder -> {
|
||||||
this.binder = binder
|
tetheringBinder = service
|
||||||
binder.fragment = this
|
service.fragment = this
|
||||||
|
requireContext().registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
|
while (false) { }
|
||||||
|
}
|
||||||
|
is LocalOnlyHotspotService.HotspotBinder -> @TargetApi(26) {
|
||||||
|
hotspotBinder = service
|
||||||
|
service.fragment = this
|
||||||
|
adapter.updateLocalOnlyViewHolder()
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("service")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
binder?.fragment = null
|
val context = requireContext()
|
||||||
binder = null
|
when (name) {
|
||||||
|
ComponentName(context, TetheringService::class.java) -> {
|
||||||
|
tetheringBinder?.fragment = null
|
||||||
|
tetheringBinder = null
|
||||||
|
context.unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
ComponentName(context, LocalOnlyHotspotService::class.java) -> {
|
||||||
|
hotspotBinder?.fragment = null
|
||||||
|
hotspotBinder = null
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("name")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.support.v4.content.LocalBroadcastManager
|
import android.support.v4.content.LocalBroadcastManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.*
|
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||||
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
|
import be.mygod.vpnhotspot.net.VpnMonitor
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
|
|
||||||
class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Callback {
|
class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_ADD_INTERFACE = "interface.add"
|
const val EXTRA_ADD_INTERFACE = "interface.add"
|
||||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||||
@@ -24,7 +26,6 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
|
|
||||||
private val binder = TetheringBinder()
|
private val binder = TetheringBinder()
|
||||||
private val routings = HashMap<String, Routing?>()
|
private val routings = HashMap<String, Routing?>()
|
||||||
private var neighbours = emptyList<IpNeighbour>()
|
|
||||||
private var upstream: String? = null
|
private var upstream: String? = null
|
||||||
private var dns: List<InetAddress> = emptyList()
|
private var dns: List<InetAddress> = emptyList()
|
||||||
private var receiverRegistered = false
|
private var receiverRegistered = false
|
||||||
@@ -32,9 +33,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
synchronized(routings) {
|
synchronized(routings) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
TetheringManager.ACTION_TETHER_STATE_CHANGED -> {
|
TetheringManager.ACTION_TETHER_STATE_CHANGED -> {
|
||||||
val remove = routings.keys - TetheringManager.getTetheredIfaces(intent.extras)
|
val failed = (routings.keys - TetheringManager.getTetheredIfaces(intent.extras))
|
||||||
if (remove.isEmpty()) return@broadcastReceiver
|
.any { routings.remove(it)?.stop() == false }
|
||||||
val failed = remove.any { routings.remove(it)?.stop() == false }
|
|
||||||
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
App.ACTION_CLEAN_ROUTINGS -> for (iface in routings.keys) routings[iface] = null
|
App.ACTION_CLEAN_ROUTINGS -> for (iface in routings.keys) routings[iface] = null
|
||||||
@@ -42,13 +42,10 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
updateRoutingsLocked()
|
updateRoutingsLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override val activeIfaces get() = synchronized(routings) { routings.keys.toList() }
|
||||||
|
|
||||||
private fun updateRoutingsLocked() {
|
fun updateRoutingsLocked() {
|
||||||
if (routings.isEmpty()) {
|
if (routings.isNotEmpty()) {
|
||||||
unregisterReceiver()
|
|
||||||
ServiceNotification.stopForeground(this)
|
|
||||||
stopSelf()
|
|
||||||
} else {
|
|
||||||
val upstream = upstream
|
val upstream = upstream
|
||||||
if (upstream != null) {
|
if (upstream != null) {
|
||||||
var failed = false
|
var failed = false
|
||||||
@@ -56,13 +53,11 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
// system tethering already has working forwarding rules
|
// system tethering already has working forwarding rules
|
||||||
// so it doesn't make sense to add additional forwarding rules
|
// so it doesn't make sense to add additional forwarding rules
|
||||||
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
|
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
|
||||||
if (routing.start()) routings[downstream] = routing else {
|
routings[downstream] = routing
|
||||||
failed = true
|
if (!routing.start()) failed = true
|
||||||
routing.stop()
|
|
||||||
routings.remove(downstream)
|
|
||||||
}
|
|
||||||
} catch (e: SocketException) {
|
} catch (e: SocketException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
routings.remove(downstream)
|
||||||
failed = true
|
failed = true
|
||||||
}
|
}
|
||||||
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||||
@@ -76,6 +71,11 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
}
|
}
|
||||||
postIpNeighbourAvailable()
|
postIpNeighbourAvailable()
|
||||||
}
|
}
|
||||||
|
if (routings.isEmpty()) {
|
||||||
|
unregisterReceiver()
|
||||||
|
ServiceNotification.stopForeground(this)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
app.handler.post { binder.fragment?.adapter?.notifyDataSetChanged() }
|
app.handler.post { binder.fragment?.adapter?.notifyDataSetChanged() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,21 +113,6 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>) {
|
|
||||||
this.neighbours = neighbours.values.toList()
|
|
||||||
}
|
|
||||||
override fun postIpNeighbourAvailable() {
|
|
||||||
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
|
|
||||||
neighbours
|
|
||||||
.filter { it.state != IpNeighbour.State.FAILED }
|
|
||||||
.distinctBy { it.lladdr }
|
|
||||||
.size
|
|
||||||
}
|
|
||||||
ServiceNotification.startForeground(this, synchronized(routings) {
|
|
||||||
routings.keys.associate { Pair(it, sizeLookup[it] ?: 0) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ class Routing(val upstream: String?, val downstream: String, ownerAddress: InetA
|
|||||||
fun start(): Boolean {
|
fun start(): Boolean {
|
||||||
if (started) return true
|
if (started) return true
|
||||||
started = true
|
started = true
|
||||||
return noisySu(startScript) == true
|
if (noisySu(startScript) != true) stop()
|
||||||
|
return started
|
||||||
}
|
}
|
||||||
fun stop(): Boolean {
|
fun stop(): Boolean {
|
||||||
if (!started) return true
|
if (!started) return true
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ object TetheringManager {
|
|||||||
stopTethering.invoke(app.connectivity, type)
|
stopTethering.invoke(app.connectivity, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTetheredIfaces(extras: Bundle) = if (Build.VERSION.SDK_INT >= 26)
|
fun getTetheredIfaces(extras: Bundle) = extras.getStringArrayList(
|
||||||
extras.getStringArrayList(EXTRA_ACTIVE_TETHER).toSet() + extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY)
|
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
|
||||||
else extras.getStringArrayList(EXTRA_ACTIVE_TETHER_LEGACY).toSet()
|
fun getLocalOnlyTetheredIfaces(extras: Bundle) =
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY) else emptyList<String>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
package be.mygod.vpnhotspot.net.wifi
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.wifi.WifiConfiguration
|
import android.net.wifi.WifiConfiguration
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
|
||||||
@Deprecated("No longer usable since API 26.")
|
|
||||||
object WifiApManager {
|
object WifiApManager {
|
||||||
private val wifi = app.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
|
||||||
private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||||
WifiConfiguration::class.java, Boolean::class.java)
|
WifiConfiguration::class.java, Boolean::class.java)
|
||||||
/**
|
/**
|
||||||
@@ -23,12 +20,14 @@ object WifiApManager {
|
|||||||
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
|
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
|
||||||
setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean
|
setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean
|
||||||
|
|
||||||
|
@Deprecated("No longer usable since API 26.")
|
||||||
fun start(wifiConfig: WifiConfiguration? = null) {
|
fun start(wifiConfig: WifiConfiguration? = null) {
|
||||||
wifi.isWifiEnabled = false
|
app.wifi.isWifiEnabled = false
|
||||||
wifi.setWifiApEnabled(wifiConfig, true)
|
app.wifi.setWifiApEnabled(wifiConfig, true)
|
||||||
}
|
}
|
||||||
|
@Deprecated("No longer usable since API 26.")
|
||||||
fun stop() {
|
fun stop() {
|
||||||
wifi.setWifiApEnabled(null, false)
|
app.wifi.setWifiApEnabled(null, false)
|
||||||
wifi.isWifiEnabled = true
|
app.wifi.isWifiEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{data.iface.name}"
|
android:text="@{data.title}"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||||
android:textIsSelectable="true"
|
android:textIsSelectable="true"
|
||||||
tools:text="wlan0"/>
|
tools:text="wlan0"/>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@{data.iface.addresses}"
|
android:text="@{data.text}"
|
||||||
tools:text="192.168.43.1/24\n01:23:45:ab:cd:ef"/>
|
tools:text="192.168.43.1/24\n01:23:45:ab:cd:ef"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<string name="title_repeater">无线中继</string>
|
<string name="title_repeater">无线中继</string>
|
||||||
<string name="title_tethering">系统共享</string>
|
<string name="title_tethering">系统共享</string>
|
||||||
<string name="title_settings">设置选项</string>
|
<string name="title_settings">设置选项</string>
|
||||||
|
<string name="service_inactive">未打开</string>
|
||||||
|
|
||||||
<string name="repeater_addresses">中继地址</string>
|
<string name="repeater_addresses">中继地址</string>
|
||||||
<string name="repeater_wps_dialog_title">输入 PIN</string>
|
<string name="repeater_wps_dialog_title">输入 PIN</string>
|
||||||
@@ -17,7 +18,6 @@
|
|||||||
<string name="repeater_reset_credentials_success">凭据已重置。</string>
|
<string name="repeater_reset_credentials_success">凭据已重置。</string>
|
||||||
<string name="repeater_reset_credentials_failure">重置凭据失败(原因:%s)</string>
|
<string name="repeater_reset_credentials_failure">重置凭据失败(原因:%s)</string>
|
||||||
|
|
||||||
<string name="repeater_inactive">未打开</string>
|
|
||||||
<string name="repeater_p2p_unavailable">Wi\u2011Fi 直连不可用</string>
|
<string name="repeater_p2p_unavailable">Wi\u2011Fi 直连不可用</string>
|
||||||
<string name="repeater_create_group_failure">创建 P2P 群组失败(原因:%s)</string>
|
<string name="repeater_create_group_failure">创建 P2P 群组失败(原因:%s)</string>
|
||||||
<string name="repeater_remove_group_failure">关闭已有 P2P 群组失败(原因:%s)</string>
|
<string name="repeater_remove_group_failure">关闭已有 P2P 群组失败(原因:%s)</string>
|
||||||
@@ -28,7 +28,13 @@
|
|||||||
<string name="repeater_failure_reason_p2p_unsupported">设备不支持 Wi\u2011Fi 直连</string>
|
<string name="repeater_failure_reason_p2p_unsupported">设备不支持 Wi\u2011Fi 直连</string>
|
||||||
<string name="repeater_failure_reason_busy">系统忙</string>
|
<string name="repeater_failure_reason_busy">系统忙</string>
|
||||||
<string name="repeater_failure_reason_no_service_requests">未添加服务请求</string>
|
<string name="repeater_failure_reason_no_service_requests">未添加服务请求</string>
|
||||||
<string name="repeater_failure_reason_unknown">未知 #%d</string>
|
|
||||||
|
<string name="tethering_temp_hotspot">临时 WLAN 热点</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure">打开热点失败 (原因:%s)</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_no_channel">无频段</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_generic">通用错误</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_incompatible_mode">模式不兼容</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_tethering_disallowed">共享被禁用</string>
|
||||||
|
|
||||||
<string name="tethering_manage">管理…</string>
|
<string name="tethering_manage">管理…</string>
|
||||||
<!--
|
<!--
|
||||||
@@ -54,7 +60,7 @@
|
|||||||
<string name="settings_service_repeater_oc">Wi\u2011Fi 运行频段 (不稳定)</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_repeater_oc_summary">"自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)"</string>
|
||||||
<string name="settings_service_repeater_strict">严格模式</string>
|
<string name="settings_service_repeater_strict">严格模式</string>
|
||||||
<string name="settings_service_repeater_strict_summary">只允许通过 VPN 隧道的包通过</string>
|
<string name="settings_service_repeater_strict_summary">只允许通过 VPN 隧道的包通过,也适用于临时热点</string>
|
||||||
<string name="settings_service_dns">备用 DNS 服务器[:端口]</string>
|
<string name="settings_service_dns">备用 DNS 服务器[:端口]</string>
|
||||||
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
||||||
<string name="settings_misc">杂项</string>
|
<string name="settings_misc">杂项</string>
|
||||||
@@ -72,6 +78,7 @@
|
|||||||
<item quantity="other">%d 个接口</item>
|
<item quantity="other">%d 个接口</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<string name="failure_reason_unknown">未知 #%d</string>
|
||||||
<string name="exception_interface_not_found">错误:未找到下游接口</string>
|
<string name="exception_interface_not_found">错误:未找到下游接口</string>
|
||||||
<string name="root_unavailable">似乎没有 root</string>
|
<string name="root_unavailable">似乎没有 root</string>
|
||||||
<string name="noisy_su_failure">发生异常,详情请查看调试信息。</string>
|
<string name="noisy_su_failure">发生异常,详情请查看调试信息。</string>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<string name="title_repeater">Repeater</string>
|
<string name="title_repeater">Repeater</string>
|
||||||
<string name="title_tethering">Tethering</string>
|
<string name="title_tethering">Tethering</string>
|
||||||
<string name="title_settings">Settings</string>
|
<string name="title_settings">Settings</string>
|
||||||
|
<string name="service_inactive">Service inactive</string>
|
||||||
|
|
||||||
<string name="repeater_addresses">Addresses</string>
|
<string name="repeater_addresses">Addresses</string>
|
||||||
<string name="repeater_wps">WPS</string>
|
<string name="repeater_wps">WPS</string>
|
||||||
@@ -28,7 +29,6 @@
|
|||||||
<string name="repeater_reset_credentials_success">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_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
|
||||||
|
|
||||||
<string name="repeater_inactive">Service inactive</string>
|
|
||||||
<string name="repeater_p2p_unavailable">Wi\u2011Fi direct unavailable</string>
|
<string name="repeater_p2p_unavailable">Wi\u2011Fi direct unavailable</string>
|
||||||
<string name="repeater_create_group_failure">Failed to create P2P group (reason: %s)</string>
|
<string name="repeater_create_group_failure">Failed to create P2P group (reason: %s)</string>
|
||||||
<string name="repeater_remove_group_failure">Failed to remove P2P group (reason: %s)</string>
|
<string name="repeater_remove_group_failure">Failed to remove P2P group (reason: %s)</string>
|
||||||
@@ -39,7 +39,13 @@
|
|||||||
<string name="repeater_failure_reason_p2p_unsupported">Wi\u2011Fi direct unsupported</string>
|
<string name="repeater_failure_reason_p2p_unsupported">Wi\u2011Fi direct unsupported</string>
|
||||||
<string name="repeater_failure_reason_busy">framework is busy</string>
|
<string name="repeater_failure_reason_busy">framework is busy</string>
|
||||||
<string name="repeater_failure_reason_no_service_requests">no service requests added</string>
|
<string name="repeater_failure_reason_no_service_requests">no service requests added</string>
|
||||||
<string name="repeater_failure_reason_unknown">unknown #%d</string>
|
|
||||||
|
<string name="tethering_temp_hotspot">Temporary Wi\u2011Fi hotspot</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure">Failed to start hotspot (reason: %s)</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_no_channel">no channel</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_generic">generic error</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_incompatible_mode">incompatible mode</string>
|
||||||
|
<string name="tethering_temp_hotspot_failure_tethering_disallowed">tethering disallowed</string>
|
||||||
|
|
||||||
<string name="tethering_manage">Manage…</string>
|
<string name="tethering_manage">Manage…</string>
|
||||||
<string name="tethering_manage_usb">USB tethering</string>
|
<string name="tethering_manage_usb">USB tethering</string>
|
||||||
@@ -57,7 +63,8 @@
|
|||||||
<string name="settings_service_repeater_oc">Operating Wi\u2011Fi channel (unstable)</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_repeater_oc_summary">Auto (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)</string>
|
||||||
<string name="settings_service_repeater_strict">Strict mode</string>
|
<string name="settings_service_repeater_strict">Strict mode</string>
|
||||||
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel</string>
|
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel, also
|
||||||
|
applies to temporary Wi\u2011Fi hotspot.</string>
|
||||||
<string name="settings_service_dns">Fallback DNS server[:port]</string>
|
<string name="settings_service_dns">Fallback DNS server[:port]</string>
|
||||||
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
||||||
<string name="settings_misc">Misc</string>
|
<string name="settings_misc">Misc</string>
|
||||||
@@ -77,6 +84,7 @@
|
|||||||
<item quantity="other">%d interfaces</item>
|
<item quantity="other">%d interfaces</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<string name="failure_reason_unknown">unknown #%d</string>
|
||||||
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
|
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
|
||||||
<string name="root_unavailable">Root unavailable</string>
|
<string name="root_unavailable">Root unavailable</string>
|
||||||
<string name="noisy_su_failure">Something went wrong, please check the debug information.</string>
|
<string name="noisy_su_failure">Something went wrong, please check the debug information.</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user