10
README.md
10
README.md
@@ -31,6 +31,14 @@ I don't know about you but I can't get my stupid Windows 10 to work with
|
|||||||
now that they introduced this
|
now that they introduced this
|
||||||
[Mobile hotspot](https://support.microsoft.com/en-us/help/4027762/windows-use-your-pc-as-a-mobile-hotspot).
|
[Mobile hotspot](https://support.microsoft.com/en-us/help/4027762/windows-use-your-pc-as-a-mobile-hotspot).
|
||||||
|
|
||||||
|
## Features that requires system app installation
|
||||||
|
|
||||||
|
The following features in the app requires it to be installed under `/system/priv-app`.
|
||||||
|
One way to do this is to use [App systemizer for Magisk](https://github.com/Magisk-Modules-Repo/terminal_systemizer).
|
||||||
|
|
||||||
|
* (prior to Android 11) Read/write system Wi-Fi hotspot configuration. ([#117](https://github.com/Mygod/VPNHotspot/issues/117))
|
||||||
|
* (since Android 11) Use the Bluetooth tethering shortcut switch in app.
|
||||||
|
|
||||||
## Settings and How to Use Them
|
## Settings and How to Use Them
|
||||||
|
|
||||||
Default settings are picked to suit general use cases and maximize compatibility but it might not be optimal for battery
|
Default settings are picked to suit general use cases and maximize compatibility but it might not be optimal for battery
|
||||||
@@ -271,4 +279,4 @@ If some of these are unavailable, you can alternatively install a recent version
|
|||||||
Wi-Fi driver `wpa_supplicant`:
|
Wi-Fi driver `wpa_supplicant`:
|
||||||
|
|
||||||
* P2P configuration file is assumed to be saved to [`/data/vendor/wifi/wpa/p2p_supplicant.conf` or `/data/misc/wifi/p2p_supplicant.conf`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/0b4856b6dc451e290f1f64f6af17e010be78c073/wpa_supplicant/hidl/1.1/supplicant.cpp#26) and have reasonable format;
|
* P2P configuration file is assumed to be saved to [`/data/vendor/wifi/wpa/p2p_supplicant.conf` or `/data/misc/wifi/p2p_supplicant.conf`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/0b4856b6dc451e290f1f64f6af17e010be78c073/wpa_supplicant/hidl/1.1/supplicant.cpp#26) and have reasonable format;
|
||||||
* Android system is expected to restart `wpa_supplicant` after it crashes.
|
* Android system is expected to restart `wpa_supplicant` after it terminates.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ buildscript {
|
|||||||
classpath(kotlin("gradle-plugin", kotlinVersion))
|
classpath(kotlin("gradle-plugin", kotlinVersion))
|
||||||
classpath("com.android.tools.build:gradle:4.1.0-beta01")
|
classpath("com.android.tools.build:gradle:4.1.0-beta01")
|
||||||
classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0")
|
classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0")
|
||||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.1.1")
|
classpath("com.google.firebase:firebase-crashlytics-gradle:2.2.0")
|
||||||
classpath("com.google.android.gms:oss-licenses-plugin:0.10.2")
|
classpath("com.google.android.gms:oss-licenses-plugin:0.10.2")
|
||||||
classpath("com.google.gms:google-services:4.3.3")
|
classpath("com.google.gms:google-services:4.3.3")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,15 +84,15 @@ dependencies {
|
|||||||
implementation("androidx.room:room-ktx:$roomVersion")
|
implementation("androidx.room:room-ktx:$roomVersion")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01")
|
||||||
implementation("com.android.billingclient:billing-ktx:3.0.0")
|
implementation("com.android.billingclient:billing-ktx:3.0.0")
|
||||||
implementation("com.github.topjohnwu.libsu:core:2.5.1")
|
|
||||||
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||||
implementation("com.google.android.material:material:1.2.0-beta01")
|
implementation("com.google.android.material:material:1.2.0-beta01")
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:17.4.3")
|
implementation("com.google.firebase:firebase-analytics-ktx:17.4.3")
|
||||||
implementation("com.google.firebase:firebase-crashlytics:17.0.1")
|
implementation("com.google.firebase:firebase-crashlytics:17.1.0")
|
||||||
implementation("com.google.zxing:core:3.4.0")
|
implementation("com.google.zxing:core:3.4.0")
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
implementation("com.linkedin.dexmaker:dexmaker:2.28.0")
|
implementation("com.linkedin.dexmaker:dexmaker:2.28.0")
|
||||||
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
|
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
|
||||||
|
implementation("eu.chainfire:librootjava:1.3.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.2")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7")
|
||||||
testImplementation("junit:junit:4.13")
|
testImplementation("junit:junit:4.13")
|
||||||
|
|||||||
@@ -36,14 +36,14 @@
|
|||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_USB"
|
<uses-permission android:name="android.permission.MANAGE_USB"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission android:name="android.permission.NETWORK_SETTINGS"
|
|
||||||
tools:ignore="ProtectedPermissions" />
|
|
||||||
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
|
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.TETHER_PRIVILEGED"
|
<uses-permission android:name="android.permission.TETHER_PRIVILEGED"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
|
||||||
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
<uses-permission android:name="android.permission.WRITE_SETTINGS"
|
||||||
tools:ignore="ProtectedPermissions"/>
|
tools:ignore="ProtectedPermissions"/>
|
||||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
|||||||
427
mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt
Normal file
427
mobile/src/main/java/be/mygod/librootkotlinx/RootServer.kt
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
package be.mygod.librootkotlinx
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.LongSparseArray
|
||||||
|
import androidx.collection.set
|
||||||
|
import androidx.collection.valueIterator
|
||||||
|
import eu.chainfire.librootjava.AppProcess
|
||||||
|
import eu.chainfire.librootjava.RootJava
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.io.*
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> Unit = { Log.w(TAG, it) }) {
|
||||||
|
private sealed class Callback {
|
||||||
|
abstract fun cancel()
|
||||||
|
abstract fun shouldRemove(result: Byte): Boolean
|
||||||
|
abstract operator fun invoke(input: DataInputStream, result: Byte)
|
||||||
|
|
||||||
|
class Ordinary(private val classLoader: ClassLoader?,
|
||||||
|
private val callback: CompletableDeferred<Parcelable?>) : Callback() {
|
||||||
|
override fun cancel() = callback.cancel()
|
||||||
|
override fun shouldRemove(result: Byte) = true
|
||||||
|
override fun invoke(input: DataInputStream, result: Byte) {
|
||||||
|
when (result.toInt()) {
|
||||||
|
SUCCESS -> callback.complete(input.readParcelable(classLoader))
|
||||||
|
EX_GENERIC -> callback.completeExceptionally(RemoteException(input.readUTF()))
|
||||||
|
EX_PARCELABLE -> callback.completeExceptionally(RemoteException().initCause(
|
||||||
|
input.readParcelable<Parcelable>(classLoader) as Throwable?))
|
||||||
|
else -> throw IllegalArgumentException("Unexpected result $result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Channel(private val classLoader: ClassLoader?,
|
||||||
|
private val channel: SendChannel<Parcelable?>,
|
||||||
|
private val server: RootServer,
|
||||||
|
private val index: Long) : Callback() {
|
||||||
|
var active = true
|
||||||
|
val finish: CompletableDeferred<Unit> = CompletableDeferred()
|
||||||
|
override fun cancel() = finish.cancel()
|
||||||
|
override fun shouldRemove(result: Byte) = result.toInt() != SUCCESS
|
||||||
|
override fun invoke(input: DataInputStream, result: Byte) {
|
||||||
|
when (result.toInt()) {
|
||||||
|
// the channel we are supporting should never block
|
||||||
|
SUCCESS -> check(try {
|
||||||
|
channel.offer(input.readParcelable(classLoader))
|
||||||
|
} catch (closed: Throwable) {
|
||||||
|
active = false
|
||||||
|
GlobalScope.launch(Dispatchers.Unconfined) { sendClosed() }
|
||||||
|
finish.completeExceptionally(closed)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
EX_GENERIC -> finish.completeExceptionally(RemoteException(input.readUTF()))
|
||||||
|
EX_PARCELABLE -> finish.completeExceptionally(RemoteException().initCause(
|
||||||
|
input.readParcelable<Parcelable>(classLoader) as Throwable?))
|
||||||
|
CHANNEL_CONSUMED -> finish.complete(Unit)
|
||||||
|
else -> throw IllegalArgumentException("Unexpected result $result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendClosed() = server.execute(ChannelClosed(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var process: Process
|
||||||
|
private lateinit var worker: Thread
|
||||||
|
/**
|
||||||
|
* Thread safety: needs to be protected by mutex.
|
||||||
|
*/
|
||||||
|
private lateinit var output: DataOutputStream
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var active = false
|
||||||
|
private var counter = 0L
|
||||||
|
private val callbackListenerExit = CompletableDeferred<Unit>()
|
||||||
|
private val callbackLookup = LongSparseArray<Callback>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we encountered unexpected output from stderr during initialization, its content will be stored here.
|
||||||
|
*
|
||||||
|
* It is advised to read this after initializing the instance.
|
||||||
|
*/
|
||||||
|
fun readUnexpectedStderr(): String? {
|
||||||
|
var available = process.errorStream.available()
|
||||||
|
return if (available <= 0) null else String(ByteArrayOutputStream().apply {
|
||||||
|
while (available > 0) {
|
||||||
|
val bytes = ByteArray(available)
|
||||||
|
val len = process.errorStream.read(bytes)
|
||||||
|
if (len < 0) throw EOFException() // should not happen
|
||||||
|
write(bytes, 0, len)
|
||||||
|
available = process.errorStream.available()
|
||||||
|
}
|
||||||
|
}.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BufferedReader.lookForToken(token: String) {
|
||||||
|
while (true) {
|
||||||
|
val line = readLine() ?: throw EOFException()
|
||||||
|
if (line.endsWith(token)) {
|
||||||
|
val extraLength = line.length - token.length
|
||||||
|
if (extraLength > 0) warnLogger(line.substring(0, extraLength))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
warnLogger(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun doInit(context: Context, niceName: String) {
|
||||||
|
val writer: DataOutputStream
|
||||||
|
val reader: BufferedReader
|
||||||
|
try {
|
||||||
|
process = ProcessBuilder("su").start()
|
||||||
|
val token1 = UUID.randomUUID().toString()
|
||||||
|
writer = DataOutputStream(process.outputStream.buffered())
|
||||||
|
writer.writeBytes("echo $token1\n")
|
||||||
|
writer.flush()
|
||||||
|
reader = process.inputStream.bufferedReader()
|
||||||
|
reader.lookForToken(token1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw NoShellException(e)
|
||||||
|
}
|
||||||
|
if (DEBUG) Log.d(TAG, "Root shell initialized")
|
||||||
|
|
||||||
|
val appProcess = AppProcess.getAppProcess()
|
||||||
|
val token2 = UUID.randomUUID().toString()
|
||||||
|
writer.writeBytes(RootJava.getLaunchString(context.packageCodePath + " exec", // hack: plugging in exec
|
||||||
|
RootServer::class.java.name, appProcess, AppProcess.guessIfAppProcessIs64Bits(appProcess),
|
||||||
|
arrayOf("$token2\n"), niceName))
|
||||||
|
writer.flush()
|
||||||
|
reader.lookForToken(token2) // wait for ready signal
|
||||||
|
output = writer
|
||||||
|
require(!active)
|
||||||
|
active = true
|
||||||
|
if (DEBUG) Log.d(TAG, "Root server initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun callbackSpin() {
|
||||||
|
val input = DataInputStream(process.inputStream.buffered())
|
||||||
|
while (active) {
|
||||||
|
val index = try {
|
||||||
|
input.readLong()
|
||||||
|
} catch (_: EOFException) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val result = input.readByte()
|
||||||
|
val callback = mutex.synchronized {
|
||||||
|
callbackLookup[index]!!.also { if (it.shouldRemove(result)) callbackLookup.remove(index) }
|
||||||
|
}
|
||||||
|
if (DEBUG) Log.d(TAG, "Received callback #$index: $result")
|
||||||
|
callback(input, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a RootServer synchronously, can throw a lot of exceptions.
|
||||||
|
*
|
||||||
|
* @param context Any [Context] from the app.
|
||||||
|
* @param niceName Name to call the rooted Java process.
|
||||||
|
*/
|
||||||
|
suspend fun init(context: Context, niceName: String = "${context.packageName}:root") {
|
||||||
|
val future = CompletableDeferred<Unit>()
|
||||||
|
worker = Thread {
|
||||||
|
try {
|
||||||
|
doInit(context, niceName)
|
||||||
|
future.complete(Unit)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
future.completeExceptionally(e)
|
||||||
|
callbackListenerExit.complete(Unit)
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
callbackSpin()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
callbackListenerExit.completeExceptionally(e)
|
||||||
|
return@Thread
|
||||||
|
} finally {
|
||||||
|
if (DEBUG) Log.d(TAG, "Waiting for exit")
|
||||||
|
process.waitFor()
|
||||||
|
runBlocking { closeInternal(true) }
|
||||||
|
}
|
||||||
|
check(process.errorStream.available() == 0) // stderr should not be used
|
||||||
|
callbackListenerExit.complete(Unit)
|
||||||
|
}
|
||||||
|
worker.start()
|
||||||
|
future.await()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convenience function that initializes and also logs warnings to [Log].
|
||||||
|
*/
|
||||||
|
suspend fun initAndroidLog(context: Context, niceName: String = "${context.packageName}:root") = try {
|
||||||
|
init(context, niceName)
|
||||||
|
} finally {
|
||||||
|
readUnexpectedStderr()?.let { Log.e(TAG, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caller should check for active.
|
||||||
|
*/
|
||||||
|
private fun sendLocked(command: Parcelable) {
|
||||||
|
output.writeParcelable(command)
|
||||||
|
output.flush()
|
||||||
|
if (DEBUG) Log.d(TAG, "Sent #$counter: $command")
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun execute(command: RootCommandOneWay) = mutex.withLock { if (active) sendLocked(command) }
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
suspend inline fun <reified T : Parcelable?> execute(command: RootCommand<T>) =
|
||||||
|
execute(command, T::class.java.classLoader)
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
suspend fun <T : Parcelable?> execute(command: RootCommand<T>, classLoader: ClassLoader?): T {
|
||||||
|
val future = CompletableDeferred<T>()
|
||||||
|
mutex.withLock {
|
||||||
|
if (active) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
callbackLookup[counter] = Callback.Ordinary(classLoader, future as CompletableDeferred<Parcelable?>)
|
||||||
|
sendLocked(command)
|
||||||
|
} else future.cancel()
|
||||||
|
}
|
||||||
|
return future.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
inline fun <reified T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope) =
|
||||||
|
create(command, scope, T::class.java.classLoader)
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
fun <T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope,
|
||||||
|
classLoader: ClassLoader?) = scope.produce<T>(
|
||||||
|
capacity = command.capacity.also {
|
||||||
|
when (it) {
|
||||||
|
Channel.UNLIMITED, Channel.CONFLATED -> { }
|
||||||
|
else -> throw IllegalArgumentException("Unsupported channel capacity $it")
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val callback = Callback.Channel(classLoader, this as SendChannel<Parcelable?>, this@RootServer, counter)
|
||||||
|
mutex.withLock {
|
||||||
|
if (active) {
|
||||||
|
callbackLookup[counter] = callback
|
||||||
|
sendLocked(command)
|
||||||
|
} else callback.finish.cancel()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
callback.finish.await()
|
||||||
|
} finally {
|
||||||
|
if (callback.active) withContext(NonCancellable) { callback.sendClosed() }
|
||||||
|
callback.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun closeInternal(fromWorker: Boolean = false) = mutex.withLock {
|
||||||
|
if (active) {
|
||||||
|
active = false
|
||||||
|
if (DEBUG) Log.d(TAG, "Shutting down from client")
|
||||||
|
sendLocked(Shutdown())
|
||||||
|
output.close()
|
||||||
|
process.outputStream.close()
|
||||||
|
if (DEBUG) Log.d(TAG, "Client closed")
|
||||||
|
}
|
||||||
|
if (fromWorker) {
|
||||||
|
for (callback in callbackLookup.valueIterator()) callback.cancel()
|
||||||
|
callbackLookup.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shutdown the instance gracefully.
|
||||||
|
*/
|
||||||
|
suspend fun close() {
|
||||||
|
closeInternal()
|
||||||
|
callbackListenerExit.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* If set to true, debug information will be printed to logcat.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
var DEBUG = false
|
||||||
|
|
||||||
|
private const val TAG = "RootServer"
|
||||||
|
private const val SUCCESS = 0
|
||||||
|
private const val EX_GENERIC = 1
|
||||||
|
private const val EX_PARCELABLE = 2
|
||||||
|
private const val CHANNEL_CONSUMED = 3
|
||||||
|
|
||||||
|
private inline fun <reified T : Parcelable> DataInputStream.readParcelable(
|
||||||
|
classLoader: ClassLoader? = T::class.java.classLoader
|
||||||
|
) = ByteArray(readInt()).also { readFully(it) }.toParcelable<T>(classLoader)
|
||||||
|
private fun DataOutputStream.writeParcelable(data: Parcelable?, parcelableFlags: Int = 0) {
|
||||||
|
val bytes = data.toByteArray(parcelableFlags)
|
||||||
|
writeInt(bytes.size)
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> Mutex.synchronized(crossinline block: () -> T): T = runBlocking {
|
||||||
|
withLock { block() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
|
Log.e(TAG, "Uncaught exception from $thread", throwable)
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
rootMain(args)
|
||||||
|
exitProcess(0) // there might be other non-daemon threads
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DataOutputStream.pushThrowable(callback: Long, e: Throwable) {
|
||||||
|
writeLong(callback)
|
||||||
|
if (e is Parcelable) {
|
||||||
|
writeByte(EX_PARCELABLE)
|
||||||
|
writeParcelable(e)
|
||||||
|
} else {
|
||||||
|
writeByte(EX_GENERIC)
|
||||||
|
writeUTF(StringWriter().also {
|
||||||
|
e.printStackTrace(PrintWriter(it))
|
||||||
|
}.toString())
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
private fun DataOutputStream.pushResult(callback: Long, result: Parcelable?) {
|
||||||
|
writeLong(callback)
|
||||||
|
writeByte(SUCCESS)
|
||||||
|
writeParcelable(result)
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootMain(args: Array<String>) {
|
||||||
|
require(args.isNotEmpty())
|
||||||
|
RootJava.restoreOriginalLdLibraryPath()
|
||||||
|
val mainInitialized = CountDownLatch(1)
|
||||||
|
val main = Thread({
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Looper.prepareMainLooper()
|
||||||
|
mainInitialized.countDown()
|
||||||
|
Looper.loop()
|
||||||
|
}, "main")
|
||||||
|
main.start()
|
||||||
|
val job = Job()
|
||||||
|
val defaultWorker by lazy {
|
||||||
|
mainInitialized.await()
|
||||||
|
CoroutineScope(Dispatchers.Main.immediate + job)
|
||||||
|
}
|
||||||
|
val callbackWorker = newSingleThreadContext("callbackWorker")
|
||||||
|
val channels = LongSparseArray<WeakReference<ReceiveChannel<Parcelable?>>>()
|
||||||
|
|
||||||
|
// thread safety: usage of output should be guarded by callbackWorker
|
||||||
|
val output = DataOutputStream(System.out.buffered().apply {
|
||||||
|
val writer = writer()
|
||||||
|
writer.appendln(args[0]) // echo ready signal
|
||||||
|
writer.flush()
|
||||||
|
})
|
||||||
|
// thread safety: usage of input should be in main thread
|
||||||
|
val input = DataInputStream(System.`in`.buffered())
|
||||||
|
var counter = 0L
|
||||||
|
if (DEBUG) Log.d(TAG, "Server entering main loop")
|
||||||
|
loop@ while (true) {
|
||||||
|
val command = try {
|
||||||
|
input.readParcelable<Parcelable>(RootServer::class.java.classLoader)
|
||||||
|
} catch (e: EOFException) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val callback = counter
|
||||||
|
if (DEBUG) Log.d(TAG, "Received #$callback: $command")
|
||||||
|
when (command) {
|
||||||
|
is ChannelClosed -> channels[command.index]?.get()?.cancel()
|
||||||
|
is RootCommandOneWay -> defaultWorker.launch {
|
||||||
|
try {
|
||||||
|
command.execute()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(command.javaClass.simpleName, "Unexpected exception in RootCommandOneWay", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is RootCommand<*> -> defaultWorker.launch {
|
||||||
|
val result = try {
|
||||||
|
val result = command.execute();
|
||||||
|
{ output.pushResult(callback, result) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
{ output.pushThrowable(callback, e) }
|
||||||
|
}
|
||||||
|
withContext(callbackWorker) { result() }
|
||||||
|
}
|
||||||
|
is RootCommandChannel<*> -> defaultWorker.launch {
|
||||||
|
val result = try {
|
||||||
|
command.create(defaultWorker).also {
|
||||||
|
channels[callback] = WeakReference(it)
|
||||||
|
}.consumeEach { result ->
|
||||||
|
withContext(callbackWorker) { output.pushResult(callback, result) }
|
||||||
|
};
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext") {
|
||||||
|
output.writeByte(CHANNEL_CONSUMED)
|
||||||
|
output.writeLong(callback)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
{ output.pushThrowable(callback, e) }
|
||||||
|
} finally {
|
||||||
|
channels.remove(callback)
|
||||||
|
}
|
||||||
|
withContext(callbackWorker) { result() }
|
||||||
|
}
|
||||||
|
is Shutdown -> break@loop
|
||||||
|
else -> throw IllegalArgumentException("Unrecognized input: $command")
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
job.cancel()
|
||||||
|
if (DEBUG) Log.d(TAG, "Clean up initiated before exit. Jobs: ${job.children.joinToString()}")
|
||||||
|
if (runBlocking { withTimeoutOrNull(5000) { job.join() } } == null) {
|
||||||
|
Log.w(TAG, "Clean up timeout: ${job.children.joinToString()}")
|
||||||
|
} else if (DEBUG) Log.d(TAG, "Clean up finished, exiting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt
Normal file
106
mobile/src/main/java/be/mygod/librootkotlinx/RootSession.kt
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package be.mygod.librootkotlinx
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object manages creation of [RootServer] and times them out automagically, with default timeout of 5 minutes.
|
||||||
|
*/
|
||||||
|
abstract class RootSession {
|
||||||
|
protected open fun createServer() = RootServer()
|
||||||
|
protected abstract suspend fun initServer(server: RootServer)
|
||||||
|
/**
|
||||||
|
* Timeout to close [RootServer] in milliseconds.
|
||||||
|
*/
|
||||||
|
protected open val timeout get() = TimeUnit.MINUTES.toMillis(5)
|
||||||
|
protected open val timeoutDispatcher get() = Dispatchers.Default
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var server: RootServer? = null
|
||||||
|
private var timeoutJob: Job? = null
|
||||||
|
private var usersCount = 0L
|
||||||
|
private var closePending = false
|
||||||
|
|
||||||
|
private suspend fun ensureServerLocked(): RootServer {
|
||||||
|
server?.let { return it }
|
||||||
|
check(usersCount == 0L)
|
||||||
|
val server = createServer()
|
||||||
|
try {
|
||||||
|
initServer(server)
|
||||||
|
this.server = server
|
||||||
|
return server
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
try {
|
||||||
|
server.close()
|
||||||
|
} catch (eClose: Throwable) {
|
||||||
|
throw eClose.apply { addSuppressed(e) }
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun closeLocked() {
|
||||||
|
server?.close()
|
||||||
|
server = null
|
||||||
|
}
|
||||||
|
private fun startTimeoutLocked() {
|
||||||
|
check(timeoutJob == null)
|
||||||
|
timeoutJob = GlobalScope.launch(timeoutDispatcher, CoroutineStart.UNDISPATCHED) {
|
||||||
|
delay(timeout)
|
||||||
|
mutex.withLock {
|
||||||
|
check(usersCount == 0L)
|
||||||
|
closeLocked()
|
||||||
|
timeoutJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun haltTimeoutLocked() {
|
||||||
|
timeoutJob?.cancel()
|
||||||
|
timeoutJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun acquire() = withContext(NonCancellable) {
|
||||||
|
mutex.withLock {
|
||||||
|
haltTimeoutLocked()
|
||||||
|
closePending = false
|
||||||
|
ensureServerLocked().also { ++usersCount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suspend fun release(server: RootServer) = withContext(NonCancellable) {
|
||||||
|
mutex.withLock {
|
||||||
|
if (this@RootSession.server != server) return@withLock // outdated reference
|
||||||
|
require(usersCount > 0)
|
||||||
|
when {
|
||||||
|
!server.active -> {
|
||||||
|
usersCount = 0
|
||||||
|
closeLocked()
|
||||||
|
closePending = false
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
|
--usersCount > 0L -> return@withLock
|
||||||
|
closePending -> {
|
||||||
|
closeLocked()
|
||||||
|
closePending = false
|
||||||
|
}
|
||||||
|
else -> startTimeoutLocked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suspend inline fun <T> use(block: (RootServer) -> T): T {
|
||||||
|
val server = acquire()
|
||||||
|
try {
|
||||||
|
return block(server)
|
||||||
|
} finally {
|
||||||
|
release(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun closeExisting() = mutex.withLock {
|
||||||
|
if (usersCount > 0) closePending = true else {
|
||||||
|
haltTimeoutLocked()
|
||||||
|
closeLocked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package be.mygod.librootkotlinx
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
|
||||||
|
interface RootCommand<Result : Parcelable?> : Parcelable {
|
||||||
|
/**
|
||||||
|
* If a throwable was thrown, it will be wrapped in RemoteException only if it implements [Parcelable].
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
suspend fun execute(): Result
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias RootCommandNoResult = RootCommand<Parcelable?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command and discards its result, even if an exception occurs.
|
||||||
|
*
|
||||||
|
* If you want to catch exception, use e.g. [RootCommandNoResult] and return null.
|
||||||
|
*/
|
||||||
|
interface RootCommandOneWay : Parcelable {
|
||||||
|
@MainThread
|
||||||
|
suspend fun execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RootCommandChannel<T : Parcelable?> : Parcelable {
|
||||||
|
/**
|
||||||
|
* The capacity of the channel that is returned by [create] to be used by client.
|
||||||
|
* Only [Channel.UNLIMITED] and [Channel.CONFLATED] is supported for now to avoid blocking the entire connection.
|
||||||
|
*/
|
||||||
|
val capacity: Int get() = Channel.UNLIMITED
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun create(scope: CoroutineScope): ReceiveChannel<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
internal class ChannelClosed(val index: Long) : RootCommandOneWay {
|
||||||
|
override suspend fun execute() = throw IllegalStateException("Internal implementation")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
internal class Shutdown : Parcelable
|
||||||
201
mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt
Normal file
201
mobile/src/main/java/be/mygod/librootkotlinx/Utils.kt
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package be.mygod.librootkotlinx
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.*
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
class NoShellException(cause: Throwable) : Exception("Root missing", cause)
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableByte(val value: Byte) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableShort(val value: Short) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableInt(val value: Int) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableLong(val value: Long) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableFloat(val value: Float) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableDouble(val value: Double) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableBoolean(val value: Boolean) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableString(val value: String) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableByteArray(val value: ByteArray) : Parcelable {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ParcelableByteArray
|
||||||
|
|
||||||
|
if (!value.contentEquals(other.value)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableIntArray(val value: IntArray) : Parcelable {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ParcelableIntArray
|
||||||
|
|
||||||
|
if (!value.contentEquals(other.value)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableLongArray(val value: LongArray) : Parcelable {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ParcelableLongArray
|
||||||
|
|
||||||
|
if (!value.contentEquals(other.value)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableFloatArray(val value: FloatArray) : Parcelable {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ParcelableFloatArray
|
||||||
|
|
||||||
|
if (!value.contentEquals(other.value)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableDoubleArray(val value: DoubleArray) : Parcelable {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ParcelableDoubleArray
|
||||||
|
|
||||||
|
if (!value.contentEquals(other.value)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableBooleanArray(val value: BooleanArray) : Parcelable {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ParcelableBooleanArray
|
||||||
|
|
||||||
|
if (!value.contentEquals(other.value)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableStringArray(val value: Array<String>) : Parcelable {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ParcelableStringArray
|
||||||
|
|
||||||
|
if (!value.contentEquals(other.value)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return value.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableStringList(val value: List<String>) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableSparseIntArray(val value: SparseIntArray) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableSparseLongArray(val value: SparseLongArray) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableSparseBooleanArray(val value: SparseBooleanArray) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableCharSequence(val value: CharSequence) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableSize(val value: Size) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableSizeF(val value: SizeF) : Parcelable
|
||||||
|
|
||||||
|
@SuppressLint("Recycle")
|
||||||
|
inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
|
||||||
|
try {
|
||||||
|
block(this)
|
||||||
|
} finally {
|
||||||
|
recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Parcelable?.toByteArray(parcelableFlags: Int = 0) = useParcel { p ->
|
||||||
|
p.writeParcelable(this, parcelableFlags)
|
||||||
|
p.marshall()
|
||||||
|
}
|
||||||
|
inline fun <reified T : Parcelable> ByteArray.toParcelable(classLoader: ClassLoader? = T::class.java.classLoader) =
|
||||||
|
useParcel { p ->
|
||||||
|
p.unmarshall(this, 0, size)
|
||||||
|
p.setDataPosition(0)
|
||||||
|
p.readParcelable<T>(classLoader)
|
||||||
|
}
|
||||||
@@ -2,11 +2,9 @@ package be.mygod.vpnhotspot
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.UiModeManager
|
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.Size
|
import androidx.annotation.Size
|
||||||
@@ -18,10 +16,12 @@ import androidx.core.provider.FontRequest
|
|||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.emoji.text.FontRequestEmojiCompatConfig
|
import androidx.emoji.text.FontRequestEmojiCompatConfig
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import be.mygod.librootkotlinx.NoShellException
|
||||||
import be.mygod.vpnhotspot.net.DhcpWorkaround
|
import be.mygod.vpnhotspot.net.DhcpWorkaround
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.util.DeviceStorageApp
|
import be.mygod.vpnhotspot.util.DeviceStorageApp
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import com.google.firebase.analytics.ktx.ParametersBuilder
|
import com.google.firebase.analytics.ktx.ParametersBuilder
|
||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
@@ -38,6 +38,10 @@ class App : Application() {
|
|||||||
lateinit var app: App
|
lateinit var app: App
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
app = this
|
app = this
|
||||||
@@ -47,6 +51,7 @@ class App : Application() {
|
|||||||
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
|
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
|
||||||
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
|
||||||
} else deviceStorage = this
|
} else deviceStorage = this
|
||||||
|
Services.init(this)
|
||||||
Firebase.initialize(deviceStorage)
|
Firebase.initialize(deviceStorage)
|
||||||
when (val codename = Build.VERSION.CODENAME) {
|
when (val codename = Build.VERSION.CODENAME) {
|
||||||
"REL" -> { }
|
"REL" -> { }
|
||||||
@@ -62,7 +67,9 @@ class App : Application() {
|
|||||||
FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message")
|
FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message")
|
||||||
} else {
|
} else {
|
||||||
if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message)
|
if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message)
|
||||||
if (priority >= Log.INFO) FirebaseCrashlytics.getInstance().recordException(t)
|
if (priority >= Log.INFO && t !is NoShellException) {
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -89,7 +96,7 @@ class App : Application() {
|
|||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
super.onTrimMemory(level)
|
super.onTrimMemory(level)
|
||||||
if (level >= TRIM_MEMORY_RUNNING_CRITICAL) GlobalScope.launch { RootSession.trimMemory() }
|
if (level >= TRIM_MEMORY_RUNNING_CRITICAL) GlobalScope.launch { RootManager.closeExisting() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,10 +117,7 @@ class App : Application() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
|
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
|
||||||
val connectivity by lazy { getSystemService<ConnectivityManager>()!! }
|
|
||||||
val clipboard by lazy { getSystemService<ClipboardManager>()!! }
|
val clipboard by lazy { getSystemService<ClipboardManager>()!! }
|
||||||
val uiMode by lazy { getSystemService<UiModeManager>()!! }
|
|
||||||
val wifi by lazy { getSystemService<WifiManager>()!! }
|
|
||||||
|
|
||||||
val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") }
|
val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") }
|
||||||
val customTabsIntent by lazy {
|
val customTabsIntent by lazy {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -27,7 +28,7 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true
|
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
if (RepeaterService.supported) {
|
if (Services.p2p != null) {
|
||||||
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
|
import java.net.Inet4Address
|
||||||
|
|
||||||
abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback {
|
abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback {
|
||||||
private var neighbours: Collection<IpNeighbour> = emptyList()
|
private var neighbours: Collection<IpNeighbour> = emptyList()
|
||||||
@@ -17,7 +18,7 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
|
|||||||
protected fun updateNotification() {
|
protected fun updateNotification() {
|
||||||
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
|
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
|
||||||
neighbours
|
neighbours
|
||||||
.filter { it.state != IpNeighbour.State.FAILED }
|
.filter { it.ip is Inet4Address && it.state != IpNeighbour.State.FAILED }
|
||||||
.distinctBy { it.lladdr }
|
.distinctBy { it.lladdr }
|
||||||
.size
|
.size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.IntentFilter
|
|||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
|
||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
||||||
@@ -13,11 +12,13 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
|||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.util.StickyEvent1
|
import be.mygod.vpnhotspot.util.StickyEvent1
|
||||||
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 kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.net.Inet4Address
|
||||||
|
|
||||||
@RequiresApi(26)
|
@RequiresApi(26)
|
||||||
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
||||||
@@ -80,7 +81,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
binder.iface = ""
|
binder.iface = ""
|
||||||
updateNotification() // show invisible foreground notification to avoid being killed
|
updateNotification() // show invisible foreground notification to avoid being killed
|
||||||
try {
|
try {
|
||||||
app.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
|
Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
|
||||||
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
|
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
|
||||||
if (reservation == null) onFailed(-2) else {
|
if (reservation == null) onFailed(-2) else {
|
||||||
this@LocalOnlyHotspotService.reservation = reservation
|
this@LocalOnlyHotspotService.reservation = reservation
|
||||||
@@ -128,7 +129,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
|
|||||||
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
|
||||||
super.onIpNeighbourAvailable(neighbours)
|
super.onIpNeighbourAvailable(neighbours)
|
||||||
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none {
|
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none {
|
||||||
it.state != IpNeighbour.State.FAILED
|
it.ip is Inet4Address && it.state != IpNeighbour.State.FAILED
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import be.mygod.vpnhotspot.manage.TetheringFragment
|
|||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
@@ -28,7 +29,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
|
|||||||
binding.navigation.setOnNavigationItemSelectedListener(this)
|
binding.navigation.setOnNavigationItemSelectedListener(this)
|
||||||
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
if (savedInstanceState == null) displayFragment(TetheringFragment())
|
||||||
val model by viewModels<ClientViewModel>()
|
val model by viewModels<ClientViewModel>()
|
||||||
if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class)
|
if (Services.p2p != null) ServiceForegroundConnector(this, model, RepeaterService::class)
|
||||||
model.clients.observe(this) { clients ->
|
model.clients.observe(this) { clients ->
|
||||||
val count = clients.count {
|
val count = clients.count {
|
||||||
it.ip.any { (ip, state) -> ip is Inet4Address && state != IpNeighbour.State.FAILED }
|
it.ip.any { (ip, state) -> ip is Inet4Address && state != IpNeighbour.State.FAILED }
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import android.provider.Settings
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||||
@@ -25,6 +24,8 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
|
|||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
|
||||||
|
import be.mygod.vpnhotspot.root.RepeaterCommands
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.util.*
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -51,18 +52,6 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
*/
|
*/
|
||||||
private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot"
|
private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot"
|
||||||
|
|
||||||
/**
|
|
||||||
* This is only a "ServiceConnection" to system service and its impact on system is minimal.
|
|
||||||
*/
|
|
||||||
private val p2pManager: WifiP2pManager? by lazy {
|
|
||||||
try {
|
|
||||||
app.getSystemService<WifiP2pManager>()
|
|
||||||
} catch (e: RuntimeException) {
|
|
||||||
Timber.w(e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val supported get() = p2pManager != null
|
|
||||||
var persistentSupported = false
|
var persistentSupported = false
|
||||||
|
|
||||||
@delegate:TargetApi(29)
|
@delegate:TargetApi(29)
|
||||||
@@ -145,7 +134,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val p2pManager get() = RepeaterService.p2pManager!!
|
private val p2pManager get() = Services.p2p!!
|
||||||
private var channel: WifiP2pManager.Channel? = null
|
private var channel: WifiP2pManager.Channel? = null
|
||||||
private val binder = Binder()
|
private val binder = Binder()
|
||||||
@RequiresApi(28)
|
@RequiresApi(28)
|
||||||
@@ -207,14 +196,23 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
|
|
||||||
override fun onBind(intent: Intent) = binder
|
override fun onBind(intent: Intent) = binder
|
||||||
|
|
||||||
private fun setOperatingChannel(oc: Int = operatingChannel) = try {
|
private fun setOperatingChannel(forceReinit: Boolean = false, oc: Int = operatingChannel) = try {
|
||||||
val channel = channel
|
val channel = channel
|
||||||
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
|
||||||
// we don't care about listening channel
|
// we don't care about listening channel
|
||||||
else p2pManager.setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
|
else p2pManager.setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
|
||||||
override fun onSuccess() { }
|
override fun onSuccess() { }
|
||||||
override fun onFailure(reason: Int) {
|
override fun onFailure(reason: Int) {
|
||||||
SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
|
if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) launch(start = CoroutineStart.UNDISPATCHED) {
|
||||||
|
val rootReason = try {
|
||||||
|
RootManager.use { it.execute(RepeaterCommands.SetChannel(oc, forceReinit)) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e).show()
|
||||||
|
null
|
||||||
|
} ?: return@launch
|
||||||
|
SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, rootReason.value)).show()
|
||||||
|
} else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
@@ -229,7 +227,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
channel = null
|
channel = null
|
||||||
if (status != Status.DESTROYED) try {
|
if (status != Status.DESTROYED) try {
|
||||||
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
|
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
|
||||||
if (!safeMode) setOperatingChannel()
|
if (!safeMode) setOperatingChannel(true)
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
@@ -240,18 +238,19 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
if (!safeMode && key == KEY_OPERATING_CHANNEL) setOperatingChannel()
|
if (!safeMode) when (key) {
|
||||||
|
KEY_OPERATING_CHANNEL -> setOperatingChannel()
|
||||||
|
KEY_SAFE_MODE -> setOperatingChannel(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi") // networkId is available since Android 4.2
|
@SuppressLint("NewApi") // networkId is available since Android 4.2
|
||||||
private fun onPersistentGroupsChanged() = launch {
|
private fun onPersistentGroupsChanged() = launch {
|
||||||
val ownerAddress = lastMac?.let(MacAddressCompat.Companion::fromString) ?: withContext(Dispatchers.Default) {
|
val ownerAddress = lastMac?.let(MacAddressCompat.Companion::fromString) ?: try {
|
||||||
try {
|
P2pSupplicantConfiguration().apply { init() }.bssid
|
||||||
P2pSupplicantConfiguration().bssid
|
} catch (e: RuntimeException) {
|
||||||
} catch (e: RuntimeException) {
|
Timber.d(e)
|
||||||
Timber.d(e)
|
null
|
||||||
null
|
|
||||||
}
|
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
val channel = channel ?: return@launch
|
val channel = channel ?: return@launch
|
||||||
try {
|
try {
|
||||||
@@ -287,12 +286,8 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
if (status != Status.IDLE) return START_NOT_STICKY
|
if (status != Status.IDLE) return START_NOT_STICKY
|
||||||
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
|
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
|
||||||
status = Status.STARTING
|
status = Status.STARTING
|
||||||
// bump self to foreground location service to use foreground location permission later
|
// bump self to foreground location service (API 29+) to use location later, also to avoid getting killed
|
||||||
if (Build.VERSION.SDK_INT >= 29 ||
|
if (Build.VERSION.SDK_INT >= 26) showNotification()
|
||||||
// or show invisible foreground notification on television to avoid being killed
|
|
||||||
Build.VERSION.SDK_INT >= 26 && app.uiMode.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
|
|
||||||
showNotification()
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
||||||
@@ -420,7 +415,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
|
|||||||
SmartSnackbar.make(msg).apply {
|
SmartSnackbar.make(msg).apply {
|
||||||
if (showWifiEnable) action(R.string.repeater_p2p_unavailable_enable) {
|
if (showWifiEnable) action(R.string.repeater_p2p_unavailable_enable) {
|
||||||
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
|
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
|
||||||
app.wifi.isWifiEnabled = true
|
Services.wifi.isWifiEnabled = true
|
||||||
} else it.context.startActivity(Intent(Settings.Panel.ACTION_WIFI))
|
} else it.context.startActivity(Intent(Settings.Panel.ACTION_WIFI))
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import be.mygod.vpnhotspot.net.Routing
|
|||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
|
|||||||
if (!reinit && active.isEmpty()) return@synchronized
|
if (!reinit && active.isEmpty()) return@synchronized
|
||||||
for (manager in active.values) manager.routing?.stop()
|
for (manager in active.values) manager.routing?.stop()
|
||||||
try {
|
try {
|
||||||
Routing.clean()
|
runBlocking { Routing.clean() }
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
Timber.d(e)
|
Timber.d(e)
|
||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -9,7 +8,6 @@ import androidx.preference.Preference
|
|||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.SwitchPreference
|
import androidx.preference.SwitchPreference
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
|
||||||
import be.mygod.vpnhotspot.net.TetherOffloadManager
|
import be.mygod.vpnhotspot.net.TetherOffloadManager
|
||||||
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpMonitor
|
||||||
@@ -18,7 +16,9 @@ import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
|
|||||||
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment
|
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment
|
||||||
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
||||||
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
|
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.root.Dump
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.util.launchUrl
|
import be.mygod.vpnhotspot.util.launchUrl
|
||||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
@@ -27,9 +27,9 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@@ -48,34 +48,30 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
if (Build.VERSION.SDK_INT >= 27) {
|
if (Build.VERSION.SDK_INT >= 27) {
|
||||||
isChecked = TetherOffloadManager.enabled
|
isChecked = TetherOffloadManager.enabled
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (TetherOffloadManager.enabled != newValue) {
|
if (TetherOffloadManager.enabled != newValue) GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
GlobalScope.launch {
|
try {
|
||||||
try {
|
TetherOffloadManager.setEnabled(newValue as Boolean)
|
||||||
TetherOffloadManager.enabled = newValue as Boolean
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Timber.w(e)
|
||||||
Timber.d(e)
|
SmartSnackbar.make(e).show()
|
||||||
SmartSnackbar.make(e).show()
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
isChecked = TetherOffloadManager.enabled
|
|
||||||
isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
isChecked = TetherOffloadManager.enabled
|
||||||
|
isEnabled = true
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else parent!!.removePreference(this)
|
} else parent!!.removePreference(this)
|
||||||
}
|
}
|
||||||
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
|
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
|
||||||
if (RepeaterService.supported) {
|
if (Services.p2p != null) {
|
||||||
boot.setOnPreferenceChangeListener { _, value ->
|
boot.setOnPreferenceChangeListener { _, value ->
|
||||||
BootReceiver.enabled = value as Boolean
|
BootReceiver.enabled = value as Boolean
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
boot.isChecked = BootReceiver.enabled
|
boot.isChecked = BootReceiver.enabled
|
||||||
} else boot.parent!!.removePreference(boot)
|
} else boot.parent!!.removePreference(boot)
|
||||||
if (!RepeaterService.supported || !RepeaterService.safeModeConfigurable) {
|
if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
|
||||||
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
|
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
|
||||||
safeMode.parent!!.removePreference(safeMode)
|
safeMode.parent!!.removePreference(safeMode)
|
||||||
}
|
}
|
||||||
@@ -88,7 +84,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
setAction(R.string.settings_exit_app) {
|
setAction(R.string.settings_exit_app) {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
RoutingManager.clean(false)
|
RoutingManager.clean(false)
|
||||||
RootSession.trimMemory()
|
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,57 +104,19 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
Runtime.getRuntime().exec(arrayOf("logcat", "-d")).inputStream.use { it.copyTo(out) }
|
Runtime.getRuntime().exec(arrayOf("logcat", "-d")).inputStream.use { it.copyTo(out) }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
|
e.printStackTrace(writer)
|
||||||
}
|
}
|
||||||
writer.println()
|
writer.println()
|
||||||
val commands = StringBuilder()
|
|
||||||
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
|
|
||||||
val iptablesSave = if (Build.VERSION.SDK_INT >= 24) "iptables-save" else
|
|
||||||
File(app.deviceStorage.cacheDir, "iptables-save").absolutePath.also {
|
|
||||||
commands.appendln("ln -sf /system/bin/iptables $it")
|
|
||||||
}
|
|
||||||
val ip6tablesSave = if (Build.VERSION.SDK_INT >= 24) "ip6tables-save" else
|
|
||||||
File(app.deviceStorage.cacheDir, "ip6tables-save").absolutePath.also {
|
|
||||||
commands.appendln("ln -sf /system/bin/ip6tables $it")
|
|
||||||
}
|
|
||||||
commands.append("""
|
|
||||||
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|
|
||||||
|dumpsys ${Context.WIFI_P2P_SERVICE}
|
|
||||||
|echo
|
|
||||||
|echo dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|
|
||||||
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|
|
||||||
|echo
|
|
||||||
|echo iptables -t filter
|
|
||||||
|$iptablesSave -t filter
|
|
||||||
|echo
|
|
||||||
|echo iptables -t nat
|
|
||||||
|$iptablesSave -t nat
|
|
||||||
|echo
|
|
||||||
|echo ip6tables-save
|
|
||||||
|$ip6tablesSave
|
|
||||||
|echo
|
|
||||||
|echo ip rule
|
|
||||||
|ip rule
|
|
||||||
|echo
|
|
||||||
|echo ip neigh
|
|
||||||
|ip neigh
|
|
||||||
|echo
|
|
||||||
|echo iptables -nvx -L vpnhotspot_fwd
|
|
||||||
|$IPTABLES -nvx -L vpnhotspot_fwd
|
|
||||||
|echo
|
|
||||||
|echo iptables -nvx -L vpnhotspot_acl
|
|
||||||
|$IPTABLES -nvx -L vpnhotspot_acl
|
|
||||||
|echo
|
|
||||||
|echo logcat-su
|
|
||||||
|logcat -d
|
|
||||||
""".trimMargin())
|
|
||||||
try {
|
|
||||||
RootSession.use { it.execQuiet(commands.toString(), true).out.forEach(writer::println) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace(writer)
|
|
||||||
Timber.i(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
RootManager.use {
|
||||||
|
it.execute(Dump(logFile.absolutePath))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
PrintWriter(FileOutputStream(logFile, true)).use { e.printStackTrace(it) }
|
||||||
|
}
|
||||||
context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND)
|
context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND)
|
||||||
.setType("text/x-log")
|
.setType("text/x-log")
|
||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
@@ -187,8 +144,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
when (preference.key) {
|
when (preference.key) {
|
||||||
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
|
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
|
||||||
AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply {
|
AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply {
|
||||||
setArguments(preference.key, app.connectivity.allNetworks.mapNotNull {
|
setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull {
|
||||||
app.connectivity.getLinkProperties(it)?.interfaceName
|
Services.connectivity.getLinkProperties(it)?.interfaceName
|
||||||
}.toTypedArray())
|
}.toTypedArray())
|
||||||
setTargetFragment(this@SettingsPreferenceFragment, 0)
|
setTargetFragment(this@SettingsPreferenceFragment, 0)
|
||||||
}.showAllowingStateLoss(parentFragmentManager, preference.key)
|
}.showAllowingStateLoss(parentFragmentManager, preference.key)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
@@ -93,6 +94,8 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
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 {
|
||||||
|
// call this first just in case we are shutting down immediately
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) updateNotification()
|
||||||
launch {
|
launch {
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
|
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
|
||||||
@@ -109,7 +112,6 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
|
|||||||
} else downstream.monitor = true
|
} else downstream.monitor = true
|
||||||
}
|
}
|
||||||
intent.getStringExtra(EXTRA_REMOVE_INTERFACE)?.also { downstreams.remove(it)?.stop() }
|
intent.getStringExtra(EXTRA_REMOVE_INTERFACE)?.also { downstreams.remove(it)?.stop() }
|
||||||
updateNotification() // call this first just in case we are shutting down immediately
|
|
||||||
onDownstreamsChangedLocked()
|
onDownstreamsChangedLocked()
|
||||||
} else if (downstreams.isEmpty()) stopSelf(startId)
|
} else if (downstreams.isEmpty()) stopSelf(startId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.os.Parcelable
|
|||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.LongSparseArray
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -15,6 +14,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import androidx.collection.LongSparseArray
|
||||||
import androidx.databinding.BaseObservable
|
import androidx.databinding.BaseObservable
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
@@ -145,7 +145,7 @@ class ClientsFragment : Fragment() {
|
|||||||
AppDatabase.instance.clientRecordDao.update(this@apply)
|
AppDatabase.instance.clientRecordDao.update(this@apply)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IpNeighbourMonitor.instance?.flush()
|
IpNeighbourMonitor.instance?.flushAsync()
|
||||||
if (!wasWorking && item.itemId == R.id.block) {
|
if (!wasWorking && item.itemId == R.id.block) {
|
||||||
SmartSnackbar.make(R.string.clients_popup_block_service_inactive).show()
|
SmartSnackbar.make(R.string.clients_popup_block_service_inactive).show()
|
||||||
}
|
}
|
||||||
@@ -223,9 +223,7 @@ class ClientsFragment : Fragment() {
|
|||||||
binding.clients.itemAnimator = DefaultItemAnimator()
|
binding.clients.itemAnimator = DefaultItemAnimator()
|
||||||
binding.clients.adapter = adapter
|
binding.clients.adapter = adapter
|
||||||
binding.swipeRefresher.setColorSchemeResources(R.color.colorSecondary)
|
binding.swipeRefresher.setColorSchemeResources(R.color.colorSecondary)
|
||||||
binding.swipeRefresher.setOnRefreshListener {
|
binding.swipeRefresher.setOnRefreshListener { IpNeighbourMonitor.instance?.flushAsync() }
|
||||||
IpNeighbourMonitor.instance?.flush()
|
|
||||||
}
|
|
||||||
activityViewModels<ClientViewModel>().value.clients.observe(viewLifecycleOwner) {
|
activityViewModels<ClientViewModel>().value.clients.observe(viewLifecycleOwner) {
|
||||||
adapter.submitList(it.toMutableList())
|
adapter.submitList(it.toMutableList())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,11 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import be.mygod.vpnhotspot.util.readableMessage
|
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
|
||||||
class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
||||||
@@ -26,9 +22,16 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
* PAN Profile
|
* PAN Profile
|
||||||
*/
|
*/
|
||||||
private const val PAN = 5
|
private const val PAN = 5
|
||||||
private val isTetheringOn by lazy {
|
private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") }
|
||||||
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn")
|
private val constructor by lazy {
|
||||||
|
clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java)
|
||||||
}
|
}
|
||||||
|
private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") }
|
||||||
|
|
||||||
|
fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) =
|
||||||
|
constructor.newInstance(context, serviceListener) as BluetoothProfile
|
||||||
|
val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
|
||||||
|
fun BluetoothProfile.closePan() = BluetoothAdapter.getDefaultAdapter()!!.closeProfileProxy(PAN, this)
|
||||||
|
|
||||||
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
|
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
|
||||||
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
||||||
@@ -41,23 +44,8 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
@TargetApi(24)
|
@TargetApi(24)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
|
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
|
||||||
BluetoothAdapter.STATE_ON -> try {
|
BluetoothAdapter.STATE_ON -> {
|
||||||
TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, pendingCallback!!)
|
TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, pendingCallback!!)
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.w(e)
|
|
||||||
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
|
|
||||||
pendingCallback!!.onException()
|
|
||||||
} 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(context, cause.readableMessage, Toast.LENGTH_LONG).show()
|
|
||||||
pendingCallback!!.onException()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
BluetoothAdapter.STATE_OFF, BluetoothAdapter.ERROR -> { }
|
BluetoothAdapter.STATE_OFF, BluetoothAdapter.ERROR -> { }
|
||||||
else -> return // ignore transition states
|
else -> return // ignore transition states
|
||||||
@@ -81,18 +69,18 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var connected = false
|
||||||
private var pan: BluetoothProfile? = null
|
private var pan: BluetoothProfile? = null
|
||||||
var activeFailureCause: Throwable? = null
|
var activeFailureCause: Throwable? = null
|
||||||
/**
|
/**
|
||||||
* Requires BLUETOOTH_PRIVILEGED on API 30+.
|
|
||||||
*
|
|
||||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
|
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
|
||||||
*/
|
*/
|
||||||
val active: Boolean? get() {
|
val active: Boolean? get() {
|
||||||
activeFailureCause = null
|
|
||||||
val pan = pan ?: return null
|
val pan = pan ?: return null
|
||||||
|
if (!connected) return null
|
||||||
|
activeFailureCause = null
|
||||||
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try {
|
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try {
|
||||||
isTetheringOn(pan) as Boolean
|
pan.isTetheringOn
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
activeFailureCause = e.cause ?: e
|
activeFailureCause = e.cause ?: e
|
||||||
if (e.cause is SecurityException && Build.VERSION.SDK_INT >= 30) Timber.d(e) else Timber.w(e)
|
if (e.cause is SecurityException && Build.VERSION.SDK_INT >= 30) Timber.d(e) else Timber.w(e)
|
||||||
@@ -104,24 +92,23 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
try {
|
||||||
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(context, this, PAN)
|
pan = pan(context, this)
|
||||||
} catch (e: SecurityException) {
|
} catch (e: InvocationTargetException) {
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
SmartSnackbar.make(e).show()
|
activeFailureCause = e
|
||||||
}
|
}
|
||||||
registerBluetoothStateListener(receiver)
|
registerBluetoothStateListener(receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(profile: Int) {
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
pan = null
|
connected = false
|
||||||
}
|
}
|
||||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
pan = proxy
|
connected = true
|
||||||
stateListener()
|
stateListener()
|
||||||
}
|
}
|
||||||
override fun close() {
|
override fun close() {
|
||||||
app.unregisterReceiver(receiver)
|
app.unregisterReceiver(receiver)
|
||||||
BluetoothAdapter.getDefaultAdapter()?.closeProfileProxy(PAN, pan)
|
pan?.closePan()
|
||||||
pan = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import be.mygod.vpnhotspot.R
|
|||||||
import be.mygod.vpnhotspot.net.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.util.KillableTileService
|
import be.mygod.vpnhotspot.util.KillableTileService
|
||||||
|
import java.net.Inet4Address
|
||||||
|
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
|
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
|
||||||
@@ -24,7 +25,7 @@ abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeigh
|
|||||||
|
|
||||||
protected fun Tile.subtitleDevices(filter: (String) -> Boolean) {
|
protected fun Tile.subtitleDevices(filter: (String) -> Boolean) {
|
||||||
val size = neighbours
|
val size = neighbours
|
||||||
.filter { it.state != IpNeighbour.State.FAILED && filter(it.dev) }
|
.filter { it.ip is Inet4Address && it.state != IpNeighbour.State.FAILED && filter(it.dev) }
|
||||||
.distinctBy { it.lladdr }
|
.distinctBy { it.lladdr }
|
||||||
.size
|
.size
|
||||||
if (size > 0) subtitle(resources.getQuantityString(
|
if (size > 0) subtitle(resources.getQuantityString(
|
||||||
|
|||||||
@@ -210,9 +210,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
|
|||||||
band = SoftApConfigurationCompat.BAND_ANY
|
band = SoftApConfigurationCompat.BAND_ANY
|
||||||
channel = RepeaterService.operatingChannel
|
channel = RepeaterService.operatingChannel
|
||||||
try {
|
try {
|
||||||
val config = withContext(Dispatchers.Default) {
|
val config = P2pSupplicantConfiguration(group)
|
||||||
P2pSupplicantConfiguration(group, RepeaterService.lastMac)
|
config.init(RepeaterService.lastMac)
|
||||||
}
|
|
||||||
holder.config = config
|
holder.config = config
|
||||||
passphrase = config.psk
|
passphrase = config.psk
|
||||||
bssid = config.bssid
|
bssid = config.bssid
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.util.KillableTileService
|
import be.mygod.vpnhotspot.util.KillableTileService
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||||
|
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
@@ -22,13 +23,13 @@ class RepeaterTileService : KillableTileService() {
|
|||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
if (RepeaterService.supported) {
|
if (Services.p2p != null) {
|
||||||
bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
|
bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
|
||||||
} else updateTile()
|
} else updateTile()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopListening() {
|
override fun onStopListening() {
|
||||||
if (RepeaterService.supported) stopAndUnbind(this)
|
if (Services.p2p != null) stopAndUnbind(this)
|
||||||
super.onStopListening()
|
super.onStopListening()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import be.mygod.vpnhotspot.net.TetheringManager
|
|||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
import be.mygod.vpnhotspot.util.readableMessage
|
import be.mygod.vpnhotspot.util.readableMessage
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
|
||||||
sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
||||||
@@ -50,25 +52,12 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
app.logEvent("manage_write_settings") { param("message", e.toString()) }
|
app.logEvent("manage_write_settings") { param("message", e.toString()) }
|
||||||
}
|
}
|
||||||
val started = manager.isStarted
|
if (manager.isStarted) try {
|
||||||
try {
|
manager.stop()
|
||||||
if (started) manager.stop() else manager.start()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.w(e)
|
|
||||||
Toast.makeText(mainActivity, e.readableMessage, Toast.LENGTH_LONG).show()
|
|
||||||
ManageBar.start(itemView.context)
|
|
||||||
} catch (e: InvocationTargetException) {
|
} catch (e: InvocationTargetException) {
|
||||||
if (e.targetException !is SecurityException) Timber.w(e)
|
if (e.targetException !is SecurityException) Timber.w(e)
|
||||||
var cause: Throwable? = e
|
manager.onException(e)
|
||||||
while (cause != null) {
|
} else manager.start()
|
||||||
cause = cause.cause
|
|
||||||
if (cause != null && cause !is InvocationTargetException) {
|
|
||||||
Toast.makeText(mainActivity, cause.readableMessage, Toast.LENGTH_LONG).show()
|
|
||||||
ManageBar.start(itemView.context)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +85,14 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
error?.let { SmartSnackbar.make("$tetherType: ${TetheringManager.tetherErrorMessage(it)}") }
|
error?.let { SmartSnackbar.make("$tetherType: ${TetheringManager.tetherErrorMessage(it)}") }
|
||||||
data.notifyChange()
|
data.notifyChange()
|
||||||
}
|
}
|
||||||
|
override fun onException(e: Exception) {
|
||||||
|
if (e !is InvocationTargetException || e.targetException !is SecurityException) Timber.w(e)
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
val context = parent.context ?: app
|
||||||
|
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
|
||||||
|
ManageBar.start(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
|
||||||
(viewHolder as ViewHolder).manager = this
|
(viewHolder as ViewHolder).manager = this
|
||||||
@@ -124,7 +121,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override val type get() = VIEW_TYPE_WIFI
|
override val type get() = VIEW_TYPE_WIFI
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class Usb(parent: TetheringFragment) : TetherManager(parent) {
|
class Usb(parent: TetheringFragment) : TetherManager(parent) {
|
||||||
@@ -133,7 +130,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override val type get() = VIEW_TYPE_USB
|
override val type get() = VIEW_TYPE_USB
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
|
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
|
||||||
@@ -153,8 +150,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override val type get() = VIEW_TYPE_BLUETOOTH
|
override val type get() = VIEW_TYPE_BLUETOOTH
|
||||||
override val isStarted get() = tethering.active == true
|
override val isStarted get() = tethering.active == true
|
||||||
|
|
||||||
override fun onException() = ManageBar.start(parent.context ?: app)
|
|
||||||
|
|
||||||
private var baseError: CharSequence? = null
|
private var baseError: CharSequence? = null
|
||||||
private fun makeErrorMessage(): CharSequence = listOfNotNull(
|
private fun makeErrorMessage(): CharSequence = listOfNotNull(
|
||||||
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
|
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
|
||||||
@@ -166,7 +161,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
|
|
||||||
override fun start() = BluetoothTethering.start(this)
|
override fun start() = BluetoothTethering.start(this)
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH)
|
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
|
||||||
Thread.sleep(1) // give others a room to breathe
|
Thread.sleep(1) // give others a room to breathe
|
||||||
onTetheringStarted() // force flush state
|
onTetheringStarted() // force flush state
|
||||||
}
|
}
|
||||||
@@ -178,7 +173,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override val type get() = VIEW_TYPE_ETHERNET
|
override val type get() = VIEW_TYPE_ETHERNET
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
class Ncm(parent: TetheringFragment) : TetherManager(parent) {
|
class Ncm(parent: TetheringFragment) : TetherManager(parent) {
|
||||||
@@ -187,7 +182,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
|
|||||||
override val type get() = VIEW_TYPE_NCM
|
override val type get() = VIEW_TYPE_NCM
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
|
|||||||
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
|
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
|
||||||
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
import be.mygod.vpnhotspot.util.ServiceForegroundConnector
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.root.WifiApCommands
|
||||||
import be.mygod.vpnhotspot.util.isNotGone
|
import be.mygod.vpnhotspot.util.*
|
||||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -89,7 +88,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
updateEnabledTypes()
|
updateEnabledTypes()
|
||||||
|
|
||||||
val list = ArrayList<Manager>()
|
val list = ArrayList<Manager>()
|
||||||
if (RepeaterService.supported) list.add(repeaterManager)
|
if (Services.p2p != null) list.add(repeaterManager)
|
||||||
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
|
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
|
||||||
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
|
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
|
||||||
updateMonitorList(activeIfaces - monitoredIfaces)
|
updateMonitorList(activeIfaces - monitoredIfaces)
|
||||||
@@ -150,10 +149,12 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var apConfigurationRunning = false
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
return when (item?.itemId) {
|
return when (item?.itemId) {
|
||||||
R.id.configuration -> item.subMenu.run {
|
R.id.configuration -> item.subMenu.run {
|
||||||
findItem(R.id.configuration_repeater).isNotGone = RepeaterService.supported
|
findItem(R.id.configuration_repeater).isNotGone = Services.p2p != null
|
||||||
findItem(R.id.configuration_temp_hotspot).isNotGone =
|
findItem(R.id.configuration_temp_hotspot).isNotGone =
|
||||||
adapter.localOnlyHotspotManager.binder?.configuration != null
|
adapter.localOnlyHotspotManager.binder?.configuration != null
|
||||||
true
|
true
|
||||||
@@ -170,16 +171,30 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
}.showAllowingStateLoss(parentFragmentManager)
|
}.showAllowingStateLoss(parentFragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.configuration_ap -> try {
|
R.id.configuration_ap -> if (apConfigurationRunning) false else {
|
||||||
WifiApDialogFragment().apply {
|
apConfigurationRunning = true
|
||||||
arg(WifiApDialogFragment.Arg(WifiApManager.configuration))
|
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
||||||
key()
|
try {
|
||||||
}.showAllowingStateLoss(parentFragmentManager)
|
WifiApManager.configuration
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
if (e.targetException !is SecurityException) Timber.w(e)
|
||||||
|
try {
|
||||||
|
RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }
|
||||||
|
} catch (eRoot: Exception) {
|
||||||
|
eRoot.addSuppressed(e)
|
||||||
|
Timber.w(eRoot)
|
||||||
|
SmartSnackbar.make(eRoot).show()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}?.let { configuration ->
|
||||||
|
WifiApDialogFragment().apply {
|
||||||
|
arg(WifiApDialogFragment.Arg(configuration))
|
||||||
|
key()
|
||||||
|
}.showAllowingStateLoss(parentFragmentManager)
|
||||||
|
}
|
||||||
|
apConfigurationRunning = false
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} catch (e: InvocationTargetException) {
|
|
||||||
if (e.targetException !is SecurityException) Timber.w(e)
|
|
||||||
SmartSnackbar.make(e.targetException).show()
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
@@ -187,13 +202,20 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret ->
|
AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret ->
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) try {
|
if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
|
||||||
WifiApManager.configuration = ret!!.configuration
|
val success = try {
|
||||||
} catch (e: IllegalArgumentException) {
|
WifiApManager.setConfiguration(ret!!.configuration)
|
||||||
Timber.d(e)
|
} catch (e: InvocationTargetException) {
|
||||||
SmartSnackbar.make(R.string.configuration_rejected).show()
|
try {
|
||||||
} catch (e: InvocationTargetException) {
|
RootManager.use { it.execute(WifiApCommands.SetConfiguration(ret!!.configuration)) }
|
||||||
SmartSnackbar.make(e.targetException).show()
|
} catch (eRoot: Exception) {
|
||||||
|
eRoot.addSuppressed(e)
|
||||||
|
Timber.w(eRoot)
|
||||||
|
SmartSnackbar.make(eRoot).show()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success == false) SmartSnackbar.make(R.string.configuration_rejected).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding = FragmentTetheringBinding.inflate(inflater, container, false)
|
binding = FragmentTetheringBinding.inflate(inflater, container, false)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.service.quicksettings.Tile
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import be.mygod.vpnhotspot.App
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.TetheringService
|
import be.mygod.vpnhotspot.TetheringService
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
@@ -20,8 +21,10 @@ import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
|||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import be.mygod.vpnhotspot.util.readableMessage
|
import be.mygod.vpnhotspot.util.readableMessage
|
||||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
import be.mygod.vpnhotspot.util.stopAndUnbind
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
@@ -97,30 +100,17 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inline fun safeInvoker(func: () -> Unit) = try {
|
|
||||||
func()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.w(e)
|
|
||||||
Toast.makeText(this, e.readableMessage, 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.readableMessage, Toast.LENGTH_LONG).show()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
val interested = interested ?: return
|
val interested = interested ?: return
|
||||||
if (interested.isEmpty()) safeInvoker { start() } else {
|
if (interested.isEmpty()) start() else {
|
||||||
val binder = binder
|
val binder = binder
|
||||||
if (binder == null) tapPending = true else {
|
if (binder == null) tapPending = true else {
|
||||||
val inactive = interested.filterNot(binder::isActive)
|
val inactive = interested.filterNot(binder::isActive)
|
||||||
if (inactive.isEmpty()) safeInvoker { stop() }
|
if (inactive.isEmpty()) try {
|
||||||
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
stop()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onException(e)
|
||||||
|
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +122,12 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
error?.let { Toast.makeText(this, TetheringManager.tetherErrorMessage(it), Toast.LENGTH_LONG).show() }
|
error?.let { Toast.makeText(this, TetheringManager.tetherErrorMessage(it), Toast.LENGTH_LONG).show() }
|
||||||
updateTile()
|
updateTile()
|
||||||
}
|
}
|
||||||
|
override fun onException(e: Exception) {
|
||||||
|
if (e !is InvocationTargetException || e.targetException !is SecurityException) Timber.w(e)
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
Toast.makeText(this@TetheringTileService, e.readableMessage, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Wifi : TetheringTileService() {
|
class Wifi : TetheringTileService() {
|
||||||
override val labelString get() = R.string.tethering_manage_wifi
|
override val labelString get() = R.string.tethering_manage_wifi
|
||||||
@@ -139,14 +135,14 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
override val icon get() = R.drawable.ic_device_wifi_tethering
|
override val icon get() = R.drawable.ic_device_wifi_tethering
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
|
||||||
}
|
}
|
||||||
class Usb : TetheringTileService() {
|
class Usb : TetheringTileService() {
|
||||||
override val labelString get() = R.string.tethering_manage_usb
|
override val labelString get() = R.string.tethering_manage_usb
|
||||||
override val tetherType get() = TetherType.USB
|
override val tetherType get() = TetherType.USB
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
|
||||||
}
|
}
|
||||||
class Bluetooth : TetheringTileService() {
|
class Bluetooth : TetheringTileService() {
|
||||||
private var tethering: BluetoothTethering? = null
|
private var tethering: BluetoothTethering? = null
|
||||||
@@ -156,7 +152,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
|
|
||||||
override fun start() = BluetoothTethering.start(this)
|
override fun start() = BluetoothTethering.start(this)
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH)
|
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
|
||||||
Thread.sleep(1) // give others a room to breathe
|
Thread.sleep(1) // give others a room to breathe
|
||||||
onTetheringStarted() // force flush state
|
onTetheringStarted() // force flush state
|
||||||
}
|
}
|
||||||
@@ -202,12 +198,15 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
val binder = binder
|
val binder = binder
|
||||||
if (binder == null) tapPending = true else {
|
if (binder == null) tapPending = true else {
|
||||||
val inactive = (interested ?: return).filterNot(binder::isActive)
|
val inactive = (interested ?: return).filterNot(binder::isActive)
|
||||||
if (inactive.isEmpty()) safeInvoker { stop() }
|
if (inactive.isEmpty()) try {
|
||||||
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
stop()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onException(e)
|
||||||
|
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
|
||||||
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false -> safeInvoker { start() }
|
false -> start()
|
||||||
else -> tapPending = true
|
else -> tapPending = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +217,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
override val tetherType get() = TetherType.ETHERNET
|
override val tetherType get() = TetherType.ETHERNET
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
|
||||||
}
|
}
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
class Ncm : TetheringTileService() {
|
class Ncm : TetheringTileService() {
|
||||||
@@ -226,7 +225,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
|
|||||||
override val tetherType get() = TetherType.NCM
|
override val tetherType get() = TetherType.NCM
|
||||||
|
|
||||||
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
|
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
|
||||||
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM)
|
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.net
|
|||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -32,11 +33,11 @@ object DhcpWorkaround : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
RootSession.use {
|
RootSession.use {
|
||||||
try {
|
try {
|
||||||
it.exec("ip rule $action iif lo uidrange 0-0 lookup local_network priority 11000")
|
it.exec("ip rule $action iif lo uidrange 0-0 lookup local_network priority 11000")
|
||||||
} catch (e: RootSession.UnexpectedOutputException) {
|
} catch (e: RoutingCommands.UnexpectedOutputException) {
|
||||||
if (e.result.out.isEmpty() && (e.result.code == 2 || e.result.code == 254) && if (enabled) {
|
if (e.result.out.isEmpty() && (e.result.exit == 2 || e.result.exit == 254) && if (enabled) {
|
||||||
e.result.err.joinToString("\n") == "RTNETLINK answers: File exists"
|
e.result.err == "RTNETLINK answers: File exists"
|
||||||
} else {
|
} else {
|
||||||
e.result.err.joinToString("\n") == "RTNETLINK answers: No such file or directory"
|
e.result.err == "RTNETLINK answers: No such file or directory"
|
||||||
}) return@use
|
}) return@use
|
||||||
Timber.w(IOException("Failed to tweak dhcp workaround rule", e))
|
Timber.w(IOException("Failed to tweak dhcp workaround rule", e))
|
||||||
SmartSnackbar.make(e).show()
|
SmartSnackbar.make(e).show()
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package be.mygod.vpnhotspot.net
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
|
import be.mygod.vpnhotspot.root.ReadArp
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@@ -35,6 +38,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
|||||||
private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
|
private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
|
||||||
|
|
||||||
fun parse(line: String): List<IpNeighbour> {
|
fun parse(line: String): List<IpNeighbour> {
|
||||||
|
if (line.isBlank()) return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val match = parser.matchEntire(line)!!
|
val match = parser.matchEntire(line)!!
|
||||||
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty
|
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty
|
||||||
@@ -87,17 +91,24 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
|||||||
private const val ARP_CACHE_EXPIRE = 1L * 1000 * 1000 * 1000
|
private const val ARP_CACHE_EXPIRE = 1L * 1000 * 1000 * 1000
|
||||||
private var arpCache = emptyList<List<String>>()
|
private var arpCache = emptyList<List<String>>()
|
||||||
private var arpCacheTime = -ARP_CACHE_EXPIRE
|
private var arpCacheTime = -ARP_CACHE_EXPIRE
|
||||||
|
private fun Sequence<String>.makeArp() = this
|
||||||
|
.map { it.split(spaces) }
|
||||||
|
.drop(1)
|
||||||
|
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
|
||||||
|
.toList()
|
||||||
private fun arp(): List<List<String>> {
|
private fun arp(): List<List<String>> {
|
||||||
if (System.nanoTime() - arpCacheTime >= ARP_CACHE_EXPIRE) try {
|
if (System.nanoTime() - arpCacheTime >= ARP_CACHE_EXPIRE) try {
|
||||||
arpCache = File("/proc/net/arp").bufferedReader().readLines()
|
arpCache = File("/proc/net/arp").bufferedReader().lineSequence().makeArp()
|
||||||
.asSequence()
|
|
||||||
.map { it.split(spaces) }
|
|
||||||
.drop(1)
|
|
||||||
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
|
|
||||||
.toList()
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
if (e !is FileNotFoundException || Build.VERSION.SDK_INT < 29 ||
|
if (e is FileNotFoundException && Build.VERSION.SDK_INT >= 29 &&
|
||||||
(e.cause as? ErrnoException)?.errno != OsConstants.EACCES) Timber.w(e)
|
(e.cause as? ErrnoException)?.errno == OsConstants.EACCES) try {
|
||||||
|
arpCache = runBlocking {
|
||||||
|
RootManager.use { it.execute(ReadArp()) }
|
||||||
|
}.value.lineSequence().makeArp()
|
||||||
|
} catch (eRoot: Exception) {
|
||||||
|
eRoot.addSuppressed(e)
|
||||||
|
Timber.w(eRoot)
|
||||||
|
} else Timber.w(e)
|
||||||
}
|
}
|
||||||
return arpCache
|
return arpCache
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
|||||||
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
||||||
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
|
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.BufferedWriter
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
@@ -41,42 +44,38 @@ class Routing(private val caller: Any, private val downstream: String,
|
|||||||
private const val RULE_PRIORITY_UPSTREAM_FALLBACK = 17900
|
private const val RULE_PRIORITY_UPSTREAM_FALLBACK = 17900
|
||||||
private const val RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM = 17980
|
private const val RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM = 17980
|
||||||
|
|
||||||
/**
|
const val IPTABLES ="iptables -w"
|
||||||
* -w <seconds> is not supported on 7.1-.
|
const val IP6TABLES = "ip6tables -w"
|
||||||
* Fortunately there also isn't a time limit for starting a foreground service back in 7.1-.
|
|
||||||
*
|
|
||||||
* Source: https://android.googlesource.com/platform/external/iptables/+/android-5.0.0_r1/iptables/iptables.c#1574
|
|
||||||
*/
|
|
||||||
val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
|
|
||||||
val IP6TABLES = if (Build.VERSION.SDK_INT >= 26) "ip6tables -w 1" else "ip6tables -w"
|
|
||||||
|
|
||||||
fun clean() {
|
fun appendCleanCommands(commands: BufferedWriter) {
|
||||||
|
commands.appendln("$IPTABLES -t nat -F PREROUTING")
|
||||||
|
commands.appendln("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
|
||||||
|
commands.appendln("$IPTABLES -F vpnhotspot_fwd")
|
||||||
|
commands.appendln("$IPTABLES -X vpnhotspot_fwd")
|
||||||
|
commands.appendln("$IPTABLES -F vpnhotspot_acl")
|
||||||
|
commands.appendln("$IPTABLES -X vpnhotspot_acl")
|
||||||
|
commands.appendln("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
|
||||||
|
commands.appendln("$IPTABLES -t nat -F vpnhotspot_masquerade")
|
||||||
|
commands.appendln("$IPTABLES -t nat -X vpnhotspot_masquerade")
|
||||||
|
commands.appendln("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done")
|
||||||
|
commands.appendln("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done")
|
||||||
|
commands.appendln("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done")
|
||||||
|
commands.appendln("$IP6TABLES -F vpnhotspot_filter")
|
||||||
|
commands.appendln("$IP6TABLES -X vpnhotspot_filter")
|
||||||
|
commands.appendln("while ip rule del priority $RULE_PRIORITY_DNS; do done")
|
||||||
|
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
|
||||||
|
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
|
||||||
|
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM; do done")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clean() {
|
||||||
TrafficRecorder.clean()
|
TrafficRecorder.clean()
|
||||||
RootSession.use {
|
RootManager.use { it.execute(RoutingCommands.Clean()) }
|
||||||
it.execQuiet("$IPTABLES -t nat -F PREROUTING")
|
|
||||||
it.execQuiet("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
|
|
||||||
it.execQuiet("$IPTABLES -F vpnhotspot_fwd")
|
|
||||||
it.execQuiet("$IPTABLES -X vpnhotspot_fwd")
|
|
||||||
it.execQuiet("$IPTABLES -F vpnhotspot_acl")
|
|
||||||
it.execQuiet("$IPTABLES -X vpnhotspot_acl")
|
|
||||||
it.execQuiet("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
|
|
||||||
it.execQuiet("$IPTABLES -t nat -F vpnhotspot_masquerade")
|
|
||||||
it.execQuiet("$IPTABLES -t nat -X vpnhotspot_masquerade")
|
|
||||||
it.execQuiet("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done")
|
|
||||||
it.execQuiet("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done")
|
|
||||||
it.execQuiet("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done")
|
|
||||||
it.execQuiet("$IP6TABLES -F vpnhotspot_filter")
|
|
||||||
it.execQuiet("$IP6TABLES -X vpnhotspot_filter")
|
|
||||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_DNS; do done")
|
|
||||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
|
|
||||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
|
|
||||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM; do done")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RootSession.Transaction.iptables(command: String, revert: String) {
|
private fun RootSession.Transaction.iptables(command: String, revert: String) {
|
||||||
val result = execQuiet(command, revert)
|
val result = execQuiet(command, revert)
|
||||||
val message = RootSession.checkOutput(command, result, err = false)
|
val message = result.message(listOf(command), err = false)
|
||||||
if (result.err.isNotEmpty()) Timber.i(message) // busy wait message
|
if (result.err.isNotEmpty()) Timber.i(message) // busy wait message
|
||||||
}
|
}
|
||||||
private fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") =
|
private fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") =
|
||||||
@@ -88,9 +87,9 @@ class Routing(private val caller: Any, private val downstream: String,
|
|||||||
|
|
||||||
private fun RootSession.Transaction.ndc(name: String, command: String, revert: String? = null) {
|
private fun RootSession.Transaction.ndc(name: String, command: String, revert: String? = null) {
|
||||||
val result = execQuiet(command, revert)
|
val result = execQuiet(command, revert)
|
||||||
val log = RootSession.checkOutput(command, result,
|
val suffix = "200 0 $name operation succeeded\n"
|
||||||
result.out.lastOrNull() != "200 0 $name operation succeeded")
|
result.check(listOf(command), !result.out.endsWith(suffix))
|
||||||
if (result.out.size > 1) Timber.i(log)
|
if (result.out.length > suffix.length) Timber.i(result.message(listOf(command), true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +269,7 @@ class Routing(private val caller: Any, private val downstream: String,
|
|||||||
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
|
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
|
||||||
"ndc ipfwd disable vpnhotspot_$downstream")
|
"ndc ipfwd disable vpnhotspot_$downstream")
|
||||||
return
|
return
|
||||||
} catch (e: RootSession.UnexpectedOutputException) {
|
} catch (e: RoutingCommands.UnexpectedOutputException) {
|
||||||
Timber.w(IOException("ndc ipfwd enable failure", e))
|
Timber.w(IOException("ndc ipfwd enable failure", e))
|
||||||
}
|
}
|
||||||
transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")
|
transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package be.mygod.vpnhotspot.net
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
|
import be.mygod.vpnhotspot.root.SettingsGlobalPut
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
|
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
|
||||||
@@ -16,11 +18,18 @@ import be.mygod.vpnhotspot.util.RootSession
|
|||||||
@RequiresApi(27)
|
@RequiresApi(27)
|
||||||
object TetherOffloadManager {
|
object TetherOffloadManager {
|
||||||
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
||||||
var enabled: Boolean
|
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
||||||
get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
suspend fun setEnabled(value: Boolean) {
|
||||||
set(value) {
|
val int = if (value) 0 else 1
|
||||||
RootSession.use {
|
try {
|
||||||
it.exec("settings put global $TETHER_OFFLOAD_DISABLED ${if (value) 0 else 1}")
|
check(Settings.Global.putInt(Services.context.contentResolver, TETHER_OFFLOAD_DISABLED, int))
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
try {
|
||||||
|
RootManager.use { it.execute(SettingsGlobalPut(TETHER_OFFLOAD_DISABLED, int.toString())) }
|
||||||
|
} catch (eRoot: Exception) {
|
||||||
|
eRoot.addSuppressed(e)
|
||||||
|
throw eRoot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,19 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.collection.SparseArrayCompat
|
import androidx.collection.SparseArrayCompat
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
|
import be.mygod.vpnhotspot.root.StartTethering
|
||||||
|
import be.mygod.vpnhotspot.root.StopTethering
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import be.mygod.vpnhotspot.util.callSuper
|
import be.mygod.vpnhotspot.util.callSuper
|
||||||
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
|
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
|
||||||
import com.android.dx.stock.ProxyBuilder
|
import com.android.dx.stock.ProxyBuilder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
@@ -49,7 +57,10 @@ object TetheringManager {
|
|||||||
*/
|
*/
|
||||||
fun onTetheringFailed(error: Int? = null) { }
|
fun onTetheringFailed(error: Int? = null) { }
|
||||||
|
|
||||||
fun onException() { }
|
/**
|
||||||
|
* ADDED: Called when a local Exception occurred.
|
||||||
|
*/
|
||||||
|
fun onException(e: Exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,7 +141,6 @@ object TetheringManager {
|
|||||||
/**
|
/**
|
||||||
* Ncm local tethering type.
|
* Ncm local tethering type.
|
||||||
*
|
*
|
||||||
* Requires NETWORK_SETTINGS permission, which is sadly not obtainable.
|
|
||||||
* @see [startTethering]
|
* @see [startTethering]
|
||||||
*/
|
*/
|
||||||
@RequiresApi(30)
|
@RequiresApi(30)
|
||||||
@@ -149,7 +159,7 @@ object TetheringManager {
|
|||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val instance by lazy @TargetApi(30) {
|
private val instance by lazy @TargetApi(30) {
|
||||||
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
|
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
|
||||||
val service = app.getSystemService(TETHERING_SERVICE)
|
val service = Services.context.getSystemService(TETHERING_SERVICE)
|
||||||
service
|
service
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
@@ -211,12 +221,64 @@ object TetheringManager {
|
|||||||
|
|
||||||
private fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
|
private fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
|
||||||
|
|
||||||
|
@Deprecated("Legacy API")
|
||||||
|
@RequiresApi(24)
|
||||||
|
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||||
|
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||||
|
val reference = WeakReference(callback)
|
||||||
|
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
|
||||||
|
dexCache(cacheDir)
|
||||||
|
handler { proxy, method, args ->
|
||||||
|
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
|
||||||
|
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||||
|
when (method.name) {
|
||||||
|
"onTetheringStarted" -> callback?.onTetheringStarted()
|
||||||
|
"onTetheringFailed" -> callback?.onTetheringFailed()
|
||||||
|
else -> ProxyBuilder.callSuper(proxy, method, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
|
||||||
|
}
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun startTethering(type: Int, exemptFromEntitlementCheck: Boolean, showProvisioningUi: Boolean, executor: Executor,
|
||||||
|
proxy: Any) {
|
||||||
|
startTethering(instance, newTetheringRequestBuilder.newInstance(type).let { builder ->
|
||||||
|
// setting exemption requires TETHER_PRIVILEGED permission
|
||||||
|
if (exemptFromEntitlementCheck) setExemptFromEntitlementCheck(builder, true)
|
||||||
|
setShouldShowEntitlementUi(builder, showProvisioningUi)
|
||||||
|
build(builder)
|
||||||
|
}, executor, proxy)
|
||||||
|
}
|
||||||
|
@RequiresApi(30)
|
||||||
|
fun proxy(callback: StartTetheringCallback): Any {
|
||||||
|
val reference = WeakReference(callback)
|
||||||
|
return Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader,
|
||||||
|
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
|
||||||
|
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||||
|
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||||
|
return when (val name = method.name) {
|
||||||
|
"onTetheringStarted" -> {
|
||||||
|
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
|
||||||
|
callback?.onTetheringStarted()
|
||||||
|
}
|
||||||
|
"onTetheringFailed" -> {
|
||||||
|
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
|
||||||
|
callback?.onTetheringFailed(args?.get(0) as Int)
|
||||||
|
}
|
||||||
|
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Runs tether provisioning for the given type if needed and then starts tethering if
|
* Runs tether provisioning for the given type if needed and then starts tethering if
|
||||||
* the check succeeds. If no carrier provisioning is required for tethering, tethering is
|
* the check succeeds. If no carrier provisioning is required for tethering, tethering is
|
||||||
* enabled immediately. If provisioning fails, tethering will not be enabled. It also
|
* enabled immediately. If provisioning fails, tethering will not be enabled. It also
|
||||||
* schedules tether provisioning re-checks if appropriate.
|
* schedules tether provisioning re-checks if appropriate.
|
||||||
*
|
*
|
||||||
|
* CHANGED BEHAVIOR: This method will not throw Exceptions, instead, callback.onException will be called.
|
||||||
|
*
|
||||||
* @param type The type of tethering to start. Must be one of
|
* @param type The type of tethering to start. Must be one of
|
||||||
* {@link ConnectivityManager.TETHERING_WIFI},
|
* {@link ConnectivityManager.TETHERING_WIFI},
|
||||||
* {@link ConnectivityManager.TETHERING_USB}, or
|
* {@link ConnectivityManager.TETHERING_USB}, or
|
||||||
@@ -234,48 +296,57 @@ object TetheringManager {
|
|||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||||
handler: Handler? = null) {
|
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||||
val reference = WeakReference(callback)
|
if (Build.VERSION.SDK_INT >= 30) try {
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
val proxy = proxy(callback)
|
||||||
val request = newTetheringRequestBuilder.newInstance(type).let { builder ->
|
val executor = handler.makeExecutor()
|
||||||
// setting exemption requires TETHER_PRIVILEGED permission
|
try {
|
||||||
if (app.checkSelfPermission("android.permission.TETHER_PRIVILEGED") ==
|
startTethering(type, true, showProvisioningUi, executor, proxy)
|
||||||
PackageManager.PERMISSION_GRANTED) setExemptFromEntitlementCheck(builder, true)
|
} catch (e1: InvocationTargetException) {
|
||||||
setShouldShowEntitlementUi(builder, showProvisioningUi)
|
if (e1.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
|
||||||
build(builder)
|
val result = try {
|
||||||
|
RootManager.use { it.execute(StartTethering(type, showProvisioningUi)) }
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
e2.addSuppressed(e1)
|
||||||
|
try {
|
||||||
|
// last resort: start tethering without trying to bypass entitlement check
|
||||||
|
startTethering(type, false, showProvisioningUi, executor, proxy)
|
||||||
|
Timber.w(e2)
|
||||||
|
} catch (e3: Exception) {
|
||||||
|
e3.addSuppressed(e2)
|
||||||
|
Timber.w(e3)
|
||||||
|
callback.onException(e3)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
if (result == null) callback.onTetheringStarted()
|
||||||
|
else callback.onTetheringFailed(result.value)
|
||||||
|
} else callback.onException(e1)
|
||||||
}
|
}
|
||||||
val proxy = Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader,
|
} catch (e: Exception) {
|
||||||
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
|
callback.onException(e)
|
||||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
} else @Suppress("DEPRECATION") try {
|
||||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
startTetheringLegacy(type, showProvisioningUi, callback, handler, cacheDir)
|
||||||
return when (val name = method.name) {
|
} catch (e: InvocationTargetException) {
|
||||||
"onTetheringStarted" -> {
|
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
|
||||||
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
|
val result = try {
|
||||||
callback?.onTetheringStarted()
|
val rootCache = File(cacheDir, "root")
|
||||||
}
|
rootCache.mkdirs()
|
||||||
"onTetheringFailed" -> {
|
check(rootCache.exists()) { "Creating root cache dir failed" }
|
||||||
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
|
RootManager.use {
|
||||||
callback?.onTetheringFailed(args?.getOrNull(0) as? Int?)
|
it.execute(be.mygod.vpnhotspot.root.StartTetheringLegacy(
|
||||||
}
|
rootCache, type, showProvisioningUi))
|
||||||
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
|
}.value
|
||||||
}
|
} catch (eRoot: Exception) {
|
||||||
|
eRoot.addSuppressed(e)
|
||||||
|
Timber.w(eRoot)
|
||||||
|
callback.onException(eRoot)
|
||||||
|
return@launch
|
||||||
}
|
}
|
||||||
})
|
if (result) callback.onTetheringStarted() else callback.onTetheringFailed()
|
||||||
startTethering(instance, request, handler.makeExecutor(), proxy)
|
} else callback.onException(e)
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
|
callback.onException(e)
|
||||||
dexCache(app.deviceStorage.cacheDir)
|
|
||||||
handler { proxy, method, args ->
|
|
||||||
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
|
|
||||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
|
||||||
when (method.name) {
|
|
||||||
"onTetheringStarted" -> callback?.onTetheringStarted()
|
|
||||||
"onTetheringFailed" -> callback?.onTetheringFailed()
|
|
||||||
else -> ProxyBuilder.callSuper(proxy, method, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
startTetheringLegacy(app.connectivity, type, showProvisioningUi, proxy, handler)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +361,21 @@ object TetheringManager {
|
|||||||
*/
|
*/
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
fun stopTethering(type: Int) {
|
fun stopTethering(type: Int) {
|
||||||
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type) else stopTetheringLegacy(app.connectivity, type)
|
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
|
||||||
|
else stopTetheringLegacy(Services.connectivity, type)
|
||||||
|
}
|
||||||
|
@RequiresApi(24)
|
||||||
|
fun stopTethering(type: Int, callback: (Exception) -> Unit) = try {
|
||||||
|
stopTethering(type)
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
|
||||||
|
try {
|
||||||
|
RootManager.use { it.execute(StopTethering(type)) }
|
||||||
|
} catch (eRoot: Exception) {
|
||||||
|
eRoot.addSuppressed(e)
|
||||||
|
callback(eRoot)
|
||||||
|
}
|
||||||
|
} else callback(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -517,7 +602,7 @@ object TetheringManager {
|
|||||||
* @return error The error code of the last error tethering or untethering the named
|
* @return error The error code of the last error tethering or untethering the named
|
||||||
* interface
|
* interface
|
||||||
*/
|
*/
|
||||||
fun getLastTetherError(iface: String): Int = getLastTetherError(app.connectivity, iface) as Int
|
fun getLastTetherError(iface: String): Int = getLastTetherError(Services.connectivity, iface) as Int
|
||||||
|
|
||||||
// tether errors defined in ConnectivityManager up to Android 10
|
// tether errors defined in ConnectivityManager up to Android 10
|
||||||
private val tetherErrors29 = arrayOf("TETHER_ERROR_NO_ERROR", "TETHER_ERROR_UNKNOWN_IFACE",
|
private val tetherErrors29 = arrayOf("TETHER_ERROR_NO_ERROR", "TETHER_ERROR_UNKNOWN_IFACE",
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.net.*
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.LinkProperties
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -23,7 +26,7 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
|||||||
.build()
|
.build()
|
||||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
val properties = app.connectivity.getLinkProperties(network)
|
val properties = Services.connectivity.getLinkProperties(network)
|
||||||
val ifname = properties?.interfaceName ?: return
|
val ifname = properties?.interfaceName ?: return
|
||||||
var switching = false
|
var switching = false
|
||||||
synchronized(this@DefaultNetworkMonitor) {
|
synchronized(this@DefaultNetworkMonitor) {
|
||||||
@@ -83,9 +86,9 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
||||||
app.connectivity.registerDefaultNetworkCallback(networkCallback)
|
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
|
||||||
} else try {
|
} else try {
|
||||||
app.connectivity.requestNetwork(networkRequest, networkCallback)
|
Services.connectivity.requestNetwork(networkRequest, networkCallback)
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
|
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
|
||||||
if (Build.VERSION.SDK_INT != 23) throw e
|
if (Build.VERSION.SDK_INT != 23) throw e
|
||||||
@@ -98,7 +101,7 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
|||||||
|
|
||||||
override fun destroyLocked() {
|
override fun destroyLocked() {
|
||||||
if (!registered) return
|
if (!registered) return
|
||||||
app.connectivity.unregisterNetworkCallback(networkCallback)
|
Services.connectivity.unregisterNetworkCallback(networkCallback)
|
||||||
registered = false
|
registered = false
|
||||||
currentNetwork = null
|
currentNetwork = null
|
||||||
currentLinkProperties = null
|
currentLinkProperties = null
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
import android.net.LinkProperties
|
import android.net.LinkProperties
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ class InterfaceMonitor(val iface: String) : UpstreamMonitor() {
|
|||||||
private var registered = false
|
private var registered = false
|
||||||
override var currentIface: String? = null
|
override var currentIface: String? = null
|
||||||
private set
|
private set
|
||||||
override val currentLinkProperties get() = app.connectivity.allNetworks
|
override val currentLinkProperties get() = Services.connectivity.allNetworks
|
||||||
.map { app.connectivity.getLinkProperties(it) }
|
.map { Services.connectivity.getLinkProperties(it) }
|
||||||
.singleOrNull { it?.interfaceName == iface }
|
.singleOrNull { it?.interfaceName == iface }
|
||||||
|
|
||||||
override fun registerCallbackLocked(callback: Callback) {
|
override fun registerCallbackLocked(callback: Callback) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class IpLinkMonitor private constructor() : IpMonitor() {
|
|||||||
monitor = IpLinkMonitor()
|
monitor = IpLinkMonitor()
|
||||||
instance = monitor
|
instance = monitor
|
||||||
}
|
}
|
||||||
monitor.flush()
|
monitor.flushAsync()
|
||||||
}
|
}
|
||||||
fun unregisterCallback(owner: Any) = synchronized(this) {
|
fun unregisterCallback(owner: Any) = synchronized(this) {
|
||||||
if (callbacks.remove(owner) == null || callbacks.isNotEmpty()) return@synchronized
|
if (callbacks.remove(owner) == null || callbacks.isNotEmpty()) return@synchronized
|
||||||
|
|||||||
@@ -4,20 +4,25 @@ import android.os.Build
|
|||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import be.mygod.librootkotlinx.RootServer
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.BuildConfig
|
import be.mygod.vpnhotspot.BuildConfig
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.root.ProcessData
|
||||||
|
import be.mygod.vpnhotspot.root.ProcessListener
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
|
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InterruptedIOException
|
import java.io.InterruptedIOException
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.ScheduledExecutorService
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
abstract class IpMonitor : Runnable {
|
abstract class IpMonitor {
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY = "service.ipMonitor"
|
const val KEY = "service.ipMonitor"
|
||||||
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
|
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
|
||||||
@@ -51,7 +56,7 @@ abstract class IpMonitor : Runnable {
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var destroyed = false
|
private var destroyed = false
|
||||||
private var monitor: Process? = null
|
private var monitor: Process? = null
|
||||||
private var pool: ScheduledExecutorService? = null
|
private val worker = Job()
|
||||||
|
|
||||||
private fun handleProcess(builder: ProcessBuilder) {
|
private fun handleProcess(builder: ProcessBuilder) {
|
||||||
val process = try {
|
val process = try {
|
||||||
@@ -79,8 +84,18 @@ abstract class IpMonitor : Runnable {
|
|||||||
if ((e.cause as? ErrnoException)?.errno != OsConstants.EBADF) Timber.w(e)
|
if ((e.cause as? ErrnoException)?.errno != OsConstants.EBADF) Timber.w(e)
|
||||||
}
|
}
|
||||||
err.join()
|
err.join()
|
||||||
process.waitFor()
|
Timber.d("Monitor process exited with ${process.waitFor()}")
|
||||||
Timber.d("Monitor process exited with ${process.exitValue()}")
|
}
|
||||||
|
private suspend fun handleChannel(channel: ReceiveChannel<ProcessData>) {
|
||||||
|
channel.consumeEach {
|
||||||
|
when (it) {
|
||||||
|
is ProcessData.StdoutLine -> if (errorMatcher.containsMatchIn(it.line)) {
|
||||||
|
Timber.w(it.line)
|
||||||
|
} else processLine(it.line)
|
||||||
|
is ProcessData.StderrLine -> Timber.e(it.line)
|
||||||
|
is ProcessData.Exit -> Timber.d("Root monitor process exited with ${it.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -92,17 +107,64 @@ abstract class IpMonitor : Runnable {
|
|||||||
handleProcess(ProcessBuilder("ip", "monitor", monitoredObject))
|
handleProcess(ProcessBuilder("ip", "monitor", monitoredObject))
|
||||||
if (destroyed) return@thread
|
if (destroyed) return@thread
|
||||||
}
|
}
|
||||||
handleProcess(ProcessBuilder("su", "-c", "exec ip monitor $monitoredObject"))
|
try {
|
||||||
|
runBlocking(EmptyCoroutineContext + worker) {
|
||||||
|
RootManager.use { server ->
|
||||||
|
// while we only need to use this server once, we need to also keep the server alive
|
||||||
|
handleChannel(server.create(ProcessListener(errorMatcher, "ip", "monitor", monitoredObject),
|
||||||
|
this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
if (destroyed) return@thread
|
if (destroyed) return@thread
|
||||||
app.logEvent("ip_monitor_failure")
|
app.logEvent("ip_monitor_failure")
|
||||||
}
|
}
|
||||||
val pool = Executors.newScheduledThreadPool(1)
|
GlobalScope.launch(Dispatchers.IO + worker) {
|
||||||
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
|
var server: RootServer? = null
|
||||||
this.pool = pool
|
try {
|
||||||
|
while (isActive) {
|
||||||
|
delay(1000)
|
||||||
|
server = work(server)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (server != null) RootManager.release(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flush() = thread(name = "${javaClass.simpleName}-flush") { run() }
|
/**
|
||||||
|
* Possibly blocking. Should run in IO dispatcher or use [flushAsync].
|
||||||
|
*/
|
||||||
|
suspend fun flush() = work(null)?.let { RootManager.release(it) }
|
||||||
|
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
|
||||||
|
|
||||||
|
private suspend fun work(server: RootServer?): RootServer? {
|
||||||
|
if (currentMode != Mode.PollRoot) try {
|
||||||
|
poll()
|
||||||
|
return server
|
||||||
|
} catch (e: IOException) {
|
||||||
|
app.logEvent("ip_poll_failure")
|
||||||
|
Timber.d(e)
|
||||||
|
}
|
||||||
|
var newServer = server
|
||||||
|
try {
|
||||||
|
val command = listOf("ip", monitoredObject)
|
||||||
|
val result = (server ?: RootManager.acquire().also { newServer = it })
|
||||||
|
.execute(RoutingCommands.Process(command))
|
||||||
|
result.check(command, false)
|
||||||
|
val lines = result.out.lines()
|
||||||
|
if (lines.any { errorMatcher.containsMatchIn(it) }) throw IOException(result.out)
|
||||||
|
processLines(lines.asSequence())
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
app.logEvent("ip_su_poll_failure") { param("cause", e.message.toString()) }
|
||||||
|
Timber.d(e)
|
||||||
|
}
|
||||||
|
return newServer
|
||||||
|
}
|
||||||
|
|
||||||
private fun poll() {
|
private fun poll() {
|
||||||
val process = ProcessBuilder("ip", monitoredObject)
|
val process = ProcessBuilder("ip", monitoredObject)
|
||||||
@@ -125,32 +187,9 @@ abstract class IpMonitor : Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
if (currentMode != Mode.PollRoot) try {
|
|
||||||
return poll()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
app.logEvent("ip_poll_failure")
|
|
||||||
Timber.d(e)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val command = "ip $monitoredObject"
|
|
||||||
RootSession.use { shell ->
|
|
||||||
val result = shell.execQuiet(command)
|
|
||||||
RootSession.checkOutput(command, result, false)
|
|
||||||
if (result.out.any { errorMatcher.containsMatchIn(it) }) {
|
|
||||||
throw IOException(result.out.joinToString("\n"))
|
|
||||||
}
|
|
||||||
processLines(result.out.asSequence())
|
|
||||||
}
|
|
||||||
} catch (e: RuntimeException) {
|
|
||||||
app.logEvent("ip_su_poll_failure") { param("cause", e.message.toString()) }
|
|
||||||
Timber.d(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
destroyed = true
|
destroyed = true
|
||||||
monitor?.destroy()
|
monitor?.destroy()
|
||||||
pool?.shutdown()
|
worker.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class IpNeighbourMonitor private constructor() : IpMonitor() {
|
|||||||
if (monitor == null) {
|
if (monitor == null) {
|
||||||
monitor = IpNeighbourMonitor()
|
monitor = IpNeighbourMonitor()
|
||||||
instance = monitor
|
instance = monitor
|
||||||
monitor.flush()
|
monitor.flushAsync()
|
||||||
null
|
null
|
||||||
} else monitor.neighbours.values
|
} else monitor.neighbours.values
|
||||||
}?.let { callback.onIpNeighbourAvailable(it) }
|
}?.let { callback.onIpNeighbourAvailable(it) }
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class TetherTimeoutMonitor(private val context: Context, private val onTimeout:
|
|||||||
var enabled
|
var enabled
|
||||||
get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
|
get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
|
||||||
set(value) {
|
set(value) {
|
||||||
|
// TODO: WRITE_SECURE_SETTINGS permission
|
||||||
check(Settings.Global.putInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0))
|
check(Settings.Global.putInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0))
|
||||||
}
|
}
|
||||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
import android.util.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
|
import androidx.collection.set
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
@@ -63,10 +64,11 @@ object TrafficRecorder {
|
|||||||
loop@ for (line in RootSession.use {
|
loop@ for (line in RootSession.use {
|
||||||
val command = "$IPTABLES -nvx -L vpnhotspot_acl"
|
val command = "$IPTABLES -nvx -L vpnhotspot_acl"
|
||||||
val result = it.execQuiet(command)
|
val result = it.execQuiet(command)
|
||||||
val message = RootSession.checkOutput(command, result, false, false)
|
val message = result.message(listOf(command))
|
||||||
if (result.err.isNotEmpty()) Timber.i(message)
|
if (result.err.isNotEmpty()) Timber.i(message)
|
||||||
result.out.drop(2)
|
result.out.lineSequence().drop(2)
|
||||||
}) {
|
}) {
|
||||||
|
if (line.isBlank()) continue
|
||||||
val columns = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }
|
val columns = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }
|
||||||
try {
|
try {
|
||||||
check(columns.size >= 9)
|
check(columns.size >= 9)
|
||||||
@@ -104,7 +106,7 @@ object TrafficRecorder {
|
|||||||
}
|
}
|
||||||
if (oldRecord.id != null) {
|
if (oldRecord.id != null) {
|
||||||
check(records.put(key, record) == oldRecord)
|
check(records.put(key, record) == oldRecord)
|
||||||
oldRecords.put(oldRecord.id!!, oldRecord)
|
oldRecords[oldRecord.id!!] = oldRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> check(false)
|
else -> check(false)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
import android.net.*
|
import android.net.ConnectivityManager
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import android.net.LinkProperties
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -21,7 +24,7 @@ object VpnMonitor : UpstreamMonitor() {
|
|||||||
}
|
}
|
||||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
val properties = app.connectivity.getLinkProperties(network)
|
val properties = Services.connectivity.getLinkProperties(network)
|
||||||
val ifname = properties?.interfaceName ?: return
|
val ifname = properties?.interfaceName ?: return
|
||||||
var switching = false
|
var switching = false
|
||||||
synchronized(this@VpnMonitor) {
|
synchronized(this@VpnMonitor) {
|
||||||
@@ -88,14 +91,14 @@ object VpnMonitor : UpstreamMonitor() {
|
|||||||
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties)
|
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.connectivity.registerNetworkCallback(request, networkCallback)
|
Services.connectivity.registerNetworkCallback(request, networkCallback)
|
||||||
registered = true
|
registered = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun destroyLocked() {
|
override fun destroyLocked() {
|
||||||
if (!registered) return
|
if (!registered) return
|
||||||
app.connectivity.unregisterNetworkCallback(networkCallback)
|
Services.connectivity.unregisterNetworkCallback(networkCallback)
|
||||||
registered = false
|
registered = false
|
||||||
available.clear()
|
available.clear()
|
||||||
currentNetwork = null
|
currentNetwork = null
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
package be.mygod.vpnhotspot.net.wifi
|
package be.mygod.vpnhotspot.net.wifi
|
||||||
|
|
||||||
import android.net.wifi.p2p.WifiP2pGroup
|
import android.net.wifi.p2p.WifiP2pGroup
|
||||||
import android.os.Build
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.root.RepeaterCommands
|
||||||
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This parser is based on:
|
* This parser is based on:
|
||||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
||||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
|
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
|
||||||
*/
|
*/
|
||||||
class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerAddress: String? = null) {
|
class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "P2pSupplicantConfiguration"
|
private const val TAG = "P2pSupplicantConfiguration"
|
||||||
private const val CONF_PATH_TREBLE = "/data/vendor/wifi/wpa/p2p_supplicant.conf"
|
|
||||||
private const val CONF_PATH_LEGACY = "/data/misc/wifi/p2p_supplicant.conf"
|
|
||||||
private const val PERSISTENT_MAC = "p2p_device_persistent_mac_addr="
|
private const val PERSISTENT_MAC = "p2p_device_persistent_mac_addr="
|
||||||
private val networkParser =
|
private val networkParser =
|
||||||
"^(bssid=(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})|psk=(ext:|\"(.*)\"|[0-9a-fA-F]{64}\$)?)".toRegex()
|
"^(bssid=(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})|psk=(ext:|\"(.*)\"|[0-9a-fA-F]{64}\$)?)".toRegex()
|
||||||
@@ -36,12 +32,11 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
|||||||
override fun toString() = joinToString("\n")
|
override fun toString() = joinToString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Parser(val lines: List<String>) {
|
private class Parser(val lines: Iterator<String>) {
|
||||||
private val iterator = lines.iterator()
|
|
||||||
lateinit var line: String
|
lateinit var line: String
|
||||||
lateinit var trimmed: String
|
lateinit var trimmed: String
|
||||||
fun next() = if (iterator.hasNext()) {
|
fun next() = if (lines.hasNext()) {
|
||||||
line = iterator.next().apply { trimmed = trimStart('\r', '\t', ' ') }
|
line = lines.next().apply { trimmed = trimStart('\r', '\t', ' ') }
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
@@ -49,14 +44,12 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
|||||||
private data class Content(val lines: ArrayList<Any>, var target: NetworkBlock, var persistentMacLine: Int?,
|
private data class Content(val lines: ArrayList<Any>, var target: NetworkBlock, var persistentMacLine: Int?,
|
||||||
var legacy: Boolean)
|
var legacy: Boolean)
|
||||||
|
|
||||||
private val content = RootSession.use {
|
private lateinit var content: Content
|
||||||
|
suspend fun init(ownerAddress: String? = null) {
|
||||||
val result = ArrayList<Any>()
|
val result = ArrayList<Any>()
|
||||||
var target: NetworkBlock? = null
|
var target: NetworkBlock? = null
|
||||||
var persistentMacLine: Int? = null
|
var persistentMacLine: Int? = null
|
||||||
val command = "cat $CONF_PATH_TREBLE || cat $CONF_PATH_LEGACY"
|
val (config, legacy) = RootManager.use { it.execute(RepeaterCommands.ReadP2pConfig()) }
|
||||||
val shell = it.execQuiet(command)
|
|
||||||
RootSession.checkOutput(command, shell, false, false)
|
|
||||||
val parser = Parser(shell.out)
|
|
||||||
try {
|
try {
|
||||||
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
|
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -68,6 +61,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val parser = Parser(config.lineSequence().iterator())
|
||||||
while (parser.next()) {
|
while (parser.next()) {
|
||||||
if (parser.trimmed.startsWith("network={")) {
|
if (parser.trimmed.startsWith("network={")) {
|
||||||
val block = NetworkBlock()
|
val block = NetworkBlock()
|
||||||
@@ -129,22 +123,22 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
|||||||
if (target == null) target = this
|
if (target == null) target = this
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Content(result, target!!.apply {
|
content = Content(result, target!!.apply {
|
||||||
RepeaterService.lastMac = bssid!!
|
RepeaterService.lastMac = bssid!!
|
||||||
}, persistentMacLine, shell.err.isNotEmpty())
|
}, persistentMacLine, legacy)
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: Exception) {
|
||||||
FirebaseCrashlytics.getInstance().apply {
|
FirebaseCrashlytics.getInstance().apply {
|
||||||
setCustomKey(TAG, parser.lines.joinToString("\n"))
|
setCustomKey(TAG, config)
|
||||||
setCustomKey("$TAG.ownerAddress", ownerAddress.toString())
|
setCustomKey("$TAG.ownerAddress", ownerAddress.toString())
|
||||||
setCustomKey("$TAG.p2pGroup", group.toString())
|
setCustomKey("$TAG.p2pGroup", group.toString())
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val psk = group?.passphrase ?: content.target.psk!!
|
val psk by lazy { group?.passphrase ?: content.target.psk!! }
|
||||||
val bssid = MacAddressCompat.fromString(content.target.bssid!!)
|
val bssid by lazy { MacAddressCompat.fromString(content.target.bssid!!) }
|
||||||
|
|
||||||
fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
|
suspend fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
|
||||||
val (lines, block, persistentMacLine, legacy) = content
|
val (lines, block, persistentMacLine, legacy) = content
|
||||||
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
|
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
|
||||||
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
||||||
@@ -153,25 +147,6 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
|||||||
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
|
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
|
||||||
block[block.bssidLine!!] = "\tbssid=$bssid"
|
block[block.bssidLine!!] = "\tbssid=$bssid"
|
||||||
}
|
}
|
||||||
val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.deviceStorage.cacheDir)
|
RootManager.use { it.execute(RepeaterCommands.WriteP2pConfig(lines.joinToString("\n"), legacy)) }
|
||||||
try {
|
|
||||||
tempFile.printWriter().use { writer ->
|
|
||||||
lines.forEach { writer.println(it) }
|
|
||||||
}
|
|
||||||
// pkill not available on Lollipop. Source: https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
|
|
||||||
RootSession.use {
|
|
||||||
it.exec("cat ${tempFile.absolutePath} > ${if (legacy) CONF_PATH_LEGACY else CONF_PATH_TREBLE}")
|
|
||||||
if (Build.VERSION.SDK_INT >= 23) it.exec("pkill wpa_supplicant") else {
|
|
||||||
val result = try {
|
|
||||||
it.execOut("ps | grep wpa_supplicant").split(whitespaceMatcher).apply { check(size >= 2) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IllegalStateException("wpa_supplicant not found, please toggle Airplane mode manually", e)
|
|
||||||
}
|
|
||||||
it.exec("kill ${result[1]}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (!tempFile.delete()) tempFile.deleteOnExit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ data class SoftApConfigurationCompat(
|
|||||||
/**
|
/**
|
||||||
* TODO
|
* TODO
|
||||||
*/
|
*/
|
||||||
const val BAND_ANY = -1
|
const val BAND_ANY = 0
|
||||||
const val BAND_2GHZ = 0
|
const val BAND_2GHZ = 1
|
||||||
const val BAND_5GHZ = 1
|
const val BAND_5GHZ = 2
|
||||||
const val BAND_6GHZ = 2
|
const val BAND_6GHZ = 3
|
||||||
const val CH_INVALID = 0
|
const val CH_INVALID = 0
|
||||||
|
|
||||||
// TODO: localize?
|
// TODO: localize?
|
||||||
@@ -144,7 +144,9 @@ data class SoftApConfigurationCompat(
|
|||||||
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
|
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setChannel by lazy { classBuilder.getDeclaredMethod("setChannel", Int::class.java) }
|
private val setChannel by lazy {
|
||||||
|
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setClientControlByUserEnabled by lazy {
|
private val setClientControlByUserEnabled by lazy {
|
||||||
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
|
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
|
||||||
@@ -156,7 +158,9 @@ data class SoftApConfigurationCompat(
|
|||||||
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
|
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
|
||||||
}
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setPassphrase by lazy { classBuilder.getDeclaredMethod("setPassphrase", String::class.java) }
|
private val setPassphrase by lazy {
|
||||||
|
classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java)
|
||||||
|
}
|
||||||
@get:RequiresApi(30)
|
@get:RequiresApi(30)
|
||||||
private val setShutdownTimeoutMillis by lazy {
|
private val setShutdownTimeoutMillis by lazy {
|
||||||
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
|
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
|
||||||
@@ -186,7 +190,7 @@ data class SoftApConfigurationCompat(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
preSharedKey,
|
preSharedKey,
|
||||||
if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) else BAND_ANY, // TODO
|
if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) + 1 else BAND_ANY, // TODO
|
||||||
if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else CH_INVALID, // TODO
|
if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else CH_INVALID, // TODO
|
||||||
BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
|
BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
|
||||||
0, // TODO: unsupported field should have @RequiresApi?
|
0, // TODO: unsupported field should have @RequiresApi?
|
||||||
@@ -275,10 +279,10 @@ data class SoftApConfigurationCompat(
|
|||||||
// TODO: can we always call copy constructor?
|
// TODO: can we always call copy constructor?
|
||||||
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
|
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
|
||||||
setSsid(builder, ssid)
|
setSsid(builder, ssid)
|
||||||
// TODO: setSecurityType
|
setPassphrase(builder, passphrase, securityType)
|
||||||
setPassphrase(builder, passphrase)
|
// TODO: how to use these?
|
||||||
setBand(builder, band)
|
// setBand(builder, band)
|
||||||
setChannel(builder, channel)
|
// setChannel(builder, band, channel)
|
||||||
setBssid(builder, bssid?.toPlatform())
|
setBssid(builder, bssid?.toPlatform())
|
||||||
setMaxNumberOfClients(builder, maxNumberOfClients)
|
setMaxNumberOfClients(builder, maxNumberOfClients)
|
||||||
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import android.net.wifi.SoftApConfiguration
|
|||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
|
||||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
|
||||||
object WifiApManager {
|
object WifiApManager {
|
||||||
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
||||||
@@ -22,22 +22,18 @@ object WifiApManager {
|
|||||||
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
|
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
var configuration: SoftApConfigurationCompat
|
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
|
||||||
(getWifiApConfiguration(app.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
|
?: SoftApConfigurationCompat.empty()
|
||||||
?: SoftApConfigurationCompat.empty()
|
} else (getSoftApConfiguration(Services.wifi) as SoftApConfiguration).toCompat()
|
||||||
} else (getSoftApConfiguration(app.wifi) as SoftApConfiguration).toCompat()
|
fun setConfiguration(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||||
set(value) = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
||||||
require(setWifiApConfiguration(app.wifi,
|
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
|
||||||
value.toWifiConfiguration()) as Boolean) { "setWifiApConfiguration failed" }
|
|
||||||
} else require(setSoftApConfiguration(app.wifi, value.toPlatform()) as Boolean) {
|
|
||||||
"setSoftApConfiguration failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cancelLocalOnlyHotspotRequest by lazy {
|
private val cancelLocalOnlyHotspotRequest by lazy {
|
||||||
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
||||||
}
|
}
|
||||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(app.wifi)
|
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private val setWifiApEnabled by lazy {
|
private val setWifiApEnabled by lazy {
|
||||||
@@ -66,13 +62,13 @@ object WifiApManager {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||||
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
|
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
|
||||||
app.wifi.isWifiEnabled = false
|
Services.wifi.isWifiEnabled = false
|
||||||
app.wifi.setWifiApEnabled(wifiConfig, true)
|
Services.wifi.setWifiApEnabled(wifiConfig, true)
|
||||||
}
|
}
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("Not usable since API 26")
|
@Deprecated("Not usable since API 26")
|
||||||
fun stop() {
|
fun stop() {
|
||||||
app.wifi.setWifiApEnabled(null, false)
|
Services.wifi.setWifiApEnabled(null, false)
|
||||||
app.wifi.isWifiEnabled = true
|
Services.wifi.isWifiEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.core.content.getSystemService
|
|||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This mechanism is used to maximize profit. Source: https://stackoverflow.com/a/29657230/2245107
|
* This mechanism is used to maximize profit. Source: https://stackoverflow.com/a/29657230/2245107
|
||||||
@@ -91,7 +92,7 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
|||||||
override fun onDestroy(owner: LifecycleOwner) = app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
override fun onDestroy(owner: LifecycleOwner) = app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val wifi = app.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
|
private val wifi = Services.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
|
||||||
@SuppressLint("WakelockTimeout")
|
@SuppressLint("WakelockTimeout")
|
||||||
private val power = service.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "vpnhotspot:power").apply { acquire() }
|
private val power = service.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "vpnhotspot:power").apply { acquire() }
|
||||||
|
|
||||||
|
|||||||
185
mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt
Normal file
185
mobile/src/main/java/be/mygod/vpnhotspot/root/MiscCommands.kt
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import be.mygod.librootkotlinx.*
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.produce
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InterruptedIOException
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
|
FileOutputStream(path, true).use { out ->
|
||||||
|
val process = ProcessBuilder("sh").redirectErrorStream(true).start()
|
||||||
|
process.outputStream.bufferedWriter().use { commands ->
|
||||||
|
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
|
||||||
|
val iptablesSave = if (Build.VERSION.SDK_INT >= 24) "iptables-save" else
|
||||||
|
File(cacheDir, "iptables-save").absolutePath.also {
|
||||||
|
commands.appendln("ln -sf /system/bin/iptables $it")
|
||||||
|
}
|
||||||
|
val ip6tablesSave = if (Build.VERSION.SDK_INT >= 24) "ip6tables-save" else
|
||||||
|
File(cacheDir, "ip6tables-save").absolutePath.also {
|
||||||
|
commands.appendln("ln -sf /system/bin/ip6tables $it")
|
||||||
|
}
|
||||||
|
commands.appendln("""
|
||||||
|
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|
||||||
|
|dumpsys ${Context.WIFI_P2P_SERVICE}
|
||||||
|
|echo
|
||||||
|
|echo dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|
||||||
|
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|
||||||
|
|echo
|
||||||
|
|echo iptables -t filter
|
||||||
|
|$iptablesSave -t filter
|
||||||
|
|echo
|
||||||
|
|echo iptables -t nat
|
||||||
|
|$iptablesSave -t nat
|
||||||
|
|echo
|
||||||
|
|echo ip6tables-save
|
||||||
|
|$ip6tablesSave
|
||||||
|
|echo
|
||||||
|
|echo ip rule
|
||||||
|
|ip rule
|
||||||
|
|echo
|
||||||
|
|echo ip neigh
|
||||||
|
|ip neigh
|
||||||
|
|echo
|
||||||
|
|echo iptables -nvx -L vpnhotspot_fwd
|
||||||
|
|${Routing.IPTABLES} -nvx -L vpnhotspot_fwd
|
||||||
|
|echo
|
||||||
|
|echo iptables -nvx -L vpnhotspot_acl
|
||||||
|
|${Routing.IPTABLES} -nvx -L vpnhotspot_acl
|
||||||
|
|echo
|
||||||
|
|echo logcat-su
|
||||||
|
|logcat -d
|
||||||
|
""".trimMargin())
|
||||||
|
}
|
||||||
|
process.inputStream.copyTo(out)
|
||||||
|
check(process.waitFor() == 0)
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ProcessData : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data class StdoutLine(val line: String) : ProcessData()
|
||||||
|
@Parcelize
|
||||||
|
data class StderrLine(val line: String) : ProcessData()
|
||||||
|
@Parcelize
|
||||||
|
data class Exit(val code: Int) : ProcessData()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class ProcessListener(private val terminateRegex: Regex,
|
||||||
|
private vararg val command: String) : RootCommandChannel<ProcessData> {
|
||||||
|
override fun create(scope: CoroutineScope) = scope.produce(Dispatchers.IO, capacity) {
|
||||||
|
val process = ProcessBuilder(*command).start()
|
||||||
|
val parent = Job() // we need to destroy process before joining, so we cannot use coroutineScope
|
||||||
|
try {
|
||||||
|
launch(parent) {
|
||||||
|
try {
|
||||||
|
process.inputStream.bufferedReader().forEachLine {
|
||||||
|
check(offer(ProcessData.StdoutLine(it)))
|
||||||
|
if (terminateRegex.containsMatchIn(it)) process.destroy()
|
||||||
|
}
|
||||||
|
} catch (_: InterruptedIOException) { }
|
||||||
|
}
|
||||||
|
launch(parent) {
|
||||||
|
try {
|
||||||
|
process.errorStream.bufferedReader().forEachLine { check(offer(ProcessData.StderrLine(it))) }
|
||||||
|
} catch (_: InterruptedIOException) { }
|
||||||
|
}
|
||||||
|
launch(parent) { check(offer(ProcessData.Exit(process.waitFor()))) }
|
||||||
|
parent.join()
|
||||||
|
} finally {
|
||||||
|
parent.cancel()
|
||||||
|
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
|
||||||
|
parent.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class ReadArp : RootCommand<ParcelableString> {
|
||||||
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
|
ParcelableString(File("/proc/net/arp").bufferedReader().readText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(30)
|
||||||
|
class StartTethering(private val type: Int, private val showProvisioningUi: Boolean) : RootCommand<ParcelableInt?> {
|
||||||
|
override suspend fun execute(): ParcelableInt? {
|
||||||
|
val future = CompletableDeferred<Int?>()
|
||||||
|
val callback = object : TetheringManager.StartTetheringCallback {
|
||||||
|
override fun onTetheringStarted() {
|
||||||
|
future.complete(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTetheringFailed(error: Int?) {
|
||||||
|
future.complete(error!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TetheringManager.startTethering(type, true, showProvisioningUi, Executor {
|
||||||
|
GlobalScope.launch(Dispatchers.Unconfined) { it.run() }
|
||||||
|
}, TetheringManager.proxy(callback))
|
||||||
|
return future.await()?.let { ParcelableInt(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Old API since API 30")
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(24)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
|
||||||
|
private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> {
|
||||||
|
override suspend fun execute(): ParcelableBoolean {
|
||||||
|
val future = CompletableDeferred<Boolean>()
|
||||||
|
val callback = object : TetheringManager.StartTetheringCallback {
|
||||||
|
override fun onTetheringStarted() {
|
||||||
|
future.complete(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTetheringFailed(error: Int?) {
|
||||||
|
check(error == null)
|
||||||
|
future.complete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TetheringManager.startTetheringLegacy(type, showProvisioningUi, callback, cacheDir = cacheDir)
|
||||||
|
return ParcelableBoolean(future.await())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@RequiresApi(24)
|
||||||
|
class StopTethering(private val type: Int) : RootCommandNoResult {
|
||||||
|
override suspend fun execute(): Parcelable? {
|
||||||
|
TetheringManager.stopTethering(type)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class SettingsGlobalPut(val name: String, val value: String) : RootCommandNoResult {
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
|
val process = ProcessBuilder("settings", "put", "global", name, value).redirectErrorStream(true).start()
|
||||||
|
val error = process.inputStream.bufferedReader().readText()
|
||||||
|
check(process.waitFor() == 0)
|
||||||
|
if (error.isNotEmpty()) throw RemoteException(error)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.net.wifi.p2p.WifiP2pManager
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import android.text.TextUtils
|
||||||
|
import be.mygod.librootkotlinx.ParcelableInt
|
||||||
|
import be.mygod.librootkotlinx.RootCommand
|
||||||
|
import be.mygod.librootkotlinx.RootCommandNoResult
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import eu.chainfire.librootjava.RootJava
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object RepeaterCommands {
|
||||||
|
@Parcelize
|
||||||
|
class SetChannel(private val oc: Int, private val forceReinit: Boolean = false) : RootCommand<ParcelableInt?> {
|
||||||
|
override suspend fun execute() = Services.p2p!!.run {
|
||||||
|
if (forceReinit) channel = null
|
||||||
|
val uninitializer = object : WifiP2pManager.ChannelListener {
|
||||||
|
var target: WifiP2pManager.Channel? = null
|
||||||
|
override fun onChannelDisconnected() {
|
||||||
|
if (target == channel) channel = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val channel = channel ?: initialize(RootJava.getSystemContext(),
|
||||||
|
Looper.getMainLooper(), uninitializer)
|
||||||
|
uninitializer.target = channel
|
||||||
|
RepeaterCommands.channel = channel // cache the instance until invalidated
|
||||||
|
val future = CompletableDeferred<Int?>()
|
||||||
|
setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
|
||||||
|
override fun onSuccess() {
|
||||||
|
future.complete(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(reason: Int) {
|
||||||
|
future.complete(reason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
future.await()?.let { ParcelableInt(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult {
|
||||||
|
override suspend fun execute(): Parcelable? {
|
||||||
|
File(if (legacy) CONF_PATH_LEGACY else CONF_PATH_TREBLE).writeText(data)
|
||||||
|
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }!!) {
|
||||||
|
if (File(File(process, "cmdline").inputStream().bufferedReader().readText()
|
||||||
|
.split(Char.MIN_VALUE, limit = 2).first()).name == "wpa_supplicant") {
|
||||||
|
Os.kill(process.name.toInt(), OsConstants.SIGTERM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class ReadP2pConfig : RootCommand<WriteP2pConfig> {
|
||||||
|
private fun test(path: String) = File(path).run {
|
||||||
|
if (canRead()) readText() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun execute(): WriteP2pConfig {
|
||||||
|
test(CONF_PATH_TREBLE)?.let { return WriteP2pConfig(it, false) }
|
||||||
|
test(CONF_PATH_LEGACY)?.let { return WriteP2pConfig(it, true) }
|
||||||
|
throw IllegalStateException("p2p config file not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val CONF_PATH_TREBLE = "/data/vendor/wifi/wpa/p2p_supplicant.conf"
|
||||||
|
private const val CONF_PATH_LEGACY = "/data/misc/wifi/p2p_supplicant.conf"
|
||||||
|
private var channel: WifiP2pManager.Channel? = null
|
||||||
|
}
|
||||||
33
mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt
Normal file
33
mobile/src/main/java/be/mygod/vpnhotspot/root/RootManager.kt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import be.mygod.librootkotlinx.RootCommandNoResult
|
||||||
|
import be.mygod.librootkotlinx.RootServer
|
||||||
|
import be.mygod.librootkotlinx.RootSession
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import be.mygod.vpnhotspot.BuildConfig
|
||||||
|
import be.mygod.vpnhotspot.util.Services
|
||||||
|
import eu.chainfire.librootjava.RootJava
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object RootManager : RootSession() {
|
||||||
|
@Parcelize
|
||||||
|
class RootInit : RootCommandNoResult {
|
||||||
|
override suspend fun execute(): Parcelable? {
|
||||||
|
Services.init(RootJava.getSystemContext())
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createServer() = RootServer { Timber.w(it) }
|
||||||
|
override suspend fun initServer(server: RootServer) {
|
||||||
|
RootServer.DEBUG = BuildConfig.DEBUG
|
||||||
|
try {
|
||||||
|
server.init(app)
|
||||||
|
} finally {
|
||||||
|
server.readUnexpectedStderr()?.let { Timber.e(it) }
|
||||||
|
}
|
||||||
|
server.execute(RootInit())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import be.mygod.librootkotlinx.RootCommand
|
||||||
|
import be.mygod.librootkotlinx.RootCommandOneWay
|
||||||
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
object RoutingCommands {
|
||||||
|
@Parcelize
|
||||||
|
class Clean : RootCommandOneWay {
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
|
val process = ProcessBuilder("sh").redirectErrorStream(true).start()
|
||||||
|
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
|
||||||
|
when (val code = process.waitFor()) {
|
||||||
|
0 -> { }
|
||||||
|
else -> Log.d("RoutingCommands.Clean", "Unexpected exit code $code")
|
||||||
|
}
|
||||||
|
check(process.waitFor() == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnexpectedOutputException(msg: String, val result: ProcessResult) : RuntimeException(msg)
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ProcessResult(val exit: Int, val out: String, val err: String) : Parcelable {
|
||||||
|
fun message(command: List<String>, out: Boolean = this.out.isNotEmpty(),
|
||||||
|
err: Boolean = this.err.isNotEmpty()): String? {
|
||||||
|
val msg = StringBuilder("${command.joinToString(" ")} exited with $exit")
|
||||||
|
if (out) msg.append("\n${this.out}")
|
||||||
|
if (err) msg.append("\n=== stderr ===\n${this.err}")
|
||||||
|
return if (exit != 0 || out || err) msg.toString() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun check(command: List<String>, out: Boolean = this.out.isNotEmpty(),
|
||||||
|
err: Boolean = this.err.isNotEmpty()) = message(command, out, err)?.let { msg ->
|
||||||
|
throw UnexpectedOutputException(msg, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class Process(val command: List<String>, private val redirect: Boolean = false) : RootCommand<ProcessResult> {
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
override suspend fun execute() = withContext(Dispatchers.IO) {
|
||||||
|
val process = ProcessBuilder(command).redirectErrorStream(redirect).start()
|
||||||
|
coroutineScope {
|
||||||
|
val output = async { process.inputStream.bufferedReader().readText() }
|
||||||
|
val error = async { if (redirect) "" else process.errorStream.bufferedReader().readText() }
|
||||||
|
ProcessResult(process.waitFor(), output.await(), error.await())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package be.mygod.vpnhotspot.root
|
||||||
|
|
||||||
|
import be.mygod.librootkotlinx.ParcelableBoolean
|
||||||
|
import be.mygod.librootkotlinx.RootCommand
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
|
||||||
|
import be.mygod.vpnhotspot.net.wifi.WifiApManager
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
object WifiApCommands {
|
||||||
|
@Parcelize
|
||||||
|
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
|
||||||
|
override suspend fun execute() = WifiApManager.configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
|
||||||
|
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,127 +1,50 @@
|
|||||||
package be.mygod.vpnhotspot.util
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
import android.os.Looper
|
import be.mygod.librootkotlinx.RootServer
|
||||||
import androidx.annotation.WorkerThread
|
import be.mygod.vpnhotspot.root.RootManager
|
||||||
import com.topjohnwu.superuser.Shell
|
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
class RootSession : AutoCloseable {
|
class RootSession : AutoCloseable {
|
||||||
companion object {
|
companion object {
|
||||||
private val monitor = ReentrantLock()
|
private val monitor = ReentrantLock()
|
||||||
private fun onUnlock() {
|
|
||||||
if (monitor.holdCount == 1) instance?.startTimeoutLocked()
|
|
||||||
}
|
|
||||||
private fun unlock() {
|
|
||||||
onUnlock()
|
|
||||||
monitor.unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var instance: RootSession? = null
|
fun <T> use(operation: (RootSession) -> T) = monitor.withLock { operation(RootSession()) }
|
||||||
private fun ensureInstance(): RootSession {
|
|
||||||
var instance = instance
|
|
||||||
if (instance == null || !instance.isAlive) instance = RootSession().also { RootSession.instance = it }
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
fun <T> use(operation: (RootSession) -> T) = monitor.withLock {
|
|
||||||
val instance = ensureInstance()
|
|
||||||
instance.haltTimeoutLocked()
|
|
||||||
operation(instance).also { onUnlock() }
|
|
||||||
}
|
|
||||||
fun beginTransaction(): Transaction {
|
fun beginTransaction(): Transaction {
|
||||||
monitor.lock()
|
monitor.lock()
|
||||||
val instance = try {
|
val instance = try {
|
||||||
ensureInstance()
|
RootSession()
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
unlock()
|
monitor.unlock()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
instance.haltTimeoutLocked()
|
|
||||||
return instance.Transaction()
|
return instance.Transaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
fun trimMemory() = monitor.withLock {
|
|
||||||
val instance = instance ?: return
|
|
||||||
instance.haltTimeoutLocked()
|
|
||||||
instance.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkOutput(command: String, result: Shell.Result, out: Boolean = result.out.isNotEmpty(),
|
|
||||||
err: Boolean = result.err.isNotEmpty()): String {
|
|
||||||
val msg = StringBuilder("$command exited with ${result.code}")
|
|
||||||
if (out) result.out.forEach { msg.append("\n$it") }
|
|
||||||
if (err) result.err.forEach { msg.append("\nE $it") }
|
|
||||||
if (!result.isSuccess || out || err) throw UnexpectedOutputException(msg.toString(), result)
|
|
||||||
return msg.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnexpectedOutputException(msg: String, val result: Shell.Result) : RuntimeException(msg)
|
private var server: RootServer? = runBlocking { RootManager.acquire() }
|
||||||
|
|
||||||
init {
|
|
||||||
check(Looper.getMainLooper().thread != Thread.currentThread()) {
|
|
||||||
"Unable to initialize shell in main thread" // https://github.com/topjohnwu/libsu/issues/33
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val shell = Shell.newInstance("su")
|
|
||||||
private val stdout = ArrayList<String>()
|
|
||||||
private val stderr = ArrayList<String>()
|
|
||||||
|
|
||||||
private val isAlive get() = shell.isAlive
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
shell.close()
|
server = null
|
||||||
if (instance == this) instance = null
|
server?.let { runBlocking { RootManager.release(it) } }
|
||||||
}
|
|
||||||
|
|
||||||
private var timeoutJob: Job? = null
|
|
||||||
private fun startTimeoutLocked() {
|
|
||||||
check(timeoutJob == null)
|
|
||||||
timeoutJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
|
||||||
delay(TimeUnit.MINUTES.toMillis(5))
|
|
||||||
monitor.withLock {
|
|
||||||
close()
|
|
||||||
timeoutJob = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun haltTimeoutLocked() {
|
|
||||||
timeoutJob?.cancel()
|
|
||||||
timeoutJob = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't care about the results, but still sync.
|
* Don't care about the results, but still sync.
|
||||||
*/
|
*/
|
||||||
fun submit(command: String) {
|
fun submit(command: String) = execQuiet(command).message(listOf(command))?.let { Timber.v(it) }
|
||||||
val result = execQuiet(command)
|
|
||||||
val err = result.err.joinToString("\n") { "E $it" }.trim()
|
|
||||||
val out = result.out.joinToString("\n").trim()
|
|
||||||
if (result.code != 0 || err.isNotEmpty() || out.isNotEmpty()) {
|
|
||||||
Timber.v("$command exited with ${result.code}")
|
|
||||||
if (err.isNotEmpty()) Timber.v(err)
|
|
||||||
if (out.isNotEmpty()) Timber.v(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun execQuiet(command: String, redirect: Boolean = false): Shell.Result {
|
fun execQuiet(command: String, redirect: Boolean = false) = runBlocking {
|
||||||
stdout.clear()
|
server!!.execute(RoutingCommands.Process(listOf("sh", "-c", command), redirect))
|
||||||
return shell.newJob().add(command).to(stdout, if (redirect) stdout else {
|
|
||||||
stderr.clear()
|
|
||||||
stderr
|
|
||||||
}).exec()
|
|
||||||
}
|
}
|
||||||
fun exec(command: String) = checkOutput(command, execQuiet(command))
|
fun exec(command: String) = execQuiet(command).check(listOf(command))
|
||||||
fun execOut(command: String): String {
|
fun execOut(command: String): String {
|
||||||
val result = execQuiet(command)
|
val result = execQuiet(command)
|
||||||
checkOutput(command, result, false)
|
result.check(listOf(command), false)
|
||||||
return result.out.joinToString("\n")
|
return result.out
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,13 +53,13 @@ class RootSession : AutoCloseable {
|
|||||||
inner class Transaction {
|
inner class Transaction {
|
||||||
private val revertCommands = LinkedList<String>()
|
private val revertCommands = LinkedList<String>()
|
||||||
|
|
||||||
fun exec(command: String, revert: String? = null) = checkOutput(command, execQuiet(command, revert))
|
fun exec(command: String, revert: String? = null) = execQuiet(command, revert).check(listOf(command))
|
||||||
fun execQuiet(command: String, revert: String? = null): Shell.Result {
|
fun execQuiet(command: String, revert: String? = null): RoutingCommands.ProcessResult {
|
||||||
if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails
|
if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails
|
||||||
return this@RootSession.execQuiet(command)
|
return this@RootSession.execQuiet(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun commit() = unlock()
|
fun commit() = monitor.unlock()
|
||||||
|
|
||||||
fun revert() {
|
fun revert() {
|
||||||
if (revertCommands.isEmpty()) return
|
if (revertCommands.isEmpty()) return
|
||||||
@@ -145,15 +68,14 @@ class RootSession : AutoCloseable {
|
|||||||
val shell = if (locked) this@RootSession else {
|
val shell = if (locked) this@RootSession else {
|
||||||
monitor.lock()
|
monitor.lock()
|
||||||
locked = true
|
locked = true
|
||||||
ensureInstance()
|
RootSession()
|
||||||
}
|
}
|
||||||
shell.haltTimeoutLocked()
|
|
||||||
revertCommands.forEach { shell.submit(it) }
|
revertCommands.forEach { shell.submit(it) }
|
||||||
} catch (e: RuntimeException) { // if revert fails, it should fail silently
|
} catch (e: RuntimeException) { // if revert fails, it should fail silently
|
||||||
Timber.d(e)
|
Timber.d(e)
|
||||||
} finally {
|
} finally {
|
||||||
revertCommands.clear()
|
revertCommands.clear()
|
||||||
if (locked) unlock() // commit
|
if (locked) monitor.unlock() // commit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt
Normal file
29
mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package be.mygod.vpnhotspot.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.net.wifi.p2p.WifiP2pManager
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@SuppressLint("LogNotTimber")
|
||||||
|
object Services {
|
||||||
|
lateinit var context: Context
|
||||||
|
fun init(context: Context) {
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
val connectivity by lazy { context.getSystemService<ConnectivityManager>()!! }
|
||||||
|
val p2p by lazy {
|
||||||
|
try {
|
||||||
|
context.getSystemService<WifiP2pManager>()
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
if (android.os.Process.myUid() == 0) Log.w("WifiP2pManager", e) else Timber.w(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val wifi by lazy { context.getSystemService<WifiManager>()!! }
|
||||||
|
}
|
||||||
@@ -25,12 +25,15 @@ import be.mygod.vpnhotspot.net.MacAddressCompat
|
|||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import java.lang.invoke.MethodHandles
|
import java.lang.invoke.MethodHandles
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
|
|
||||||
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
|
val Throwable.readableMessage: String get() = if (this is InvocationTargetException) {
|
||||||
|
targetException.readableMessage
|
||||||
|
} else localizedMessage ?: javaClass.name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a hack: we wrap longs around in 1 billion and such. Hopefully every language counts in base 10 and this works
|
* This is a hack: we wrap longs around in 1 billion and such. Hopefully every language counts in base 10 and this works
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
|
|||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.util.readableMessage
|
import be.mygod.vpnhotspot.util.readableMessage
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.superuser.NoShellException
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
sealed class SmartSnackbar {
|
sealed class SmartSnackbar {
|
||||||
@@ -26,10 +25,7 @@ sealed class SmartSnackbar {
|
|||||||
ToastWrapper(Toast.makeText(app, text, Toast.LENGTH_LONG))
|
ToastWrapper(Toast.makeText(app, text, Toast.LENGTH_LONG))
|
||||||
} else SnackbarWrapper(Snackbar.make(holder, text, Snackbar.LENGTH_LONG))
|
} else SnackbarWrapper(Snackbar.make(holder, text, Snackbar.LENGTH_LONG))
|
||||||
}
|
}
|
||||||
fun make(e: Throwable) = make(when (e) {
|
fun make(e: Throwable) = make(e.readableMessage)
|
||||||
is NoShellException -> e.cause ?: e
|
|
||||||
else -> e
|
|
||||||
}.readableMessage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Register(private val view: View) : DefaultLifecycleObserver {
|
class Register(private val view: View) : DefaultLifecycleObserver {
|
||||||
|
|||||||
Reference in New Issue
Block a user