From: Harsh Shandilya <me@msfjarvis.dev>
To: Anton Kovalenko <anton@sw4me.com>, wireguard@lists.zx2c4.com
Cc: "Jason A . Donenfeld" <Jason@zx2c4.com>
Subject: Re: [PATCH wireguard-android] ui: add import config from clipboard
Date: Mon, 29 Jun 2026 09:34:44 +0530 [thread overview]
Message-ID: <41772cd9-8632-454f-b2a5-dae297398109@msfjarvis.dev> (raw)
In-Reply-To: <20260621074710.1106438-1-anton@sw4me.com>
On 21-06-2026 13:17, Anton Kovalenko wrote:
> Adds a fourth action to the add-tunnel bottom sheet that reads plain
> text from the system clipboard and routes it through the existing
> text-import path (validate, name, create) used for QR codes.
>
> Signed-off-by: Anton Kovalenko <anton@sw4me.com>
> ---
> The add-tunnel sheet currently offers file/archive, QR scan and
> create-from-scratch. This adds a clipboard option for the common case
> of getting a config as text over chat or email, routed through the
> existing QR-text import path so validation, naming and storage are
> unchanged.
>
> Built and tested on Samsung S24 Ultra, Android 16: a valid config
> imports and names correctly; empty or non-config clipboard shows the
> expected error snackbar.
>
> First patch I've sent to the list — happy to adjust style or approach.
>
> Disclosure: this patch was written by Claude (Anthropic's AI), which I
> reviewed, built and tested.
>
> .../android/fragment/AddTunnelsSheet.kt | 9 +++++
> .../android/fragment/TunnelListFragment.kt | 15 ++++++++
> .../wireguard/android/util/ClipboardUtils.kt | 13 +++++++
> ui/src/main/res/drawable/ic_action_paste.xml | 14 ++++++++
> .../res/layout/add_tunnels_bottom_sheet.xml | 34 ++++++++++++++++---
> ui/src/main/res/values/strings.xml | 2 ++
> 6 files changed, 82 insertions(+), 5 deletions(-)
> create mode 100644 ui/src/main/res/drawable/ic_action_paste.xml
>
> diff --git a/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
> index 161709c3..d5f01b40 100644
> --- a/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
> +++ b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
> @@ -64,6 +64,10 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
> dismiss()
> onRequestImportConfig()
> }
> + dialog.findViewById<View>(R.id.create_from_clipboard)?.setOnClickListener {
> + dismiss()
> + onRequestImportFromClipboard()
> + }
> dialog.findViewById<View>(R.id.create_from_qrcode)?.setOnClickListener {
> dismiss()
> onRequestScanQRCode()
> @@ -89,6 +93,10 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
> setFragmentResult(REQUEST_KEY_NEW_TUNNEL, Bundle().apply { putString(REQUEST_METHOD, REQUEST_IMPORT) })
> }
>
> + private fun onRequestImportFromClipboard() {
> + setFragmentResult(REQUEST_KEY_NEW_TUNNEL, Bundle().apply { putString(REQUEST_METHOD, REQUEST_CLIPBOARD) })
> + }
> +
> private fun onRequestScanQRCode() {
> setFragmentResult(REQUEST_KEY_NEW_TUNNEL, Bundle().apply { putString(REQUEST_METHOD, REQUEST_SCAN) })
> }
> @@ -98,6 +106,7 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
> const val REQUEST_METHOD = "request_method"
> const val REQUEST_CREATE = "request_create"
> const val REQUEST_IMPORT = "request_import"
> + const val REQUEST_CLIPBOARD = "request_clipboard"
> const val REQUEST_SCAN = "request_scan"
> }
> }
> 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 119b6afe..8bde6794 100644
> --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
> +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
> @@ -34,6 +34,7 @@ import com.wireguard.android.databinding.TunnelListFragmentBinding
> import com.wireguard.android.databinding.TunnelListItemBinding
> import com.wireguard.android.model.ObservableTunnel
> import com.wireguard.android.updater.SnackbarUpdateShower
> +import com.wireguard.android.util.ClipboardUtils
> import com.wireguard.android.util.ErrorMessages
> import com.wireguard.android.util.QrCodeFromFileScanner
> import com.wireguard.android.util.TunnelImporter
> @@ -83,6 +84,16 @@ class TunnelListFragment : BaseFragment() {
>
> private val snackbarUpdateShower = SnackbarUpdateShower(this)
>
> + private fun importFromClipboard() {
> + val context = context ?: return
> + val text = ClipboardUtils.readPlainText(context)
> + if (text.isNullOrBlank()) {
> + showSnackbar(getString(R.string.clipboard_no_text))
> + return
> + }
> + TunnelImporter.importTunnel(parentFragmentManager, text.toString()) { showSnackbar(it) }
> + }
> +
> override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
> super.onViewCreated(view, savedInstanceState)
> if (savedInstanceState != null) {
> @@ -114,6 +125,10 @@ class TunnelListFragment : BaseFragment() {
> tunnelFileImportResultLauncher.launch("*/*")
> }
>
> + AddTunnelsSheet.REQUEST_CLIPBOARD -> {
> + importFromClipboard()
> + }
> +
> AddTunnelsSheet.REQUEST_SCAN -> {
> qrImportResultLauncher.launch(
> ScanOptions()
> diff --git a/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
> index 8968979f..76eb2b9a 100644
> --- a/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
> +++ b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
> @@ -6,6 +6,7 @@ package com.wireguard.android.util
>
> import android.content.ClipData
> import android.content.ClipboardManager
> +import android.content.Context
> import android.os.Build
> import android.view.View
> import android.widget.TextView
> @@ -18,6 +19,18 @@ import com.wireguard.android.R
> * Standalone utilities for interacting with the system clipboard.
> */
> object ClipboardUtils {
> + /**
> + * Returns the plain-text contents of the primary clip, or null if the
> + * clipboard is unavailable, empty, or holds no coercible text.
> + */
> + @JvmStatic
> + fun readPlainText(context: Context): CharSequence? {
> + val service = context.getSystemService<ClipboardManager>() ?: return null
> + val clip = service.primaryClip ?: return null
> + if (clip.itemCount == 0) return null
> + return clip.getItemAt(0).coerceToText(context)
> + }
> +
> @JvmStatic
> fun copyTextView(view: View) {
> val data = when (view) {
> diff --git a/ui/src/main/res/drawable/ic_action_paste.xml b/ui/src/main/res/drawable/ic_action_paste.xml
> new file mode 100644
> index 00000000..1841b21e
> --- /dev/null
> +++ b/ui/src/main/res/drawable/ic_action_paste.xml
> @@ -0,0 +1,14 @@
> +<?xml version="1.0" encoding="utf-8"?><!--
> + ~ Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
> + ~ SPDX-License-Identifier: Apache-2.0
> + -->
> +<vector xmlns:android="http://schemas.android.com/apk/res/android"
> + android:width="24dp"
> + android:height="24dp"
> + android:tint="?attr/colorControlNormal"
> + android:viewportWidth="24"
> + android:viewportHeight="24">
> + <path
> + android:fillColor="#FFFFFFFF"
> + android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0S9.6,0.84 9.18,2H5C3.9,2 3,2.9 3,4v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z" />
> +</vector>
> diff --git a/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
> index 0ad1ef23..310a2d75 100644
> --- a/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
> +++ b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
> @@ -18,22 +18,22 @@
> android:layout_marginLeft="@dimen/normal_margin"
> android:layout_marginEnd="@dimen/normal_margin"
> android:layout_marginRight="@dimen/normal_margin"
> - android:nextFocusDown="@id/create_from_qrcode"
> - android:nextFocusForward="@id/create_from_qrcode"
> + android:nextFocusDown="@id/create_from_clipboard"
> + android:nextFocusForward="@id/create_from_clipboard"
> android:text="@string/create_from_file"
> android:textAlignment="viewStart"
> android:textColor="?attr/colorOnSurface"
> app:icon="@drawable/ic_action_open"
> app:iconPadding="@dimen/bottom_sheet_icon_padding"
> app:iconTint="?attr/colorSecondary"
> - app:layout_constraintBottom_toTopOf="@+id/create_from_qrcode"
> + app:layout_constraintBottom_toTopOf="@+id/create_from_clipboard"
> app:layout_constraintEnd_toEndOf="parent"
> app:layout_constraintStart_toStartOf="parent"
> app:layout_constraintTop_toBottomOf="parent"
> app:rippleColor="?attr/colorSecondary" />
>
> <com.google.android.material.button.MaterialButton
> - android:id="@+id/create_from_qrcode"
> + android:id="@+id/create_from_clipboard"
> style="@style/Widget.Material3.Button.TextButton.Icon"
> android:layout_width="match_parent"
> android:layout_height="@dimen/bottom_sheet_item_height"
> @@ -42,6 +42,30 @@
> android:layout_marginEnd="@dimen/normal_margin"
> android:layout_marginRight="@dimen/normal_margin"
> android:nextFocusUp="@id/create_from_file"
> + android:nextFocusDown="@id/create_from_qrcode"
> + android:nextFocusForward="@id/create_from_qrcode"
> + android:text="@string/create_from_clipboard"
> + android:textAlignment="viewStart"
> + android:textColor="?attr/colorOnSurface"
> + app:icon="@drawable/ic_action_paste"
> + app:iconPadding="@dimen/bottom_sheet_icon_padding"
> + app:iconTint="?attr/colorSecondary"
> + app:layout_constraintBottom_toTopOf="@+id/create_from_qrcode"
> + app:layout_constraintEnd_toEndOf="parent"
> + app:layout_constraintStart_toStartOf="parent"
> + app:layout_constraintTop_toBottomOf="@+id/create_from_file"
> + app:rippleColor="?attr/colorSecondary" />
> +
> + <com.google.android.material.button.MaterialButton
> + android:id="@+id/create_from_qrcode"
> + style="@style/Widget.Material3.Button.TextButton.Icon"
> + android:layout_width="match_parent"
> + android:layout_height="@dimen/bottom_sheet_item_height"
> + android:layout_marginStart="@dimen/normal_margin"
> + android:layout_marginLeft="@dimen/normal_margin"
> + android:layout_marginEnd="@dimen/normal_margin"
> + android:layout_marginRight="@dimen/normal_margin"
> + android:nextFocusUp="@id/create_from_clipboard"
> android:nextFocusDown="@id/create_empty"
> android:nextFocusForward="@id/create_empty"
> android:text="@string/create_from_qr_code"
> @@ -53,7 +77,7 @@
> app:layout_constraintBottom_toBottomOf="@+id/create_empty"
> app:layout_constraintEnd_toEndOf="parent"
> app:layout_constraintStart_toStartOf="parent"
> - app:layout_constraintTop_toBottomOf="@+id/create_from_file"
> + app:layout_constraintTop_toBottomOf="@+id/create_from_clipboard"
> app:rippleColor="?attr/colorSecondary" />
>
> <com.google.android.material.button.MaterialButton
> diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
> index 32e797e6..757c78c9 100644
> --- a/ui/src/main/res/values/strings.xml
> +++ b/ui/src/main/res/values/strings.xml
> @@ -86,6 +86,7 @@
> <string name="error_no_qr_found">QR code not found in image</string>
> <string name="error_qr_checksum">QR code checksum verification failed</string>
> <string name="cancel">Cancel</string>
> + <string name="clipboard_no_text">No text on the clipboard</string>
> <string name="config_delete_error">Cannot delete configuration file %s</string>
> <string name="config_exists_error">Configuration for “%s” already exists</string>
> <string name="config_file_exists_error">Configuration file “%s” already exists</string>
> @@ -97,6 +98,7 @@
> <string name="create_bin_dir_error">Cannot create local binary directory</string>
> <string name="create_downloads_file_error">Cannot create file in downloads directory</string>
> <string name="create_empty">Create from scratch</string>
> + <string name="create_from_clipboard">Import from clipboard</string>
> <string name="create_from_file">Import from file or archive</string>
> <string name="create_from_qr_code">Scan from QR code</string>
> <string name="create_output_dir_error">Cannot create output directory</string>
The code looks fine at a cursory glance but I'm not sure about the
WireGuard project's current policy around LLM-generated code. I'll defer
to Jason to make a call on that before I pull the patch down for testing
and a proper review.
--
Harsh Shandilya
prev parent reply other threads:[~2026-10-05 8:16 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-21 7:47 [PATCH wireguard-android] ui: add import config from clipboard Anton Kovalenko
2026-06-29 4:04 ` Harsh Shandilya [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=41772cd9-8632-454f-b2a5-dae297398109@msfjarvis.dev \
--to=me@msfjarvis.dev \
--cc=Jason@zx2c4.com \
--cc=anton@sw4me.com \
--cc=wireguard@lists.zx2c4.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox