[PM-40488] fix: clear clipboard setting does not clear the clipboard on Android 13+ - #7169
[PM-40488] fix: clear clipboard setting does not clear the clipboard on Android 13+#7169mvanhorn wants to merge 3 commits into
Conversation
|
Thank you for your contribution! We've added this to our internal tracking system for review. Details on our contribution process can be found here: https://contributing.bitwarden.com/contributing/pull-requests/community-pr-process. |
| ) | ||
| }, | ||
| ) | ||
| if (isBuildVersionAtLeast(version = Build.VERSION_CODES.P)) { |
There was a problem hiding this comment.
Do we need this version check here?
There was a problem hiding this comment.
Can you remove this since we now have tests
Remove the isBuildVersionAtLeast(P) gate and the OmitFromCoverage annotation now that the worker has test coverage.
|
Both removed in 5e2465c: the version check is gone and OmitFromCoverage dropped now that the tests cover the worker. |
| clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager | ||
| worker = ClearClipboardWorker( | ||
| appContext = context, | ||
| workerParams = mockk(relaxed = true), |
There was a problem hiding this comment.
Can this be done inline above?
private val context = RuntimeEnvironment.getApplication()
private val clipboardManager = context.getSystemService<ClipboardManager>()
private val worker: ClearClipboardWorker = ClearClipboardWorker(
appContext = context,
workerParams = mockk(relaxed = true),
)or do these tests require a new instance each time?
Per review, fold the @before setup into property initializers. getSystemService<T>() returns T?, so the clipboard manager is unwrapped with requireNotNull rather than !!, which detekt's UnsafeCallOnNullableType rejects. Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
|
Pushed a0302ac with the inline setup you suggested. One deviation from your snippet: On your other two comments, I believe both are already addressed and GitHub has them marked outdated:
If either of those is not what you meant, point me at the current line and I will fix it. Verified locally: |
| appContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager | ||
|
|
||
| override fun doWork(): Result { | ||
| clipboardManager.setPrimaryClip( |
There was a problem hiding this comment.
To make sure I'm on the up-and-up... The idea is to set "nothing" to the clipboard, overwriting what sensitive information was there, and then invoke .clearPrimaryClip (removing the new empty content) as per Android platforms on which the method is not a no-op.
Is this correct?
|
Yes, that's the intent. setPrimaryClip replaces whatever sensitive content was on the clipboard with an empty clip flagged IS_SENSITIVE, so the data is gone from the clipboard regardless of what happens next. clearPrimaryClip then removes that empty entry so the clipboard isn't left holding a blank item, on platforms where it actually does something. The overwrite is the part that protects the data; the clear is cleanup. |
|
Love it. Thanks for the breakdown!
All the best,
Franco N. Colaizzi
…On Thu, Sep 3, 2026 at 2:18 AM Matt Van Horn ***@***.***> wrote:
*mvanhorn* left a comment (bitwarden/android#7169)
<#7169 (comment)>
Yes, that's the intent. setPrimaryClip replaces whatever sensitive content
was on the clipboard with an empty clip flagged IS_SENSITIVE, so the data
is gone from the clipboard regardless of what happens next.
clearPrimaryClip then removes that empty entry so the clipboard isn't left
holding a blank item, on platforms where it actually does something. The
overwrite is the part that protects the data; the clear is cleanup.
—
Reply to this email directly, view it on GitHub
<#7169?email_source=notifications&email_token=ABN6AIENIZBOFQMITC3OSCT5NEEJTA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNJSGE2DCNRQHEZ2M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5521416093>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABN6AIGL3OESL4L3Y2GYUNL5NEEJTAVCNFSNUABEKJSXA33TNF2G64TZHM2TONBVGIYTIMZ3JFZXG5LFHM2DQOJYHE4DONZRGOQXMAQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
| ClipData | ||
| .newPlainText("", "") | ||
| .apply { | ||
| description.extras = persistableBundleOf( |
There was a problem hiding this comment.
Since this function is deprecated, can we do something like this:
description.extras = PersistableBundle().apply {
putBoolean(
if (isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
ClipDescription.EXTRA_IS_SENSITIVE
} else {
"android.content.extra.IS_SENSITIVE"
},
true,
)
}|
Since emails make it difficult to follow *exactly which function* is
deprecated, can you share that explicitly?
I cannot get back to my machine until the end of the weekend.
All the best,
Franco N. Colaizzi
…On Fri, Sep 4, 2026 at 11:06 AM David Perez ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In
app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/clipboard/ClearClipboardWorker.kt
<#7169 (comment)>:
> @@ -20,6 +23,19 @@ class ClearClipboardWorker(
appContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
override fun doWork(): Result {
+ clipboardManager.setPrimaryClip(
+ ClipData
+ .newPlainText("", "")
+ .apply {
+ description.extras = persistableBundleOf(
Since this function is deprecated, can we do something like this:
description.extras = PersistableBundle().apply {
putBoolean(
if (isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
ClipDescription.EXTRA_IS_SENSITIVE
} else {
"android.content.extra.IS_SENSITIVE"
},
true,
)
}
—
Reply to this email directly, view it on GitHub
<#7169?email_source=notifications&email_token=ABN6AIBVSEL7TEGFFIUMGIL5NLK7DA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTKMJRGQ3DMNBSGAY2M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#pullrequestreview-5114664201>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABN6AIBEK6X53IH2OLQ3SQT5NLK7DAVCNFSNUABEKJSXA33TNF2G64TZHM2TONBVGIYTIMZ3JFZXG5LFHM2DQOJYHE4DONZRGOQXMAQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
|
|
Thank you so much kind sir!
All the best,
Franco N. Colaizzi
On Fri, Sep 4, 2026 at 12:13 PM David Perez ***@***.***>
wrote:
… *david-livefront* left a comment (bitwarden/android#7169)
<#7169 (comment)>
Since emails make it difficult to follow *exactly which function* is
deprecated, can you share that explicitly? I cannot get back to my machine
until the end of the weekend. All the best, Franco N. Colaizzi
… <#m_-4472582976834905842_>
On Fri, Sep 4, 2026 at 11:06 AM David Perez *@*.*> wrote: @.** commented
on this pull request. ------------------------------ In
app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/clipboard/ClearClipboardWorker.kt
<#7169 (comment)
<#7169 (comment)>>:
> @@ -20,6 +23,19 @@ class ClearClipboardWorker(
appContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager override
fun doWork(): Result { + clipboardManager.setPrimaryClip( + ClipData +
.newPlainText("", "") + .apply { + description.extras =
persistableBundleOf( Since this function is deprecated, can we do something
like this: description.extras = PersistableBundle().apply { putBoolean( if
(isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
ClipDescription.EXTRA_IS_SENSITIVE } else {
"android.content.extra.IS_SENSITIVE" }, true, ) } — Reply to this email
directly, view it on GitHub <#7169
<#7169>?email_source=notifications&email_token=ABN6AIBVSEL7TEGFFIUMGIL5NLK7DA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTKMJRGQ3DMNBSGAY2M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#pullrequestreview-5114664201>,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ABN6AIBEK6X53IH2OLQ3SQT5NLK7DAVCNFSNUABEKJSXA33TNF2G64TZHM2TONBVGIYTIMZ3JFZXG5LFHM2DQOJYHE4DONZRGOQXMAQ
. You are receiving this because you commented.Message ID: *@*.***>
persistableBundleOf is deprecated
Image: Screenshot 2026-09-04 at 11 12 00 AM (view on web)
<https://github.com/user-attachments/assets/8607ee63-676b-4189-a90e-a3b05dba8b15>
—
Reply to this email directly, view it on GitHub
<#7169?email_source=notifications&email_token=ABN6AICAVQ6IRAWC2TXCDR35NLS2VA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNJUGMZTANRWGQ42M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5543306649>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABN6AIAQSJTATBRBCAVVLET5NLS2VAVCNFSNUABEKJSXA33TNF2G64TZHM2TONBVGIYTIMZ3JFZXG5LFHM2DQOJYHE4DONZRGOQXMAQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|

🎟️ Tracking
Fixes #7069
📔 Objective
The "clear clipboard" setting does not clear the clipboard on Android 13+. Setting a short interval (e.g. 10 seconds), copying a password, and waiting leaves the secret on the clipboard. Multiple users have confirmed this across recent devices (Samsung Galaxy S25+ on Android 16, Fairphone FP5 on Android 15, Samsung SM-A366B on Android 16) and app versions 2026.2.1 through 2026.6.x.
The clear is scheduled by
BitwardenClipboardManagerImpl.setText()as a delayedOneTimeWorkRequestand executed byClearClipboardWorker.doWork(), which relies solely onclipboardManager.clearPrimaryClip(). On newer Android versionsclearPrimaryClip()can be a no-op, so the secret is never evicted.This change makes
ClearClipboardWorker.doWork()clear robustly: it first overwrites the primary clip with an empty, sensitiveClipData(markingClipDescription.EXTRA_IS_SENSITIVEon API 33+, and the legacyandroid.content.extra.IS_SENSITIVEkey below that, matching the pattern already used inBitwardenClipboardManagerImpl.setText), then callsclearPrimaryClip()(guarded to API 28+) to remove the placeholder entry on devices that honor it. Background clipboard restrictions on Android 10+ gate reads, not writes, so the background overwrite/clear remains permitted. The fix stays entirely within the existing worker, with no scheduling or DI changes.A unit test (
ClearClipboardWorkerTest) covers the Android 13+ path to prevent regression.📸 Screenshots
Not applicable - this is a background-worker/behavioral fix with no UI surface.
AI was used for assistance.