Show an app's latest notification under its name in the drawer list
PRD AD-11, the last buildable row of M4. - FlexeraNotificationPreview keeps the text of the notifications that currently show a dot, fed by Launcher3's NotificationListener on its worker thread, and sets a second, smaller and dimmer line on a drawer row whenever the row's dot state is applied. - It uses what the dots already use: the same notification access and the same filter for which notifications count. Without notification access nothing changes. - PopupDataProvider now refreshes an app's icons on every posted notification, so a notification updated in place brings its new text to the row. - Only rows of the drawer list take the second line (marked with a view tag); search result icons share the "all apps" display type but stay as they were. - New setting under Settings > App drawer: Notification previews, on by default. Checked on the Android 17 emulator (debug build): a test SMS showed under Messages, a second one replaced it while the drawer was open, removing the notification put the plain name back, and the setting switches the line off and on. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
f92276da15
commit
4e48615b5a
9 files changed
+172
-4
No files matched your search
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<!-- The wave alphabet view inside the drawer (app.flexera.drawer.WaveAlphabetView). -->
|
||||
<item name="flexera_wave_alphabet" type="id" />
|
||||
<!-- View tag that marks an app row of the drawer list (FlexeraDrawer.styleAppRow). -->
|
||||
<item name="flexera_drawer_row" type="id" />
|
||||
<!-- View tag on a drawer row: set while the row shows a notification preview under the app name. -->
|
||||
<item name="flexera_notification_preview" type="id" />
|
||||
</resources>
|
||||
@@ -40,6 +40,8 @@
|
||||
<!-- Read by screen readers as the name of the alphabet strip, followed by the current letter -->
|
||||
<string name="flexera_alphabet_description">Alphabet</string>
|
||||
<!-- App drawer settings (app.flexera.ui.FlexeraDrawerSettings) -->
|
||||
<string name="flexera_notification_previews">Notification previews</string>
|
||||
<string name="flexera_notification_previews_description">Show an app\'s latest notification under its name in the app list. Works when notification dots are on (Settings, General).</string>
|
||||
<string name="flexera_alphabet_heading">Alphabet</string>
|
||||
<string name="flexera_alphabet_on_left">Alphabet on the left edge</string>
|
||||
<string name="flexera_alphabet_on_left_description">For left-handed use. The letters normally sit on the right edge.</string>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<String, Preview>()
|
||||
|
||||
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<StatusBarNotification>) {
|
||||
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()
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user