WireGuard Archive on lore.kernel.org
 help / color / mirror / Atom feed
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

      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