Add support for modifying repeater credentials

Credits go to @fxsheep: https://forum.xda-developers.com/showpost.php?p=76298728&postcount=5

Currently it only works on later versions of Android due to usage of `killall`. A workaround is in progress.
This commit is contained in:
Mygod
2018-04-21 20:18:15 -07:00
parent 6608a76297
commit 570998b255
15 changed files with 393 additions and 84 deletions

View File

@@ -1,12 +1,10 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.ComponentName import android.content.*
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.databinding.BaseObservable import android.databinding.BaseObservable
import android.databinding.Bindable import android.databinding.Bindable
import android.databinding.DataBindingUtil import android.databinding.DataBindingUtil
import android.net.wifi.WifiConfiguration
import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pGroup
import android.os.Bundle import android.os.Bundle
@@ -24,12 +22,15 @@ import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import android.view.* import android.view.*
import android.widget.EditText import android.widget.EditText
import android.widget.Toast
import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding import be.mygod.vpnhotspot.databinding.FragmentRepeaterBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.IpNeighbourMonitor import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetherType 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.NetworkInterface
import java.net.SocketException import java.net.SocketException
import java.util.* 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 ssid @Bindable get() = binder?.ssid ?: getText(R.string.repeater_inactive)
val password @Bindable get() = binder?.password ?: ""
val addresses @Bindable get(): String { val addresses @Bindable get(): String {
return try { return try {
NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: "" NetworkInterface.getByName(p2pInterface ?: return "")?.formatAddresses() ?: ""
@@ -78,7 +78,6 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
} }
fun onGroupChanged(group: WifiP2pGroup?) { fun onGroupChanged(group: WifiP2pGroup?) {
notifyPropertyChanged(BR.ssid) notifyPropertyChanged(BR.ssid)
notifyPropertyChanged(BR.password)
p2pInterface = group?.`interface` p2pInterface = group?.`interface`
notifyPropertyChanged(BR.addresses) notifyPropertyChanged(BR.addresses)
adapter.p2p = group?.clientList ?: emptyList() adapter.p2p = group?.clientList ?: emptyList()
@@ -227,19 +226,46 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
dialog.show() dialog.show()
true true
} else false } else false
R.id.resetGroup -> { R.id.edit -> {
AlertDialog.Builder(requireContext()) editConfigurations()
.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()
true true
} }
else -> false 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<String, IpNeighbour>) { override fun onIpNeighbourAvailable(neighbours: Map<String, IpNeighbour>) {
adapter.neighbours = neighbours.values.toList() adapter.neighbours = neighbours.values.toList()
} }

View File

@@ -18,12 +18,12 @@ import android.widget.Toast
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
import be.mygod.vpnhotspot.net.VpnMonitor import be.mygod.vpnhotspot.net.VpnMonitor
import be.mygod.vpnhotspot.net.WifiP2pManagerHelper import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.deletePersistentGroup import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.netId import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.netId
import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.requestPersistentGroupInfo import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.WifiP2pManagerHelper.startWps import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import java.net.InetAddress import java.net.InetAddress
import java.net.SocketException 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() 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 private lateinit var p2pManager: WifiP2pManager
@@ -152,18 +168,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
override fun onChannelDisconnected() { override fun onChannelDisconnected() {
channel = p2pManager.initialize(this, Looper.getMainLooper(), this) channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
setOperatingChannel(true) setOperatingChannel(true)
try { binder.requestGroupUpdate()
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()
}
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {

View File

@@ -30,7 +30,7 @@ import be.mygod.vpnhotspot.databinding.ListitemInterfaceBinding
import be.mygod.vpnhotspot.databinding.ListitemManageTetherBinding import be.mygod.vpnhotspot.databinding.ListitemManageTetherBinding
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -1,4 +1,4 @@
package be.mygod.vpnhotspot.net package be.mygod.vpnhotspot.net.wifi
import android.content.Context import android.content.Context
import android.net.wifi.WifiConfiguration import android.net.wifi.WifiConfiguration

View File

@@ -1,4 +1,4 @@
package be.mygod.vpnhotspot.net package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.wifi.WpsInfo import android.net.wifi.WpsInfo

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Source: https://android.googlesource.com/platform/packages/apps/Settings/+/6b4a31c/res/layout/wifi_ap_dialog.xml -->
<!-- Copyright (C) 2010 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300sp"
android:layout_height="wrap_content"
android:fadeScrollbars="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:orientation="vertical">
<LinearLayout android:id="@+id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_section" />
<LinearLayout android:id="@+id/type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_section">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_label"
android:layout_marginTop="8dip"
android:text="@string/wifi_ssid" />
<EditText android:id="@+id/ssid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:hint="@string/wifi_ssid_hint"
android:inputType="textNoSuggestions"
android:maxLength="32" />
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/fields"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_section">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_label"
android:layout_marginTop="8dip"
android:text="@string/wifi_password" />
<EditText android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:singleLine="true"
android:inputType="textPassword"
android:maxLength="63"
android:imeOptions="flagForceAscii" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item">
<TextView android:id="@+id/hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_label"
android:text="@string/credentials_password_too_short"
android:layout_marginTop="8dip"
android:layout_marginBottom="10sp"/>
<CheckBox android:id="@+id/show_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_content"
android:text="@string/wifi_show_password" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -40,7 +40,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/repeater_ssid" android:text="@string/wifi_ssid"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/> android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<Space <Space
@@ -57,31 +57,13 @@
android:focusable="false" android:focusable="false"
android:text="@{data.ssid}" android:text="@{data.ssid}"
android:textIsSelectable="true" android:textIsSelectable="true"
tools:text="DIRECT-rAnd0m"/> tools:text="DIRECT-rA-nd0m"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_column="0" android:layout_column="0"
android:layout_row="1" android:layout_row="1"
android:text="@string/repeater_password"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="2"
android:layout_row="1"
android:focusable="false"
android:text="@{data.password}"
android:textIsSelectable="true"
tools:text="p4ssW0rd"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="0"
android:layout_row="2"
android:text="@string/repeater_addresses" android:text="@string/repeater_addresses"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/> android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
@@ -89,7 +71,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_column="2" android:layout_column="2"
android:layout_row="2" android:layout_row="1"
android:focusable="false" android:focusable="false"
android:text="@{data.addresses}" android:text="@{data.addresses}"
android:textIsSelectable="true" android:textIsSelectable="true"

View File

@@ -8,8 +8,8 @@
android:title="@string/repeater_wps" android:title="@string/repeater_wps"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/resetGroup" android:id="@+id/edit"
android:icon="@drawable/ic_action_autorenew" android:icon="@drawable/ic_image_edit"
android:title="@string/repeater_reset_credentials" android:title="@string/repeater_reset_credentials"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
</menu> </menu>

View File

@@ -5,15 +5,15 @@
<string name="title_tethering">系统共享</string> <string name="title_tethering">系统共享</string>
<string name="title_settings">设置选项</string> <string name="title_settings">设置选项</string>
<string name="repeater_ssid">中继名称</string>
<string name="repeater_password">中继密码</string>
<string name="repeater_addresses">中继地址</string> <string name="repeater_addresses">中继地址</string>
<string name="repeater_wps_dialog_title">输入 PIN</string> <string name="repeater_wps_dialog_title">输入 PIN</string>
<string name="repeater_wps_dialog_pbc">一键加密</string> <string name="repeater_wps_dialog_pbc">一键加密</string>
<string name="repeater_wps_success_pbc">请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。</string> <string name="repeater_wps_success_pbc">请在 2 分钟内在需要连接的设备上使用一键加密以连接到此中继。</string>
<string name="repeater_wps_success_keypad">成功注册 PIN。</string> <string name="repeater_wps_success_keypad">成功注册 PIN。</string>
<string name="repeater_wps_failure">打开 WPS 失败(原因:%s</string> <string name="repeater_wps_failure">打开 WPS 失败(原因:%s</string>
<string name="repeater_reset_credentials">重置凭据</string> <string name="repeater_configure">设置 WLAN 中继</string>
<string name="repeater_configure_failure">未能找到有效的档案。请尝试先打开中继。</string>
<string name="repeater_reset_credentials">重置</string>
<string name="repeater_reset_credentials_dialog_message">Android <string name="repeater_reset_credentials_dialog_message">Android
系统将在下次打开中继时生成新的中继名称和密码。该操作不可撤销。</string> 系统将在下次打开中继时生成新的中继名称和密码。该操作不可撤销。</string>
<string name="repeater_reset_credentials_dialog_reset">重置</string> <string name="repeater_reset_credentials_dialog_reset">重置</string>
@@ -78,4 +78,12 @@
<string name="exception_interface_not_found">错误:未找到下游接口</string> <string name="exception_interface_not_found">错误:未找到下游接口</string>
<string name="root_unavailable">似乎没有 root</string> <string name="root_unavailable">似乎没有 root</string>
<string name="noisy_su_failure">发生异常,详情请查看调试信息。</string> <string name="noisy_su_failure">发生异常,详情请查看调试信息。</string>
<string name="wifi_ssid" msgid="5519636102673067319">"网络名称"</string>
<string name="wifi_ssid_hint" msgid="897593601067321355">"输入 SSID"</string>
<string name="wifi_password" msgid="5948219759936151048">"密码"</string>
<string name="wifi_show_password" msgid="6461249871236968884">"显示密码"</string>
<string name="credentials_password_too_short" msgid="7502749986405522663">"密码至少应包含 8 个字符。"</string>
<string name="wifi_save" msgid="3331121567988522826">"保存"</string>
<string name="wifi_cancel" msgid="6763568902542968964">"取消"</string>
</resources> </resources>

View File

@@ -1,12 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Values copied from:
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/e5ed810/res/values/strings.xml
* @string/wifi_tether_configure_ap_text
* @string/usb_tethering_button_text
* @string/wifi_hotspot_checkbox_text
* @string/bluetooth_tether_checkbox_text
-->
<resources> <resources>
<string name="app_name">VPN Hotspot</string> <string name="app_name">VPN Hotspot</string>
<string name="title_repeater">Repeater</string> <string name="title_repeater">Repeater</string>
<string name="title_tethering">Tethering</string> <string name="title_tethering">Tethering</string>
<string name="title_settings">Settings</string> <string name="title_settings">Settings</string>
<string name="repeater_ssid">Network name</string>
<string name="repeater_password">Password</string>
<string name="repeater_addresses">Addresses</string> <string name="repeater_addresses">Addresses</string>
<string name="repeater_wps">WPS</string> <string name="repeater_wps">WPS</string>
<string name="repeater_wps_dialog_title">Enter PIN</string> <string name="repeater_wps_dialog_title">Enter PIN</string>
@@ -15,10 +22,9 @@
device.</string> device.</string>
<string name="repeater_wps_success_keypad">PIN registered.</string> <string name="repeater_wps_success_keypad">PIN registered.</string>
<string name="repeater_wps_failure">Failed to start WPS (reason: %s)</string> <string name="repeater_wps_failure">Failed to start WPS (reason: %s)</string>
<string name="repeater_reset_credentials">Reset credentials</string> <string name="repeater_configure">Configure Wi\u2011Fi repeater</string>
<string name="repeater_reset_credentials_dialog_message">Android system will generate new network name and password <string name="repeater_configure_failure">Valid config not found. Please start repeater first.</string>
next time repeater is activated. This is irreversible.</string> <string name="repeater_reset_credentials">Reset</string>
<string name="repeater_reset_credentials_dialog_reset">Reset</string>
<string name="repeater_reset_credentials_success">Credentials reset.</string> <string name="repeater_reset_credentials_success">Credentials reset.</string>
<string name="repeater_reset_credentials_failure">Failed to reset credentials (reason: %s)</string> <string name="repeater_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
@@ -36,14 +42,6 @@
<string name="repeater_failure_reason_unknown">unknown #%d</string> <string name="repeater_failure_reason_unknown">unknown #%d</string>
<string name="tethering_manage">Manage…</string> <string name="tethering_manage">Manage…</string>
<!--
Values copied from:
* https://android.googlesource.com/platform/packages/apps/Settings/+/7686ef8/res/xml/tether_prefs.xml
* https://android.googlesource.com/platform/packages/apps/Settings/+/e5ed810/res/values/strings.xml
* @string/usb_tethering_button_text
* @string/wifi_hotspot_checkbox_text
* @string/bluetooth_tether_checkbox_text
-->
<string name="tethering_manage_usb">USB tethering</string> <string name="tethering_manage_usb">USB tethering</string>
<string name="tethering_manage_wifi">Wi\u2011Fi hotspot</string> <string name="tethering_manage_wifi">Wi\u2011Fi hotspot</string>
<string name="tethering_manage_wifi_legacy">Wi\u2011Fi hotspot (legacy)</string> <string name="tethering_manage_wifi_legacy">Wi\u2011Fi hotspot (legacy)</string>
@@ -82,4 +80,12 @@
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string> <string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
<string name="root_unavailable">Root unavailable</string> <string name="root_unavailable">Root unavailable</string>
<string name="noisy_su_failure">Something went wrong, please check the debug information.</string> <string name="noisy_su_failure">Something went wrong, please check the debug information.</string>
<string name="wifi_ssid">Network name</string>
<string name="wifi_ssid_hint">Enter the SSID</string>
<string name="wifi_password">Password</string>
<string name="wifi_show_password">Show password</string>
<string name="credentials_password_too_short">The password must have at least 8 characters.</string>
<string name="wifi_save">Save</string>
<string name="wifi_cancel">Cancel</string>
</resources> </resources>

View File

@@ -13,4 +13,35 @@
<item name="preferenceCategory_marginBottom">8dp</item> <item name="preferenceCategory_marginBottom">8dp</item>
</style> </style>
<!-- https://android.googlesource.com/platform/packages/apps/Settings/+/7efcc35/res/values/styles.xml -->
<style name="wifi_item">
<item name="android:layout_marginTop">8dip</item>
<item name="android:layout_marginStart">8dip</item>
<item name="android:layout_marginEnd">8dip</item>
<item name="android:paddingStart">8dip</item>
<item name="android:paddingEnd">8dip</item>
<item name="android:orientation">vertical</item>
<item name="android:gravity">start</item>
</style>
<style name="wifi_item_label">
<item name="android:paddingStart">8dip</item>
<item name="android:textSize">14sp</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:textAppearance">@android:style/TextAppearance.Material.Body1</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<style name="wifi_item_edit_content">
<item name="android:paddingStart">4dip</item>
<item name="android:layout_marginStart">4dip</item>
<item name="android:textSize">18sp</item>
</style>
<style name="wifi_section">
<item name="android:orientation">vertical</item>
</style>
<style name="wifi_item_content">
<item name="android:textAlignment">viewStart</item>
<item name="android:textAppearance">@android:style/TextAppearance.Material.Subhead</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
</resources> </resources>