Skip to content

Commit

Permalink
Support static IP configuration
Browse files Browse the repository at this point in the history
Fixes #193.
Mygod committed Feb 21, 2024
1 parent cbb5056 commit 044206f
Showing 13 changed files with 287 additions and 0 deletions.
84 changes: 84 additions & 0 deletions mobile/src/main/java/be/mygod/vpnhotspot/StaticIpSetter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package be.mygod.vpnhotspot

import android.content.Context
import androidx.core.content.edit
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.root.RoutingCommands
import be.mygod.vpnhotspot.util.Event0
import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.io.IOException
import java.net.NetworkInterface
import java.net.SocketException
import java.security.SecureRandom

@Parcelize
class StaticIpSetter : BootReceiver.Startable {
companion object {
private const val IFACE = "staticip"
private const val KEY = "service.staticIp"

val ifaceEvent = Event0()

val iface get() = try {
NetworkInterface.getByName(IFACE)
} catch (_: SocketException) {
null
} catch (e: Exception) {
Timber.w(e)
null
}

var ips: String
get() {
app.pref.getString(KEY, null)?.let { return it }
val octets = ByteArray(3)
SecureRandom.getInstanceStrong().nextBytes(octets)
return "10.${octets.joinToString(".") { it.toUByte().toString() }}".also { ips = it }
}
set(value) = app.pref.edit { putString(KEY, value) }

fun enable(enabled: Boolean) = GlobalScope.launch {
val success = try {
RootSession.use {
try {
if (enabled) {
it.exec("${Routing.IP} link add $IFACE type dummy")
ips.lineSequence().forEach { ip ->
it.exec("${Routing.IP} addr add $ip dev $IFACE")
}
it.exec("${Routing.IP} link set $IFACE up")
true
} else {
it.exec("${Routing.IP} link del $IFACE")
false
}
} catch (e: RoutingCommands.UnexpectedOutputException) {
if (Routing.shouldSuppressIpError(e, enabled)) return@use false
Timber.w(IOException("Failed to add link", e))
SmartSnackbar.make(e).show()
false
}
}
} catch (_: CancellationException) {
false
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
false
}
if (success) BootReceiver.add<StaticIpSetter>(StaticIpSetter()) else BootReceiver.delete<StaticIpSetter>()
ifaceEvent()
}
}

override fun start(context: Context) {
enable(true)
}
}
3 changes: 3 additions & 0 deletions mobile/src/main/java/be/mygod/vpnhotspot/manage/Manager.kt
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.databinding.ListitemManageBinding
import be.mygod.vpnhotspot.databinding.ListitemRepeaterBinding
import be.mygod.vpnhotspot.databinding.ListitemStaticIpBinding

