mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Use only ClipboardHelper to copy to the clipboard, and do not render feedback on Android 13+
Since Android 13, the system displays its own confirmation when content is copied to the clipboard, so the application does not render its own Toast or Snackbar anymore on those versions. Context.copyToClipboard, CopyToClipboardUseCase and the direct ClipboardManager usage in EventDebugInfoView are removed. ClipboardHelper.copyPlainText(text, isSensitive, onSuccessOnOldDevice) is now the only entry point, and AndroidClipboardHelper is the only class using ClipboardManager. Copy actions from the views are moved to their Presenter through new events, or to the Node when there is no Presenter. A ToastHelper is introduced so that Toasts are kept where they were used before. Snackbars are kept where they were already used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
-2
@@ -9,8 +9,11 @@
|
||||
package io.element.android.libraries.androidutils.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import androidx.core.content.getSystemService
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
@@ -24,7 +27,30 @@ class AndroidClipboardHelper(
|
||||
) : ClipboardHelper {
|
||||
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
|
||||
|
||||
override fun copyPlainText(text: String) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
override fun copyPlainText(
|
||||
text: String,
|
||||
isSensitive: Boolean,
|
||||
onSuccessOnOldDevice: () -> Unit,
|
||||
) {
|
||||
val clipData = ClipData.newPlainText("", text)
|
||||
if (isSensitive) {
|
||||
clipData.description.extras = PersistableBundle().apply {
|
||||
putBoolean(EXTRA_IS_SENSITIVE, true)
|
||||
}
|
||||
}
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
// On Android 13+, the system already displays a confirmation when content is copied to the clipboard.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
onSuccessOnOldDevice()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// The constant only exists from API 33, but the documentation recommends using its value on older versions too.
|
||||
val EXTRA_IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ClipDescription.EXTRA_IS_SENSITIVE
|
||||
} else {
|
||||
"android.content.extra.IS_SENSITIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -8,9 +8,18 @@
|
||||
|
||||
package io.element.android.libraries.androidutils.clipboard
|
||||
|
||||
/**
|
||||
* Wrapper class for handling clipboard operations so it can be used in JVM environments.
|
||||
*/
|
||||
interface ClipboardHelper {
|
||||
fun copyPlainText(text: String)
|
||||
/**
|
||||
* Copy a text to the clipboard.
|
||||
*
|
||||
* @param text the text to copy
|
||||
* @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content.
|
||||
* @param onSuccessOnOldDevice invoked after the copy only on Android versions older than 13, since newer versions already
|
||||
* display a confirmation. Can be used to render a Toast or a Snackbar.
|
||||
*/
|
||||
fun copyPlainText(
|
||||
text: String,
|
||||
isSensitive: Boolean = false,
|
||||
onSuccessOnOldDevice: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
+13
-2
@@ -8,10 +8,21 @@
|
||||
|
||||
package io.element.android.libraries.androidutils.clipboard
|
||||
|
||||
class FakeClipboardHelper : ClipboardHelper {
|
||||
class FakeClipboardHelper(
|
||||
private val isOldDevice: Boolean = true,
|
||||
) : ClipboardHelper {
|
||||
var clipboardContents: Any? = null
|
||||
var isSensitive: Boolean? = null
|
||||
|
||||
override fun copyPlainText(text: String) {
|
||||
override fun copyPlainText(
|
||||
text: String,
|
||||
isSensitive: Boolean,
|
||||
onSuccessOnOldDevice: () -> Unit,
|
||||
) {
|
||||
clipboardContents = text
|
||||
this.isSensitive = isSensitive
|
||||
if (isOldDevice) {
|
||||
onSuccessOnOldDevice()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.system
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
class CopyToClipboardUseCase(
|
||||
private val context: Context,
|
||||
) {
|
||||
/**
|
||||
* @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content.
|
||||
*/
|
||||
fun execute(text: CharSequence, isSensitive: Boolean = false) {
|
||||
val clipData = ClipData.newPlainText("", text)
|
||||
if (isSensitive) {
|
||||
clipData.description.extras = PersistableBundle().apply {
|
||||
putBoolean(EXTRA_IS_SENSITIVE, true)
|
||||
}
|
||||
}
|
||||
context.getSystemService<ClipboardManager>()?.setPrimaryClip(clipData)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// The constant only exists from API 33, but the documentation recommends using its value on older versions too.
|
||||
val EXTRA_IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ClipDescription.EXTRA_IS_SENSITIVE
|
||||
} else {
|
||||
"android.content.extra.IS_SENSITIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
-20
@@ -50,26 +50,6 @@ fun Context.getVersionCodeFromManifest(): Long {
|
||||
)
|
||||
}
|
||||
|
||||
// ==============================================================================================================
|
||||
// Clipboard helper
|
||||
// ==============================================================================================================
|
||||
|
||||
/**
|
||||
* Copy a text to the clipboard, and display a Toast when done.
|
||||
*
|
||||
* @receiver the context
|
||||
* @param text the text to copy
|
||||
* @param toastMessage content of the toast message as a String resource. Null for no toast
|
||||
*/
|
||||
fun Context.copyToClipboard(
|
||||
text: CharSequence,
|
||||
toastMessage: String? = null,
|
||||
isSensitive: Boolean = false,
|
||||
) {
|
||||
CopyToClipboardUseCase(this).execute(text, isSensitive)
|
||||
toastMessage?.let { toast(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification settings for the current app.
|
||||
* In android O will directly opens the notification settings, in lower version it will show the App settings
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.toast
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.system.toast
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidToastHelper(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ToastHelper {
|
||||
override fun show(resId: Int) {
|
||||
context.toast(resId)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.toast
|
||||
|
||||
class FakeToastHelper : ToastHelper {
|
||||
val shownToasts = mutableListOf<Int>()
|
||||
|
||||
override fun show(resId: Int) {
|
||||
shownToasts.add(resId)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.toast
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
interface ToastHelper {
|
||||
fun show(@StringRes resId: Int)
|
||||
}
|
||||
Reference in New Issue
Block a user