diff options
Diffstat (limited to 'ui/src')
53 files changed, 970 insertions, 386 deletions
diff --git a/ui/src/googleplay/AndroidManifest.xml b/ui/src/googleplay/AndroidManifest.xml deleted file mode 100644 index 1343edbb..00000000 --- a/ui/src/googleplay/AndroidManifest.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools"> - <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" /> - <application> - <receiver android:name=".updater.Updater$AppUpdatedReceiver" tools:node="remove" /> - </application> -</manifest> diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index 42341226..05f20984 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -3,7 +3,9 @@ xmlns:tools="http://schemas.android.com/tools" android:installLocation="internalOnly"> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> @@ -45,10 +47,12 @@ <activity android:name=".activity.TunnelToggleActivity" - android:theme="@style/NoBackgroundTheme" - android:excludeFromRecents="true"/> + android:excludeFromRecents="true" + android:theme="@style/NoBackgroundTheme" /> - <activity android:name=".activity.MainActivity" android:exported="true"> + <activity + android:name=".activity.MainActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -62,8 +66,8 @@ <activity android:name=".activity.TvMainActivity" - android:theme="@style/TvTheme" - android:exported="true"> + android:exported="true" + android:theme="@style/TvTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> @@ -87,8 +91,8 @@ <activity android:name=".activity.LogViewerActivity" - android:label="@string/log_viewer_title" - android:exported="false"> + android:exported="false" + android:label="@string/log_viewer_title"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> @@ -100,14 +104,18 @@ android:exported="false" android:grantUriPermissions="true" /> - <receiver android:name=".BootShutdownReceiver" android:exported="true"> + <receiver + android:name=".BootShutdownReceiver" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.ACTION_SHUTDOWN" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> - <receiver android:name=".updater.Updater$AppUpdatedReceiver" android:exported="true"> + <receiver + android:name=".updater.Updater$AppUpdatedReceiver" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> </intent-filter> @@ -115,8 +123,8 @@ <receiver android:name=".model.TunnelManager$IntentReceiver" - android:permission="${applicationId}.permission.CONTROL_TUNNELS" - android:exported="true"> + android:exported="true" + android:permission="${applicationId}.permission.CONTROL_TUNNELS"> <intent-filter> <action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" /> <action android:name="com.wireguard.android.action.SET_TUNNEL_UP" /> @@ -126,9 +134,9 @@ <service android:name=".QuickTileService" + android:exported="true" android:icon="@drawable/ic_tile" - android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" - android:exported="true"> + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> <intent-filter> <action android:name="android.service.quicksettings.action.QS_TILE" /> @@ -149,5 +157,10 @@ <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent> + + <intent> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> + </intent> </queries> </manifest> diff --git a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt index cfd34e42..56810377 100644 --- a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt @@ -67,7 +67,8 @@ abstract class BaseActivity : AppCompatActivity() { protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean fun removeOnSelectedTunnelChangedListener( - listener: OnSelectedTunnelChangedListener) { + listener: OnSelectedTunnelChangedListener + ) { selectionChangeRegistry.remove(listener) } @@ -77,17 +78,17 @@ abstract class BaseActivity : AppCompatActivity() { private class SelectionChangeNotifier : NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>() { override fun onNotifyCallback( - listener: OnSelectedTunnelChangedListener, - oldTunnel: ObservableTunnel?, - ignored: Int, - newTunnel: ObservableTunnel? + listener: OnSelectedTunnelChangedListener, + oldTunnel: ObservableTunnel?, + ignored: Int, + newTunnel: ObservableTunnel? ) { listener.onSelectedTunnelChanged(oldTunnel, newTunnel) } } private class SelectionChangeRegistry : - CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier()) + CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier()) companion object { private const val KEY_SELECTED_TUNNEL = "selected_tunnel" diff --git a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt index 9deed440..155dff36 100644 --- a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt @@ -112,19 +112,21 @@ class LogViewerActivity : AppCompatActivity() { } binding.shareFab.setOnClickListener { - revokeLastUri() - val key = KeyPair().privateKey.toHex() - LOGS[key] = rawLogBytes() - lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key") - val shareIntent = ShareCompat.IntentBuilder(this) + lifecycleScope.launch { + revokeLastUri() + val key = KeyPair().privateKey.toHex() + LOGS[key] = rawLogBytes() + lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key") + val shareIntent = ShareCompat.IntentBuilder(this@LogViewerActivity) .setType("text/plain") .setSubject(getString(R.string.log_export_subject)) .setStream(lastUri) .setChooserTitle(R.string.log_export_title) .createChooserIntent() .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - revokeLastActivityResultLauncher.launch(shareIntent) + grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + revokeLastActivityResultLauncher.launch(shareIntent) + } } } @@ -140,22 +142,26 @@ class LogViewerActivity : AppCompatActivity() { finish() true } + R.id.save_log -> { saveButton?.isEnabled = false lifecycleScope.launch { saveLog() } true } + else -> super.onOptionsItemSelected(item) } } private val downloadsFileSaver = DownloadsFileSaver(this) - private fun rawLogBytes() : ByteArray { + private suspend fun rawLogBytes(): ByteArray { val builder = StringBuilder() - for (i in 0 until rawLogLines.size()) { - builder.append(rawLogLines[i]) - builder.append('\n') + withContext(Dispatchers.IO) { + for (i in 0 until rawLogLines.size()) { + builder.append(rawLogLines[i]) + builder.append('\n') + } } return builder.toString().toByteArray(Charsets.UTF_8) } @@ -175,12 +181,14 @@ class LogViewerActivity : AppCompatActivity() { saveButton?.isEnabled = true if (outputFile == null) return - Snackbar.make(findViewById(android.R.id.content), - if (exception == null) getString(R.string.log_export_success, outputFile?.fileName) - else getString(R.string.log_export_error, ErrorMessages[exception]), - if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG) - .setAnchorView(binding.shareFab) - .show() + Snackbar.make( + findViewById(android.R.id.content), + if (exception == null) getString(R.string.log_export_success, outputFile?.fileName) + else getString(R.string.log_export_error, ErrorMessages[exception]), + if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG + ) + .setAnchorView(binding.shareFab) + .show() } private suspend fun streamingLog() = withContext(Dispatchers.IO) { @@ -283,7 +291,8 @@ class LogViewerActivity : AppCompatActivity() { * * <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre> */ - private val THREADTIME_LINE: Pattern = Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$") + private val THREADTIME_LINE: Pattern = + Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$") private val LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap() private const val TAG = "WireGuard/LogViewerActivity" } @@ -306,7 +315,7 @@ class LogViewerActivity : AppCompatActivity() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.log_viewer_entry, parent, false) + .inflate(R.layout.log_viewer_entry, parent, false) return ViewHolder(view) } @@ -317,8 +326,10 @@ class LogViewerActivity : AppCompatActivity() { else SpannableString("${line.tag}: ${line.msg}").apply { setSpan(StyleSpan(BOLD), 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - setSpan(ForegroundColorSpan(levelToColor(line.level)), - 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + setSpan( + ForegroundColorSpan(levelToColor(line.level)), + 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) } holder.layout.apply { findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString() @@ -340,11 +351,11 @@ class LogViewerActivity : AppCompatActivity() { override fun insert(uri: Uri, values: ContentValues?): Uri? = null override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = - logForUri(uri)?.let { - val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1) - m.addRow(arrayOf("wireguard-log.txt", it.size.toLong())) - m - } + logForUri(uri)?.let { + val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1) + m.addRow(arrayOf("wireguard-log.txt", it.size.toLong())) + m + } override fun onCreate(): Boolean = true @@ -354,7 +365,8 @@ class LogViewerActivity : AppCompatActivity() { override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" } - override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null } + override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = + getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null } override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { if (mode != "r") return null diff --git a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt index b6c67e88..80c4868c 100644 --- a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt @@ -77,6 +77,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener onBackPressedDispatcher.onBackPressed() true } + R.id.menu_action_edit -> { supportFragmentManager.commit { replace(R.id.detail_container, TunnelEditorFragment()) @@ -91,12 +92,15 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener startActivity(Intent(this, SettingsActivity::class.java)) true } + else -> super.onOptionsItemSelected(item) } } - override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, - newTunnel: ObservableTunnel?): Boolean { + override fun onSelectedTunnelChanged( + oldTunnel: ObservableTunnel?, + newTunnel: ObservableTunnel? + ): Boolean { val fragmentManager = supportFragmentManager if (fragmentManager.isStateSaved) { return false diff --git a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt index 53b25938..2b1acf56 100644 --- a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt @@ -62,9 +62,9 @@ class SettingsActivity : AppCompatActivity() { zipExporter?.parent?.removePreference(zipExporter) } val wgQuickOnlyPrefs = arrayOf( - preferenceManager.findPreference("tools_installer"), - preferenceManager.findPreference("restore_on_boot"), - preferenceManager.findPreference<Preference>("multiple_tunnels") + preferenceManager.findPreference("tools_installer"), + preferenceManager.findPreference("restore_on_boot"), + preferenceManager.findPreference<Preference>("multiple_tunnels") ).filterNotNull() wgQuickOnlyPrefs.forEach { it.isVisible = false } lifecycleScope.launch { diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt index ee95ce40..59b9349f 100644 --- a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt @@ -24,7 +24,8 @@ import kotlinx.coroutines.launch @RequiresApi(Build.VERSION_CODES.N) class TunnelToggleActivity : AppCompatActivity() { - private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() } + private val permissionActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() } private fun toggleTunnelWithPermissionsResult() { val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return diff --git a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt index 4545672f..4c86b4c8 100644 --- a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt @@ -211,12 +211,13 @@ class TvMainActivity : AppCompatActivity() { try { tunnelFileImportResultLauncher.launch("*/*") } catch (_: Throwable) { - MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false).setPositiveButton(android.R.string.ok) { _, _ -> - try { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect"))) - } catch (_: Throwable) { - } - }.show() + MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> + try { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect"))) + } catch (_: Throwable) { + } + }.show() } } } @@ -359,6 +360,7 @@ class TvMainActivity : AppCompatActivity() { binding.tunnelList.requestFocus() } } + filesRoot.get()?.isNotEmpty() == true -> { files.clear() filesRoot.set("") @@ -372,7 +374,7 @@ class TvMainActivity : AppCompatActivity() { private suspend fun updateStats() { binding.tunnelList.forEach { viewItem -> val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem) - ?: return@forEach + ?: return@forEach try { val tunnel = listItem.item!! if (tunnel.state != Tunnel.State.UP || isDeleting.get()) { diff --git a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt index 30a2674f..17e3221b 100644 --- a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt +++ b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt @@ -40,9 +40,9 @@ class FileConfigStore(private val context: Context) : ConfigStore { override fun enumerate(): Set<String> { return context.fileList() - .filter { it.endsWith(".conf") } - .map { it.substring(0, it.length - ".conf".length) } - .toSet() + .filter { it.endsWith(".conf") } + .map { it.substring(0, it.length - ".conf".length) } + .toSet() } private fun fileFor(name: String): File { diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt index fd7bc72c..afba41cb 100644 --- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt +++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt @@ -47,9 +47,11 @@ object BindingAdapters { @JvmStatic @BindingAdapter("items", "layout", "fragment") - fun <E> setItems(view: LinearLayout, - oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?, - newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?) { + fun <E> setItems( + view: LinearLayout, + oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?, + newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment? + ) { if (oldList === newList && oldLayoutId == newLayoutId) return var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener) @@ -73,9 +75,11 @@ object BindingAdapters { @JvmStatic @BindingAdapter("items", "layout") - fun <E> setItems(view: LinearLayout, - oldList: Iterable<E>?, oldLayoutId: Int, - newList: Iterable<E>?, newLayoutId: Int) { + fun <E> setItems( + view: LinearLayout, + oldList: Iterable<E>?, oldLayoutId: Int, + newList: Iterable<E>?, newLayoutId: Int + ) { if (oldList === newList && oldLayoutId == newLayoutId) return view.removeAllViews() @@ -93,11 +97,13 @@ object BindingAdapters { @JvmStatic @BindingAdapter(requireAll = false, value = ["items", "layout", "configurationHandler"]) - fun <K, E : Keyed<out K>> setItems(view: RecyclerView, - oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int, - @Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?, - newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int, - newRowConfigurationHandler: RowConfigurationHandler<*, *>?) { + fun <K, E : Keyed<out K>> setItems( + view: RecyclerView, + oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int, + @Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?, + newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int, + newRowConfigurationHandler: RowConfigurationHandler<*, *>? + ) { if (view.layoutManager == null) view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false) if (oldList === newList && oldLayoutId == newLayoutId) @@ -123,16 +129,20 @@ object BindingAdapters { @JvmStatic @BindingAdapter("onBeforeCheckedChanged") - fun setOnBeforeCheckedChanged(view: ToggleSwitch, - listener: OnBeforeCheckedChangeListener?) { + fun setOnBeforeCheckedChanged( + view: ToggleSwitch, + listener: OnBeforeCheckedChangeListener? + ) { view.setOnBeforeCheckedChangeListener(listener) } @JvmStatic @BindingAdapter("onFocusChange") - fun setOnFocusChange(view: EditText, - listener: View.OnFocusChangeListener?) { - view.setOnFocusChangeListener(listener) + fun setOnFocusChange( + view: EditText, + listener: View.OnFocusChangeListener? + ) { + view.onFocusChangeListener = listener } @JvmStatic diff --git a/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt b/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt index 93333cb6..da153bbe 100644 --- a/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt +++ b/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt @@ -61,8 +61,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v } } - override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int, - itemCount: Int) { + override fun onItemRangeChanged( + sender: ObservableList<T>, positionStart: Int, + itemCount: Int + ) { val listener = weakListener.get() if (listener != null) { for (i in positionStart until positionStart + itemCount) { @@ -75,8 +77,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v } } - override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int, - itemCount: Int) { + override fun onItemRangeInserted( + sender: ObservableList<T>, positionStart: Int, + itemCount: Int + ) { val listener = weakListener.get() if (listener != null) { for (i in positionStart until positionStart + itemCount) @@ -86,8 +90,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v } } - override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int, - toPosition: Int, itemCount: Int) { + override fun onItemRangeMoved( + sender: ObservableList<T>, fromPosition: Int, + toPosition: Int, itemCount: Int + ) { val listener = weakListener.get() if (listener != null) { val views = arrayOfNulls<View>(itemCount) @@ -99,8 +105,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v } } - override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int, - itemCount: Int) { + override fun onItemRangeRemoved( + sender: ObservableList<T>, positionStart: Int, + itemCount: Int + ) { val listener = weakListener.get() if (listener != null) { listener.container.removeViews(positionStart, itemCount) diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt index 531cf90d..1cd19934 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt @@ -49,7 +49,8 @@ class AppListDialogFragment : DialogFragment() { packageInfos.forEach { val packageName = it.packageName val appInfo = it.applicationInfo - val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName)) + val appData = + ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName)) applicationData.add(appData) appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable?, propertyId: Int) { @@ -143,10 +144,12 @@ class AppListDialogFragment : DialogFragment() { selectedApps.add(data.packageName) } } - setFragmentResult(REQUEST_SELECTION, bundleOf( + setFragmentResult( + REQUEST_SELECTION, bundleOf( KEY_SELECTED_APPS to selectedApps.toTypedArray(), KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0) - )) + ) + ) dismiss() } diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt index b70d53be..d5c1723f 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt @@ -99,8 +99,8 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener { val view = view if (view != null) Snackbar.make(view, message, Snackbar.LENGTH_LONG) - .setAnchorView(view.findViewById(R.id.create_fab)) - .show() + .setAnchorView(view.findViewById(R.id.create_fab)) + .show() else Toast.makeText(activity, message, Toast.LENGTH_LONG).show() Log.e(TAG, message, e) diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt index 5fa7297b..34c96505 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt @@ -37,6 +37,7 @@ class ConfigNamingDialogFragment : DialogFragment() { } } } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val configText = requireArguments().getString(KEY_CONFIG_TEXT) diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt index 57e6828a..fce14d20 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt @@ -5,6 +5,7 @@ package com.wireguard.android.fragment import android.os.Bundle +import android.util.Log; import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -21,6 +22,7 @@ import com.wireguard.android.databinding.TunnelDetailFragmentBinding import com.wireguard.android.databinding.TunnelDetailPeerBinding import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.QuantityFormatter +import com.wireguard.android.viewmodel.ConfigDetail import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -40,8 +42,10 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { menuInflater.inflate(R.menu.tunnel_detail, menu) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { super.onCreateView(inflater, container, savedInstanceState) binding = TunnelDetailFragmentBinding.inflate(inflater, container, false) binding?.executePendingBindings() @@ -77,7 +81,9 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { } else { lifecycleScope.launch { try { - binding.config = newTunnel.getConfigAsync() + var config = newTunnel.getConfigDetailAsync() + binding.config = config + Log.i(TAG, "onSelectedTunnelChanged " + config + ", " + config.config) } catch (_: Throwable) { binding.config = null } @@ -110,16 +116,18 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { val statistics = tunnel.getStatisticsAsync() for (i in 0 until binding.peersLayout.childCount) { val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i)) - ?: continue + ?: continue val publicKey = peer.item!!.publicKey val peerStats = statistics.peer(publicKey) if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) { peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE } else { - peer.transferText.text = getString(R.string.transfer_rx_tx, + peer.transferText.text = getString( + R.string.transfer_rx_tx, QuantityFormatter.formatBytes(peerStats.rxBytes), - QuantityFormatter.formatBytes(peerStats.txBytes)) + QuantityFormatter.formatBytes(peerStats.txBytes) + ) peer.transferLabel.visibility = View.VISIBLE peer.transferText.visibility = View.VISIBLE } @@ -135,7 +143,7 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { } catch (e: Throwable) { for (i in 0 until binding.peersLayout.childCount) { val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i)) - ?: continue + ?: continue peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE peer.latestHandshakeLabel.visibility = View.GONE @@ -143,4 +151,8 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { } } } + + companion object { + private const val TAG = "WireGuard/TunnelDetailFragment" + } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt index 7a8b822e..6be27f94 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt @@ -5,7 +5,6 @@ package com.wireguard.android.fragment import android.content.Context -import android.os.Build import android.os.Bundle import android.text.InputType import android.util.Log @@ -17,13 +16,17 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.inputmethod.InputMethodManager +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView import android.widget.EditText +import android.widget.Filter import android.widget.Toast import androidx.core.os.BundleCompat import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputLayout import com.wireguard.android.Application import com.wireguard.android.R import com.wireguard.android.backend.Tunnel @@ -33,6 +36,7 @@ import com.wireguard.android.util.AdminKnobs import com.wireguard.android.util.BiometricAuthenticator import com.wireguard.android.util.ErrorMessages import com.wireguard.android.viewmodel.ConfigProxy +import com.wireguard.android.viewmodel.Constants import com.wireguard.config.Config import kotlinx.coroutines.launch @@ -44,6 +48,21 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { private var binding: TunnelEditorFragmentBinding? = null private var tunnel: ObservableTunnel? = null + private class MaterialSpinnerAdapter<T>(context: Context, resource: Int, private val objects: List<T>) : ArrayAdapter<T>(context, resource, objects) { + private val _filter: Filter by lazy { + object : Filter() { + override fun performFiltering(constraint: CharSequence?): FilterResults { + return FilterResults() + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults?) { + } + } + } + + override fun getFilter(): Filter = _filter + } + private fun onConfigLoaded(config: Config) { binding?.config = ConfigProxy(config) } @@ -67,22 +86,27 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.config_editor, menu) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { super.onCreateView(inflater, container, savedInstanceState) binding = TunnelEditorFragmentBinding.inflate(inflater, container, false) binding?.apply { executePendingBindings() privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() } } + + var httpProxyMenu = binding?.root?.findViewById<TextInputLayout>(R.id.http_proxy_menu) + var httpProxyItems = listOf(Constants.HTTP_PROXY_NONE, Constants.HTTP_PROXY_MANUAL, Constants.HTTP_PROXY_PAC) + var httpProxyAdapter = MaterialSpinnerAdapter(requireContext(), R.layout.http_proxy_menu_item, httpProxyItems) + var httpProxyMenuText = httpProxyMenu?.editText as? AutoCompleteTextView + httpProxyMenuText?.setAdapter(httpProxyAdapter) + return binding?.root } @@ -103,8 +127,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { val focusedView = activity.currentFocus if (focusedView != null) { val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - inputManager?.hideSoftInputFromWindow(focusedView.windowToken, - InputMethodManager.HIDE_NOT_ALWAYS) + inputManager?.hideSoftInputFromWindow( + focusedView.windowToken, + InputMethodManager.HIDE_NOT_ALWAYS + ) } parentFragmentManager.popBackStackImmediate() @@ -138,6 +164,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { onTunnelCreated(null, e) } } + tunnel!!.name != binding!!.name -> { Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name) try { @@ -147,6 +174,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { onTunnelRenamed(tunnel!!, newConfig, e) } } + else -> { Log.d(TAG, "Attempting to save config of " + tunnel!!.name) try { @@ -202,8 +230,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { super.onSaveInstanceState(outState) } - override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, - newTunnel: ObservableTunnel?) { + override fun onSelectedTunnelChanged( + oldTunnel: ObservableTunnel?, + newTunnel: ObservableTunnel? + ) { tunnel = newTunnel if (binding == null) return binding!!.config = ConfigProxy() @@ -240,8 +270,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { } } - private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config, - throwable: Throwable?) { + private suspend fun onTunnelRenamed( + renamedTunnel: ObservableTunnel, newConfig: Config, + throwable: Throwable? + ) { val ctx = activity ?: Application.get() if (throwable == null) { val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name) @@ -298,13 +330,15 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { haveShownKeys = true showPrivateKey(edit) } + is BiometricAuthenticator.Result.Failure -> { Snackbar.make( - binding!!.mainContainer, - it.message, - Snackbar.LENGTH_SHORT + binding!!.mainContainer, + it.message, + Snackbar.LENGTH_SHORT ).show() } + is BiometricAuthenticator.Result.Cancelled -> {} } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt index 8c5389d9..cba7c476 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt @@ -81,6 +81,8 @@ class TunnelListFragment : BaseFragment() { } } + private val snackbarUpdateShower = SnackbarUpdateShower(this) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (savedInstanceState != null) { @@ -91,8 +93,10 @@ class TunnelListFragment : BaseFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { super.onCreateView(inflater, container, savedInstanceState) binding = TunnelListFragmentBinding.inflate(inflater, container, false) val bottomSheet = AddTunnelsSheet() @@ -105,26 +109,29 @@ class TunnelListFragment : BaseFragment() { AddTunnelsSheet.REQUEST_CREATE -> { startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java)) } + AddTunnelsSheet.REQUEST_IMPORT -> { tunnelFileImportResultLauncher.launch("*/*") } + AddTunnelsSheet.REQUEST_SCAN -> { - qrImportResultLauncher.launch(ScanOptions() + qrImportResultLauncher.launch( + ScanOptions() .setOrientationLocked(false) .setBeepEnabled(false) - .setPrompt(getString(R.string.qr_code_hint))) + .setPrompt(getString(R.string.qr_code_hint)) + ) } } } bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET") } executePendingBindings() + snackbarUpdateShower.attach(mainContainer, createFab) } backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() } backPressedCallback?.isEnabled = false - SnackbarUpdateShower.attachToActivity(requireActivity(), binding?.mainContainer!!, binding?.createFab) - return binding?.root } @@ -191,8 +198,8 @@ class TunnelListFragment : BaseFragment() { val binding = binding if (binding != null) Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG) - .setAnchorView(binding.createFab) - .show() + .setAnchorView(binding.createFab) + .show() else Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show() } @@ -234,6 +241,7 @@ class TunnelListFragment : BaseFragment() { mode.finish() true } + R.id.menu_action_select_all -> { lifecycleScope.launch { val tunnels = Application.getTunnelManager().getTunnels() @@ -243,6 +251,7 @@ class TunnelListFragment : BaseFragment() { } true } + else -> false } } @@ -308,7 +317,7 @@ class TunnelListFragment : BaseFragment() { private fun animateFab(view: View?, show: Boolean) { view ?: return val animation = AnimationUtils.loadAnimation( - context, if (show) R.anim.scale_up else R.anim.scale_down + context, if (show) R.anim.scale_up else R.anim.scale_down ) animation.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationRepeat(animation: Animation?) { diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt index c3e3405e..5c34b57f 100644 --- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt +++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt @@ -7,12 +7,20 @@ package com.wireguard.android.model import android.util.Log import androidx.databinding.BaseObservable import androidx.databinding.Bindable +import com.wireguard.android.Application import com.wireguard.android.BR +import com.wireguard.android.backend.Dhcp import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel import com.wireguard.android.databinding.Keyed import com.wireguard.android.util.applicationScope +import com.wireguard.android.viewmodel.ConfigDetail +import com.wireguard.android.viewmodel.PeerDetail import com.wireguard.config.Config +import com.wireguard.config.InetEndpoint +import com.wireguard.config.InetNetwork +import com.wireguard.crypto.Key +import java.util.Optional import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -21,10 +29,10 @@ import kotlinx.coroutines.withContext * Encapsulates the volatile and nonvolatile state of a WireGuard tunnel. */ class ObservableTunnel internal constructor( - private val manager: TunnelManager, - private var name: String, - config: Config?, - state: Tunnel.State + private val manager: TunnelManager, + private var name: String, + config: Config?, + state: Tunnel.State ) : BaseObservable(), Keyed<String>, Tunnel { override val key get() = name @@ -55,7 +63,18 @@ class ObservableTunnel internal constructor( } fun onStateChanged(state: Tunnel.State): Tunnel.State { - if (state != Tunnel.State.UP) onStatisticsChanged(null) + if (state != Tunnel.State.UP) { + onStatisticsChanged(null) + onDhcpChanged(null) + Application.getCoroutineScope().launch { + onPeersReset() + } + } else { + configDetail?.peers?.forEach { + var endpoint: InetEndpoint? = it.peer?.endpoint?.orElse(null) + it.endpoint = Optional.ofNullable(endpoint?.getResolved()?.orElse(null)); + } + } this.state = state notifyPropertyChanged(BR.state) return state @@ -68,6 +87,7 @@ class ObservableTunnel internal constructor( this@ObservableTunnel.state } + private var configDetail: ConfigDetail? = if (config != null) ConfigDetail(config) else null @get:Bindable var config = config @@ -86,7 +106,11 @@ class ObservableTunnel internal constructor( private set suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) { - config ?: manager.getTunnelConfig(this@ObservableTunnel) + config ?: manager.getTunnelConfig(this@ObservableTunnel).config!! + } + + suspend fun getConfigDetailAsync(): ConfigDetail = withContext(Dispatchers.Main.immediate) { + configDetail ?: manager.getTunnelConfig(this@ObservableTunnel) } suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) { @@ -98,10 +122,11 @@ class ObservableTunnel internal constructor( } } - fun onConfigChanged(config: Config?): Config? { + fun onConfigChanged(config: Config?): ConfigDetail? { + this.configDetail = ConfigDetail(config) this.config = config notifyPropertyChanged(BR.config) - return config + return configDetail } @@ -137,6 +162,106 @@ class ObservableTunnel internal constructor( } + @get:Bindable + var dhcp: Dhcp? = null + private set + + override fun onDhcpChange(newDhcp: Dhcp) { + onDhcpChanged(newDhcp) + } + + fun onDhcpChanged(dhcp: Dhcp?): Dhcp? { + this.dhcp = dhcp + notifyPropertyChanged(BR.dhcp) + return dhcp + } + + // Remove dynamic peers, and reset static peers + fun onPeersReset() { + Log.i(TAG, "ObservableTunnel onPeersReset") + var toRemove: MutableList<PeerDetail> = ArrayList() + + configDetail?.peers?.forEach { + if (it.peer == null) { + toRemove.add(it) + } else { + it.endpoint = Optional.empty() + } + } + + toRemove.forEach { + Log.i(TAG, "ObservableTunnel remove " + it) + configDetail?.peers?.remove(it) + } + } + + override fun onEndpointChange(publicKey: Key, newEndpoint: InetEndpoint?) { + Application.getCoroutineScope().launch { + onEndpointChanged(publicKey, newEndpoint) + } + } + + private fun onEndpointChanged(publicKey: Key, newEndpoint: InetEndpoint?) { + + Log.i(TAG, "ObservableTunnel onEndpointChange " + newEndpoint) + var peer: PeerDetail? = null + + configDetail?.peers?.forEach { + if (it.publicKey.equals(publicKey) == true) { + Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer) + peer = it; + } + } + + if (peer == null) { + Log.i(TAG, "ObservableTunnel create peer " + publicKey) + peer = PeerDetail(publicKey) + configDetail?.peers?.add(peer) + } + + var peer2: PeerDetail = peer!! + + if (newEndpoint != null) { + peer2.endpoint = newEndpoint.getResolved() + } else { + var peer3 = peer2.peer + peer2.endpoint = if (peer3 != null) peer3.endpoint else Optional.empty() + } + } + + fun lookupPeer(publicKey: Key): PeerDetail { + configDetail?.peers?.forEach { + if (it.publicKey.equals(publicKey) == true) { + Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer) + return it + } + } + + Log.i(TAG, "ObservableTunnel create peer " + publicKey) + var peer: PeerDetail = PeerDetail(publicKey) + configDetail?.peers?.add(peer) + + return peer + } + + override fun onAllowedIpsChange(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) { + Application.getCoroutineScope().launch { + onAllowedIpsChanged(publicKey, addNetworks, removeNetworks) + } + } + + private fun onAllowedIpsChanged(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) { + var peer: PeerDetail = lookupPeer(publicKey) + + removeNetworks?.let() { + peer.allowedIps.removeAll(removeNetworks) + } + addNetworks?.let() { + peer.allowedIps.addAll(addNetworks) + } + } + + suspend fun deleteAsync() = manager.delete(this) diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index e7bb751b..960adcaa 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -24,6 +24,7 @@ import com.wireguard.android.databinding.ObservableSortedKeyedArrayList import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.UserKnobs import com.wireguard.android.util.applicationScope +import com.wireguard.android.viewmodel.ConfigDetail import com.wireguard.config.Config import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers @@ -94,7 +95,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) } } - suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) { + suspend fun getTunnelConfig(tunnel: ObservableTunnel): ConfigDetail = withContext(Dispatchers.Main.immediate) { tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!! } @@ -140,7 +141,8 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { if (previouslyRunning.isEmpty()) return withContext(Dispatchers.IO) { try { - tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll() + tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } } + .awaitAll() } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } @@ -155,7 +157,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { tunnel.onConfigChanged(withContext(Dispatchers.IO) { getBackend().setState(tunnel, tunnel.state, config) configStore.save(tunnel.name, config) - })!! + })!!.config!! } suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) { diff --git a/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt index 59980dcb..16920923 100644 --- a/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt @@ -12,8 +12,8 @@ import android.util.AttributeSet import android.widget.Toast import androidx.preference.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.wireguard.android.BuildConfig import com.wireguard.android.R +import com.wireguard.android.updater.Updater import com.wireguard.android.util.ErrorMessages class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) { @@ -23,7 +23,7 @@ class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(cont override fun onClick() { /* Google Play Store forbids links to our donation page. */ - if (BuildConfig.IS_GOOGLE_PLAY) { + if (Updater.installerIsGooglePlay(context)) { MaterialAlertDialogBuilder(context) .setTitle(R.string.donate_title) .setMessage(R.string.donate_google_play_disappointment) diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt index ced95b69..220796e0 100644 --- a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt @@ -70,14 +70,16 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference val message = context.getString(R.string.zip_export_error, error) Log.e(TAG, message, e) Snackbar.make( - activity.findViewById(android.R.id.content), - message, Snackbar.LENGTH_LONG).show() + activity.findViewById(android.R.id.content), + message, Snackbar.LENGTH_LONG + ).show() isEnabled = true } } } - override fun getSummary() = if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath) + override fun getSummary() = + if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath) override fun getTitle() = context.getString(R.string.zip_export_title) @@ -91,13 +93,15 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference isEnabled = false exportZip() } + is BiometricAuthenticator.Result.Failure -> { Snackbar.make( - activity.findViewById(android.R.id.content), - it.message, - Snackbar.LENGTH_SHORT + activity.findViewById(android.R.id.content), + it.message, + Snackbar.LENGTH_SHORT ).show() } + is BiometricAuthenticator.Result.Cancelled -> {} } } diff --git a/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt index d3c1b4f8..b566a0cf 100644 --- a/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt +++ b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt @@ -7,11 +7,10 @@ package com.wireguard.android.updater import android.view.View import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar -import com.wireguard.android.BuildConfig import com.wireguard.android.R import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.QuantityFormatter @@ -21,15 +20,20 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds -object SnackbarUpdateShower { - private class SwapableSnackbar(activity: FragmentActivity, view: View, anchor: View?) { - val actionSnackbar = makeSnackbar(activity, view, anchor) - val statusSnackbar = makeSnackbar(activity, view, anchor) +class SnackbarUpdateShower(private val fragment: Fragment) { + private var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null + private val intentLauncher = fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + lastUserIntervention?.markAsDone() + } + + private class SwapableSnackbar(fragment: Fragment, view: View, anchor: View?) { + val actionSnackbar = makeSnackbar(fragment, view, anchor) + val statusSnackbar = makeSnackbar(fragment, view, anchor) var showingAction: Boolean = false var showingStatus: Boolean = false - private fun makeSnackbar(activity: FragmentActivity, view: View, anchor: View?): Snackbar { - val snackbar = Snackbar.make(activity, view, "", Snackbar.LENGTH_INDEFINITE) + private fun makeSnackbar(fragment: Fragment, view: View, anchor: View?): Snackbar { + val snackbar = Snackbar.make(fragment.requireContext(), view, "", Snackbar.LENGTH_INDEFINITE) if (anchor != null) snackbar.anchorView = anchor snackbar.setTextMaxLines(6) @@ -42,11 +46,10 @@ object SnackbarUpdateShower { override fun onDismissed(snackbar: Snackbar?, @DismissEvent event: Int) { super.onDismissed(snackbar, event) if (event == DISMISS_EVENT_MANUAL || event == DISMISS_EVENT_ACTION || - (snackbar == actionSnackbar && !showingAction) || - (snackbar == statusSnackbar && !showingStatus) + (snackbar == actionSnackbar && !showingAction) || (snackbar == statusSnackbar && !showingStatus) ) return - activity.lifecycleScope.launch { + fragment.lifecycleScope.launch { delay(5.seconds) snackbar?.show() } @@ -88,17 +91,9 @@ object SnackbarUpdateShower { } } - fun attachToActivity(activity: FragmentActivity, view: View, anchor: View?) { - if (BuildConfig.IS_GOOGLE_PLAY) - return - - val snackbar = SwapableSnackbar(activity, view, anchor) - val context = activity.applicationContext - - var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null - val intentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - lastUserIntervention?.markAsDone() - } + fun attach(view: View, anchor: View?) { + val snackbar = SwapableSnackbar(fragment, view, anchor) + val context = fragment.requireContext() Updater.state.onEach { progress -> when (progress) { @@ -106,10 +101,7 @@ object SnackbarUpdateShower { snackbar.dismiss() is Updater.Progress.Available -> - snackbar.showAction( - context.getString(R.string.updater_avalable), - context.getString(R.string.updater_action) - ) { + snackbar.showAction(context.getString(R.string.updater_avalable), context.getString(R.string.updater_action)) { progress.update() } @@ -145,16 +137,11 @@ object SnackbarUpdateShower { } is Updater.Progress.Failure -> { - snackbar.showText( - context.getString( - R.string.updater_failure, - ErrorMessages[progress.error] - ) - ) + snackbar.showText(context.getString(R.string.updater_failure, ErrorMessages[progress.error])) delay(5.seconds) progress.retry() } } - }.launchIn(activity.lifecycleScope) + }.launchIn(fragment.lifecycleScope) } }
\ No newline at end of file diff --git a/ui/src/main/java/com/wireguard/android/updater/Updater.kt b/ui/src/main/java/com/wireguard/android/updater/Updater.kt index aa3256d4..6781a60d 100644 --- a/ui/src/main/java/com/wireguard/android/updater/Updater.kt +++ b/ui/src/main/java/com/wireguard/android/updater/Updater.kt @@ -44,17 +44,32 @@ import kotlin.time.Duration.Companion.seconds object Updater { private const val TAG = "WireGuard/Updater" - private const val LATEST_VERSION_URL = - "https://download.wireguard.com/android-client/latest.sig" - private const val APK_PATH_URL = "https://download.wireguard.com/android-client/%s" - private val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID.removeSuffix(".debug") + "-" + private const val UPDATE_URL_FMT = "https://download.wireguard.com/android-client/%s" + private const val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID + "-" private const val APK_NAME_SUFFIX = ".apk" - private const val RELEASE_PUBLIC_KEY_BASE64 = - "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp" private val CURRENT_VERSION = Version(BuildConfig.VERSION_NAME.removeSuffix("-debug")) + private val LATEST_FILE = if (BuildConfig.DEBUG) "latest-debug.sig" else "latest.sig" + private val RELEASE_PUBLIC_KEY_BASE64 = + if (BuildConfig.DEBUG) "RWTKFCNaLimMkuolGwAw12XT6sx+nnS7KE1wmhJ7YHXvAPedtPR/rofU" + else "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp" private val updaterScope = CoroutineScope(Job() + Dispatchers.IO) + private fun installer(context: Context): String = try { + val packageName = context.packageName + val pm = context.packageManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + pm.getInstallSourceInfo(packageName).installingPackageName ?: "" + } else { + @Suppress("DEPRECATION") + pm.getInstallerPackageName(packageName) ?: "" + } + } catch (_: Throwable) { + "" + } + + fun installerIsGooglePlay(context: Context): Boolean = installer(context) == "com.android.vending" + sealed class Progress { object Complete : Progress() class Available(val version: String) : Progress() { @@ -131,7 +146,10 @@ object Updater { throw InvalidParameterException("Version has no parts") parts = ULongArray(strParts.size) for (i in parts.indices) { - parts[i] = strParts[i].toULong() + if (strParts[i][0] == 'g') + parts[i] = strParts[i].substring(1).toULong(16) + else + parts[i] = strParts[i].toULong() } } @@ -200,7 +218,7 @@ object Updater { } private fun checkForUpdates(): Update? { - val connection = URL(LATEST_VERSION_URL).openConnection() as HttpURLConnection + val connection = URL(UPDATE_URL_FMT.format(LATEST_FILE)).openConnection() as HttpURLConnection connection.setRequestProperty("User-Agent", Application.USER_AGENT) connection.connect() if (connection.responseCode != HttpURLConnection.HTTP_OK) @@ -219,12 +237,7 @@ object Updater { val receiver = InstallReceiver() val context = Application.get().applicationContext val pendingIntent = withContext(Dispatchers.Main) { - ContextCompat.registerReceiver( - context, - receiver, - IntentFilter(receiver.sessionId), - ContextCompat.RECEIVER_NOT_EXPORTED - ) + ContextCompat.registerReceiver(context, receiver, IntentFilter(receiver.sessionId), ContextCompat.RECEIVER_NOT_EXPORTED) PendingIntent.getBroadcast( context, 0, @@ -241,24 +254,20 @@ object Updater { } emitProgress(Progress.Downloading(0UL, 0UL), true) - val connection = - URL(APK_PATH_URL.format(update.fileName)).openConnection() as HttpURLConnection + val connection = URL(UPDATE_URL_FMT.format(update.fileName)).openConnection() as HttpURLConnection connection.setRequestProperty("User-Agent", Application.USER_AGENT) connection.connect() if (connection.responseCode != HttpURLConnection.HTTP_OK) throw IOException("Update could not be fetched: ${connection.responseCode}") var downloadedByteLen: ULong = 0UL - val totalByteLen = - (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong() - .toULong() + val totalByteLen = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong().toULong() val fileBytes = ByteArray(1024 * 32 /* 32 KiB */) val digest = MessageDigest.getInstance("SHA-256") emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true) val installer = context.packageManager.packageInstaller - val params = - PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) params.setAppPackageName(context.packageName) /* Enforces updates; disallows new apps. */ @@ -316,18 +325,10 @@ object Updater { if (sessionId != intent.action) return - when (val status = - intent.getIntExtra( - PackageInstaller.EXTRA_STATUS, - PackageInstaller.STATUS_FAILURE_INVALID - )) { + when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0) - val userIntervention = IntentCompat.getParcelableExtra( - intent, - Intent.EXTRA_INTENT, - Intent::class.java - )!! + val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!! Application.getCoroutineScope().launch { emitProgress(Progress.NeedsUserIntervention(userIntervention, id)) } @@ -346,9 +347,7 @@ object Updater { context.applicationContext.packageManager.packageInstaller.abandonSession(id) } catch (_: SecurityException) { } - val message = - intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - ?: "Installation error $status" + val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status" Application.getCoroutineScope().launch { val e = Exception(message) Log.e(TAG, "Update failure", e) @@ -361,13 +360,11 @@ object Updater { } fun monitorForUpdates() { - if (BuildConfig.IS_GOOGLE_PLAY) + if (installerIsGooglePlay(Application.get())) return updaterScope.launch { - if (UserKnobs.updaterNewerVersionSeen.firstOrNull() - ?.let { Version(it) > CURRENT_VERSION } == true - ) + if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { Version(it) > CURRENT_VERSION } == true) return@launch var waitTime = 15 @@ -387,8 +384,10 @@ object Updater { } UserKnobs.updaterNewerVersionSeen.onEach { ver -> - if (ver != null && Version(ver) > CURRENT_VERSION && UserKnobs.updaterNewerVersionConsented.firstOrNull() - ?.let { Version(it) > CURRENT_VERSION } != true + if ( + ver != null && + Version(ver) > CURRENT_VERSION && + UserKnobs.updaterNewerVersionConsented.firstOrNull()?.let { Version(it) > CURRENT_VERSION } != true ) emitProgress(Progress.Available(ver)) }.launchIn(Application.getCoroutineScope()) @@ -403,25 +402,10 @@ object Updater { class AppUpdatedReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (BuildConfig.IS_GOOGLE_PLAY) - return - if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return - val installer = try { - val packageName = context.packageName - val pm = context.packageManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - pm.getInstallSourceInfo(packageName).installingPackageName ?: "" - } else { - @Suppress("DEPRECATION") - pm.getInstallerPackageName(packageName) ?: "" - } - } catch (_: Throwable) { - "" - } - if (installer != context.packageName) + if (installer(context) != context.packageName) return /* TODO: does not work because of restrictions placed on broadcast receivers. */ @@ -431,4 +415,4 @@ object Updater { context.startActivity(start) } } -}
\ No newline at end of file +} diff --git a/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt b/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt index 430e904d..2f90b2bb 100644 --- a/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt +++ b/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt @@ -13,5 +13,5 @@ object AdminKnobs { private val restrictions: RestrictionsManager? = Application.get().getSystemService() val disableConfigExport: Boolean get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false) - ?: false + ?: false } diff --git a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt index fe36898f..54d4da87 100644 --- a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt +++ b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt @@ -31,25 +31,29 @@ object BiometricAuthenticator { } fun authenticate( - @StringRes dialogTitleRes: Int, - fragment: Fragment, - callback: (Result) -> Unit + @StringRes dialogTitleRes: Int, + fragment: Fragment, + callback: (Result) -> Unit ) { val authCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString") - callback(when (errorCode) { - BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED, - BiometricPrompt.ERROR_NEGATIVE_BUTTON -> { - Result.Cancelled - } - BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE, - BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> { - Result.HardwareUnavailableOrDisabled + callback( + when (errorCode) { + BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NEGATIVE_BUTTON -> { + Result.Cancelled + } + + BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE, + BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> { + Result.HardwareUnavailableOrDisabled + } + + else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString)) } - else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString)) - }) + ) } override fun onAuthenticationFailed() { @@ -64,9 +68,9 @@ object BiometricAuthenticator { } val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback) val promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(fragment.getString(dialogTitleRes)) - .setAllowedAuthenticators(allowedAuthenticators) - .build() + .setTitle(fragment.getString(dialogTitleRes)) + .setAllowedAuthenticators(allowedAuthenticators) + .build() if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) { biometricPrompt.authenticate(promptInfo) } else { diff --git a/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt index 8538e75e..ace1dc05 100644 --- a/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt +++ b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt @@ -46,9 +46,9 @@ class DownloadsFileSaver(private val context: ComponentActivity) { contentValues.put(MediaColumns.DISPLAY_NAME, name) contentValues.put(MediaColumns.MIME_TYPE, mimeType) val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - ?: throw IOException(context.getString(R.string.create_downloads_file_error)) + ?: throw IOException(context.getString(R.string.create_downloads_file_error)) val contentStream = contentResolver.openOutputStream(contentUri) - ?: throw IOException(context.getString(R.string.create_downloads_file_error)) + ?: throw IOException(context.getString(R.string.create_downloads_file_error)) @Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null) var path: String? = null if (cursor != null) { diff --git a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt index 66027d95..97be8c99 100644 --- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt +++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt @@ -22,47 +22,47 @@ import java.net.InetAddress object ErrorMessages { private val BCE_REASON_MAP = mapOf( - BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key, - BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number, - BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value, - BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute, - BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section, - BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error, - BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute, - BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section + BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key, + BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number, + BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value, + BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute, + BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section, + BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error, + BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute, + BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section ) private val BE_REASON_MAP = mapOf( - BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error, - BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error, - BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error, - BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error, - BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error, - BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error, - BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error, - BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure + BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error, + BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error, + BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error, + BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error, + BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error, + BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error, + BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error, + BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure ) private val KFE_FORMAT_MAP = mapOf( - Key.Format.BASE64 to R.string.key_length_explanation_base64, - Key.Format.BINARY to R.string.key_length_explanation_binary, - Key.Format.HEX to R.string.key_length_explanation_hex + Key.Format.BASE64 to R.string.key_length_explanation_base64, + Key.Format.BINARY to R.string.key_length_explanation_binary, + Key.Format.HEX to R.string.key_length_explanation_hex ) private val KFE_TYPE_MAP = mapOf( - KeyFormatException.Type.CONTENTS to R.string.key_contents_error, - KeyFormatException.Type.LENGTH to R.string.key_length_error + KeyFormatException.Type.CONTENTS to R.string.key_contents_error, + KeyFormatException.Type.LENGTH to R.string.key_length_error ) private val PE_CLASS_MAP = mapOf( - InetAddress::class.java to R.string.parse_error_inet_address, - InetEndpoint::class.java to R.string.parse_error_inet_endpoint, - InetNetwork::class.java to R.string.parse_error_inet_network, - Int::class.java to R.string.parse_error_integer + InetAddress::class.java to R.string.parse_error_inet_address, + InetEndpoint::class.java to R.string.parse_error_inet_endpoint, + InetNetwork::class.java to R.string.parse_error_inet_network, + Int::class.java to R.string.parse_error_integer ) private val RSE_REASON_MAP = mapOf( - RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root, - RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error, - RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error, - RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error, - RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error, - RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error + RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root, + RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error, + RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error, + RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error, + RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error, + RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error ) operator fun get(throwable: Throwable?): String { @@ -80,21 +80,27 @@ object ErrorMessages { val explanation = getBadConfigExceptionExplanation(resources, rootCause) resources.getString(R.string.bad_config_error, reason, context) + explanation } + rootCause is BackendException -> { resources.getString(BE_REASON_MAP.getValue(rootCause.reason), *rootCause.format) } + rootCause is RootShellException -> { resources.getString(RSE_REASON_MAP.getValue(rootCause.reason), *rootCause.format) } + rootCause is NotFoundException -> { resources.getString(R.string.error_no_qr_found) } + rootCause is ChecksumException -> { resources.getString(R.string.error_qr_checksum) } + rootCause.localizedMessage != null -> { rootCause.localizedMessage!! } + else -> { val errorType = rootCause.javaClass.simpleName resources.getString(R.string.generic_error, errorType) @@ -102,8 +108,10 @@ object ErrorMessages { } } - private fun getBadConfigExceptionExplanation(resources: Resources, - bce: BadConfigException): String { + private fun getBadConfigExceptionExplanation( + resources: Resources, + bce: BadConfigException + ): String { if (bce.cause is KeyFormatException) { val kfe = bce.cause as KeyFormatException? if (kfe!!.type == KeyFormatException.Type.LENGTH) return resources.getString(KFE_FORMAT_MAP.getValue(kfe.format)) @@ -114,14 +122,18 @@ object ErrorMessages { return resources.getString(R.string.bad_config_explanation_udp_port) } else if (bce.location == BadConfigException.Location.MTU) { return resources.getString(R.string.bad_config_explanation_positive_number) + } else if (bce.location == BadConfigException.Location.HTTP_PROXY) { + return resources.getString(R.string.bad_config_explanation_http_proxy) } else if (bce.location == BadConfigException.Location.PERSISTENT_KEEPALIVE) { return resources.getString(R.string.bad_config_explanation_pka) } return "" } - private fun getBadConfigExceptionReason(resources: Resources, - bce: BadConfigException): String { + private fun getBadConfigExceptionReason( + resources: Resources, + bce: BadConfigException + ): String { if (bce.cause is KeyFormatException) { val kfe = bce.cause as KeyFormatException? return resources.getString(KFE_TYPE_MAP.getValue(kfe!!.type)) @@ -137,7 +149,8 @@ object ErrorMessages { var cause = throwable while (cause.cause != null) { if (cause is BadConfigException || cause is BackendException || - cause is RootShellException) break + cause is RootShellException + ) break val nextCause = cause.cause!! if (nextCause is RemoteException) break cause = nextCause diff --git a/ui/src/main/java/com/wireguard/android/util/Extensions.kt b/ui/src/main/java/com/wireguard/android/util/Extensions.kt index 98f94af9..3bc85051 100644 --- a/ui/src/main/java/com/wireguard/android/util/Extensions.kt +++ b/ui/src/main/java/com/wireguard/android/util/Extensions.kt @@ -25,7 +25,7 @@ val Any.applicationScope: CoroutineScope val Preference.activity: SettingsActivity get() = context as? SettingsActivity - ?: throw IllegalStateException("Failed to resolve SettingsActivity") + ?: throw IllegalStateException("Failed to resolve SettingsActivity") val Preference.lifecycleScope: CoroutineScope get() = activity.lifecycleScope diff --git a/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt index 135fc1f3..abc025a4 100644 --- a/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt +++ b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt @@ -55,11 +55,13 @@ class QrCodeFromFileScanner( multFactor = originalWidth.toFloat() / originalHeight.toFloat() newWidth = (newHeight * multFactor).toInt() } + originalWidth > originalHeight -> { newWidth = scaledSize multFactor = originalHeight.toFloat() / originalWidth.toFloat() newHeight = (newWidth * multFactor).toInt() } + originalHeight == originalWidth -> { newHeight = scaledSize newWidth = scaledSize diff --git a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt index bc059e1b..f7de2465 100644 --- a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt +++ b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt @@ -15,7 +15,6 @@ import com.wireguard.android.Application import com.wireguard.android.R import java.util.Locale import kotlin.time.Duration.Companion.seconds -import kotlin.time.DurationUnit object QuantityFormatter { fun formatBytes(bytes: Long): String { diff --git a/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt b/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt index e66691e8..daefc378 100644 --- a/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt +++ b/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt @@ -24,7 +24,6 @@ import java.io.BufferedReader import java.io.ByteArrayInputStream import java.io.InputStreamReader import java.nio.charset.StandardCharsets -import java.util.ArrayList import java.util.zip.ZipEntry import java.util.zip.ZipInputStream @@ -135,12 +134,16 @@ object TunnelImporter { message = context.getString(R.string.import_success, tunnels[0].name) else if (tunnels.isEmpty() && throwables.size == 1) else if (throwables.isEmpty()) - message = context.resources.getQuantityString(R.plurals.import_total_success, - tunnels.size, tunnels.size) + message = context.resources.getQuantityString( + R.plurals.import_total_success, + tunnels.size, tunnels.size + ) else if (!throwables.isEmpty()) - message = context.resources.getQuantityString(R.plurals.import_partial_success, - tunnels.size + throwables.size, - tunnels.size, tunnels.size + throwables.size) + message = context.resources.getQuantityString( + R.plurals.import_partial_success, + tunnels.size + throwables.size, + tunnels.size, tunnels.size + throwables.size + ) messageCallback(message) } diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt new file mode 100644 index 00000000..af95a86a --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt @@ -0,0 +1,22 @@ +package com.wireguard.android.viewmodel + +import androidx.databinding.ObservableArrayList +import androidx.databinding.ObservableList + +import com.wireguard.config.Config + +class ConfigDetail { + val config: Config? + val peers: ObservableList<PeerDetail> = ObservableArrayList() + + constructor(other: Config?) { + config = other + if (other != null) { + other.peers.forEach { + val detail = PeerDetail(it) + peers.add(detail) + detail.bind(this) + } + } + } +} diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt index 0be18a6f..c73b1efc 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt @@ -55,9 +55,9 @@ class ConfigProxy : Parcelable { val resolvedPeers: MutableCollection<Peer> = ArrayList() peers.forEach { resolvedPeers.add(it.resolve()) } return Config.Builder() - .setInterface(`interface`.resolve()) - .addPeers(resolvedPeers) - .build() + .setInterface(`interface`.resolve()) + .addPeers(resolvedPeers) + .build() } override fun writeToParcel(dest: Parcel, flags: Int) { diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt index 004ebed1..16c3e6a3 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt @@ -4,6 +4,8 @@ */ package com.wireguard.android.viewmodel +import android.net.Uri +import android.os.Build import android.os.Parcel import android.os.Parcelable import androidx.databinding.BaseObservable @@ -18,6 +20,12 @@ import com.wireguard.crypto.Key import com.wireguard.crypto.KeyFormatException import com.wireguard.crypto.KeyPair +object Constants { + const val HTTP_PROXY_NONE = "None" + const val HTTP_PROXY_MANUAL = "Manual" + const val HTTP_PROXY_PAC = "Proxy Auto-Config" +} + class InterfaceProxy : BaseObservable, Parcelable { @get:Bindable val excludedApplications: ObservableList<String> = ObservableArrayList() @@ -54,6 +62,44 @@ class InterfaceProxy : BaseObservable, Parcelable { } @get:Bindable + var httpProxyMenu: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyMenu) + notifyPropertyChanged(BR.httpProxyManualVisibility) + notifyPropertyChanged(BR.httpProxyPacVisibility) + } + + @get:Bindable + var httpProxyManualVisibility: Int = 0 + get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_MANUAL) android.view.View.VISIBLE else android.view.View.GONE) + + @get:Bindable + var httpProxyHostname: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyHostname) + } + + @get:Bindable + var httpProxyPort: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyPort) + } + + @get:Bindable + var httpProxyPac: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.httpProxyPac) + } + + @get:Bindable + var httpProxyPacVisibility: Int = 0 + get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_PAC) android.view.View.VISIBLE else android.view.View.GONE) + + @get:Bindable var privateKey: String = "" set(value) { field = value @@ -76,6 +122,10 @@ class InterfaceProxy : BaseObservable, Parcelable { parcel.readStringList(includedApplications) listenPort = parcel.readString() ?: "" mtu = parcel.readString() ?: "" + httpProxyMenu = parcel.readString() ?: "" + httpProxyHostname = parcel.readString() ?: "" + httpProxyPort = parcel.readString() ?: "" + httpProxyPac = parcel.readString() ?: "" privateKey = parcel.readString() ?: "" } @@ -87,6 +137,10 @@ class InterfaceProxy : BaseObservable, Parcelable { includedApplications.addAll(other.includedApplications) listenPort = other.listenPort.map { it.toString() }.orElse("") mtu = other.mtu.map { it.toString() }.orElse("") + httpProxyHostname = other.httpProxy.map { if (it.getHost().startsWith('[') && it.getHost().endsWith(']')) it.getHost().substring(1, it.getHost().length-1) else it.getHost() }.orElse("") + httpProxyPort = other.httpProxy.map { if (it.getPort() <= 0) "8080" else it.getPort().toString() }.orElse("") + httpProxyPac = other.httpProxy.map { it.getPacFileUrl().toString() }.orElse("") + httpProxyMenu = other.httpProxy.map { if (it.getPacFileUrl() != null && it.getPacFileUrl() != Uri.EMPTY) Constants.HTTP_PROXY_PAC else if (it.getHost() != "") Constants.HTTP_PROXY_MANUAL else Constants.HTTP_PROXY_NONE }.orElse(Constants.HTTP_PROXY_NONE) val keyPair = other.keyPair privateKey = keyPair.privateKey.toBase64() } @@ -111,6 +165,20 @@ class InterfaceProxy : BaseObservable, Parcelable { if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications) if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort) if (mtu.isNotEmpty()) builder.parseMtu(mtu) + if (Constants.HTTP_PROXY_MANUAL.equals(httpProxyMenu) && httpProxyHostname.isNotEmpty()) { + var httpProxy: String + if (httpProxyHostname.contains(":")) { + httpProxy = "[" + httpProxyHostname + "]" + } else { + httpProxy = httpProxyHostname + } + if (httpProxyPort.isNotEmpty()) { + httpProxy += ":" + httpProxyPort; + } + builder.parseHttpProxy(httpProxy) + } else if (Constants.HTTP_PROXY_PAC.equals(httpProxyMenu) && httpProxyPac.isNotEmpty()) { + builder.parseHttpProxy("pac:" + httpProxyPac) + } if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey) return builder.build() } @@ -122,6 +190,10 @@ class InterfaceProxy : BaseObservable, Parcelable { dest.writeStringList(includedApplications) dest.writeString(listenPort) dest.writeString(mtu) + dest.writeString(httpProxyMenu) + dest.writeString(httpProxyHostname) + dest.writeString(httpProxyPort) + dest.writeString(httpProxyPac) dest.writeString(privateKey) } diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt new file mode 100644 index 00000000..80b32fd5 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt @@ -0,0 +1,85 @@ +package com.wireguard.android.viewmodel + +import android.util.Log +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import androidx.databinding.Observable +import androidx.databinding.ObservableList +import androidx.databinding.ObservableArrayList + +import com.wireguard.android.BR +import com.wireguard.config.InetEndpoint +import com.wireguard.config.InetNetwork +import com.wireguard.config.Peer +import com.wireguard.crypto.Key; + +import java.util.Optional; + +import kotlin.collections.LinkedHashSet + + +class PeerDetail : BaseObservable { + var peer: Peer? + private var owner: ConfigDetail? = null + + @get:Bindable + var publicKey: Key + + @get:Bindable + var allowedIps: ObservableList<InetNetwork> = ObservableArrayList<InetNetwork>() + + @get:Bindable + var endpoint: Optional<InetEndpoint> = Optional.empty() + get() { + if (!field.isEmpty()) { + return field + } else { + return Optional.ofNullable(peer?.endpoint?.get()) + } + } + + set(value) { + Log.i(TAG, "notifyPropertyChanged endpoint " + this + ", " + value) + field = value + notifyPropertyChanged(BR.endpoint) + } + + @get:Bindable + var persistentKeepalive: Optional<Int> = Optional.empty() + + constructor(other: Peer) { + peer = other + publicKey = other.getPublicKey() + allowedIps.addAll(other.getAllowedIps()) + endpoint = other.getEndpoint(); + persistentKeepalive = other.getPersistentKeepalive() + } + + constructor(publicKey: Key) { + peer = null + this.publicKey = publicKey + } + + fun bind(owner: ConfigDetail) { + this.owner = owner + } + + override fun addOnPropertyChangedCallback (callback: Observable.OnPropertyChangedCallback) { + Log.i(TAG, "addOnPropertyChangedCallback " + this + ", " + callback) + super.addOnPropertyChangedCallback(callback) + } + + /** + * Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is + * identified by its public key and (if known) its endpoint. + * + * @return a concise single-line identifier for the {@code Peer} + */ + override fun toString(): String { + return "(Peer " + publicKey.toBase64() + ")" + } + + companion object { + private const val TAG = "WireGuard/PeerDetail" + } +} diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt index 4bf2ce9c..e78d0826 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt @@ -16,8 +16,6 @@ import com.wireguard.config.Attribute import com.wireguard.config.BadConfigException import com.wireguard.config.Peer import java.lang.ref.WeakReference -import java.util.ArrayList -import java.util.LinkedHashSet class PeerProxy : BaseObservable, Parcelable { private val dnsRoutes: MutableList<String?> = ArrayList() @@ -240,24 +238,32 @@ class PeerProxy : BaseObservable, Parcelable { peerProxy.setTotalPeers(sender.size) } - override fun onItemRangeChanged(sender: ObservableList<PeerProxy?>, - positionStart: Int, itemCount: Int) { + override fun onItemRangeChanged( + sender: ObservableList<PeerProxy?>, + positionStart: Int, itemCount: Int + ) { // Do nothing. } - override fun onItemRangeInserted(sender: ObservableList<PeerProxy?>, - positionStart: Int, itemCount: Int) { + override fun onItemRangeInserted( + sender: ObservableList<PeerProxy?>, + positionStart: Int, itemCount: Int + ) { onChanged(sender) } - override fun onItemRangeMoved(sender: ObservableList<PeerProxy?>, - fromPosition: Int, toPosition: Int, - itemCount: Int) { + override fun onItemRangeMoved( + sender: ObservableList<PeerProxy?>, + fromPosition: Int, toPosition: Int, + itemCount: Int + ) { // Do nothing. } - override fun onItemRangeRemoved(sender: ObservableList<PeerProxy?>, - positionStart: Int, itemCount: Int) { + override fun onItemRangeRemoved( + sender: ObservableList<PeerProxy?>, + positionStart: Int, itemCount: Int + ) { onChanged(sender) } } @@ -276,12 +282,12 @@ class PeerProxy : BaseObservable, Parcelable { @JvmField val CREATOR: Parcelable.Creator<PeerProxy> = PeerProxyCreator() private val IPV4_PUBLIC_NETWORKS = setOf( - "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", - "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", - "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", - "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", - "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", - "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4" + "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", + "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", + "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", + "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", + "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", + "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4" ) private val IPV4_WILDCARD = setOf("0.0.0.0/0") } diff --git a/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt index bf42166d..8c822dcb 100644 --- a/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt +++ b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt @@ -13,10 +13,12 @@ import com.wireguard.crypto.Key * InputFilter for entering WireGuard private/public keys encoded with base64. */ class KeyInputFilter : InputFilter { - override fun filter(source: CharSequence, - sStart: Int, sEnd: Int, - dest: Spanned, - dStart: Int, dEnd: Int): CharSequence? { + override fun filter( + source: CharSequence, + sStart: Int, sEnd: Int, + dest: Spanned, + dStart: Int, dEnd: Int + ): CharSequence? { var replacement: SpannableStringBuilder? = null var rIndex = 0 val dLength = dest.length @@ -26,8 +28,9 @@ class KeyInputFilter : InputFilter { // Restrict characters to the base64 character set. // Ensure adding this character does not push the length over the limit. if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) || - dIndex + 1 == Key.Format.BASE64.length && c == '=') && - dLength + (sIndex - sStart) < Key.Format.BASE64.length) { + dIndex + 1 == Key.Format.BASE64.length && c == '=') && + dLength + (sIndex - sStart) < Key.Format.BASE64.length + ) { ++rIndex } else { if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd) diff --git a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt index 511cd287..91c7da0c 100644 --- a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt +++ b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt @@ -11,10 +11,10 @@ import android.widget.RelativeLayout import com.wireguard.android.R class MultiselectableRelativeLayout @JvmOverloads constructor( - context: Context? = null, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - defStyleRes: Int = 0 + context: Context? = null, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 ) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) { private var multiselected = false diff --git a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt index 7af514d9..e21ebaba 100644 --- a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt +++ b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt @@ -13,10 +13,12 @@ import com.wireguard.android.backend.Tunnel * InputFilter for entering WireGuard configuration names (Linux interface names). */ class NameInputFilter : InputFilter { - override fun filter(source: CharSequence, - sStart: Int, sEnd: Int, - dest: Spanned, - dStart: Int, dEnd: Int): CharSequence? { + override fun filter( + source: CharSequence, + sStart: Int, sEnd: Int, + dest: Spanned, + dStart: Int, dEnd: Int + ): CharSequence? { var replacement: SpannableStringBuilder? = null var rIndex = 0 val dLength = dest.length @@ -26,7 +28,8 @@ class NameInputFilter : InputFilter { // Restrict characters to those valid in interfaces. // Ensure adding this character does not push the length over the limit. if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) && - dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) { + dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH + ) { ++rIndex } else { if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd) diff --git a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt index 8fcee9df..0e2eeff1 100644 --- a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt +++ b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt @@ -35,10 +35,10 @@ class SlashDrawable(private val mDrawable: Drawable) : Drawable() { val radiusX = scale(CORNER_RADIUS, width) val radiusY = scale(CORNER_RADIUS, height) updateRect( - scale(LEFT, width), - scale(TOP, height), - scale(RIGHT, width), - scale(TOP + mCurrentSlashLength, height) + scale(LEFT, width), + scale(TOP, height), + scale(RIGHT, width), + scale(TOP + mCurrentSlashLength, height) ) mPath.reset() // Draw the slash vertically diff --git a/ui/src/main/res/drawable/list_item_background.xml b/ui/src/main/res/drawable/list_item_background.xml index 3a77b524..16714e7b 100644 --- a/ui/src/main/res/drawable/list_item_background.xml +++ b/ui/src/main/res/drawable/list_item_background.xml @@ -8,8 +8,7 @@ app:state_multiselected="true"> <color android:color="?attr/colorSurfaceVariant" /> </item> - <item - android:state_activated="true"> + <item android:state_activated="true"> <color android:color="?attr/colorControlHighlight" /> </item> </selector> diff --git a/ui/src/main/res/layout/config_naming_dialog_fragment.xml b/ui/src/main/res/layout/config_naming_dialog_fragment.xml index 88deb976..63d3141d 100644 --- a/ui/src/main/res/layout/config_naming_dialog_fragment.xml +++ b/ui/src/main/res/layout/config_naming_dialog_fragment.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> <data> + <import type="com.wireguard.android.widget.NameInputFilter" /> </data> @@ -24,7 +25,8 @@ android:imeOptions="actionDone" android:inputType="textNoSuggestions|textVisiblePassword" app:filter="@{NameInputFilter.newInstance()}"> - <requestFocus/> + + <requestFocus /> </com.google.android.material.textfield.TextInputEditText> </com.google.android.material.textfield.TextInputLayout> diff --git a/ui/src/main/res/layout/http_proxy_menu_item.xml b/ui/src/main/res/layout/http_proxy_menu_item.xml new file mode 100644 index 00000000..8ad5c026 --- /dev/null +++ b/ui/src/main/res/layout/http_proxy_menu_item.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:ellipsize="end" + android:maxLines="1" +/> diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml index 332df04a..425b364d 100644 --- a/ui/src/main/res/layout/tunnel_detail_fragment.xml +++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml @@ -5,6 +5,8 @@ <data> + <import type="android.os.Build" /> + <import type="com.wireguard.android.backend.Tunnel.State" /> <import type="com.wireguard.android.util.ClipboardUtils" /> @@ -19,7 +21,7 @@ <variable name="config" - type="com.wireguard.config.Config" /> + type="com.wireguard.android.viewmodel.ConfigDetail" /> </data> <ScrollView @@ -116,7 +118,7 @@ android:nextFocusForward="@id/addresses_text" android:onClick="@{ClipboardUtils::copyTextView}" android:singleLine="true" - android:text="@{config.interface.keyPair.publicKey.toBase64}" + android:text="@{config.config.interface.keyPair.publicKey.toBase64}" android:textAppearance="?attr/textAppearanceBodyLarge" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/public_key_label" @@ -129,7 +131,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/addresses_text" android:text="@string/addresses" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -139,14 +141,41 @@ android:layout_height="wrap_content" android:contentDescription="@string/addresses" android:nextFocusUp="@id/public_key_text" + android:nextFocusDown="@id/dynamic_addresses_text" + android:nextFocusForward="@id/dynamic_addresses_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:text="@{config.config.interface.addresses}" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/addresses_label" + tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> + + <TextView + android:id="@+id/dynamic_addresses_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:labelFor="@+id/dynamic_addresses_text" + android:text="@string/dynamic_addresses" + android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/addresses_text" /> + + <TextView + android:id="@+id/dynamic_addresses_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/dynamic_addresses" + android:nextFocusUp="@id/addresses_text" android:nextFocusDown="@id/dns_servers_text" android:nextFocusForward="@id/dns_servers_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.addresses}" + android:text="@{tunnel.dhcp.addresses}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/addresses_label" + app:layout_constraintTop_toBottomOf="@+id/dynamic_addresses_label" tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> <TextView @@ -156,22 +185,22 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_servers_text" android:text="@string/dns_servers" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/addresses_text" /> + app:layout_constraintTop_toBottomOf="@id/dynamic_addresses_text" /> <TextView android:id="@+id/dns_servers_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/dns_servers" - android:nextFocusUp="@id/addresses_text" + android:nextFocusUp="@id/dynamic_addresses_text" android:nextFocusDown="@id/dns_search_domains_text" android:nextFocusForward="@id/dns_search_domains_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsServers}" + android:text="@{config.config.interface.dnsServers}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_servers_label" tools:text="8.8.8.8, 8.8.4.4" /> @@ -183,7 +212,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_search_domain_text" android:text="@string/dns_search_domains" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/dns_servers_text" /> @@ -196,9 +225,9 @@ android:nextFocusDown="@id/listen_port_text" android:nextFocusForward="@id/listen_port_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsSearchDomains}" + android:text="@{config.config.interface.dnsSearchDomains}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_search_domains_label" tools:text="zx2c4.com" /> @@ -210,7 +239,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/listen_port_text" android:text="@string/listen_port" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -223,12 +252,12 @@ android:contentDescription="@string/listen_port" android:nextFocusRight="@id/mtu_text" android:nextFocusUp="@id/dns_search_domains_text" - android:nextFocusDown="@id/applications_text" + android:nextFocusDown="@id/http_proxy_text" android:nextFocusForward="@id/mtu_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.listenPort}" + android:text="@{config.config.interface.listenPort}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -242,7 +271,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/mtu_text" android:text="@string/mtu" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintLeft_toRightOf="@id/listen_port_label" @@ -256,11 +285,11 @@ android:contentDescription="@string/mtu" android:nextFocusLeft="@id/listen_port_text" android:nextFocusUp="@id/dns_servers_text" - android:nextFocusForward="@id/applications_text" + android:nextFocusForward="@id/http_proxy_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.mtu}" + android:text="@{config.config.interface.mtu}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toEndOf="@id/listen_port_label" @@ -276,15 +305,42 @@ app:constraint_referenced_ids="listen_port_text,mtu_text" /> <TextView + android:id="@+id/http_proxy_label" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:labelFor="@+id/http_proxy_text" + android:text="@string/http_proxy" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" /> + + <TextView + android:id="@+id/http_proxy_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:contentDescription="@string/http_proxy" + android:nextFocusUp="@id/listen_port_text" + android:nextFocusDown="@id/applications_text" + android:nextFocusForward="@id/applications_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:text="@{config.config.interface.httpProxy}" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintTop_toBottomOf="@id/http_proxy_label" + app:layout_constraintStart_toStartOf="parent" + tools:text="http://example.com:8888" /> + + <TextView android:id="@+id/applications_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:labelFor="@+id/applications_text" android:text="@string/applications" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" /> + app:layout_constraintTop_toBottomOf="@+id/http_proxy_text" /> <TextView android:id="@+id/applications_text" @@ -295,9 +351,9 @@ android:nextFocusDown="@id/peers_layout" android:nextFocusForward="@id/peers_layout" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}" + android:text="@{config.config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.config.interface.excludedApplications.size(), config.config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.config.interface.includedApplications.size(), config.config.interface.includedApplications.size())}" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/applications_label" tools:text="8 excluded" /> @@ -311,6 +367,7 @@ android:layout_marginTop="8dp" android:divider="@null" android:orientation="vertical" + app:fragment="@{fragment}" app:items="@{config.peers}" app:layout="@{@layout/tunnel_detail_peer}" app:layout_constraintStart_toStartOf="parent" @@ -318,4 +375,4 @@ tools:ignore="UselessLeaf" /> </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> -</layout>
\ No newline at end of file +</layout> diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml index 25081cea..89bb85ec 100644 --- a/ui/src/main/res/layout/tunnel_detail_peer.xml +++ b/ui/src/main/res/layout/tunnel_detail_peer.xml @@ -9,7 +9,7 @@ <variable name="item" - type="com.wireguard.config.Peer" /> + type="com.wireguard.android.viewmodel.PeerDetail" /> </data> <com.google.android.material.card.MaterialCardView @@ -64,7 +64,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/pre_shared_key_text" android:text="@string/pre_shared_key" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -81,7 +81,7 @@ android:singleLine="true" android:text="@string/pre_shared_key_enabled" android:textAppearance="?attr/textAppearanceBodyLarge" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label" tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" /> diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml index 0350486b..42222399 100644 --- a/ui/src/main/res/layout/tunnel_editor_fragment.xml +++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml @@ -5,6 +5,8 @@ <data> + <import type="android.os.Build" /> + <import type="com.wireguard.android.util.ClipboardUtils" /> <import type="com.wireguard.android.widget.KeyInputFilter" /> @@ -210,7 +212,7 @@ android:imeOptions="actionNext" android:inputType="textNoSuggestions|textVisiblePassword" android:nextFocusUp="@id/addresses_label_text" - android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusDown="@id/http_proxy_hostname_text" android:nextFocusForward="@id/mtu_text" android:text="@={config.interface.dnsServers}" /> </com.google.android.material.textfield.TextInputLayout> @@ -235,19 +237,112 @@ android:imeOptions="actionDone" android:inputType="number" android:nextFocusUp="@id/listen_port_text" - android:nextFocusDown="@id/set_excluded_applications" - android:nextFocusForward="@id/set_excluded_applications" + android:nextFocusDown="@id/http_proxy_hostname_text" + android:nextFocusForward="@id/http_proxy_hostname_text" android:text="@={config.interface.mtu}" android:textAlignment="center" /> </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_menu" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy" + app:expandedHintEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/dns_servers_label_layout"> + + <AutoCompleteTextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="none" + android:text="@={config.interface.httpProxyMenu}" + /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_hostname_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_hostname" + android:visibility="@{config.interface.httpProxyManualVisibility}" + app:layout_constraintEnd_toStartOf="@id/http_proxy_port_label_layout" + app:layout_constraintHorizontal_chainStyle="spread" + app:layout_constraintHorizontal_weight="0.7" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/http_proxy_menu"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_hostname_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textNoSuggestions|textVisiblePassword" + android:nextFocusUp="@id/mtu_text" + android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusForward="@id/http_proxy_port_text" + android:text="@={config.interface.httpProxyHostname}" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_port_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_port" + android:visibility="@{config.interface.httpProxyManualVisibility}" + app:expandedHintEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_weight="0.3" + app:layout_constraintStart_toEndOf="@id/http_proxy_hostname_label_layout" + app:layout_constraintTop_toBottomOf="@id/http_proxy_menu"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_port_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:nextFocusUp="@id/mtu_text" + android:nextFocusDown="@id/http_proxy_pac_label_layout" + android:nextFocusForward="@id/http_proxy_pac_label_layout" + android:text="@={config.interface.httpProxyPort}" + android:textAlignment="center" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/http_proxy_pac_label_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:hint="@string/http_proxy_pac" + android:visibility="@{config.interface.httpProxyPacVisibility}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/http_proxy_hostname_label_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/http_proxy_pac_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textNoSuggestions|textVisiblePassword" + android:nextFocusUp="@id/http_proxy_hostname_text" + android:nextFocusDown="@id/set_excluded_applications" + android:nextFocusForward="@id/set_excluded_applications" + android:text="@={config.interface.httpProxyPac}" /> + </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.button.MaterialButton android:id="@+id/set_excluded_applications" style="@style/Widget.Material3.Button.TextButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="4dp" - android:nextFocusUp="@id/dns_servers_text" + android:nextFocusUp="@id/http_proxy_hostname_text" android:nextFocusDown="@id/peers_layout" android:nextFocusForward="@id/peers_layout" android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}" @@ -256,7 +351,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/mtu_label_layout" + app:layout_constraintTop_toBottomOf="@id/http_proxy_pac_label_layout" app:rippleColor="?attr/colorSecondary" tools:text="4 excluded applications" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/ui/src/main/res/layout/tunnel_list_fragment.xml b/ui/src/main/res/layout/tunnel_list_fragment.xml index 8fc5d523..2ee2ff38 100644 --- a/ui/src/main/res/layout/tunnel_list_fragment.xml +++ b/ui/src/main/res/layout/tunnel_list_fragment.xml @@ -60,11 +60,11 @@ android:src="@mipmap/ic_launcher" /> <TextView - android:layout_marginStart="@dimen/tunnel_list_placeholder_margin" - android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" + android:layout_marginStart="@dimen/tunnel_list_placeholder_margin" + android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin" android:text="@string/tunnel_list_placeholder" android:textSize="20sp" /> </LinearLayout> diff --git a/ui/src/main/res/values-night/themes.xml b/ui/src/main/res/values-night/themes.xml index 9187e48a..e074cb92 100644 --- a/ui/src/main/res/values-night/themes.xml +++ b/ui/src/main/res/values-night/themes.xml @@ -1,5 +1,5 @@ - <resources> + <style name="WireGuardTheme" parent="Theme.Material3.Dark"> <item name="colorPrimary">@color/md_theme_dark_primary</item> <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item> diff --git a/ui/src/main/res/values-v23/styles.xml b/ui/src/main/res/values-v23/styles.xml index f6c74dd4..13feb8c3 100644 --- a/ui/src/main/res/values-v23/styles.xml +++ b/ui/src/main/res/values-v23/styles.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources xmlns:android="http://schemas.android.com/apk/res/android"> + <style name="AppTheme" parent="AppThemeBase"> <item name="android:statusBarColor">?android:colorBackground</item> <item name="android:windowLightStatusBar">@bool/light_status_bar</item> diff --git a/ui/src/main/res/values-v27/styles.xml b/ui/src/main/res/values-v27/styles.xml index 752801f9..f94cadb1 100644 --- a/ui/src/main/res/values-v27/styles.xml +++ b/ui/src/main/res/values-v27/styles.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources xmlns:android="http://schemas.android.com/apk/res/android"> + <style name="AppTheme" parent="AppThemeBase"> <item name="android:statusBarColor">?android:colorBackground</item> <item name="android:windowLightStatusBar">@bool/light_status_bar</item> diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index c9e230db..a1364123 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -70,6 +70,7 @@ <string name="bad_config_explanation_pka">: Must be positive and no more than 65535</string> <string name="bad_config_explanation_positive_number">: Must be positive</string> <string name="bad_config_explanation_udp_port">: Must be a valid UDP port number</string> + <string name="bad_config_explanation_http_proxy">: Must be valid proxy hostname and port</string> <string name="bad_config_reason_invalid_key">Invalid key</string> <string name="bad_config_reason_invalid_number">Invalid number</string> <string name="bad_config_reason_invalid_value">Invalid value</string> @@ -104,6 +105,7 @@ <string name="dark_theme_summary_on">Currently using dark (night) theme</string> <string name="dark_theme_title">Use dark theme</string> <string name="delete">Delete</string> + <string name="dynamic_addresses">Dynamic addresses</string> <string name="tv_delete">Select tunnel to delete</string> <string name="tv_select_a_storage_drive">Select a storage drive</string> <string name="tv_no_file_picker">Please install a file management utility to browse files</string> @@ -130,6 +132,10 @@ <string name="hint_optional">(optional)</string> <string name="hint_optional_discouraged">(optional, not recommended)</string> <string name="hint_random">(random)</string> + <string name="http_proxy">Proxy</string> + <string name="http_proxy_hostname">Proxy hostname</string> + <string name="http_proxy_pac">Proxy Auto-Config URL</string> + <string name="http_proxy_port">Proxy port</string> <string name="illegal_filename_error">Illegal file name ā%sā</string> <string name="import_error">Unable to import tunnel: %s</string> <string name="import_from_qr_code">Import Tunnel from QR Code</string> diff --git a/ui/src/main/res/values/themes.xml b/ui/src/main/res/values/themes.xml index e8d36cdd..0153d346 100644 --- a/ui/src/main/res/values/themes.xml +++ b/ui/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ - <resources> + <style name="WireGuardTheme" parent="Theme.Material3.Light"> <item name="colorPrimary">@color/md_theme_light_primary</item> <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item> diff --git a/ui/src/main/res/xml/preferences.xml b/ui/src/main/res/xml/preferences.xml index aa89f27c..fd3ff396 100644 --- a/ui/src/main/res/xml/preferences.xml +++ b/ui/src/main/res/xml/preferences.xml @@ -40,6 +40,5 @@ android:summaryOff="@string/allow_remote_control_intents_summary_off" android:summaryOn="@string/allow_remote_control_intents_summary_on" android:title="@string/allow_remote_control_intents_title" /> - <com.wireguard.android.preference.DonatePreference - android:singleLineTitle="false" /> + <com.wireguard.android.preference.DonatePreference android:singleLineTitle="false" /> </androidx.preference.PreferenceScreen> |