abstract class Manager {
companion object DiffCallback : DiffUtil.ItemCallback<Manager>() {
@@ -19,6 +20,7 @@ abstract class Manager {
const val VIEW_TYPE_ETHERNET = 8
const val VIEW_TYPE_LOCAL_ONLY_HOTSPOT = 6
const val VIEW_TYPE_REPEATER = 7
const val VIEW_TYPE_STATIC_IP = 9

override fun areItemsTheSame(oldItem: Manager, newItem: Manager) = oldItem.isSameItemAs(newItem)
@SuppressLint("DiffUtilEquals")
@@ -38,6 +40,7 @@ abstract class Manager {
LocalOnlyHotspotManager.ViewHolder(ListitemInterfaceBinding.inflate(inflater, parent, false))
}
VIEW_TYPE_REPEATER -> RepeaterManager.ViewHolder(ListitemRepeaterBinding.inflate(inflater, parent, false))
VIEW_TYPE_STATIC_IP -> StaticIpManager.ViewHolder(ListitemStaticIpBinding.inflate(inflater, parent, false))
else -> throw IllegalArgumentException("Invalid view type")
}
}
93 changes: 93 additions & 0 deletions mobile/src/main/java/be/mygod/vpnhotspot/manage/StaticIpManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package be.mygod.vpnhotspot.manage

import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.text.method.LinkMovementMethod
import android.view.WindowManager
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import be.mygod.vpnhotspot.AlertDialogFragment
import be.mygod.vpnhotspot.BR
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.StaticIpSetter
import be.mygod.vpnhotspot.databinding.ListitemStaticIpBinding
import be.mygod.vpnhotspot.util.formatAddresses
import be.mygod.vpnhotspot.util.showAllowingStateLoss
import kotlinx.parcelize.Parcelize

class StaticIpManager(private val parent: TetheringFragment) : Manager(), DefaultLifecycleObserver {
class ViewHolder(val binding: ListitemStaticIpBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.text.movementMethod = LinkMovementMethod.getInstance()
}
}

inner class Data : BaseObservable() {
private var iface = StaticIpSetter.iface
val active: Boolean @Bindable get() = iface != null
val addresses: CharSequence @Bindable get() = iface?.formatAddresses() ?: ""

fun onChanged() {
iface = StaticIpSetter.iface
notifyPropertyChanged(BR.serviceStarted)
notifyPropertyChanged(BR.addresses)
}

fun configure() = ConfigureDialogFragment().apply {
key()
arg(ConfigureData(StaticIpSetter.ips))
}.showAllowingStateLoss(parent.parentFragmentManager)

fun toggle() {
StaticIpSetter.enable(!active)
onChanged()
}
}

@Parcelize
data class ConfigureData(val ips: String) : Parcelable
class ConfigureDialogFragment : AlertDialogFragment<ConfigureData, ConfigureData>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.tethering_static_ip)
setView(R.layout.dialog_static_ip)
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
}

override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).apply {
create()
findViewById<EditText>(android.R.id.edit)!!.setText(arg.ips)
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
}

override val ret get() = ConfigureData(dialog!!.findViewById<EditText>(android.R.id.edit)!!.text!!.toString())
}

override val type get() = VIEW_TYPE_STATIC_IP
private val data = Data()

init {
parent.lifecycle.addObserver(this)
AlertDialogFragment.setResultListener<ConfigureDialogFragment, ConfigureData>(parent) { which, ret ->
if (which == DialogInterface.BUTTON_POSITIVE) StaticIpSetter.ips = ret!!.ips.trim()
}
}

override fun onCreate(owner: LifecycleOwner) {
StaticIpSetter.ifaceEvent[this] = data::onChanged
}

override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
(viewHolder as ViewHolder).binding.data = data
}

