@@ -65,21 +65,79 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service android:name=".LocalOnlyHotspotService"/>
|
<service
|
||||||
|
android:name=".LocalOnlyHotspotService"
|
||||||
|
android:directBootAware="true"/>
|
||||||
<service
|
<service
|
||||||
android:name=".RepeaterService"
|
android:name=".RepeaterService"
|
||||||
android:directBootAware="true"/>
|
android:directBootAware="true"/>
|
||||||
<service android:name=".TetheringService"/>
|
|
||||||
<service
|
<service
|
||||||
android:name=".RepeaterTileService"
|
android:name=".TetheringService"
|
||||||
|
android:directBootAware="true"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".manage.RepeaterTileService"
|
||||||
android:directBootAware="true"
|
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:label="@string/title_repeater"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</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
|
<receiver
|
||||||
android:name=".BootReceiver"
|
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.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||||
|
import be.mygod.vpnhotspot.util.Event0
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class TetheringService : IpNeighbourMonitoringService() {
|
class TetheringService : IpNeighbourMonitoringService() {
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_ADD_INTERFACE = "interface.add"
|
const val EXTRA_ADD_INTERFACES = "interface.add"
|
||||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Binder : android.os.Binder() {
|
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()
|
private val binder = Binder()
|
||||||
@@ -90,16 +90,16 @@ class TetheringService : IpNeighbourMonitoringService() {
|
|||||||
}
|
}
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
app.handler.post { binder.fragment?.adapter?.notifyDataSetChanged() }
|
app.handler.post { binder.routingsChanged() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?) = binder
|
override fun onBind(intent: Intent?) = binder
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE)
|
val ifaces = intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()
|
||||||
synchronized(routings) {
|
synchronized(routings) {
|
||||||
if (iface != null) {
|
for (iface in ifaces) {
|
||||||
routings[iface] = null
|
routings[iface] = null
|
||||||
if (TetherType.ofInterface(iface).isWifi && !locked) {
|
if (TetherType.ofInterface(iface).isWifi && !locked) {
|
||||||
WifiDoubleLock.acquire()
|
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)
|
if (data.active) context.startService(Intent(context, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
|
.putExtra(TetheringService.EXTRA_REMOVE_INTERFACE, iface))
|
||||||
else ContextCompat.startForegroundService(context, Intent(context, TetheringService::class.java)
|
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() {
|
private inner class Data : be.mygod.vpnhotspot.manage.Data() {
|
||||||
override val icon get() = TetherType.ofInterface(iface).icon
|
override val icon get() = TetherType.ofInterface(iface).icon
|
||||||
override val title get() = iface
|
override val title get() = iface
|
||||||
override val text get() = addresses
|
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() ?: ""
|
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
|
* 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") {
|
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
|
Settings.Secure.LOCATION_MODE_OFF) == Settings.Secure.LOCATION_MODE_OFF
|
||||||
} else context.getSystemService<LocationManager>()?.isLocationEnabled != true) {
|
} 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 {
|
try {
|
||||||
view.context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Timber.w(e)
|
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
|
package be.mygod.vpnhotspot.manage
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothProfile
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -95,8 +92,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
protected abstract fun stop()
|
protected abstract fun stop()
|
||||||
|
|
||||||
override fun onTetheringStarted() = data.notifyChange()
|
override fun onTetheringStarted() = data.notifyChange()
|
||||||
override fun onTetheringFailed() =
|
override fun onTetheringFailed() = SmartSnackbar.make(R.string.tethering_manage_failed).show()
|
||||||
SmartSnackbar.make(R.string.tethering_manage_failed).show()
|
|
||||||
|
|
||||||
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
||||||
(viewHolder as ViewHolder).manager = this
|
(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)
|
override fun stop() = TetheringManager.stop(TetheringManager.TETHERING_USB)
|
||||||
}
|
}
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), LifecycleObserver,
|
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), LifecycleObserver {
|
||||||
BluetoothProfile.ServiceListener {
|
private val tethering = BluetoothTethering(parent.requireContext())
|
||||||
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
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
parent.lifecycle.addObserver(this)
|
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)
|
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||||
fun onDestroy() {
|
fun onDestroy() = tethering.close()
|
||||||
pan = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
|
override val title get() = parent.getString(R.string.tethering_manage_bluetooth)
|
||||||
override val tetherType get() = TetherType.BLUETOOTH
|
override val tetherType get() = TetherType.BLUETOOTH
|
||||||
override val type get() = VIEW_TYPE_BLUETOOTH
|
override val type get() = VIEW_TYPE_BLUETOOTH
|
||||||
/**
|
override val isStarted get() = tethering.active
|
||||||
* 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 fun start() = TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this)
|
override fun start() = TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this)
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
|
var ifaceLookup: Map<String, NetworkInterface> = emptyMap()
|
||||||
var enabledTypes = emptySet<TetherType>()
|
var enabledTypes = emptySet<TetherType>()
|
||||||
private lateinit var binding: FragmentTetheringBinding
|
private lateinit var binding: FragmentTetheringBinding
|
||||||
var tetheringBinder: TetheringService.Binder? = null
|
var binder: TetheringService.Binder? = null
|
||||||
val adapter = ManagerAdapter()
|
private val adapter = ManagerAdapter()
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
val extras = intent.extras ?: return@broadcastReceiver
|
val extras = intent.extras ?: return@broadcastReceiver
|
||||||
adapter.update(TetheringManager.getTetheredIfaces(extras),
|
adapter.update(TetheringManager.getTetheredIfaces(extras),
|
||||||
@@ -130,14 +130,14 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
tetheringBinder = service as TetheringService.Binder
|
binder = service as TetheringService.Binder
|
||||||
service.fragment = this
|
service.routingsChanged[this] = { adapter.notifyDataSetChanged() }
|
||||||
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
requireContext().registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
(tetheringBinder ?: return).fragment = null
|
(binder ?: return).routingsChanged -= this
|
||||||
tetheringBinder = null
|
binder = null
|
||||||
requireContext().unregisterReceiver(receiver)
|
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