From 7f48fb42ddb9fac9489e27171256f5334d7b4331 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:45:26 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20toast=20feedbac?= =?UTF-8?q?k=20when=20copying=20to=20clipboard=20on=20Android=2012=20and?= =?UTF-8?q?=20below?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added explicit Toast feedback when the user copies text using `ClipboardUtil`, but restricted it to Android 12 (API 32) and below. This improves UX on older devices that lack native clipboard feedback without causing duplicate notifications on newer Android 13+ devices. Safely casted `ClipboardManager` and updated mock static tests to handle the new `Toast` behavior. Added learning to Palette journal. Co-authored-by: manupawickramasinghe <73810867+manupawickramasinghe@users.noreply.github.com> --- .Jules/palette.md | 4 +++ .../android/scan3d/util/ClipboardUtil.kt | 8 +++++- .../android/scan3d/util/ClipboardUtilTest.kt | 28 ++++++++++--------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index a9d7efb..fc9ef77 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -180,3 +180,7 @@ ## 2024-06-13 - Optimize CameraCharacteristics retrieval loop **Learning:** When querying expensive Android system services like `CameraManager.getCameraCharacteristics` inside a loop, combining filter and map operations into a single pass (e.g., using `mapNotNull`) prevents redundant IPC calls and noticeably improves execution speed. **Action:** Replaced a `filter {} .forEach {}` pipeline containing duplicated `getCameraCharacteristics` calls with a single `mapNotNull` pass that pairs valid camera IDs with their retrieved characteristics, eliminating 50% of the overhead. + +## 2026-09-09 - Copy to clipboard toast UX +**Learning:** Native clipboard feedback was added in Android 13, but custom implementations for older versions are often forgotten. When implementing copy actions using `ClipboardManager`, you must explicitly check the API version (`Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2`) and only provide manual visual feedback (like Toasts) on older OS versions to prevent annoying duplicate overlays on newer devices. +**Action:** Always condition custom copy-to-clipboard visual feedback on the target OS version to defer to native OS behavior when available. diff --git a/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt b/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt index 1ebf850..c5e7560 100644 --- a/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt +++ b/app/src/main/java/com/samsung/android/scan3d/util/ClipboardUtil.kt @@ -2,12 +2,18 @@ package com.samsung.android.scan3d.util import android.content.ClipData import android.content.Context +import android.os.Build +import android.widget.Toast +import com.samsung.android.scan3d.R object ClipboardUtil { fun copyToClipboard(context: Context?, label: String, text: String) { - val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager ?: return val clip = ClipData.newPlainText(label, text) clipboard.setPrimaryClip(clip) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + } } } \ No newline at end of file diff --git a/app/src/test/java/com/samsung/android/scan3d/util/ClipboardUtilTest.kt b/app/src/test/java/com/samsung/android/scan3d/util/ClipboardUtilTest.kt index 731743a..0cb6d74 100644 --- a/app/src/test/java/com/samsung/android/scan3d/util/ClipboardUtilTest.kt +++ b/app/src/test/java/com/samsung/android/scan3d/util/ClipboardUtilTest.kt @@ -3,6 +3,8 @@ package com.samsung.android.scan3d.util import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.os.Build +import android.widget.Toast import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.Mockito.* @@ -15,18 +17,23 @@ class ClipboardUtilTest { val mockContext = mock(Context::class.java) val mockClipboard = mock(ClipboardManager::class.java) val mockClipData = mock(ClipData::class.java) + val mockToast = mock(Toast::class.java) `when`(mockContext.getSystemService(Context.CLIPBOARD_SERVICE)).thenReturn(mockClipboard) - mockStatic(ClipData::class.java).use { mockedStatic -> - mockedStatic.`when` { ClipData.newPlainText("test_label", "test_text") }.thenReturn(mockClipData) + mockStatic(Toast::class.java).use { mockedToastStatic -> + mockedToastStatic.`when` { Toast.makeText(any(Context::class.java), anyInt(), anyInt()) }.thenReturn(mockToast) + + mockStatic(ClipData::class.java).use { mockedStatic -> + mockedStatic.`when` { ClipData.newPlainText("test_label", "test_text") }.thenReturn(mockClipData) - ClipboardUtil.copyToClipboard(mockContext, "test_label", "test_text") + ClipboardUtil.copyToClipboard(mockContext, "test_label", "test_text") - val clipCaptor = ArgumentCaptor.forClass(ClipData::class.java) - verify(mockClipboard).setPrimaryClip(clipCaptor.capture()) + val clipCaptor = ArgumentCaptor.forClass(ClipData::class.java) + verify(mockClipboard).setPrimaryClip(clipCaptor.capture()) - assertEquals(mockClipData, clipCaptor.value) + assertEquals(mockClipData, clipCaptor.value) + } } } @@ -36,13 +43,8 @@ class ClipboardUtilTest { mockStatic(ClipData::class.java).use { mockedStatic -> mockedStatic.`when` { ClipData.newPlainText("test_label", "test_text") }.thenReturn(mockClipData) - try { - ClipboardUtil.copyToClipboard(null, "test_label", "test_text") - fail("Expected Exception when context is null") - } catch (e: Exception) { - // If context is null, it throws NullPointerException because of `as android.content.ClipboardManager` - assertEquals(NullPointerException::class.java, e::class.java) - } + // Since context is null, it returns early now due to the safe cast and null check `?: return` + ClipboardUtil.copyToClipboard(null, "test_label", "test_text") } } }