@@ -65,21 +65,79 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".LocalOnlyHotspotService"/>
|
||||
<service
|
||||
android:name=".LocalOnlyHotspotService"
|
||||
android:directBootAware="true"/>
|
||||
<service
|
||||
android:name=".RepeaterService"
|
||||
android:directBootAware="true"/>
|
||||
<service android:name=".TetheringService"/>
|
||||
<service
|
||||
android:name=".RepeaterTileService"
|
||||
android:name=".TetheringService"
|
||||
android:directBootAware="true"/>
|
||||
|
||||
<service
|
||||
android:name=".manage.RepeaterTileService"
|
||||
android:directBootAware="true"
|
||||
android:icon="@drawable/ic_quick_settings_tile_off"
|
||||
android:icon="@drawable/ic_action_settings_input_antenna"
|
||||
android:label="@string/title_repeater"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".manage.LocalOnlyHotspotTileService"
|
||||
android:directBootAware="true"
|
||||
android:enabled="@bool/api_ge_26"
|
||||
android:icon="@drawable/ic_device_wifi_tethering"
|
||||
android:label="@string/tethering_temp_hotspot"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".manage.TetheringTileService$Wifi"
|
||||
android:directBootAware="true"
|
||||
android:icon="@drawable/ic_device_wifi_tethering"
|
||||
android:label="@string/tethering_manage_wifi"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".manage.TetheringTileService$Usb"
|
||||
android:directBootAware="true"
|
||||
android:icon="@drawable/ic_device_usb"
|
||||
android:label="@string/tethering_manage_usb"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".manage.TetheringTileService$Bluetooth"
|
||||
android:directBootAware="true"
|
||||
android:icon="@drawable/ic_device_bluetooth"
|
||||
android:label="@string/tethering_manage_bluetooth"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<!--suppress DeprecatedClassUsageInspection -->
|
||||
<service
|
||||
android:name=".manage.TetheringTileService$WifiLegacy"
|
||||
android:directBootAware="true"
|
||||
android:enabled="@bool/api_lt_26"
|
||||
android:icon="@drawable/ic_device_wifi_tethering"
|
||||
android:label="@string/tethering_manage_wifi_legacy"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||
|
||||
@RequiresApi(24)
|
||||
class RepeaterTileService : TileService(), ServiceConnection {
|
||||
private val tileOff by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_off) }
|
||||
private val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
|
||||
|
||||
private var binder: RepeaterService.Binder? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
if (RepeaterService.supported) {
|
||||
bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
if (RepeaterService.supported) stopAndUnbind(this)
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
val binder = binder
|
||||
when (binder?.service?.status) {
|
||||
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
||||
RepeaterService.Status.IDLE ->
|
||||
ContextCompat.startForegroundService(this, Intent(this, RepeaterService::class.java))
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||
val binder = service as RepeaterService.Binder
|
||||
this.binder = binder
|
||||
binder.statusChanged[this] = { updateTile() }
|
||||
binder.groupChanged[this] = this::updateTile
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
val binder = binder ?: return
|
||||
this.binder = null
|
||||
binder.statusChanged -= this
|
||||
binder.groupChanged -= this
|
||||
}
|
||||
|
||||
private fun updateTile(group: WifiP2pGroup? = binder?.group) {
|
||||
val qsTile = qsTile ?: return
|
||||
when (binder?.service?.status) {
|
||||
RepeaterService.Status.IDLE -> {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.icon = tileOff
|
||||
qsTile.label = getString(R.string.title_repeater)
|
||||
}
|
||||
RepeaterService.Status.STARTING -> {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.icon = tileOn
|
||||
qsTile.label = getString(R.string.title_repeater)
|
||||
}
|
||||
RepeaterService.Status.ACTIVE -> {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.icon = tileOn
|
||||
qsTile.label = group?.networkName
|
||||
}
|
||||
else -> { // null or DESTROYED, which should never occur
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.icon = tileOff
|
||||
qsTile.label = getString(R.string.title_repeater)
|
||||
}
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
@@ -3,26 +3,26 @@ package be.mygod.vpnhotspot
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||
import be.mygod.vpnhotspot.util.Event0
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import timber.log.Timber
|
||||
|
||||
class TetheringService : IpNeighbourMonitoringService() {
|
||||
companion object {
|
||||
const val EXTRA_ADD_INTERFACE = "interface.add"
|
||||
const val EXTRA_ADD_INTERFACES = "interface.add"
|
||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||
}
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
var fragment: TetheringFragment? = null
|
||||
val routingsChanged = Event0()
|
||||
|
||||
fun isActive(iface: String): Boolean = synchronized(routings) { routings.keys.contains(iface) }
|
||||
fun isActive(iface: String): Boolean = synchronized(routings) { routings.containsKey(iface) }
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
@@ -90,16 +90,16 @@ class TetheringService : IpNeighbourMonitoringService() {
|
||||
}
|
||||
updateNotification()
|
||||
}
|
||||
app.handler.post { binder.fragment?.adapter?.notifyDataSetChanged() }
|
||||
app.handler.post { binder.routingsChanged() }
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?) = binder
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent != null) {
|
||||
val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE)
|
||||
val ifaces = intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()
|
||||
synchronized(routings) {
|
||||
if (iface != null) {
|
||||
for (iface in ifaces) {
|
||||
routings[iface] = null
|
||||
if (TetherType.ofInterface(iface).isWifi && !locked) {
|
||||
WifiDoubleLock.acquire()
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.Context
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import timber.log.Timber
|
||||
|
||||
class BluetoothTethering(context: Context) : BluetoothProfile.ServiceListener, AutoCloseable {
|
||||
companion object {
|
||||
/**
|
||||
* PAN Profile
|
||||
* From BluetoothProfile.java.
|
||||
*/
|
||||
private const val PAN = 5
|
||||
private val isTetheringOn by lazy @SuppressLint("PrivateApi") {
|
||||
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn")
|
||||
}
|
||||
}
|
||||
|
||||
private val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||
private var pan: BluetoothProfile? = null
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
|
||||
*/
|
||||
val active: Boolean get() {
|
||||
val pan = pan
|
||||
return adapter?.state == BluetoothAdapter.STATE_ON && pan != null && isTetheringOn.invoke(pan) as Boolean
|
||||
}
|
||||
|
||||
init {
|
||||
try {
|
||||
adapter?.getProfileProxy(context, this, PAN)
|
||||
} catch (e: SecurityException) {
|
||||
Timber.w(e)
|
||||
SmartSnackbar.make(e).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
pan = null
|
||||
}
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
pan = proxy
|
||||
}
|
||||
override fun close() {
|
||||
adapter?.closeProfileProxy(PAN, pan)
|
||||
pan = null
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,14 @@ class InterfaceManager(private val parent: TetheringFragment, val iface: String)
|
||||
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
|
||||
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACE, iface))
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, arrayOf(iface)))
|
||||
}
|
||||
}
|
||||
private inner class Data : be.mygod.vpnhotspot.manage.Data() {
|
||||
override val icon get() = TetherType.ofInterface(iface).icon
|
||||
override val title get() = iface
|
||||
override val text get() = addresses
|
||||
override val active get() = parent.tetheringBinder?.isActive(iface) == true
|
||||
override val active get() = parent.binder?.isActive(iface) == true
|
||||
}
|
||||
|
||||
val addresses = parent.ifaceLookup[iface]?.formatAddresses() ?: ""
|
||||
|
||||
@@ -54,12 +54,12 @@ class LocalOnlyHotspotManager(private val parent: TetheringFragment) : Manager()
|
||||
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/53e0284/service/java/com/android/server/wifi/WifiSettingsStore.java#228
|
||||
*/
|
||||
if (if (Build.VERSION.SDK_INT < 28) @Suppress("DEPRECATION") {
|
||||
Settings.Secure.getInt(view.context.contentResolver, Settings.Secure.LOCATION_MODE,
|
||||
Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE,
|
||||
Settings.Secure.LOCATION_MODE_OFF) == Settings.Secure.LOCATION_MODE_OFF
|
||||
} else context.getSystemService<LocationManager>()?.isLocationEnabled != true) {
|
||||
Toast.makeText(view.context, R.string.tethering_temp_hotspot_location, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(context, R.string.tethering_temp_hotspot_location, Toast.LENGTH_LONG).show()
|
||||
try {
|
||||
view.context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.w(e)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import be.mygod.vpnhotspot.LocalOnlyHotspotService
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||
|
||||
@RequiresApi(26)
|
||||
class LocalOnlyHotspotTileService : TetherListeningTileService(), ServiceConnection {
|
||||
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_device_wifi_tethering) }
|
||||
private var binder: LocalOnlyHotspotService.Binder? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
bindService(Intent(this, LocalOnlyHotspotService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
stopAndUnbind(this)
|
||||
super.onStopListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
val binder = binder
|
||||
if (binder?.iface != null) binder.stop()
|
||||
else ContextCompat.startForegroundService(this, Intent(this, LocalOnlyHotspotService::class.java))
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||
binder = service as LocalOnlyHotspotService.Binder
|
||||
updateTile()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
binder = null
|
||||
}
|
||||
|
||||
override fun updateTile() {
|
||||
qsTile?.run {
|
||||
state = if (binder?.iface == null) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE
|
||||
icon = tile
|
||||
label = getText(R.string.tethering_temp_hotspot)
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||
|
||||
@RequiresApi(24)
|
||||
class RepeaterTileService : TileService(), ServiceConnection {
|
||||
private val tile by lazy { Icon.createWithResource(application, R.drawable.ic_action_settings_input_antenna) }
|
||||
|
||||
private var binder: RepeaterService.Binder? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
if (!RepeaterService.supported) updateTile()
|
||||
else bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
if (RepeaterService.supported) stopAndUnbind(this)
|
||||
super.onStopListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
val binder = binder
|
||||
when (binder?.service?.status) {
|
||||
RepeaterService.Status.ACTIVE -> binder.shutdown()
|
||||
RepeaterService.Status.IDLE ->
|
||||
ContextCompat.startForegroundService(this, Intent(this, RepeaterService::class.java))
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||
binder = service as RepeaterService.Binder
|
||||
service.statusChanged[this] = { updateTile() }
|
||||
service.groupChanged[this] = this::updateTile
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
val binder = binder ?: return
|
||||
this.binder = null
|
||||
binder.statusChanged -= this
|
||||
binder.groupChanged -= this
|
||||
}
|
||||
|
||||
private fun updateTile(group: WifiP2pGroup? = binder?.group) {
|
||||
qsTile?.run {
|
||||
when (binder?.service?.status) {
|
||||
RepeaterService.Status.IDLE -> {
|
||||
state = Tile.STATE_INACTIVE
|
||||
label = getText(R.string.title_repeater)
|
||||
}
|
||||
RepeaterService.Status.ACTIVE -> {
|
||||
state = Tile.STATE_ACTIVE
|
||||
label = group?.networkName
|
||||
}
|
||||
else -> { // STARTING, null or DESTROYED, which should never occur
|
||||
state = Tile.STATE_UNAVAILABLE
|
||||
label = getText(R.string.title_repeater)
|
||||
}
|
||||
}
|
||||
icon = tile
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.content.IntentFilter
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
|
||||
@RequiresApi(24)
|
||||
abstract class TetherListeningTileService : TileService() {
|
||||
protected var tethered: List<String> = emptyList()
|
||||
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
tethered = TetheringManager.getTetheredIfaces(intent.extras ?: return@broadcastReceiver)
|
||||
updateTile()
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
unregisterReceiver(receiver)
|
||||
super.onStopListening()
|
||||
}
|
||||
|
||||
protected abstract fun updateTile()
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@@ -95,8 +92,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||
protected abstract fun stop()
|
||||
|
||||
override fun onTetheringStarted() = data.notifyChange()
|
||||
override fun onTetheringFailed() =
|
||||
SmartSnackbar.make(R.string.tethering_manage_failed).show()
|
||||
override fun onTetheringFailed() = SmartSnackbar.make(R.string.tethering_manage_failed).show()
|
||||
|
||||
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
||||
(viewHolder as ViewHolder).manager = this
|
||||
@@ -148,54 +144,20 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||
override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_USB)
|
||||
}
|
||||
@RequiresApi(24)
|
||||
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), LifecycleObserver,
|
||||
BluetoothProfile.ServiceListener {
|
||||
companion object {
|
||||
/**
|
||||
* PAN Profile
|
||||
* From BluetoothProfile.java.
|
||||
*/
|
||||
private const val PAN = 5
|
||||
private val isTetheringOn by lazy @SuppressLint("PrivateApi") {
|
||||
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn")
|
||||
}
|
||||
}
|
||||
|
||||
private var pan: BluetoothProfile? = null
|
||||
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), LifecycleObserver {
|
||||
private val tethering = BluetoothTethering(parent.requireContext())
|
||||
|
||||
init {
|
||||
parent.lifecycle.addObserver(this)
|
||||
try {
|
||||
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(parent.requireContext(), this, PAN)
|
||||
} catch (e: SecurityException) {
|
||||
Timber.w(e)
|
||||
SmartSnackbar.make(e).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
pan = null
|
||||
}
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
pan = proxy
|
||||
}
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
fun onDestroy() {
|
||||
pan = null
|
||||
}
|
||||
fun onDestroy() = tethering.close()
|
||||
|
||||
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
|
||||
override val tetherType get() = TetherType.BLUETOOTH
|
||||
override val type get() = VIEW_TYPE_BLUETOOTH
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
|
||||
*/
|
||||
override val isStarted: Boolean
|
||||
get() {
|
||||
val pan = pan
|
||||
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && pan != null &&
|
||||
isTetheringOn.invoke(pan) as Boolean
|
||||
}
|
||||
override val isStarted get() = tethering.active
|
||||
|
||||
override fun start() = TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this)
|
||||
override fun stop() {
|
||||
|
||||
@@ -88,8 +88,8 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
|
||||
var enabledTypes = emptySet<TetherType>()
|
||||
private lateinit var binding: FragmentTetheringBinding
|
||||
var tetheringBinder: TetheringService.Binder? = null
|
||||
val adapter = ManagerAdapter()
|
||||
var binder: TetheringService.Binder? = null
|
||||
private val adapter = ManagerAdapter()
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
val extras = intent.extras ?: return@broadcastReceiver
|
||||
adapter.update(TetheringManager.getTetheredIfaces(extras),
|
||||
@@ -130,14 +130,14 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
tetheringBinder = service as TetheringService.Binder
|
||||
service.fragment = this
|
||||
binder = service as TetheringService.Binder
|
||||
service.routingsChanged[this] = { adapter.notifyDataSetChanged() }
|
||||
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
(tetheringBinder ?: return).fragment = null
|
||||
tetheringBinder = null
|
||||
(binder ?: return).routingsChanged -= this
|
||||
binder = null
|
||||
requireContext().unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package be.mygod.vpnhotspot.manage
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.TetheringService
|
||||
import be.mygod.vpnhotspot.net.TetherType
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
|
||||
@RequiresApi(24)
|
||||
sealed class TetheringTileService : TetherListeningTileService(), ServiceConnection,
|
||||
TetheringManager.OnStartTetheringCallback {
|
||||
protected val tileOff by lazy { Icon.createWithResource(application, icon) }
|
||||
protected val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
|
||||
|
||||
protected abstract val labelString: Int
|
||||
protected abstract val tetherType: TetherType
|
||||
protected open val icon get() = tetherType.icon
|
||||
protected val interested get() = tethered.filter { TetherType.ofInterface(it) == tetherType }
|
||||
protected var binder: TetheringService.Binder? = null
|
||||
|
||||
protected abstract fun start()
|
||||
protected abstract fun stop()
|
||||
|
||||
override fun onStartListening() {
|
||||
bindService(Intent(this, TetheringService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||
super.onStartListening()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
stopAndUnbind(this)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
binder = service as TetheringService.Binder
|
||||
service.routingsChanged[this] = { updateTile() }
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
binder = null
|
||||
}
|
||||
|
||||
override fun updateTile() {
|
||||
qsTile?.run {
|
||||
val interested = interested
|
||||
if (interested.isEmpty()) {
|
||||
state = Tile.STATE_INACTIVE
|
||||
icon = tileOff
|
||||
} else {
|
||||
state = Tile.STATE_ACTIVE
|
||||
icon = if (interested.all { binder?.isActive(it) == true }) tileOn else tileOff
|
||||
}
|
||||
label = getText(labelString)
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
protected inline fun safeInvoker(func: () -> Unit) = try {
|
||||
func()
|
||||
} catch (e: IOException) {
|
||||
Timber.w(e)
|
||||
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show()
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException !is SecurityException) Timber.w(e)
|
||||
var cause: Throwable? = e
|
||||
while (cause != null) {
|
||||
cause = cause.cause
|
||||
if (cause != null && cause !is InvocationTargetException) {
|
||||
Toast.makeText(this, cause.localizedMessage, Toast.LENGTH_LONG).show()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onClick() {
|
||||
val interested = interested
|
||||
if (interested.isEmpty()) safeInvoker { start() } else {
|
||||
val inactive = interested.filter { binder?.isActive(it) != true }
|
||||
if (inactive.isEmpty()) safeInvoker { stop() }
|
||||
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTetheringStarted() = updateTile()
|
||||
override fun onTetheringFailed() = SmartSnackbar.make(R.string.tethering_manage_failed).show()
|
||||
|
||||
class Wifi : TetheringTileService() {
|
||||
override val labelString get() = R.string.tethering_manage_wifi
|
||||
override val tetherType get() = TetherType.WIFI
|
||||
override val icon get() = R.drawable.ic_device_wifi_tethering
|
||||
|
||||
override fun start() = TetheringManager.start(TetheringManager.TETHERING_WIFI, true, this)
|
||||
override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_WIFI)
|
||||
}
|
||||
class Usb : TetheringTileService() {
|
||||
override val labelString get() = R.string.tethering_manage_usb
|
||||
override val tetherType get() = TetherType.USB
|
||||
|
||||
override fun start() = TetheringManager.start(TetheringManager.TETHERING_USB, true, this)
|
||||
override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_USB)
|
||||
}
|
||||
class Bluetooth : TetheringTileService() {
|
||||
private var tethering: BluetoothTethering? = null
|
||||
|
||||
override val labelString get() = R.string.tethering_manage_bluetooth
|
||||
override val tetherType get() = TetherType.BLUETOOTH
|
||||
|
||||
override fun start() = TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this)
|
||||
override fun stop() {
|
||||
TetheringManager.stop(TetheringManager.TETHERING_BLUETOOTH)
|
||||
Thread.sleep(1) // give others a room to breathe
|
||||
onTetheringStarted() // force flush state
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
tethering = BluetoothTethering(this)
|
||||
super.onStartListening()
|
||||
}
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
tethering?.close()
|
||||
tethering = null
|
||||
}
|
||||
|
||||
override fun updateTile() {
|
||||
qsTile?.run {
|
||||
if (tethering?.active == true) {
|
||||
state = Tile.STATE_ACTIVE
|
||||
val interested = interested
|
||||
icon = if (interested.isNotEmpty() && interested.all { binder?.isActive(it) == true })
|
||||
tileOn else tileOff
|
||||
} else {
|
||||
state = Tile.STATE_INACTIVE
|
||||
icon = tileOff
|
||||
}
|
||||
label = getText(labelString)
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() = if (tethering?.active == true) {
|
||||
val inactive = interested.filter { binder?.isActive(it) != true }
|
||||
if (inactive.isEmpty()) safeInvoker { stop() }
|
||||
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||
} else safeInvoker { start() }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26")
|
||||
class WifiLegacy : TetheringTileService() {
|
||||
override val labelString get() = R.string.tethering_manage_wifi_legacy
|
||||
override val tetherType get() = TetherType.WIFI
|
||||
override val icon get() = R.drawable.ic_device_wifi_tethering
|
||||
|
||||
override fun start() = WifiApManager.start()
|
||||
override fun stop() = WifiApManager.stop()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M14.36,10.22 A5.33,5.33,0,1,0,14.36,13.78 L18.22,13.78 L18.22,17.33 L21.78,17.33
|
||||
L21.78,13.78 L23.56,13.78 L23.56,10.22 Z M9.36,13.78 A1.78,1.78,0,1,1,11.11,12
|
||||
A1.78,1.78,0,0,1,9.33,13.78 Z" />
|
||||
</vector>
|
||||
5
mobile/src/main/res/values-v26/bools.xml
Normal file
5
mobile/src/main/res/values-v26/bools.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="api_ge_26">true</bool>
|
||||
<bool name="api_lt_26">false</bool>
|
||||
</resources>
|
||||
5
mobile/src/main/res/values/bools.xml
Normal file
5
mobile/src/main/res/values/bools.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="api_ge_26">false</bool>
|
||||
<bool name="api_lt_26">true</bool>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user