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.WRITE_SETTINGS"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -53,6 +54,8 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".LocalOnlyHotspotService">
|
||||
</service>
|
||||
<service
|
||||
android:name=".RepeaterService"
|
||||
android:directBootAware="true">
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
@@ -43,6 +44,7 @@ class App : Application() {
|
||||
val handler = Handler()
|
||||
val pref: SharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(deviceContext) }
|
||||
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 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 {
|
||||
return try {
|
||||
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
|
||||
@@ -160,7 +160,8 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
||||
private var p2pInterface: String? = null
|
||||
private var tetheredInterfaces = emptySet<String>()
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet()
|
||||
tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet() +
|
||||
TetheringManager.getLocalOnlyTetheredIfaces(intent.extras)
|
||||
adapter.recreate()
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ import android.support.v4.content.LocalBroadcastManager
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
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.deletePersistentGroup
|
||||
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 java.lang.reflect.InvocationTargetException
|
||||
import java.net.InetAddress
|
||||
import java.net.SocketException
|
||||
|
||||
class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Callback,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
companion object {
|
||||
const val ACTION_STATUS_CHANGED = "be.mygod.vpnhotspot.RepeaterService.STATUS_CHANGED"
|
||||
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_NETWORK_INFO),
|
||||
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 upstream: String? = null
|
||||
private var dns: List<InetAddress> = emptyList()
|
||||
private var routing: Routing? = null
|
||||
private var routingManager: LocalOnlyInterfaceManager? = null
|
||||
|
||||
var status = Status.IDLE
|
||||
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.BUSY -> getString(R.string.repeater_failure_reason_busy)
|
||||
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() {
|
||||
@@ -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 {
|
||||
if (status != Status.IDLE) return START_NOT_STICKY
|
||||
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(
|
||||
loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
|
||||
when {
|
||||
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
|
||||
matcher.group(2) == "true" -> {
|
||||
unregisterReceiver()
|
||||
upstream = ifname
|
||||
this.dns = dns
|
||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
||||
LocalBroadcastManager.getInstance(this)
|
||||
.registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS))
|
||||
receiverRegistered = true
|
||||
p2pManager.requestGroupInfo(channel, {
|
||||
when {
|
||||
@@ -227,72 +198,40 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
||||
}
|
||||
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun resetup(routing: Routing, ifname: String? = null, dns: List<InetAddress> = emptyList()) =
|
||||
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!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
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) {
|
||||
debugLog(TAG, "P2P connection changed: $info\n$net\n$group")
|
||||
if (!info.groupFormed || !info.isGroupOwner || !group.isGroupOwner) {
|
||||
if (routing != null) clean() // P2P shutdown
|
||||
return
|
||||
}
|
||||
if (routing == null) try {
|
||||
if (initRouting(upstream, group.`interface` ?: return, info.groupOwnerAddress ?: return, dns))
|
||||
doStart(group)
|
||||
} catch (e: SocketException) {
|
||||
startFailure(e.message, group)
|
||||
return
|
||||
} else showNotification(group)
|
||||
this.group = group
|
||||
if (routingManager != null) clean() // P2P shutdown
|
||||
} else if (routingManager != null) {
|
||||
this.group = group
|
||||
showNotification(group)
|
||||
} else doStart(group, info.groupOwnerAddress)
|
||||
}
|
||||
private fun initRouting(upstream: String?, downstream: String,
|
||||
owner: InetAddress, dns: List<InetAddress>): Boolean {
|
||||
val routing = Routing(upstream, downstream, owner)
|
||||
this.routing = routing
|
||||
this.dns = dns
|
||||
val strict = app.pref.getBoolean("service.repeater.strict", false)
|
||||
return if (strict && upstream == null || // in this case, nothing to be done
|
||||
routing.ipForward() // Wi-Fi direct doesn't enable ip_forward
|
||||
.rule().forward(strict).masquerade(strict).dnsRedirect(dns).start()) true else {
|
||||
routing.stop()
|
||||
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||
false
|
||||
}
|
||||
/**
|
||||
* startService Step 3
|
||||
*/
|
||||
private fun doStart(group: WifiP2pGroup, ownerAddress: InetAddress? = null) {
|
||||
this.group = group
|
||||
routingManager = LocalOnlyInterfaceManager(group.`interface`!!, ownerAddress)
|
||||
status = Status.ACTIVE
|
||||
showNotification(group)
|
||||
}
|
||||
private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
|
||||
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
||||
showNotification()
|
||||
if (group != null) removeGroup() else clean()
|
||||
}
|
||||
|
||||
private fun showNotification(group: WifiP2pGroup? = null) = ServiceNotification.startForeground(this,
|
||||
@@ -314,16 +253,13 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
||||
private fun unregisterReceiver() {
|
||||
if (receiverRegistered) {
|
||||
unregisterReceiver(receiver)
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
||||
receiverRegistered = false
|
||||
}
|
||||
}
|
||||
private fun clean() {
|
||||
VpnMonitor.unregisterCallback(this)
|
||||
unregisterReceiver()
|
||||
if (routing?.stop() == false)
|
||||
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||
routing = null
|
||||
routingManager?.stop()
|
||||
routingManager = null
|
||||
status = Status.IDLE
|
||||
ServiceNotification.stopForeground(this)
|
||||
stopSelf()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.databinding.BaseObservable
|
||||
import android.databinding.Bindable
|
||||
import android.databinding.DataBindingUtil
|
||||
@@ -39,12 +42,15 @@ import java.util.*
|
||||
class TetheringFragment : Fragment(), ServiceConnection {
|
||||
companion object {
|
||||
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_WIFI = 2
|
||||
private const val VIEW_TYPE_USB = 3
|
||||
private const val VIEW_TYPE_BLUETOOTH = 4
|
||||
private const val VIEW_TYPE_WIFI_LEGACY = 5
|
||||
|
||||
private const val START_LOCAL_ONLY_HOTSPOT = 1
|
||||
|
||||
/**
|
||||
* PAN Profile
|
||||
* From BluetoothProfile.java.
|
||||
@@ -55,26 +61,63 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
}
|
||||
|
||||
inner class Data(val iface: TetheredInterface) : BaseObservable() {
|
||||
val icon: Int get() = TetherType.ofInterface(iface.name).icon
|
||||
val active = binder?.isActive(iface.name) == true
|
||||
interface Data {
|
||||
val icon: Int
|
||||
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),
|
||||
View.OnClickListener {
|
||||
private open class InterfaceViewHolder(val binding: ListitemInterfaceBinding) :
|
||||
RecyclerView.ViewHolder(binding.root), View.OnClickListener {
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val context = itemView.context
|
||||
val data = binding.data!!
|
||||
val data = binding.data as TetheredData
|
||||
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, data.iface.name))
|
||||
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
|
||||
.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 {
|
||||
init {
|
||||
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>()
|
||||
@Bindable get
|
||||
set(value) {
|
||||
@@ -208,29 +251,40 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
inner class TetheringAdapter :
|
||||
ListAdapter<TetheredInterface, RecyclerView.ViewHolder>(TetheredInterface.DiffCallback) {
|
||||
fun update(data: Set<String>) {
|
||||
val lookup = try {
|
||||
private var lookup: Map<String, NetworkInterface> = emptyMap()
|
||||
|
||||
fun update(activeIfaces: List<String>, localOnlyIfaces: List<String>) {
|
||||
lookup = try {
|
||||
NetworkInterface.getNetworkInterfaces().asSequence().associateBy { it.name }
|
||||
} catch (e: SocketException) {
|
||||
e.printStackTrace()
|
||||
emptyMap<String, NetworkInterface>()
|
||||
emptyMap()
|
||||
}
|
||||
this@TetheringFragment.tetherListener.enabledTypes = data.map { TetherType.ofInterface(it) }.toSet()
|
||||
submitList(data.map { TetheredInterface(it, lookup) }.sorted())
|
||||
this@TetheringFragment.tetherListener.enabledTypes =
|
||||
(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) {
|
||||
in 0 until 24 -> 2
|
||||
in 24..25 -> 5
|
||||
else -> 4
|
||||
}
|
||||
override fun getItemViewType(position: Int) = when (position - super.getItemCount()) {
|
||||
0 -> VIEW_TYPE_MANAGE
|
||||
1 -> if (Build.VERSION.SDK_INT >= 24) VIEW_TYPE_USB else VIEW_TYPE_WIFI_LEGACY
|
||||
2 -> VIEW_TYPE_WIFI
|
||||
3 -> VIEW_TYPE_BLUETOOTH
|
||||
4 -> VIEW_TYPE_WIFI_LEGACY
|
||||
else -> VIEW_TYPE_INTERFACE
|
||||
override fun getItemCount() = super.getItemCount() + if (Build.VERSION.SDK_INT < 24) 2 else 5
|
||||
override fun getItemViewType(position: Int) = if (Build.VERSION.SDK_INT < 26) {
|
||||
when (position - super.getItemCount()) {
|
||||
0 -> VIEW_TYPE_MANAGE
|
||||
1 -> if (Build.VERSION.SDK_INT >= 24) VIEW_TYPE_USB else VIEW_TYPE_WIFI_LEGACY
|
||||
2 -> VIEW_TYPE_WIFI
|
||||
3 -> VIEW_TYPE_BLUETOOTH
|
||||
4 -> VIEW_TYPE_WIFI_LEGACY
|
||||
else -> VIEW_TYPE_INTERFACE
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
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_WIFI, VIEW_TYPE_USB, VIEW_TYPE_BLUETOOTH, VIEW_TYPE_WIFI_LEGACY ->
|
||||
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")
|
||||
}
|
||||
}
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
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 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()
|
||||
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? {
|
||||
@@ -269,14 +334,23 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val context = requireContext()
|
||||
context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
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() {
|
||||
val context = requireContext()
|
||||
context.unbindService(this)
|
||||
context.unregisterReceiver(receiver)
|
||||
requireContext().unbindService(this)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -285,14 +359,34 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val binder = service as TetheringService.TetheringBinder
|
||||
this.binder = binder
|
||||
binder.fragment = this
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) = when (service) {
|
||||
is TetheringService.TetheringBinder -> {
|
||||
tetheringBinder = service
|
||||
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?) {
|
||||
binder?.fragment = null
|
||||
binder = null
|
||||
val context = requireContext()
|
||||
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
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.widget.Toast
|
||||
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.SocketException
|
||||
|
||||
class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Callback {
|
||||
class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
||||
companion object {
|
||||
const val EXTRA_ADD_INTERFACE = "interface.add"
|
||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||
@@ -24,7 +26,6 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
||||
|
||||
private val binder = TetheringBinder()
|
||||
private val routings = HashMap<String, Routing?>()
|
||||
private var neighbours = emptyList<IpNeighbour>()
|
||||
private var upstream: String? = null
|
||||
private var dns: List<InetAddress> = emptyList()
|
||||
private var receiverRegistered = false
|
||||
@@ -32,9 +33,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
||||
synchronized(routings) {
|
||||
when (intent.action) {
|
||||
TetheringManager.ACTION_TETHER_STATE_CHANGED -> {
|
||||
val remove = routings.keys - TetheringManager.getTetheredIfaces(intent.extras)
|
||||
if (remove.isEmpty()) return@broadcastReceiver
|
||||
val failed = remove.any { routings.remove(it)?.stop() == false }
|
||||
val failed = (routings.keys - TetheringManager.getTetheredIfaces(intent.extras))
|
||||
.any { routings.remove(it)?.stop() == false }
|
||||
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
|
||||
@@ -42,13 +42,10 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
||||
updateRoutingsLocked()
|
||||
}
|
||||
}
|
||||
override val activeIfaces get() = synchronized(routings) { routings.keys.toList() }
|
||||
|
||||
private fun updateRoutingsLocked() {
|
||||
if (routings.isEmpty()) {
|
||||
unregisterReceiver()
|
||||
ServiceNotification.stopForeground(this)
|
||||
stopSelf()
|
||||
} else {
|
||||
fun updateRoutingsLocked() {
|
||||
if (routings.isNotEmpty()) {
|
||||
val upstream = upstream
|
||||
if (upstream != null) {
|
||||
var failed = false
|
||||
@@ -56,13 +53,11 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
||||
// system tethering already has working forwarding rules
|
||||
// so it doesn't make sense to add additional forwarding rules
|
||||
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
|
||||
if (routing.start()) routings[downstream] = routing else {
|
||||
failed = true
|
||||
routing.stop()
|
||||
routings.remove(downstream)
|
||||
}
|
||||
routings[downstream] = routing
|
||||
if (!routing.start()) failed = true
|
||||
} catch (e: SocketException) {
|
||||
e.printStackTrace()
|
||||
routings.remove(downstream)
|
||||
failed = true
|
||||
}
|
||||
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()
|
||||
}
|
||||
if (routings.isEmpty()) {
|
||||
unregisterReceiver()
|
||||
ServiceNotification.stopForeground(this)
|
||||
stopSelf()
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
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() {
|
||||
unregisterReceiver()
|
||||
super.onDestroy()
|
||||
|
||||
@@ -113,7 +113,8 @@ class Routing(val upstream: String?, val downstream: String, ownerAddress: InetA
|
||||
fun start(): Boolean {
|
||||
if (started) return true
|
||||
started = true
|
||||
return noisySu(startScript) == true
|
||||
if (noisySu(startScript) != true) stop()
|
||||
return started
|
||||
}
|
||||
fun stop(): Boolean {
|
||||
if (!started) return true
|
||||
|
||||
@@ -125,7 +125,8 @@ object TetheringManager {
|
||||
stopTethering.invoke(app.connectivity, type)
|
||||
}
|
||||
|
||||
fun getTetheredIfaces(extras: Bundle) = if (Build.VERSION.SDK_INT >= 26)
|
||||
extras.getStringArrayList(EXTRA_ACTIVE_TETHER).toSet() + extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY)
|
||||
else extras.getStringArrayList(EXTRA_ACTIVE_TETHER_LEGACY).toSet()
|
||||
fun getTetheredIfaces(extras: Bundle) = extras.getStringArrayList(
|
||||
if (Build.VERSION.SDK_INT >= 26) EXTRA_ACTIVE_TETHER else EXTRA_ACTIVE_TETHER_LEGACY)
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
|
||||
@Deprecated("No longer usable since API 26.")
|
||||
object WifiApManager {
|
||||
private val wifi = app.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled",
|
||||
WifiConfiguration::class.java, Boolean::class.java)
|
||||
/**
|
||||
@@ -23,12 +20,14 @@ object WifiApManager {
|
||||
private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) =
|
||||
setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean
|
||||
|
||||
@Deprecated("No longer usable since API 26.")
|
||||
fun start(wifiConfig: WifiConfiguration? = null) {
|
||||
wifi.isWifiEnabled = false
|
||||
wifi.setWifiApEnabled(wifiConfig, true)
|
||||
app.wifi.isWifiEnabled = false
|
||||
app.wifi.setWifiApEnabled(wifiConfig, true)
|
||||
}
|
||||
@Deprecated("No longer usable since API 26.")
|
||||
fun stop() {
|
||||
wifi.setWifiApEnabled(null, false)
|
||||
wifi.isWifiEnabled = true
|
||||
app.wifi.setWifiApEnabled(null, false)
|
||||
app.wifi.isWifiEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{data.iface.name}"
|
||||
android:text="@{data.title}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="wlan0"/>
|
||||
@@ -43,7 +43,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
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"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<string name="title_repeater">无线中继</string>
|
||||
<string name="title_tethering">系统共享</string>
|
||||
<string name="title_settings">设置选项</string>
|
||||
<string name="service_inactive">未打开</string>
|
||||
|
||||
<string name="repeater_addresses">中继地址</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_failure">重置凭据失败(原因:%s)</string>
|
||||
|
||||
<string name="repeater_inactive">未打开</string>
|
||||
<string name="repeater_p2p_unavailable">Wi\u2011Fi 直连不可用</string>
|
||||
<string name="repeater_create_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_busy">系统忙</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>
|
||||
<!--
|
||||
@@ -54,7 +60,7 @@
|
||||
<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_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_clean">清理/重新应用路由规则</string>
|
||||
<string name="settings_misc">杂项</string>
|
||||
@@ -72,6 +78,7 @@
|
||||
<item quantity="other">%d 个接口</item>
|
||||
</plurals>
|
||||
|
||||
<string name="failure_reason_unknown">未知 #%d</string>
|
||||
<string name="exception_interface_not_found">错误:未找到下游接口</string>
|
||||
<string name="root_unavailable">似乎没有 root</string>
|
||||
<string name="noisy_su_failure">发生异常,详情请查看调试信息。</string>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<string name="title_repeater">Repeater</string>
|
||||
<string name="title_tethering">Tethering</string>
|
||||
<string name="title_settings">Settings</string>
|
||||
<string name="service_inactive">Service inactive</string>
|
||||
|
||||
<string name="repeater_addresses">Addresses</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_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_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>
|
||||
@@ -39,7 +39,13 @@
|
||||
<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_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_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_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_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_clean">Clean/reapply routing rules</string>
|
||||
<string name="settings_misc">Misc</string>
|
||||
@@ -77,6 +84,7 @@
|
||||
<item quantity="other">%d interfaces</item>
|
||||
</plurals>
|
||||
|
||||
<string name="failure_reason_unknown">unknown #%d</string>
|
||||
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
|
||||
<string name="root_unavailable">Root unavailable</string>
|
||||
<string name="noisy_su_failure">Something went wrong, please check the debug information.</string>
|
||||
|
||||
Reference in New Issue
Block a user