diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt index ff27132e..b886f49f 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt @@ -1,12 +1,10 @@ package be.mygod.vpnhotspot -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection +import android.content.* import android.databinding.BaseObservable import android.databinding.Bindable import android.databinding.DataBindingUtil +import android.net.wifi.WifiConfiguration import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pGroup import android.os.Bundle @@ -24,12 +22,15 @@ import android.support.v7.widget.RecyclerView import android.support.v7.widget.Toolbar import android.view.* import android.widget.EditText +import android.widget.Toast import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding import be.mygod.vpnhotspot.databinding.ListitemClientBinding import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbourMonitor import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetherType +import be.mygod.vpnhotspot.net.wifi.P2pSupplicantConfiguration +import be.mygod.vpnhotspot.net.wifi.WifiApDialog import java.net.NetworkInterface import java.net.SocketException import java.util.* @@ -60,7 +61,6 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL } val ssid @Bindable get() = binder?.ssid ?: getText(R.string.repeater_inactive) - val password @Bindable get() = binder?.password ?: "" val addresses @Bindable get(): String { return try { NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: "" @@ -78,7 +78,6 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL } fun onGroupChanged(group: WifiP2pGroup?) { notifyPropertyChanged(BR.ssid) - notifyPropertyChanged(BR.password) p2pInterface = group?.`interface` notifyPropertyChanged(BR.addresses) adapter.p2p = group?.clientList ?: emptyList() @@ -227,19 +226,46 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL dialog.show() true } else false - R.id.resetGroup -> { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.repeater_reset_credentials) - .setMessage(getString(R.string.repeater_reset_credentials_dialog_message)) - .setPositiveButton(R.string.repeater_reset_credentials_dialog_reset, - { _, _ -> binder?.resetCredentials() }) - .setNegativeButton(android.R.string.cancel, null) - .show() + R.id.edit -> { + editConfigurations() true } else -> false } + private fun editConfigurations() { + val binder = binder + val ssid = binder?.ssid + val context = requireContext() + if (ssid != null) { + val wifi = WifiConfiguration() + val conf = P2pSupplicantConfiguration() + wifi.SSID = ssid + wifi.preSharedKey = binder.password + if (wifi.preSharedKey == null || wifi.preSharedKey.length < 8) wifi.preSharedKey = conf.readPsk() + if (wifi.preSharedKey == null || wifi.preSharedKey.length < 8) { + Toast.makeText(context, R.string.root_unavailable, Toast.LENGTH_SHORT).show() + return + } + if (wifi.preSharedKey != null && wifi.preSharedKey.length >= 8) { + var dialog: WifiApDialog? = null + dialog = WifiApDialog(context, DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> when (conf.update(dialog!!.config!!)) { + true -> binder.requestGroupUpdate() + false -> Toast.makeText(context, R.string.noisy_su_failure, Toast.LENGTH_SHORT).show() + null -> Toast.makeText(context, R.string.root_unavailable, Toast.LENGTH_SHORT).show() + } + DialogInterface.BUTTON_NEUTRAL -> binder.resetCredentials() + } + }, wifi) + dialog.show() + return + } + } + Toast.makeText(context, R.string.repeater_configure_failure, Toast.LENGTH_LONG).show() + } + override fun onIpNeighbourAvailable(neighbours: Map) { adapter.neighbours = neighbours.values.toList() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index 55aaaa25..3093970c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -18,12 +18,12 @@ import android.widget.Toast import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.VpnMonitor -import be.mygod.vpnhotspot.net.WifiP2pManagerHelper -import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.deletePersistentGroup -import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.netId -import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.requestPersistentGroupInfo -import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.setWifiP2pChannels -import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.startWps +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels +import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps import java.net.InetAddress import java.net.SocketException @@ -74,6 +74,22 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca formatReason(R.string.repeater_reset_credentials_failure, reason), Toast.LENGTH_SHORT).show() }) } + + fun requestGroupUpdate() { + group = null + try { + p2pManager.requestPersistentGroupInfo(channel, { + when (it.size) { + 0 -> { } + 1 -> group = it.single() + else -> Log.w(TAG, "Unexpected groups: $it") + } + }) + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(this@RepeaterService, e.message, Toast.LENGTH_LONG).show() + } + } } private lateinit var p2pManager: WifiP2pManager @@ -152,18 +168,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca override fun onChannelDisconnected() { channel = p2pManager.initialize(this, Looper.getMainLooper(), this) setOperatingChannel(true) - try { - p2pManager.requestPersistentGroupInfo(channel, { - when (it.size) { - 0 -> { } - 1 -> group = it.single() - else -> Log.w(TAG, "Unexpected groups: $it") - } - }) - } catch (e: Exception) { - e.printStackTrace() - Toast.makeText(this, e.message, Toast.LENGTH_LONG).show() - } + binder.requestGroupUpdate() } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt index 5f31a825..794b9813 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringFragment.kt @@ -30,7 +30,7 @@ import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding 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 be.mygod.vpnhotspot.net.wifi.WifiApManager import java.lang.reflect.InvocationTargetException import java.net.NetworkInterface import java.net.SocketException diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt new file mode 100644 index 00000000..4b8e2245 --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/P2pSupplicantConfiguration.kt @@ -0,0 +1,61 @@ +package be.mygod.vpnhotspot.net.wifi + +import android.net.wifi.WifiConfiguration +import android.util.Log +import android.widget.Toast +import be.mygod.vpnhotspot.App.Companion.app +import be.mygod.vpnhotspot.loggerSu +import be.mygod.vpnhotspot.noisySu +import java.io.File + +class P2pSupplicantConfiguration { + companion object { + private const val TAG = "P2pSupplicationConf" + // format for ssid is much more complicated, therefore we are only trying to find the line + private val ssidMatcher = "^[\\r\\t ]*ssid=".toRegex() + private val pskParser = "^[\\r\\t ]*psk=\"(.*)\"\$".toRegex(RegexOption.MULTILINE) + } + + private val content by lazy { loggerSu("cat /data/misc/wifi/p2p_supplicant.conf") } + + fun readPsk(): String? { + return try { + pskParser.findAll(content ?: return null).single().groupValues[1] + } catch (e: Exception) { + Log.w(TAG, content) + e.printStackTrace() + Toast.makeText(app, e.message, Toast.LENGTH_LONG).show() + null + } + } + + fun update(config: WifiConfiguration): Boolean? { + val content = content ?: return null + val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.cacheDir) + try { + var ssidFound = false + var pskFound = false + tempFile.printWriter().use { + for (line in content.lineSequence()) it.println(when { + ssidMatcher.containsMatchIn(line) -> { + ssidFound = true + "\tssid=" + config.SSID.toByteArray() + .joinToString("") { it.toInt().toString(16).padStart(2, '0') } + } + pskParser.containsMatchIn(line) -> { + pskFound = true + "\tpsk=\"${config.preSharedKey}\"" // no control chars or weird stuff + } + else -> line // do nothing + }) + } + if (!ssidFound || !pskFound) { + Log.w(TAG, "Invalid conf ($ssidFound, $pskFound): $content") + return false + } + return noisySu("cat ${tempFile.absolutePath} > /data/misc/wifi/p2p_supplicant.conf", "killall wpa_supplicant") + } finally { + if (!tempFile.delete()) tempFile.deleteOnExit() + } + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialog.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialog.kt new file mode 100644 index 00000000..00778a3e --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApDialog.kt @@ -0,0 +1,97 @@ +package be.mygod.vpnhotspot.net.wifi + +import android.content.Context +import android.content.DialogInterface +import android.net.wifi.WifiConfiguration +import android.net.wifi.WifiConfiguration.AuthAlgorithm +import android.os.Bundle +import android.support.v7.app.AlertDialog +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.view.View +import android.widget.CheckBox +import android.widget.EditText +import android.widget.TextView +import be.mygod.vpnhotspot.R +import java.nio.charset.Charset + +/** + * https://android.googlesource.com/platform/packages/apps/Settings/+/39b4674/src/com/android/settings/wifi/WifiApDialog.java + */ +class WifiApDialog(mContext: Context, private val mListener: DialogInterface.OnClickListener, + private val mWifiConfig: WifiConfiguration?) : AlertDialog(mContext), View.OnClickListener, TextWatcher { + companion object { + private const val BUTTON_SUBMIT = DialogInterface.BUTTON_POSITIVE + } + + private lateinit var mView: View + private lateinit var mSsid: TextView + private lateinit var mPassword: EditText + /** + * TODO: SSID in WifiConfiguration for soft ap + * is being stored as a raw string without quotes. + * This is not the case on the client side. We need to + * make things consistent and clean it up + */ + val config: WifiConfiguration? + get() { + val config = WifiConfiguration() + config.SSID = mSsid.text.toString() + config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN) + if (mPassword.length() != 0) { + val password = mPassword.text.toString() + config.preSharedKey = password + } + return config + } + + override fun onCreate(savedInstanceState: Bundle?) { + mView = layoutInflater.inflate(R.layout.dialog_wifi_ap, null) + setView(mView) + val context = context + setTitle(R.string.repeater_configure) + mSsid = mView.findViewById(R.id.ssid) + mPassword = mView.findViewById(R.id.password) + setButton(BUTTON_SUBMIT, context.getString(R.string.wifi_save), mListener) + setButton(DialogInterface.BUTTON_NEGATIVE, + context.getString(R.string.wifi_cancel), mListener) + setButton(DialogInterface.BUTTON_NEUTRAL, context.getString(R.string.repeater_reset_credentials), mListener) + if (mWifiConfig != null) { + mSsid.text = mWifiConfig.SSID + mPassword.setText(mWifiConfig.preSharedKey) + } + mSsid.addTextChangedListener(this) + mPassword.addTextChangedListener(this) + (mView.findViewById(R.id.show_password) as CheckBox).setOnClickListener(this) + super.onCreate(savedInstanceState) + validate() + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + mPassword.inputType = InputType.TYPE_CLASS_TEXT or + if ((mView.findViewById(R.id.show_password) as CheckBox).isChecked) + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + else InputType.TYPE_TEXT_VARIATION_PASSWORD + } + + private fun validate() { + val mSsidString = mSsid.text.toString() + getButton(BUTTON_SUBMIT).isEnabled = mSsid.length() != 0 && + mPassword.length() >= 8 && Charset.forName("UTF-8").encode(mSsidString).limit() <= 32 + } + + override fun onClick(view: View) { + mPassword.inputType = InputType.TYPE_CLASS_TEXT or if ((view as CheckBox).isChecked) + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + else + InputType.TYPE_TEXT_VARIATION_PASSWORD + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { } + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } + override fun afterTextChanged(editable: Editable) { + validate() + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/WifiApManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt similarity index 97% rename from mobile/src/main/java/be/mygod/vpnhotspot/net/WifiApManager.kt rename to mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt index 20309ae2..62238251 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/WifiApManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiApManager.kt @@ -1,4 +1,4 @@ -package be.mygod.vpnhotspot.net +package be.mygod.vpnhotspot.net.wifi import android.content.Context import android.net.wifi.WifiConfiguration diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/WifiP2pManagerHelper.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt similarity index 99% rename from mobile/src/main/java/be/mygod/vpnhotspot/net/WifiP2pManagerHelper.kt rename to mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt index 344113bb..78157450 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/WifiP2pManagerHelper.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/wifi/WifiP2pManagerHelper.kt @@ -1,4 +1,4 @@ -package be.mygod.vpnhotspot.net +package be.mygod.vpnhotspot.net.wifi import android.annotation.SuppressLint import android.net.wifi.WpsInfo diff --git a/mobile/src/main/res/drawable/ic_action_autorenew.xml b/mobile/src/main/res/drawable/ic_action_autorenew.xml deleted file mode 100644 index 794ca6ad..00000000 --- a/mobile/src/main/res/drawable/ic_action_autorenew.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mobile/src/main/res/drawable/ic_image_edit.xml b/mobile/src/main/res/drawable/ic_image_edit.xml new file mode 100644 index 00000000..2ab2fb75 --- /dev/null +++ b/mobile/src/main/res/drawable/ic_image_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/mobile/src/main/res/layout/dialog_wifi_ap.xml b/mobile/src/main/res/layout/dialog_wifi_ap.xml new file mode 100644 index 00000000..77adb6bc --- /dev/null +++ b/mobile/src/main/res/layout/dialog_wifi_ap.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/src/main/res/layout/fragment_repeater.xml b/mobile/src/main/res/layout/fragment_repeater.xml index 63964926..69286462 100644 --- a/mobile/src/main/res/layout/fragment_repeater.xml +++ b/mobile/src/main/res/layout/fragment_repeater.xml @@ -40,7 +40,7 @@ + tools:text="DIRECT-rA-nd0m"/> - - - - @@ -89,7 +71,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_column="2" - android:layout_row="2" + android:layout_row="1" android:focusable="false" android:text="@{data.addresses}" android:textIsSelectable="true" diff --git a/mobile/src/main/res/menu/repeater.xml b/mobile/src/main/res/menu/repeater.xml index 8d8ff517..080f0a17 100644 --- a/mobile/src/main/res/menu/repeater.xml +++ b/mobile/src/main/res/menu/repeater.xml @@ -8,8 +8,8 @@ android:title="@string/repeater_wps" app:showAsAction="ifRoom"/> \ No newline at end of file diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index 8413265a..6bb73d79 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -5,15 +5,15 @@ 系统共享 设置选项 - 中继名称 - 中继密码 中继地址 输入 PIN 一键加密 请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。 成功注册 PIN。 打开 WPS 失败(原因:%s) - 重置凭据 + 设置 WLAN 中继 + 未能找到有效的档案。请尝试先打开中继。 + 重置 Android 系统将在下次打开中继时生成新的中继名称和密码。该操作不可撤销。 重置 @@ -78,4 +78,12 @@ 错误:未找到下游接口 似乎没有 root 发生异常,详情请查看调试信息。 + + "网络名称" + "输入 SSID" + "密码" + "显示密码" + "密码至少应包含 8 个字符。" + "保存" + "取消" diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 30a3df60..16b80dbd 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -1,12 +1,19 @@ + VPN Hotspot Repeater Tethering Settings - Network name - Password Addresses WPS Enter PIN @@ -15,10 +22,9 @@ device. PIN registered. Failed to start WPS (reason: %s) - Reset credentials - Android system will generate new network name and password - next time repeater is activated. This is irreversible. - Reset + Configure Wi\u2011Fi repeater + Valid config not found. Please start repeater first. + Reset Credentials reset. Failed to reset credentials (reason: %s) @@ -36,14 +42,6 @@ unknown #%d Manage… - USB tethering Wi\u2011Fi hotspot Wi\u2011Fi hotspot (legacy) @@ -82,4 +80,12 @@ Fatal: Downstream interface not found Root unavailable Something went wrong, please check the debug information. + + Network name + Enter the SSID + Password + Show password + The password must have at least 8 characters. + Save + Cancel diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml index 4ada3577..95c9fe8c 100644 --- a/mobile/src/main/res/values/styles.xml +++ b/mobile/src/main/res/values/styles.xml @@ -13,4 +13,35 @@ 8dp + + + + + + +