diff --git a/flexera/res/values/flexera_ids.xml b/flexera/res/values/flexera_ids.xml
index aae70bbec8..6731978e18 100644
--- a/flexera/res/values/flexera_ids.xml
+++ b/flexera/res/values/flexera_ids.xml
@@ -2,4 +2,8 @@
+
+
+
+
diff --git a/flexera/res/values/flexera_strings.xml b/flexera/res/values/flexera_strings.xml
index 157a9aa446..03b8803898 100644
--- a/flexera/res/values/flexera_strings.xml
+++ b/flexera/res/values/flexera_strings.xml
@@ -40,6 +40,8 @@
Alphabet
+ Notification previews
+ Show an app\'s latest notification under its name in the app list. Works when notification dots are on (Settings, General).
Alphabet
Alphabet on the left edge
For left-handed use. The letters normally sit on the right edge.
diff --git a/flexera/src/app/flexera/drawer/FlexeraDrawer.kt b/flexera/src/app/flexera/drawer/FlexeraDrawer.kt
index 74ece1ccc1..39079beb2b 100644
--- a/flexera/src/app/flexera/drawer/FlexeraDrawer.kt
+++ b/flexera/src/app/flexera/drawer/FlexeraDrawer.kt
@@ -87,6 +87,9 @@ object FlexeraDrawer {
@JvmStatic
fun styleAppRow(icon: BubbleTextView) {
val res = icon.resources
+ // Other icons share the "all apps" display type (search results, for one); this tag
+ // tells list rows apart from them.
+ icon.setTag(R.id.flexera_drawer_row, true)
icon.layoutParams.height = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_height)
icon.compoundDrawablePadding = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_icon_gap)
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimension(R.dimen.flexera_drawer_row_text_size))
diff --git a/flexera/src/app/flexera/drawer/FlexeraNotificationPreview.kt b/flexera/src/app/flexera/drawer/FlexeraNotificationPreview.kt
new file mode 100644
index 0000000000..c09ffecc29
--- /dev/null
+++ b/flexera/src/app/flexera/drawer/FlexeraNotificationPreview.kt
@@ -0,0 +1,132 @@
+package app.flexera.drawer
+
+import android.app.Notification
+import android.service.notification.StatusBarNotification
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.TextUtils
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import app.lawnchair.preferences.PreferenceManager
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.R
+import com.android.launcher3.dot.DotInfo
+import com.android.launcher3.model.data.ItemInfo
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Inline notification previews (PRD AD-11): in the drawer list, a row whose app has
+ * notifications shows the latest one under the app name, smaller and dimmer.
+ *
+ * It rides on what Launcher3 already has for notification dots. The same notification access
+ * feeds it, the same filter decides which notifications count, and a row is refreshed whenever
+ * its dot is. Without notification access there are no dots and no previews.
+ *
+ * Launcher3's listener hands each notification to [onPosted], [onRemoved] or [onFullRefresh] on
+ * its worker thread; the rows read the stored text on the main thread through [apply].
+ */
+object FlexeraNotificationPreview {
+
+ private class Preview(val text: String, val postTime: Long)
+
+ /** Latest text per notification key, for the notifications that currently show a dot. */
+ private val previews = ConcurrentHashMap()
+
+ private const val MAX_LENGTH = 140
+
+ /** The preview line's text size as a share of the app name's. */
+ private const val TEXT_SCALE = 0.78f
+
+ /** White at 70%, so the app name stays the stronger of the two lines. */
+ private const val TEXT_COLOR = 0xB3FFFFFF.toInt()
+
+ @JvmStatic
+ fun onPosted(sbn: StatusBarNotification) {
+ val text = previewText(sbn)
+ if (text == null) previews.remove(sbn.key) else previews[sbn.key] = Preview(text, sbn.postTime)
+ }
+
+ @JvmStatic
+ fun onRemoved(sbn: StatusBarNotification) {
+ previews.remove(sbn.key)
+ }
+
+ @JvmStatic
+ fun onFullRefresh(active: List) {
+ previews.clear()
+ active.forEach(::onPosted)
+ }
+
+ /**
+ * Sets [icon]'s text for [info]: the app name, plus the latest notification under it when
+ * [dot] says the app has any. Called for drawer rows whenever their dot state is applied.
+ */
+ @JvmStatic
+ fun apply(icon: BubbleTextView, info: ItemInfo, dot: DotInfo?) {
+ // Only rows of the drawer list have room for a second line.
+ if (icon.getTag(R.id.flexera_drawer_row) == null) return
+ val name = info.title ?: return
+ val preview = if (dot != null && isEnabled(icon)) latest(dot) else null
+
+ if (preview == null) {
+ if (icon.getTag(R.id.flexera_notification_preview) != null) {
+ // The row was showing a preview, for this app earlier or for another app before
+ // the view was reused. Back to the plain one-line name.
+ icon.setTag(R.id.flexera_notification_preview, null)
+ icon.setLines(1)
+ icon.text = name
+ }
+ return
+ }
+
+ // The text is set afresh every time. Binding a row puts the plain name back first, so
+ // what the row showed before says nothing about what it shows now.
+
+ // Keep a long app name on its own line, so the preview is never pushed off the row.
+ val width = icon.width - icon.compoundPaddingLeft - icon.compoundPaddingRight
+ val firstLine = if (width > 0) {
+ TextUtils.ellipsize(name, icon.paint, width.toFloat(), TextUtils.TruncateAt.END)
+ } else {
+ name
+ }
+ val label = SpannableStringBuilder(firstLine).append('\n')
+ val start = label.length
+ label.append(preview)
+ label.setSpan(RelativeSizeSpan(TEXT_SCALE), start, label.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ label.setSpan(ForegroundColorSpan(TEXT_COLOR), start, label.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+
+ icon.setLines(2)
+ icon.ellipsize = TextUtils.TruncateAt.END
+ icon.text = label
+ icon.setTag(R.id.flexera_notification_preview, true)
+ }
+
+ private fun isEnabled(icon: BubbleTextView): Boolean =
+ PreferenceManager.getInstance(icon.context).flexeraNotificationPreviews.get()
+
+ /** The text of the newest of the app's notifications that has any text. */
+ private fun latest(dot: DotInfo): String? {
+ var newest: Preview? = null
+ for (key in dot.notificationKeys) {
+ val candidate = previews[key.notificationKey] ?: continue
+ if (newest == null || candidate.postTime > newest.postTime) newest = candidate
+ }
+ return newest?.text
+ }
+
+ /** "Title: text" in one line, or whichever of the two the notification has. */
+ private fun previewText(sbn: StatusBarNotification): String? {
+ val extras = sbn.notification.extras ?: return null
+ val title = extras.getCharSequence(Notification.EXTRA_TITLE).oneLine()
+ val text = extras.getCharSequence(Notification.EXTRA_TEXT).oneLine()
+ val combined = when {
+ title.isEmpty() -> text
+ text.isEmpty() -> title
+ else -> "$title: $text"
+ }
+ return combined.take(MAX_LENGTH).ifEmpty { null }
+ }
+
+ private fun CharSequence?.oneLine(): String =
+ this?.toString()?.replace('\n', ' ')?.trim().orEmpty()
+}
diff --git a/flexera/src/app/flexera/ui/FlexeraDrawerSettings.kt b/flexera/src/app/flexera/ui/FlexeraDrawerSettings.kt
index a6d56410ee..c13fb99d2e 100644
--- a/flexera/src/app/flexera/ui/FlexeraDrawerSettings.kt
+++ b/flexera/src/app/flexera/ui/FlexeraDrawerSettings.kt
@@ -49,6 +49,11 @@ fun FlexeraDrawerSettings(modifier: Modifier = Modifier) {
label = stringResource(id = R.string.suggestion_pref_screen_title),
destination = Predictions,
)
+ SwitchPreference(
+ label = stringResource(id = R.string.flexera_notification_previews),
+ description = stringResource(id = R.string.flexera_notification_previews_description),
+ adapter = prefs.flexeraNotificationPreviews.getAdapter(),
+ )
SwitchPreference(
label = stringResource(id = R.string.pref_all_apps_remember_position_title),
description = stringResource(id = R.string.pref_all_apps_remember_position_description),
diff --git a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt
index 226b632b59..809da92a05 100644
--- a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt
+++ b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt
@@ -220,6 +220,9 @@ class PreferenceManager @Inject constructor(
// Flexera Launcher: whether a slide along the home screen's edge opens the drawer at a letter (PRD AD-10).
val flexeraScrubFromHome = BoolPref("pref_flexeraScrubFromHome", false)
+ // Flexera Launcher: whether drawer rows show the app's latest notification under its name (PRD AD-11).
+ val flexeraNotificationPreviews = BoolPref("pref_flexeraNotificationPreviews", true, recreate)
+
override fun close() {
TODO("Not yet implemented")
}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index eac6f1ce33..3b866368c3 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -1413,6 +1413,11 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
setContentDescription(itemInfo.contentDescription);
}
}
+ // Flexera Launcher: a row in the drawer list shows its app's latest notification
+ // under the app name (PRD AD-11).
+ if (mDisplay == DISPLAY_ALL_APPS) {
+ app.flexera.drawer.FlexeraNotificationPreview.apply(this, itemInfo, mDotInfo);
+ }
}
}
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 945e7968ee..3f34400fdc 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -129,13 +129,23 @@ public class NotificationListener extends NotificationListenerService {
switch (message.what) {
case MSG_NOTIFICATION_POSTED: {
StatusBarNotification sbn = (StatusBarNotification) message.obj;
- mUiHandler.obtainMessage(notificationIsValidForUI(sbn)
+ // Flexera Launcher: keep the text for the drawer's notification previews in step
+ // with the dots, before the rows are told to refresh (PRD AD-11).
+ boolean validForUi = notificationIsValidForUI(sbn);
+ if (validForUi) {
+ app.flexera.drawer.FlexeraNotificationPreview.onPosted(sbn);
+ } else {
+ app.flexera.drawer.FlexeraNotificationPreview.onRemoved(sbn);
+ }
+ mUiHandler.obtainMessage(validForUi
? MSG_NOTIFICATION_POSTED : MSG_NOTIFICATION_REMOVED,
toKeyPair(sbn)).sendToTarget();
return true;
}
case MSG_NOTIFICATION_REMOVED: {
StatusBarNotification sbn = (StatusBarNotification) message.obj;
+ // Flexera Launcher: drop the notification's preview text with its dot.
+ app.flexera.drawer.FlexeraNotificationPreview.onRemoved(sbn);
mUiHandler.obtainMessage(MSG_NOTIFICATION_REMOVED,
toKeyPair(sbn)).sendToTarget();
@@ -158,6 +168,8 @@ public class NotificationListener extends NotificationListenerService {
} else {
activeNotifications = new ArrayList<>();
}
+ // Flexera Launcher: rebuild the preview texts from the same list as the dots.
+ app.flexera.drawer.FlexeraNotificationPreview.onFullRefresh(activeNotifications);
mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
return true;
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 95110329d3..a0e41ea45a 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -102,9 +102,11 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
dotInfo = new DotInfo();
mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
}
- if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
- updateNotificationDots(postedPackageUserKey::equals);
- }
+ // Flexera Launcher: refresh the app's icons on every posted notification, not only when
+ // the dot or its count changes. A notification that is updated in place keeps its dot
+ // but brings new text for the drawer's preview line (PRD AD-11).
+ dotInfo.addOrUpdateNotificationKey(notificationKey);
+ updateNotificationDots(postedPackageUserKey::equals);
}
@Override