diff --git a/README.md b/README.md index 25cb39c4..70cecbd3 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ You'll have to use WPS for now to make the repeater switch to 2.4GHz. ### [IPv6 tethering?](https://github.com/Mygod/VPNHotspot/issues/6) +### Missing `android.permission.MANAGE_USB` permission? + +Toggling USB tethering only works if you install this app as a system app (`/system/priv-app`). +Alternatively, use the toggle in your system settings instead. + ### No root? Without root, you can only: diff --git a/mobile/build.gradle b/mobile/build.gradle index 6c95e396..7c02c6f8 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation "com.android.support:design:$supportLibraryVersion" implementation "com.android.support:preference-v14:$supportLibraryVersion" implementation 'com.android.support.constraint:constraint-layout:1.1.0' + implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.16.0' implementation "com.takisoft.fix:preference-v7:$takisoftFixVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion" testImplementation 'junit:junit:4.12' diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro index 97f6f95a..bcf2764a 100644 --- a/mobile/proguard-rules.pro +++ b/mobile/proguard-rules.pro @@ -20,3 +20,5 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +-dontwarn org.mockito.** +-dontwarn org.objenesis.instantiator.** diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index b43e1900..7b325027 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -1,7 +1,11 @@ + @@ -14,8 +18,14 @@ + + + + () private val receiver = broadcastReceiver { _, intent -> - tetheredInterfaces = ConnectivityManagerHelper.getTetheredIfaces(intent.extras).toSet() + tetheredInterfaces = TetheringManager.getTetheredIfaces(intent.extras).toSet() adapter.recreate() } @@ -185,7 +185,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL val context = requireContext() context.bindService(Intent(context, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE) IpNeighbourMonitor.registerCallback(this) - context.registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED)) + context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) } override fun onStop() { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt index dc5b3fff..08d9b4db 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt @@ -1,10 +1,18 @@ package be.mygod.vpnhotspot +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothProfile import android.content.* import android.databinding.BaseObservable +import android.databinding.Bindable import android.databinding.DataBindingUtil +import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.IBinder +import android.provider.Settings +import android.support.annotation.RequiresApi import android.support.v4.app.Fragment import android.support.v4.content.ContextCompat import android.support.v7.recyclerview.extensions.ListAdapter @@ -15,10 +23,15 @@ import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.databinding.FragmentTetheringBinding import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding -import be.mygod.vpnhotspot.net.ConnectivityManagerHelper +import be.mygod.vpnhotspot.databinding.ListitemManageTetherBinding import be.mygod.vpnhotspot.net.TetherType +import be.mygod.vpnhotspot.net.TetheringManager +import be.mygod.vpnhotspot.net.WifiApManager +import java.lang.reflect.InvocationTargetException import java.net.NetworkInterface import java.net.SocketException import java.util.* @@ -27,6 +40,19 @@ class TetheringFragment : Fragment(), ServiceConnection { companion object { private const val VIEW_TYPE_INTERFACE = 0 private const val VIEW_TYPE_MANAGE = 1 + private const val VIEW_TYPE_WIFI = 2 + private const val VIEW_TYPE_USB = 3 + private const val VIEW_TYPE_BLUETOOTH = 4 + private const val VIEW_TYPE_WIFI_LEGACY = 5 + + /** + * PAN Profile + * From BluetoothProfile.java. + */ + private const val PAN = 5 + private val isTetheringOn by lazy @SuppressLint("PrivateApi") { + Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn") + } } inner class Data(val iface: TetheredInterface) : BaseObservable() { @@ -62,6 +88,105 @@ class TetheringFragment : Fragment(), ServiceConnection { .setClassName("com.android.settings", "com.android.settings.TetherSettings")) } } + private inner class ManageItemHolder(binding: ListitemManageTetherBinding, private val type: Int) + : RecyclerView.ViewHolder(binding.root), View.OnClickListener, TetheringManager.OnStartTetheringCallback { + val tetherType = when (type) { + VIEW_TYPE_WIFI, VIEW_TYPE_WIFI_LEGACY -> TetherType.WIFI + VIEW_TYPE_USB -> TetherType.USB + VIEW_TYPE_BLUETOOTH -> TetherType.BLUETOOTH + else -> TetherType.NONE + } + init { + itemView.setOnClickListener(this) + binding.icon = tetherType.icon + binding.title = getString(when (type) { + VIEW_TYPE_USB -> R.string.tethering_manage_usb + VIEW_TYPE_WIFI -> R.string.tethering_manage_wifi + VIEW_TYPE_WIFI_LEGACY -> R.string.tethering_manage_wifi_legacy + VIEW_TYPE_BLUETOOTH -> R.string.tethering_manage_bluetooth + else -> throw IllegalStateException() + }) + binding.tetherListener = tetherListener + binding.type = tetherType + } + + override fun onClick(v: View?) { + val context = requireContext() + if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(context)) { + startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, + Uri.parse("package:${context.packageName}"))) + return + } + val started = tetherListener.isStarted(tetherType) + try { + when (type) { + VIEW_TYPE_WIFI -> @RequiresApi(24) { + if (started) TetheringManager.stop(TetheringManager.TETHERING_WIFI) + else TetheringManager.start(TetheringManager.TETHERING_WIFI, true, this) + } + VIEW_TYPE_USB -> @RequiresApi(24) { + if (started) TetheringManager.stop(TetheringManager.TETHERING_USB) + else TetheringManager.start(TetheringManager.TETHERING_USB, true, this) + } + VIEW_TYPE_BLUETOOTH -> @RequiresApi(24) { + if (started) { + TetheringManager.stop(TetheringManager.TETHERING_BLUETOOTH) + Thread.sleep(1) // give others a room to breathe + onTetheringStarted() // force flush state + } else TetheringManager.start(TetheringManager.TETHERING_BLUETOOTH, true, this) + } + VIEW_TYPE_WIFI_LEGACY -> @Suppress("DEPRECATION") { + if (started) WifiApManager.stop() else WifiApManager.start() + } + } + } catch (e: InvocationTargetException) { + e.printStackTrace() + var cause: Throwable? = e + while (cause != null) { + cause = cause.cause + if (cause != null && cause !is InvocationTargetException) { + Toast.makeText(context, cause.message, Toast.LENGTH_LONG).show() + break + } + } + } + } + + override fun onTetheringStarted() = tetherListener.notifyPropertyChanged(BR.enabledTypes) + override fun onTetheringFailed() { + app.handler.post { + Toast.makeText(requireContext(), "Android system has failed to start tethering.", Toast.LENGTH_SHORT).show() + } + } + } + + class TetherListener : BaseObservable(), BluetoothProfile.ServiceListener { + var enabledTypes = emptySet() + @Bindable get + set(value) { + field = value + notifyPropertyChanged(BR.enabledTypes) + } + var pan: BluetoothProfile? = null + + override fun onServiceDisconnected(profile: Int) { + pan = null + } + override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { + pan = proxy + } + + /** + * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java + */ + fun isStarted(type: TetherType, enabledTypes: Set = this.enabledTypes): Boolean { + return if (type == TetherType.BLUETOOTH) { + val pan = pan + BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && pan != null && + isTetheringOn.invoke(pan) as Boolean + } else enabledTypes.contains(type) + } + } class TetheredInterface(val name: String, lookup: Map) : Comparable { val addresses = lookup[name]?.formatAddresses() ?: "" @@ -91,17 +216,30 @@ class TetheringFragment : Fragment(), ServiceConnection { e.printStackTrace() emptyMap() } + this@TetheringFragment.tetherListener.enabledTypes = data.map { TetherType.ofInterface(it) }.toSet() submitList(data.map { TetheredInterface(it, lookup) }.sorted()) } - override fun getItemCount() = super.getItemCount() + 1 - override fun getItemViewType(position: Int) = - if (position == super.getItemCount()) VIEW_TYPE_MANAGE else VIEW_TYPE_INTERFACE + override fun getItemCount() = super.getItemCount() + when (Build.VERSION.SDK_INT) { + in 0 until 24 -> 2 + in 24..25 -> 5 + else -> 4 + } + override fun getItemViewType(position: Int) = when (position - super.getItemCount()) { + 0 -> VIEW_TYPE_MANAGE + 1 -> if (Build.VERSION.SDK_INT >= 24) VIEW_TYPE_USB else VIEW_TYPE_WIFI_LEGACY + 2 -> VIEW_TYPE_WIFI + 3 -> VIEW_TYPE_BLUETOOTH + 4 -> VIEW_TYPE_WIFI_LEGACY + else -> VIEW_TYPE_INTERFACE + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { VIEW_TYPE_INTERFACE -> InterfaceViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false)) VIEW_TYPE_MANAGE -> ManageViewHolder(inflater.inflate(R.layout.listitem_manage, parent, false)) + VIEW_TYPE_WIFI, VIEW_TYPE_USB, VIEW_TYPE_BLUETOOTH, VIEW_TYPE_WIFI_LEGACY -> + ManageItemHolder(ListitemManageTetherBinding.inflate(inflater, parent, false), viewType) else -> throw IllegalArgumentException("Invalid view type") } } @@ -112,11 +250,12 @@ class TetheringFragment : Fragment(), ServiceConnection { } } + private val tetherListener = TetherListener() private lateinit var binding: FragmentTetheringBinding private var binder: TetheringService.TetheringBinder? = null val adapter = TetheringAdapter() private val receiver = broadcastReceiver { _, intent -> - adapter.update(ConnectivityManagerHelper.getTetheredIfaces(intent.extras)) + adapter.update(TetheringManager.getTetheredIfaces(intent.extras)) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -124,13 +263,14 @@ class TetheringFragment : Fragment(), ServiceConnection { binding.interfaces.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) binding.interfaces.itemAnimator = DefaultItemAnimator() binding.interfaces.adapter = adapter + BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(requireContext(), tetherListener, PAN) return binding.root } override fun onStart() { super.onStart() val context = requireContext() - context.registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED)) + context.registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) context.bindService(Intent(context, TetheringService::class.java), this, Context.BIND_AUTO_CREATE) } @@ -141,6 +281,11 @@ class TetheringFragment : Fragment(), ServiceConnection { super.onStop() } + override fun onDestroy() { + tetherListener.pan = null + super.onDestroy() + } + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val binder = service as TetheringService.TetheringBinder this.binder = binder diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index 5fd10ace..ce9c6263 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -31,8 +31,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call private val receiver = broadcastReceiver { _, intent -> synchronized(routings) { when (intent.action) { - ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED -> { - val remove = routings.keys - ConnectivityManagerHelper.getTetheredIfaces(intent.extras) + TetheringManager.ACTION_TETHER_STATE_CHANGED -> { + val remove = routings.keys - TetheringManager.getTetheredIfaces(intent.extras) if (remove.isEmpty()) return@broadcastReceiver val failed = remove.any { routings.remove(it)?.stop() == false } if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show() @@ -67,7 +67,7 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call } if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show() } else if (!receiverRegistered) { - registerReceiver(receiver, intentFilter(ConnectivityManagerHelper.ACTION_TETHER_STATE_CHANGED)) + registerReceiver(receiver, intentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) LocalBroadcastManager.getInstance(this) .registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS)) IpNeighbourMonitor.registerCallback(this) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/ConnectivityManagerHelper.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/ConnectivityManagerHelper.kt deleted file mode 100644 index 3240eebc..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/ConnectivityManagerHelper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package be.mygod.vpnhotspot.net - -import android.os.Build -import android.os.Bundle -import android.support.annotation.RequiresApi - -/** - * Hidden constants from ConnectivityManager and some helpers. - */ -object ConnectivityManagerHelper { - /** - * This is a sticky broadcast since almost forever. - * - * https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java - */ - const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED" - private const val EXTRA_ACTIVE_TETHER_LEGACY = "activeArray" - @RequiresApi(26) - private const val EXTRA_ACTIVE_LOCAL_ONLY = "localOnlyArray" - @RequiresApi(26) - private const val EXTRA_ACTIVE_TETHER = "tetherArray" - - fun getTetheredIfaces(extras: Bundle) = if (Build.VERSION.SDK_INT >= 26) - extras.getStringArrayList(EXTRA_ACTIVE_TETHER).toSet() + extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY) - else extras.getStringArrayList(EXTRA_ACTIVE_TETHER_LEGACY).toSet() -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt new file mode 100644 index 00000000..ae3126f7 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/TetheringManager.kt @@ -0,0 +1,131 @@ +package be.mygod.vpnhotspot.net + +import android.annotation.SuppressLint +import android.net.ConnectivityManager +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.support.annotation.RequiresApi +import android.util.Log +import be.mygod.vpnhotspot.App.Companion.app +import com.android.dx.stock.ProxyBuilder + +/** + * Heavily based on: + * https://github.com/aegis1980/WifiHotSpot + * https://android.googlesource.com/platform/frameworks/base.git/+/android-7.0.0_r1/core/java/android/net/ConnectivityManager.java + */ +object TetheringManager { + /** + * Callback for use with [.startTethering] to find out whether tethering succeeded. + */ + interface OnStartTetheringCallback { + /** + * Called when tethering has been successfully started. + */ + fun onTetheringStarted() { } + + /** + * Called when starting tethering failed. + */ + fun onTetheringFailed() { } + } + + private const val TAG = "TetheringManager" + + /** + * This is a sticky broadcast since almost forever. + * + * https://android.googlesource.com/platform/frameworks/base.git/+/2a091d7aa0c174986387e5d56bf97a87fe075bdb%5E%21/services/java/com/android/server/connectivity/Tethering.java + */ + const val ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED" + private const val EXTRA_ACTIVE_TETHER_LEGACY = "activeArray" + @RequiresApi(26) + private const val EXTRA_ACTIVE_LOCAL_ONLY = "localOnlyArray" + @RequiresApi(26) + private const val EXTRA_ACTIVE_TETHER = "tetherArray" + + const val TETHERING_WIFI = 0 + /** + * Requires MANAGE_USB permission, unfortunately. + * + * Source: https://android.googlesource.com/platform/frameworks/base/+/7ca5d3a/services/usb/java/com/android/server/usb/UsbService.java#389 + */ + const val TETHERING_USB = 1 + /** + * Requires BLUETOOTH permission. + */ + const val TETHERING_BLUETOOTH = 2 + + private val classOnStartTetheringCallback by lazy @SuppressLint("PrivateApi") { + Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback") + } + private val startTethering by lazy { + ConnectivityManager::class.java.getDeclaredMethod("startTethering", + Int::class.java, Boolean::class.java, classOnStartTetheringCallback, Handler::class.java) + } + private val stopTethering by lazy { + ConnectivityManager::class.java.getDeclaredMethod("stopTethering", Int::class.java) + } + + /** + * 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 + * enabled immediately. If provisioning fails, tethering will not be enabled. It also + * schedules tether provisioning re-checks if appropriate. + * + * @param type The type of tethering to start. Must be one of + * {@link ConnectivityManager.TETHERING_WIFI}, + * {@link ConnectivityManager.TETHERING_USB}, or + * {@link ConnectivityManager.TETHERING_BLUETOOTH}. + * @param showProvisioningUi a boolean indicating to show the provisioning app UI if there + * is one. This should be true the first time this function is called and also any time + * the user can see this UI. It gives users information from their carrier about the + * check failing and how they can sign up for tethering if possible. + * @param callback an {@link OnStartTetheringCallback} which will be called to notify the caller + * of the result of trying to tether. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + */ + @RequiresApi(24) + fun start(type: Int, showProvisioningUi: Boolean, callback: OnStartTetheringCallback, handler: Handler? = null) { + val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback) + .dexCache(app.cacheDir) + .handler { proxy, method, args -> + if (args.isNotEmpty()) Log.w(TAG, "Unexpected args for ${method.name}: $args") + when (method.name) { + "onTetheringStarted" -> { + callback.onTetheringStarted() + null + } + "onTetheringFailed" -> { + callback.onTetheringFailed() + null + } + else -> { + Log.w(TAG, "Unexpected method, calling super: $method") + ProxyBuilder.callSuper(proxy, method, args) + } + } + } + .build() + startTethering.invoke(app.connectivity, type, showProvisioningUi, proxy, handler) + } + + /** + * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if + * applicable. + * + * @param type The type of tethering to stop. Must be one of + * {@link ConnectivityManager.TETHERING_WIFI}, + * {@link ConnectivityManager.TETHERING_USB}, or + * {@link ConnectivityManager.TETHERING_BLUETOOTH}. + */ + @RequiresApi(24) + fun stop(type: Int) { + stopTethering.invoke(app.connectivity, type) + } + + fun getTetheredIfaces(extras: Bundle) = if (Build.VERSION.SDK_INT >= 26) + extras.getStringArrayList(EXTRA_ACTIVE_TETHER).toSet() + extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY) + else extras.getStringArrayList(EXTRA_ACTIVE_TETHER_LEGACY).toSet() +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt index 8e9a8724..6f492d5c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt @@ -17,7 +17,6 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() { private const val TAG = "VpnMonitor" - private val manager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager private val request = NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_VPN) .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) @@ -31,7 +30,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() { private val available = HashMap() private var currentNetwork: Network? = null override fun onAvailable(network: Network) { - val properties = manager.getLinkProperties(network) + val properties = app.connectivity.getLinkProperties(network) val ifname = properties?.interfaceName ?: return synchronized(this) { if (available.put(network, ifname) != null) return @@ -55,7 +54,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() { while (available.isNotEmpty()) { val next = available.entries.first() currentNetwork = next.key - val properties = manager.getLinkProperties(next.key) + val properties = app.connectivity.getLinkProperties(next.key) if (properties != null) { debugLog(TAG, "Switching to ${next.value} as VPN interface") callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) } @@ -70,16 +69,17 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() { if (synchronized(this) { if (!callbacks.add(callback)) return if (!registered) { - manager.registerNetworkCallback(request, this) + app.connectivity.registerNetworkCallback(request, this) registered = true - manager.allNetworks.all { - val cap = manager.getNetworkCapabilities(it) + app.connectivity.allNetworks.all { + val cap = app.connectivity.getNetworkCapabilities(it) !cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) } } else if (available.isEmpty()) true else { available.forEach { - callback.onAvailable(it.value, manager.getLinkProperties(it.key)?.dnsServers ?: emptyList()) + callback.onAvailable(it.value, + app.connectivity.getLinkProperties(it.key)?.dnsServers ?: emptyList()) } false } @@ -87,7 +87,7 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() { } fun unregisterCallback(callback: Callback) = synchronized(this) { if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return - manager.unregisterNetworkCallback(this) + app.connectivity.unregisterNetworkCallback(this) registered = false available.clear() currentNetwork = null diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/WifiApManager.kt new file mode 100644 index 00000000..20309ae2 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/WifiApManager.kt @@ -0,0 +1,34 @@ +package be.mygod.vpnhotspot.net + +import android.content.Context +import android.net.wifi.WifiConfiguration +import android.net.wifi.WifiManager +import be.mygod.vpnhotspot.App.Companion.app + +@Deprecated("No longer usable since API 26.") +object WifiApManager { + private val wifi = app.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val setWifiApEnabled = WifiManager::class.java.getDeclaredMethod("setWifiApEnabled", + WifiConfiguration::class.java, Boolean::class.java) + /** + * Start AccessPoint mode with the specified + * configuration. If the radio is already running in + * AP mode, update the new configuration + * Note that starting in access point mode disables station + * mode operation + * @param wifiConfig SSID, security and channel details as + * part of WifiConfiguration + * @return {@code true} if the operation succeeds, {@code false} otherwise + */ + private fun WifiManager.setWifiApEnabled(wifiConfig: WifiConfiguration?, enabled: Boolean) = + setWifiApEnabled.invoke(this, wifiConfig, enabled) as Boolean + + fun start(wifiConfig: WifiConfiguration? = null) { + wifi.isWifiEnabled = false + wifi.setWifiApEnabled(wifiConfig, true) + } + fun stop() { + wifi.setWifiApEnabled(null, false) + wifi.isWifiEnabled = true + } +} diff --git a/mobile/src/main/res/layout/listitem_manage_tether.xml b/mobile/src/main/res/layout/listitem_manage_tether.xml new file mode 100644 index 00000000..e20b5395 --- /dev/null +++ b/mobile/src/main/res/layout/listitem_manage_tether.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index fe0dea5c..56c2175a 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -21,18 +21,30 @@ 重置凭据失败(原因:%s) 未打开 - Wi-Fi 直连不可用 + Wi\u2011Fi 直连不可用 创建 P2P 群组失败(原因:%s) 关闭已有 P2P 群组失败(原因:%s) 关闭 P2P 群组失败(原因:%s) 内部异常 - 设备不支持 Wi-Fi 直连 + 设备不支持 Wi\u2011Fi 直连 系统忙 未添加服务请求 未知 #%d 管理… + + USB 网络共享 + WLAN 热点 + WLAN 热点 (旧 API) + 蓝牙网络共享 已连接设备 %s (正在连接) diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 405f4b67..cf9dfc0c 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -23,18 +23,30 @@ Failed to reset credentials (reason: %s) Service inactive - Wi-Fi direct unavailable + Wi\u2011Fi direct unavailable Failed to create P2P group (reason: %s) Failed to remove P2P group (reason: %s) Failed to remove old P2P group (reason: %s) internal error - Wi-Fi direct unsupported + Wi\u2011Fi direct unsupported framework is busy no service requests added unknown #%d Manage… + + USB tethering + Wi\u2011Fi hotspot + Wi\u2011Fi hotspot (legacy) + Bluetooth tethering Connected devices %s (connecting)