From ca8bd4aa1668415745d5ea77f8705e203cc1d8bd Mon Sep 17 00:00:00 2001 From: Shalin Date: Fri, 15 May 2026 09:13:12 -0700 Subject: [PATCH] Fix HTML tags rendered as plain text in received notifications When receiving notifications from the desktop, HTML tags (, ,
, etc.) were displayed as raw text in the Android notification shade instead of being rendered or stripped. Parse notification text through Html.fromHtml() before passing it to NotificationCompat.Builder. This strips HTML tags and preserves intended formatting (bold, italic) as Spanned text that Android notifications can render natively. Fixes #17 Co-authored-by: shalin-dev --- .../ReceiveNotificationsPlugin.java | 23 +++- .../ReceiveNotificationsPluginTest.java | 112 ++++++++++++++++++ 2 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 tests/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPluginTest.java diff --git a/src/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPlugin.java index 26b7da1..b63f4d2 100644 --- a/src/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPlugin.java @@ -14,6 +14,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; +import android.text.Html; import android.util.Log; import androidx.annotation.NonNull; @@ -98,17 +99,20 @@ public boolean onPacketReceived(final NetworkPacket np) { NotificationManager notificationManager = ContextCompat.getSystemService(context, NotificationManager.class); + String rawTicker = np.getString("ticker"); + CharSequence styledTicker = fromHtml(rawTicker); + Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.RECEIVENOTIFICATION) .setContentTitle(np.getString("appName")) - .setContentText(np.getString("ticker")) + .setContentText(styledTicker) .setContentIntent(resultPendingIntent) - .setTicker(np.getString("ticker")) + .setTicker(styledTicker) .setSmallIcon(R.drawable.ic_notification) .setLargeIcon(largeIcon) .setAutoCancel(true) .setLocalOnly(true) // to avoid bouncing the notification back to other kdeconnect nodes .setDefaults(Notification.DEFAULT_ALL) - .setStyle(new NotificationCompat.BigTextStyle().bigText(np.getString("ticker"))) + .setStyle(new NotificationCompat.BigTextStyle().bigText(styledTicker)) .build(); NotificationHelper.notifyCompat(notificationManager, "kdeconnectId:" + np.getString("id", "0"), np.getInt("id", 0), noti); @@ -116,6 +120,19 @@ public boolean onPacketReceived(final NetworkPacket np) { return true; } + @NonNull + static CharSequence fromHtml(@NonNull String text) { + if (!text.contains("<") && !text.contains("&")) { + return text; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT); + } else { + //noinspection deprecation + return Html.fromHtml(text); + } + } + @Override public @NonNull String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_NOTIFICATION}; diff --git a/tests/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPluginTest.java b/tests/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPluginTest.java new file mode 100644 index 0000000..ea798f9 --- /dev/null +++ b/tests/org/kde/kdeconnect/Plugins/ReceiveNotificationsPlugin/ReceiveNotificationsPluginTest.java @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2025 Contributors + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.Plugins.ReceiveNotificationsPlugin; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28) +public class ReceiveNotificationsPluginTest { + + @Test + public void fromHtml_plainTextNoAngleBrackets_returnsSameString() { + String input = "Hello world"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Hello world", result.toString()); + } + + @Test + public void fromHtml_plainTextWithNumbers_returnsSameString() { + String input = "Battery level 85%"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Battery level 85%", result.toString()); + } + + @Test + public void fromHtml_boldTag_stripsTag() { + String input = "Important message"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Important message", result.toString()); + } + + @Test + public void fromHtml_italicTag_stripsTag() { + String input = "emphasis here"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("emphasis here", result.toString()); + } + + @Test + public void fromHtml_brTag_convertsToNewline() { + String input = "Line1
Line2"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Line1\nLine2", result.toString()); + } + + @Test + public void fromHtml_brSlashTag_convertsToNewline() { + String input = "Line1
Line2"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Line1\nLine2", result.toString()); + } + + @Test + public void fromHtml_multipleTags_stripsAll() { + String input = "Bold and italic
new line"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Bold and italic\nnew line", result.toString()); + } + + @Test + public void fromHtml_htmlEntities_decodesThem() { + String input = "5 < 10 & 10 > 5"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("5 < 10 & 10 > 5", result.toString()); + } + + @Test + public void fromHtml_lessThanSymbol_treatedAsHtml() { + String input = "5 < 10"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("5 < 10", result.toString()); + } + + @Test + public void fromHtml_emptyString_returnsEmpty() { + String input = ""; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("", result.toString()); + } + + @Test + public void fromHtml_pTags_stripsTags() { + String input = "

Paragraph

"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Paragraph", result.toString().trim()); + } + + @Test + public void fromHtml_malformedHtml_handlesGracefully() { + String input = "unclosed bold"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("unclosed bold", result.toString()); + } + + @Test + @Config(sdk = 23) + public void fromHtml_preApi24_handlesHtml() { + String input = "Bold text"; + CharSequence result = ReceiveNotificationsPlugin.fromHtml(input); + assertEquals("Bold text", result.toString()); + } +}