override fun onDestroy(owner: LifecycleOwner) {
StaticIpSetter.ifaceEvent -= this
}
}
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
TetheringManager.TetheringEventCallback {
internal val repeaterManager by lazy { RepeaterManager(this@TetheringFragment) }
internal val localOnlyHotspotManager by lazy { LocalOnlyHotspotManager(this@TetheringFragment) }
private val staticIpManager by lazy { StaticIpManager(this@TetheringFragment) }
internal val bluetoothManager by lazy {
requireContext().getSystemService<BluetoothManager>()?.adapter?.let {
TetherManager.Bluetooth(this@TetheringFragment, it)
@@ -107,6 +108,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
val list = ArrayList<Manager>()
if (Services.p2p != null) list.add(repeaterManager)
list.add(localOnlyHotspotManager)
list.add(staticIpManager)
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
updateMonitorList(activeIfaces - monitoredIfaces.toSet())
list.addAll((activeIfaces + monitoredIfaces).toSortedSet()
5 changes: 5 additions & 0 deletions mobile/src/main/res/drawable/ic_content_push_pin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z"/>

</vector>
23 changes: 23 additions & 0 deletions mobile/src/main/res/layout/dialog_static_ip.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:viewBindingIgnore="true">

<com.google.android.material.textfield.TextInputEditText
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:hint="@null"
android:imeOptions="flagNoExtractUi"
android:importantForAutofill="no"
android:inputType="textUri|textMultiLine"
android:minHeight="@dimen/touch_target_min"
tools:text="10.9.8.7\nfce5::1">

<requestFocus />
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
65 changes: 65 additions & 0 deletions mobile/src/main/res/layout/listitem_static_ip.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="be.mygod.vpnhotspot.manage.StaticIpManager.Data"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:onClick="@{_ -> data.configure()}">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:importantForAccessibility="no"
android:src="@drawable/ic_content_push_pin"
android:tint="?android:attr/textColorPrimary"/>

<Space
android:layout_width="16dp"
android:layout_height="0dp"/>

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
android:layout_gravity="center_vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tethering_static_ip"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>

<be.mygod.vpnhotspot.widget.AutoCollapseTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.addresses}"
android:textIsSelectable="true"
tools:text="01:23:45:ab:cd:ef\n10.9.8.7"/>
</LinearLayout>

<com.google.android.material.materialswitch.MaterialSwitch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="@{data.active}"
android:ellipsize="end"
android:gravity="center_vertical"
android:onClick="@{_ -> data.toggle()}"/>

</LinearLayout>
</layout>
2 changes: 2 additions & 0 deletions mobile/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
@@ -56,6 +56,8 @@
<string name="tethering_manage_bluetooth">Tethering Bluetooth</string>
<string name="tethering_manage_ethernet" msgid="959743110824197356">"Tethering Ethernet"</string>

<string name="tethering_static_ip">IP Statico</string>

<string name="connected_state_incomplete">" (in connessione)"</string>
<string name="connected_state_valid">" (raggiungibile)"</string>
<string name="connected_state_failed">" (perso)"</string>
2 changes: 2 additions & 0 deletions mobile/src/main/res/values-pt-rBR/strings.xml
Original file line number Diff line number Diff line change
@@ -76,6 +76,8 @@
<string name="tethering_manage_wifi_client_blocked">Bloqueado %1$s: %2$s</string>
<string name="tethering_manage_wifi_copy_mac">Copiar MAC</string>

<string name="tethering_static_ip">IP Estático</string>

<string name="connected_state_incomplete">" (conectando)"</string>
<string name="connected_state_valid">" (alcançável)"</string>
<string name="connected_state_failed">" (perdido)"</string>
2 changes: 2 additions & 0 deletions mobile/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
@@ -24,6 +24,8 @@
<string name="tethering_manage_bluetooth" msgid="2379175828878753652">"Bluetooth-модем"</string>
<string name="tethering_manage_ethernet" msgid="959743110824197356">"Режим Ethernet-модема"</string>

<string name="tethering_static_ip">Статический IP</string>

<string name="connected_state_incomplete">" (подключение)"</string>
<string name="connected_state_valid">" (доступный)"</string>
<string name="connected_state_failed">" (потеря)"</string>
2 changes: 2 additions & 0 deletions mobile/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
@@ -81,6 +81,8 @@
<string name="tethering_manage_wifi_client_blocked">已屏蔽 %1$s:%2$s</string>
<string name="tethering_manage_wifi_copy_mac">复制 MAC</string>

<string name="tethering_static_ip">静态 IP</string>

<string name="connected_state_incomplete">(正在连接)</string>
<string name="connected_state_valid">(已连接)</string>
<string name="connected_state_failed">(已断开)</string>
2 changes: 2 additions & 0 deletions mobile/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
@@ -89,6 +89,8 @@
<string name="tethering_manage_wifi_client_blocked">已隱藏 %1$s:%2$s</string>
<string name="tethering_manage_wifi_copy_mac">複製 MAC</string>

<string name="tethering_static_ip">靜態 IP</string>

<string name="connected_state_incomplete">(正在連線)</string>
<string name="connected_state_valid">(已連線)</string>
<string name="connected_state_failed">(已中斷)</string>
2 changes: 2 additions & 0 deletions mobile/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -95,6 +95,8 @@
<string name="tethering_manage_wifi_client_blocked">Blocked %1$s: %2$s</string>
<string name="tethering_manage_wifi_copy_mac">Copy MAC</string>

<string name="tethering_static_ip">Static IP</string>

<string name="connected_state_incomplete">" (connecting)"</string>
<string name="connected_state_valid">" (reachable)"</string>
<string name="connected_state_failed">" (lost)"</string>

0 comments on commit 044206f

Please sign in to comment.